From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目
---
src/views/mall/promotion/kefu/components/tools/constants.ts | 17
src/api/system/dict/dict.type.ts | 53
src/views/mall/promotion/kefu/components/asserts/kun.png | 0
src/views/mp/components/wx-material-select/types.ts | 11
src/views/erp/purchase/order/components/PurchaseOrderReturnEnableList.vue | 212
src/api/infra/apiAccessLog/index.ts | 34
src/views/Profile/components/index.ts | 7
src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue | 74
src/views/mall/promotion/kefu/components/asserts/shengqi.png | 0
src/components/FormCreate/src/config/useDictSelectRule.ts | 64
src/api/mall/trade/delivery/express/index.ts | 45
src/views/crm/contract/index.vue | 398
src/views/crm/customer/CustomerForm.vue | 260
src/api/mp/autoReply/index.ts | 39
src/views/ai/knowledge/document/form/index.vue | 193
src/views/ai/music/index/list/songInfo/index.vue | 22
src/views/mall/promotion/seckill/activity/index.vue | 256
src/api/erp/finance/account/index.ts | 61
src/views/member/group/components/MemberGroupSelect.vue | 45
src/views/crm/receivable/components/ReceivableList.vue | 164
src/views/mall/promotion/combination/record/CombinationRecordListDialog.vue | 89
src/views/infra/codegen/ImportTable.vue | 160
src/api/system/social/client/index.ts | 38
src/plugins/tongji/index.ts | 23
src/layout/components/Message/src/Message.vue | 137
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue | 174
src/views/crm/contract/detail/ContractProductList.vue | 66
src/components/UserSelectForm/index.vue | 171
src/plugins/elementPlus/index.ts | 17
.image/应用管理.jpg | 0
src/views/crm/contract/config/index.vue | 103
src/views/bpm/processInstance/detail/PrintDialog.vue | 234
src/views/member/level/components/MemberLevelSelect.vue | 45
src/views/crm/business/components/BusinessProductForm.vue | 183
src/api/crm/statistics/portrait.ts | 60
src/api/system/post/index.ts | 51
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue | 688
src/views/bpm/model/form/index.vue | 456
.image/OA请假-详情.jpg | 0
src/assets/svgs/bpm/parallel.svg | 1
src/store/modules/user.ts | 108
src/views/ai/knowledge/document/form/SplitStep.vue | 238
src/main.ts | 85
src/views/mall/product/property/index.vue | 177
src/views/bpm/category/index.vue | 199
src/views/mall/product/comment/CommentForm.vue | 167
src/views/erp/finance/payment/FinancePaymentForm.vue | 278
src/views/crm/business/detail/BusinessDetailsHeader.vue | 37
src/assets/imgs/profile.jpg | 0
src/views/erp/stock/check/StockCheckForm.vue | 148
.image/common/ruoyi-vue-pro-biz.png | 0
src/assets/svgs/pay/icon/wallet.svg | 1
src/views/infra/demo/demo03/erp/Demo03StudentForm.vue | 124
src/views/mall/statistics/member/components/MemberFunnelCard.vue | 121
src/views/bpm/model/form/PrintTemplate/module/utils/dom.ts | 21
src/api/crm/customer/poolConfig/index.ts | 19
src/views/iot/thingmodel/dataSpecs/ThingModelNumberDataSpecs.vue | 139
src/views/erp/sale/return/components/SaleReturnItemForm.vue | 300
src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue | 280
src/views/crm/statistics/customer/components/CustomerConversionStat.vue | 170
.image/操作日志.jpg | 0
src/views/mall/promotion/kefu/components/asserts/mengbi.png | 0
src/views/mp/tag/TagForm.vue | 98
src/hooks/web/useDesign.ts | 18
src/api/erp/stock/out/index.ts | 62
.image/admin-uniapp/05.png | 0
src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue | 234
src/styles/FormCreate/index.scss | 22
src/views/mall/trade/afterSale/detail/index.vue | 358
src/views/member/user/detail/UserFavoriteList.vue | 96
src/views/erp/stock/out/StockOutForm.vue | 170
src/api/iot/rule/data/sink/index.ts | 126
src/components/ContentDetailWrap/src/ContentDetailWrap.vue | 58
src/utils/auth.ts | 80
src/assets/svgs/bpm/simple-process-bg.svg | 1
src/views/iot/alert/config/index.vue | 210
src/views/bpm/form/editor/index.vue | 174
src/components/Verifition/src/utils/util.ts | 97
src/views/infra/codegen/components/index.ts | 4
src/views/pay/refund/index.vue | 298
src/api/mall/product/comment.ts | 49
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js | 8
src/views/infra/config/ConfigForm.vue | 131
src/views/infra/demo/demo03/normal/index.vue | 253
src/views/mp/material/components/UploadVideo.vue | 129
src/views/bpm/model/form/ProcessDesign.vue | 72
src/views/mall/promotion/kefu/components/asserts/keai.png | 0
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue | 199
src/views/crm/business/BusinessForm.vue | 287
src/components/Map/index.vue | 268
src/views/crm/customer/detail/CustomerDetailsInfo.vue | 72
src/views/bpm/model/form/BasicInfo.vue | 360
src/views/ai/chat/index/components/role/RoleRepository.vue | 246
src/components/Dialog/src/Dialog.vue | 157
src/api/mall/promotion/article/index.ts | 42
src/views/crm/contact/index.vue | 332
src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue | 264
src/views/ai/image/index/index.vue | 114
src/views/bpm/task/copy/index.vue | 161
.image/Redis.jpg | 0
src/api/ai/knowledge/document/index.ts | 54
src/views/mall/statistics/trade/index.vue | 363
src/views/crm/clue/index.vue | 270
src/api/erp/sale/order/index.ts | 64
src/views/mall/promotion/components/index.ts | 14
src/api/pay/demo/order/index.ts | 29
src/views/system/oauth2/client/ClientForm.vue | 261
package.json | 159
src/views/pay/app/components/channel/WalletChannelForm.vue | 122
src/views/crm/contract/components/ContractList.vue | 136
src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue | 57
src/assets/imgs/diy/statusBar.png | 0
src/views/mall/promotion/kefu/components/asserts/baiyan.png | 0
.image/文件管理2.jpg | 0
.image/商户信息.jpg | 0
src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js | 251
src/views/iot/thingmodel/dataSpecs/ThingModelArrayDataSpecs.vue | 56
src/views/member/config/index.vue | 121
src/views/member/signin/config/SignInConfigForm.vue | 132
src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js | 14
src/views/mall/promotion/point/activity/PointActivityForm.vue | 227
src/api/mall/promotion/combination/combinationActivity.ts | 72
src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue | 190
src/views/mp/tag/index.vue | 158
src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue | 72
src/views/system/notify/message/index.vue | 212
src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue | 216
src/views/mp/draft/mock.js | 151
uno.config.ts | 107
src/locales/zh-CN.ts | 458
src/views/crm/product/category/index.vue | 139
src/assets/svgs/bpm/add-user.svg | 1
src/components/AppLinkInput/data.ts | 236
src/views/iot/rule/data/rule/DataRuleForm.vue | 158
src/components/DiyEditor/components/mobile/Divider/index.vue | 29
src/views/Home/Index2.vue | 319
src/api/mall/statistics/common.ts | 5
src/views/member/point/record/index.vue | 161
src/store/modules/bpm/simpleWorkflow.ts | 55
src/components/Form/src/helper.ts | 148
src/api/erp/product/unit/index.ts | 46
src/api/system/dict/dict.data.ts | 59
src/views/infra/apiAccessLog/index.vue | 226
src/components/DiyEditor/components/mobile/PromotionCombination/config.ts | 96
src/components/DiyEditor/components/mobile/UserOrder/config.ts | 23
src/views/Profile/components/UserSocial.vue | 107
src/views/erp/finance/account/index.vue | 235
src/components/Icon/index.ts | 4
src/views/system/sms/template/index.vue | 345
src/assets/svgs/bpm/condition.svg | 1
src/views/erp/product/unit/ProductUnitForm.vue | 108
src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue | 379
src/components/DiyEditor/components/mobile/TitleBar/index.vue | 75
src/views/mall/promotion/rewardActivity/components/RewardRule.vue | 138
src/layout/components/Setting/src/Setting.vue | 303
src/components/FormCreate/index.ts | 4
src/views/mall/promotion/rewardActivity/RewardForm.vue | 224
src/api/infra/file/index.ts | 46
src/components/Cropper/src/Cropper.vue | 183
src/views/crm/product/category/ProductCategoryForm.vue | 110
src/views/iot/thingmodel/ThingModelProperty.vue | 177
src/views/infra/job/index.vue | 332
src/views/crm/backlog/components/ContractAuditList.vue | 247
src/views/mall/promotion/coupon/components/index.ts | 4
src/components/FormCreate/src/config/useUploadImgsRule.ts | 84
src/api/erp/product/product/index.ts | 57
src/views/system/role/RoleDataPermissionForm.vue | 169
src/views/infra/job/JobDetail.vue | 73
src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue | 245
src/views/system/notify/template/index.vue | 265
src/views/pay/app/components/channel/AlipayChannelForm.vue | 351
src/assets/svgs/login-bg.svg | 1
src/views/erp/sale/customer/CustomerForm.vue | 210
src/api/system/user/profile.ts | 57
src/components/RouterSearch/index.vue | 121
src/views/ai/knowledge/segment/index.vue | 242
src/views/ai/chat/index/components/conversation/ConversationList.vue | 391
src/views/Error/404.vue | 7
src/layout/components/TagsView/src/TagsView.vue | 661
src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue | 97
src/views/mp/components/wx-msg/components/Msg.vue | 69
src/views/mp/menu/assets/menu_head.png | 0
README.md | 293
src/layout/components/Setting/src/components/ColorRadioPicker.vue | 67
src/views/ai/write/index/components/Left.vue | 213
src/views/iot/rule/scene/index.vue | 492
src/views/crm/statistics/funnel/components/BusinessSummary.vue | 259
.image/admin-uniapp/03.png | 0
src/layout/components/Menu/src/Menu.vue | 272
src/views/mall/promotion/bargain/activity/bargainActivity.data.ts | 146
src/views/iot/home/components/MessageTrendCard.vue | 227
src/views/Profile/components/ProfileUser.vue | 118
src/views/mall/promotion/kefu/components/member/MemberInfo.vue | 255
src/views/member/user/detail/UserOrderList.vue | 279
src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js | 6
src/views/iot/rule/scene/form/inputs/ValueInput.vue | 266
src/assets/svgs/bpm/auditor.svg | 1
src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue | 100
src/views/mall/trade/order/detail/index.vue | 427
src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/index.vue | 236
src/api/erp/purchase/return/index.ts | 62
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js | 83
src/views/mall/promotion/kefu/components/asserts/danao.png | 0
src/api/iot/alert/config/index.ts | 46
src/views/mall/home/components/ShortcutCard.vue | 82
src/views/mall/trade/order/components/OrderTableColumn.vue | 303
src/views/bpm/processExpression/ProcessExpressionForm.vue | 114
src/views/iot/alert/config/AlertConfigForm.vue | 201
src/views/bpm/model/form/editor/index.vue | 124
.env | 37
src/api/login/oauth2/index.ts | 41
src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js | 16
src/views/iot/ota/firmware/index.vue | 232
src/views/mp/components/wx-reply/components/TabVideo.vue | 128
src/components/DiyEditor/components/mobile/TabBar/config.ts | 97
src/utils/propTypes.ts | 24
src/views/iot/thingmodel/index.vue | 176
src/views/mall/product/spu/components/SpuTableSelect.vue | 303
src/api/system/dept/index.ts | 53
src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue | 510
src/layout/components/TabMenu/src/helper.ts | 51
src/views/iot/device/device/detail/DeviceDetailsMessage.vue | 201
src/api/mall/promotion/combination/combinationRecord.ts | 28
src/hooks/web/useEmitt.ts | 22
src/types/contextMenu.d.ts | 7
src/components/Form/src/components/useRenderCheckbox.tsx | 26
src/components/index.ts | 6
src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue | 148
src/api/erp/purchase/order/index.ts | 64
src/views/erp/stock/warehouse/WarehouseForm.vue | 157
src/views/member/level/index.vue | 171
src/assets/ai/copy-style2.svg | 1
src/components/bpmnProcessDesigner/package/penal/task/data.ts | 36
src/layout/components/Setting/src/components/InterfaceDisplay.vue | 236
build/vite/index.ts | 99
src/views/Home/types.ts | 57
src/views/mall/home/components/TradeTrendCard.vue | 208
src/views/Redirect/Redirect.vue | 28
src/views/iot/thingmodel/ThingModelTSL.vue | 50
src/views/ai/model/model/index.vue | 192
src/hooks/event/useScrollTo.ts | 60
src/views/mall/product/category/components/ProductCategorySelect.vue | 51
src/views/erp/purchase/in/components/PurchaseInItemForm.vue | 294
src/components/DiyEditor/components/ComponentContainer.vue | 239
src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts | 96
src/views/mp/autoReply/components/types.ts | 7
src/api/mp/tag/index.ts | 60
src/views/iot/rule/scene/form/selectors/OperatorSelector.vue | 264
src/views/report/jmreport/bi.vue | 15
src/locales/en.ts | 462
.image/大屏设计器-列表.jpg | 0
src/views/mall/promotion/kefu/components/index.ts | 5
src/api/ai/write/index.ts | 85
src/layout/components/TabMenu/index.ts | 3
src/views/bpm/form/index.vue | 205
src/plugins/echarts/index.ts | 51
src/layout/components/SizeDropdown/index.ts | 3
src/views/member/user/detail/UserBasicInfo.vue | 165
src/components/FormCreate/src/type/index.ts | 31
src/views/bpm/model/form/ExtraSettings.vue | 507
src/layout/components/Setting/src/components/LayoutRadioPicker.vue | 172
src/components/DictTag/src/DictTag.vue | 90
src/views/erp/home/index.vue | 93
src/views/mall/product/category/CategoryForm.vue | 139
src/views/member/user/components/UserBalanceUpdateForm.vue | 144
src/components/Card/src/CardTitle.vue | 37
src/views/system/mail/template/index.vue | 302
src/components/DiyEditor/components/mobile/MenuSwiper/index.vue | 119
.image/短信模板.jpg | 0
src/hooks/web/useCrudSchemas.ts | 326
src/views/pay/wallet/balance/WalletForm.vue | 22
src/views/infra/codegen/components/ColumInfoForm.vue | 167
src/views/iot/device/group/index.vue | 169
src/components/DiyEditor/components/mobile/PromotionCombination/property.vue | 164
src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue | 1140
src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue | 263
src/views/crm/backlog/components/ReceivablePlanRemindList.vue | 220
src/views/erp/home/components/SummaryCard.vue | 21
src/hooks/web/useTitle.ts | 24
src/views/infra/demo/demo02/Demo02CategoryForm.vue | 114
src/api/bpm/task/index.ts | 122
src/api/bpm/processExpression/index.ts | 42
src/components/Verifition/src/Verify.vue | 446
src/assets/svgs/bpm/running.svg | 1
src/views/ai/chat/manager/ChatConversationList.vue | 163
src/views/erp/stock/in/index.vue | 376
src/views/bpm/category/CategoryForm.vue | 130
src/views/crm/statistics/customer/components/CustomerFollowUpType.vue | 120
index.html | 151
.image/admin-uniapp/09.png | 0
src/layout/components/Menu/index.ts | 3
src/views/system/oauth2/token/index.vue | 164
src/utils/constants.ts | 465
src/views/system/mail/log/index.vue | 279
src/views/mall/promotion/kefu/components/asserts/bukesiyi.png | 0
src/views/pay/notify/NotifyDetail.vue | 92
src/views/Login/components/LoginForm.vue | 360
src/views/iot/thingmodel/ThingModelService.vue | 64
src/api/infra/demo/demo01/index.ts | 50
src/components/UploadFile/src/UploadFile.vue | 235
src/views/iot/thingmodel/dataSpecs/ThingModelStructDataSpecs.vue | 167
src/views/crm/receivable/plan/detail/ReceivablePlanDetailsHeader.vue | 44
src/components/Echart/index.ts | 3
.gitignore | 9
src/components/JsonEditor/types/index.ts | 80
src/views/bpm/processInstance/detail/SignDialog.vue | 50
src/components/DiyEditor/components/ComponentContainerProperty.vue | 168
src/views/infra/demo/demo01/Demo01ContactForm.vue | 129
src/api/mall/promotion/reward/rewardActivity.ts | 58
src/views/mall/promotion/article/index.vue | 230
src/views/mall/promotion/bargain/activity/BargainActivityForm.vue | 233
src/views/mall/promotion/kefu/components/KeFuConversationList.vue | 254
src/components/Echart/src/Echart.vue | 120
src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue | 156
src/layout/components/TagsView/src/helper.ts | 21
src/components/Verifition/src/Verify/VerifySlide.vue | 376
src/components/Table/src/helper.ts | 8
src/views/ai/workflow/form/WorkflowDesign.vue | 250
src/hooks/web/useValidator.ts | 60
src/views/mp/components/wx-msg/index.ts | 6
src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue | 150
src/views/bpm/model/form/PrintTemplate/module/elem-to-html.ts | 12
src/views/mall/promotion/point/activity/pointActivity.data.ts | 55
src/views/mall/promotion/seckill/components/SeckillShowcase.vue | 156
src/views/erp/stock/move/components/StockMoveItemForm.vue | 292
.image/错误日志.jpg | 0
src/store/modules/lock.ts | 48
src/views/crm/statistics/rank/components/ContractCountRank.vue | 98
.image/错误码管理.jpg | 0
src/views/mp/components/wx-voice-play/main.vue | 105
src/store/modules/permission.ts | 71
src/components/DiyEditor/components/mobile/ProductList/property.vue | 99
src/views/erp/product/product/ProductForm.vue | 242
src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue | 309
src/plugins/formCreate/index.ts | 135
.image/登录日志.jpg | 0
.image/表单构建.jpg | 0
src/components/bpmnProcessDesigner/package/theme/element-variables.scss | 70
src/views/erp/purchase/order/index.vue | 407
src/components/SimpleProcessDesignerV2/src/consts.ts | 902
src/utils/formatter.ts | 7
src/views/infra/druid/index.vue | 28
src/types/localeDropdown.d.ts | 10
src/views/erp/stock/record/index.vue | 250
src/assets/svgs/member_point.svg | 1
src/components/SimpleProcessDesignerV2/src/node.ts | 617
src/components/Editor/index.ts | 8
src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue | 150
src/components/InputWithColor/index.vue | 35
src/views/mall/promotion/kefu/components/asserts/fennu.png | 0
src/api/system/notice/index.ts | 47
src/components/DiyEditor/components/mobile/UserCard/property.vue | 17
src/views/iot/rule/scene/form/configs/ConditionConfig.vue | 301
src/views/pay/demo/withdraw/index.vue | 172
src/views/ai/model/model/ModelForm.vue | 223
src/components/Table/src/TableSelectForm.vue | 92
src/utils/formCreate.ts | 68
src/assets/ai/delete.svg | 1
src/views/bpm/processListener/index.vue | 185
src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue | 73
src/views/mall/promotion/kefu/components/asserts/fankun.png | 0
src/components/bpmnProcessDesigner/package/penal/index.js | 7
src/permission.ts | 107
src/views/mp/menu/index.vue | 403
src/views/iot/product/product/components/ProductTableSelect.vue | 220
src/api/ai/music/index.ts | 41
src/api/erp/stock/check/index.ts | 61
src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue | 78
src/views/member/user/detail/UserExperienceRecordList.vue | 158
src/api/mall/promotion/bargain/bargainActivity.ts | 68
.image/common/ai-feature.png | 0
src/views/system/sms/channel/index.vue | 238
src/api/mall/promotion/diy/page.ts | 45
src/views/system/sms/template/SmsTemplateForm.vue | 163
src/views/mall/product/spu/form/DeliveryForm.vue | 96
src/api/mall/market/banner/index.ts | 37
src/views/system/social/client/SocialClientForm.vue | 159
src/views/crm/backlog/components/CustomerPutPoolRemindList.vue | 169
.image/数据库文档.jpg | 0
src/views/crm/contact/detail/ContactDetailsHeader.vue | 33
src/components/Draggable/index.vue | 86
src/components/FormCreate/src/config/selectRule.ts | 181
src/views/iot/ota/task/OtaTaskDetail.vue | 285
src/views/infra/fileConfig/FileConfigForm.vue | 224
src/views/mp/menu/components/menuOptions.ts | 42
src/views/crm/statistics/rank/components/ContractPriceRank.vue | 105
types/wangeditor-types.d.ts | 27
src/api/pay/transfer/index.ts | 16
src/assets/svgs/pay/icon/alipay_bar.svg | 2
src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue | 147
src/views/mall/product/brand/BrandForm.vue | 123
src/components/DiyEditor/components/mobile/MenuList/index.vue | 31
src/views/mall/promotion/rewardActivity/components/RewardRuleCouponSelect.vue | 133
src/views/crm/business/components/BusinessList.vue | 186
src/views/mall/promotion/kefu/components/asserts/xiaodiaoya.png | 0
.image/common/system-feature.png | 0
src/components/DiyEditor/components/mobile/index.ts | 61
src/components/DiyEditor/components/mobile/UserCoupon/property.vue | 17
src/views/pay/wallet/rechargePackage/index.vue | 185
src/components/ContentDetailWrap/index.ts | 3
src/api/erp/stock/warehouse/index.ts | 64
src/views/mall/product/comment/ReplyForm.vue | 76
src/views/mall/promotion/discountActivity/DiscountActivityForm.vue | 265
src/views/iot/thingmodel/ThingModelForm.vue | 221
.image/应用信息-编辑.jpg | 0
src/components/Qrcode/index.ts | 3
src/views/iot/rule/data/sink/config/RedisStreamConfigForm.vue | 57
src/api/system/user/socialUser.ts | 31
src/views/iot/rule/scene/form/sections/ActionSection.vue | 272
src/views/crm/statistics/rank/components/FollowCustomerCountRank.vue | 98
src/views/mall/promotion/kefu/components/asserts/aini.png | 0
src/components/bpmnProcessDesigner/package/utils.ts | 77
.image/common/ruoyi-vue-pro-architecture.png | 0
src/views/ai/mindmap/index/components/Right.vue | 167
src/api/erp/sale/return/index.ts | 62
src/views/iot/ota/firmware/detail/index.vue | 143
src/views/system/social/client/index.vue | 227
src/layout/components/UserInfo/src/components/LockDialog.vue | 98
src/views/iot/ota/task/OtaTaskForm.vue | 132
src/layout/components/Menu/src/helper.ts | 54
.image/流程模型-列表.jpg | 0
src/views/mall/promotion/kefu/components/asserts/ganga.png | 0
src/views/mall/trade/delivery/pickUpOrder/index.vue | 436
src/views/system/notify/template/NotifyTemplateSendForm.vue | 146
src/components/ImageViewer/src/ImageViewer.vue | 35
src/views/system/sms/log/index.vue | 268
src/views/member/signin/config/index.vue | 106
src/utils/file.ts | 37
src/views/mall/promotion/kefu/components/asserts/dianzan.png | 0
src/views/erp/product/product/index.vue | 224
src/assets/svgs/money.svg | 1
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js | 151
src/store/modules/mall/kefu.ts | 81
src/api/system/role/index.ts | 56
.image/admin-uniapp/07.png | 0
src/components/VerticalButtonGroup/index.vue | 44
src/views/crm/permission/components/TransferForm.vue | 162
src/views/ai/model/chatRole/ChatRoleForm.vue | 223
src/views/member/user/detail/UserPointList.vue | 152
src/views/crm/followup/components/FollowUpRecordContactForm.vue | 47
src/views/infra/job/logger/index.vue | 200
src/views/ai/chat/manager/ChatMessageList.vue | 175
.env.test | 34
src/views/erp/sale/order/components/SaleOrderItemForm.vue | 271
src/components/DocAlert/index.vue | 34
src/views/mall/product/spu/form/InfoForm.vue | 153
src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue | 192
src/api/mall/trade/order/index.ts | 188
src/views/crm/contract/detail/index.vue | 139
src/views/infra/demo/demo03/erp/index.vue | 274
src/assets/svgs/403.svg | 1
src/components/DiyEditor/components/mobile/Carousel/config.ts | 53
src/components/DiyEditor/components/mobile/SearchBar/property.vue | 87
src/components/DiyEditor/components/mobile/ImageBar/property.vue | 34
src/views/iot/home/components/ComparisonCard.vue | 50
src/api/erp/stock/in/index.ts | 62
src/views/ai/chat/index/components/message/MessageKnowledge.vue | 104
src/views/erp/stock/in/components/StockInItemForm.vue | 267
src/utils/is.ts | 118
src/api/ai/image/index.ts | 102
src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue | 143
src/api/mall/promotion/coupon/coupon.ts | 26
src/api/mp/user/index.ts | 31
src/api/member/config/index.ts | 19
src/api/infra/apiErrorLog/index.ts | 48
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue | 312
src/api/bpm/userGroup/index.ts | 47
src/views/mall/promotion/combination/components/CombinationTableSelect.vue | 345
.image/我的流程-详情.jpg | 0
vite.config.ts | 88
src/layout/components/Message/index.ts | 3
src/components/DiyEditor/components/mobile/UserWallet/config.ts | 23
src/views/mall/product/spu/form/OtherForm.vue | 91
src/views/system/menu/MenuForm.vue | 257
src/assets/svgs/bpm/cancel.svg | 1
src/views/member/tag/index.vue | 155
src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue | 100
src/api/mall/trade/delivery/pickUpStore/index.ts | 52
src/views/Profile/components/UserAvatar.vue | 54
src/views/mp/components/wx-reply/components/TabMusic.vue | 116
src/views/bpm/processListener/ProcessListenerForm.vue | 162
src/views/crm/backlog/components/ClueFollowList.vue | 153
src/views/system/user/UserForm.vue | 219
src/views/crm/permission/components/PermissionForm.vue | 137
src/views/erp/purchase/in/PurchaseInForm.vue | 325
src/views/erp/sale/return/components/SaleReturnRefundEnableList.vue | 199
src/views/pay/order/index.vue | 275
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue | 655
src/views/iot/product/product/detail/ProductDetailsInfo.vue | 37
src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue | 321
src/components/FormCreate/src/config/index.ts | 15
.image/短信渠道.jpg | 0
src/views/system/social/user/SocialUserDetail.vue | 60
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js | 11
src/api/mall/product/property.ts | 89
LICENSE | 21
src/utils/formatTime.ts | 332
src/views/crm/statistics/customer/components/CustomerDealCycleByArea.vue | 153
src/views/mall/trade/config/index.vue | 291
src/assets/svgs/iot/cube.svg | 1
src/views/member/user/detail/UserBrokerageList.vue | 125
src/components/bpmnProcessDesigner/src/translations.ts | 25
src/views/mall/promotion/kefu/components/asserts/ganmao.png | 0
src/views/mall/promotion/combination/activity/combinationActivity.data.ts | 140
src/components/SimpleProcessDesignerV2/theme/iconfont.ttf | 0
src/views/ai/chat/index/components/message/MessageFileUpload.vue | 394
src/views/iot/rule/scene/form/selectors/DeviceSelector.vue | 103
src/components/DiyEditor/components/mobile/TitleBar/property.vue | 139
src/api/iot/device/device/index.ts | 165
src/views/system/tenant/TenantForm.vue | 186
src/views/crm/business/BusinessUpdateStatusForm.vue | 108
src/utils/Logger.ts | 100
src/api/crm/business/index.ts | 98
src/views/iot/rule/data/sink/config/MqttConfigForm.vue | 45
src/views/ai/workflow/form/index.vue | 240
src/api/system/notify/message/index.ts | 49
src/api/iot/rule/data/rule/index.ts | 39
src/api/system/area/index.ts | 11
src/components/bpmnProcessDesigner/package/theme/process-designer.scss | 159
src/views/member/user/detail/UserBalanceList.vue | 67
src/components/Cropper/src/CropperAvatar.vue | 142
src/components/JsonEditor/src/JsonEditor.vue | 126
src/views/iot/product/product/detail/ProductDetailsHeader.vue | 113
src/views/crm/statistics/funnel/components/FunnelBusiness.vue | 149
src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue | 421
src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue | 154
src/views/infra/codegen/index.vue | 287
src/views/iot/rule/scene/form/sections/BasicInfoSection.vue | 86
src/views/bpm/processInstance/detail/index.vue | 363
src/hooks/web/useNProgress.ts | 33
src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue | 160
src/assets/svgs/iot/card-fill.svg | 1
src/api/mall/product/history.ts | 10
src/components/Highlight/src/Highlight.vue | 65
src/views/pay/notify/index.vue | 250
src/assets/svgs/shopping.svg | 1
src/components/DiyEditor/components/mobile/PromotionCombination/index.vue | 201
src/views/infra/dataSourceConfig/index.vue | 136
src/components/CountTo/src/CountTo.vue | 182
src/components/DiyEditor/components/mobile/MenuSwiper/config.ts | 66
src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue | 307
src/api/mp/statistics/index.ts | 33
src/components/bpmnProcessDesigner/package/penal/task/task-components/HttpHeaderEditor.vue | 178
src/views/mp/components/wx-reply/components/TabVoice.vue | 160
src/components/DiyEditor/components/mobile/CouponCard/config.ts | 47
src/views/mp/components/wx-msg/types.ts | 17
src/components/Table/index.ts | 13
.image/common/erp-feature.png | 0
src/views/ai/chat/index/index.vue | 630
src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js | 219
src/assets/ai/dall3.jpg | 0
src/views/mall/promotion/kefu/components/asserts/hanyan.png | 0
src/components/bpmnProcessDesigner/src/utils/xml2json.js | 50
src/views/erp/sale/return/index.vue | 443
src/views/crm/customer/CustomerImportForm.vue | 158
src/api/system/notify/template/index.ts | 54
src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue | 470
src/api/bpm/form/index.ts | 56
src/views/iot/rule/data/sink/config/index.ts | 15
src/views/infra/file/FileForm.vue | 107
src/views/mall/promotion/diy/page/decorate.vue | 74
.image/短信日志.jpg | 0
src/components/DiyEditor/util.ts | 125
src/views/pay/transfer/TransferDetail.vue | 80
src/components/Tooltip/src/Tooltip.vue | 17
src/views/mp/components/wx-reply/components/TabNews.vue | 76
src/views/ai/music/index/list/songCard/index.vue | 36
.image/系统接口.jpg | 0
src/views/crm/business/components/BusinessListModal.vue | 156
src/views/iot/rule/data/sink/config/KafkaMQConfigForm.vue | 45
src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue | 154
src/hooks/web/useTimeAgo.ts | 49
src/views/Login/components/LoginFormTitle.vue | 26
.image/字典类型.jpg | 0
src/components/Verifition/src/Verify/index.ts | 5
src/views/bpm/model/form/PrintTemplate/Index.vue | 116
src/views/ai/utils/constants.ts | 470
src/views/erp/purchase/order/PurchaseOrderForm.vue | 269
src/api/crm/statistics/customer.ts | 168
types/env.d.ts | 41
src/components/DiyEditor/components/mobile/PageConfig/property.vue | 34
src/views/member/user/index.vue | 317
src/layout/components/Breadcrumb/src/helper.ts | 31
src/api/ai/knowledge/knowledge/index.ts | 44
src/components/DiyEditor/components/mobile/MenuList/config.ts | 48
src/views/member/group/index.vue | 176
src/api/infra/job/index.ts | 68
src/styles/index.scss | 37
src/utils/filt.ts | 157
src/views/ai/model/apiKey/index.vue | 182
src/components/Crontab/src/Crontab.vue | 1015
src/views/mall/promotion/kefu/components/asserts/liuhan.png | 0
src/views/system/loginlog/index.vue | 180
src/views/system/notify/message/NotifyMessageDetail.vue | 66
src/views/crm/clue/ClueForm.vue | 260
src/layout/components/Collapse/src/Collapse.vue | 35
src/api/crm/permission/index.ts | 72
src/views/crm/statistics/funnel/index.vue | 171
src/views/mall/promotion/discountActivity/discountActivity.data.ts | 106
src/config/axios/config.ts | 28
src/views/infra/demo/demo02/index.vue | 207
src/components/MagicCubeEditor/util.ts | 72
src/views/mp/draft/components/index.ts | 7
src/views/mall/promotion/kefu/components/asserts/kaixin.png | 0
src/views/mp/freePublish/index.vue | 338
.image/部门管理.jpg | 0
.image/流程模型-定义.jpg | 0
src/views/mp/components/wx-msg/card.scss | 116
src/components/Verifition/src/utils/ase.ts | 14
src/views/crm/business/detail/BusinessProductList.vue | 66
src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js | 423
src/types/table.d.ts | 44
src/views/crm/clue/detail/index.vue | 130
src/views/member/level/LevelForm.vue | 175
src/api/pay/channel/index.ts | 46
src/components/DiyEditor/components/mobile/TabBar/property.vue | 103
src/api/erp/stock/stock/index.ts | 41
src/views/mall/promotion/kefu/components/member/OrderBrowsingHistory.vue | 44
src/views/member/user/detail/UserAftersaleList.vue | 276
src/layout/components/ContextMenu/src/ContextMenu.vue | 76
src/views/erp/sale/out/index.vue | 438
src/components/Sticky/index.ts | 3
src/views/mp/autoReply/index.vue | 241
src/api/erp/statistics/sale/index.ts | 28
src/hooks/web/useTagsView.ts | 63
src/views/ai/mindmap/index/index.vue | 94
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js | 83
src/views/ai/chat/index/components/role/RoleCategoryList.vue | 39
src/views/erp/finance/receipt/index.vue | 394
src/views/crm/backlog/components/CustomerFollowList.vue | 170
src/views/mall/promotion/kefu/components/asserts/fadai.png | 0
.vscode/launch.json | 16
src/layout/components/SizeDropdown/src/SizeDropdown.vue | 40
src/views/iot/rule/scene/form/configs/AlertConfig.vue | 81
src/layout/components/TenantVisit/index.vue | 46
src/views/system/mail/account/index.vue | 213
src/views/bpm/group/UserGroupForm.vue | 132
src/views/crm/statistics/customer/components/CustomerSummary.vue | 183
src/layout/components/AppView.vue | 55
src/views/system/oauth2/client/index.vue | 220
src/api/system/sms/smsLog/index.ts | 37
src/types/infoTip.d.ts | 4
src/components/ConfigGlobal/src/ConfigGlobal.vue | 62
src/layout/components/Breadcrumb/src/Breadcrumb.vue | 130
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue | 61
src/api/crm/receivable/plan/index.ts | 74
src/views/crm/contact/ContactForm.vue | 311
src/api/pay/wallet/balance/index.ts | 32
src/views/crm/followup/index.vue | 207
src/views/infra/file/index.vue | 238
src/views/mall/promotion/coupon/index.vue | 201
src/store/modules/locale.ts | 59
src/assets/svgs/bpm/reject.svg | 1
src/views/ai/image/square/index.vue | 67
src/views/crm/backlog/components/common.ts | 39
src/views/infra/build/index.vue | 184
src/views/mall/promotion/kefu/components/asserts/daxiao.png | 0
src/views/erp/stock/stock/index.vue | 186
src/views/iot/rule/data/sink/index.vue | 212
src/components/DiyEditor/components/mobile/PromotionArticle/index.vue | 27
src/views/ai/mindmap/manager/index.vue | 191
src/views/mp/components/wx-material-select/index.ts | 6
src/components/DiyEditor/components/mobile/FloatingActionButton/config.ts | 36
src/views/crm/statistics/performance/components/ContractCountPerformance.vue | 236
.image/字典数据.jpg | 0
src/api/erp/sale/customer/index.ts | 58
src/views/ai/music/index/title/index.vue | 25
src/styles/variables.scss | 4
.image/应用信息-列表.jpg | 0
src/api/system/loginLog/index.ts | 25
src/views/mall/promotion/kefu/components/asserts/emo.png | 0
src/components/Table/src/Table.vue | 311
src/assets/ai/copy.svg | 1
src/router/modules/remaining.ts | 752
src/views/erp/sale/return/SaleReturnForm.vue | 341
src/api/crm/statistics/rank.ts | 67
src/components/DiyEditor/components/mobile/VideoPlayer/property.vue | 55
src/views/ai/chat/index/components/message/MessageNewConversation.vue | 19
.image/链路追踪.jpg | 0
src/views/erp/sale/order/SaleOrderForm.vue | 289
src/api/mp/material/index.ts | 16
src/layout/components/Screenfull/src/Screenfull.vue | 32
src/api/pay/order/index.ts | 110
src/views/ai/knowledge/knowledge/KnowledgeForm.vue | 162
src/views/system/role/index.vue | 299
src/api/mall/promotion/kefu/message/index.ts | 36
src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue | 86
src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue | 99
src/views/crm/statistics/rank/components/FollowCountRank.vue | 98
src/views/iot/home/components/DeviceStateCountCard.vue | 163
src/assets/ai/gpt.svg | 1
src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js | 6
src/views/ai/image/index/components/ImageList.vue | 208
src/views/iot/rule/data/rule/components/SourceConfigForm.vue | 262
src/views/mall/promotion/kefu/index.vue | 136
.image/生成效果.jpg | 0
src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue | 150
src/views/mall/trade/delivery/expressTemplate/index.vue | 165
src/types/icon.d.ts | 5
src/views/mall/home/components/OperationDataCard.vue | 106
src/views/member/tag/TagForm.vue | 91
.image/用户管理.jpg | 0
src/views/crm/contact/detail/ContactDetailsInfo.vue | 69
src/views/infra/redis/index.vue | 269
src/utils/index.ts | 537
src/hooks/web/useWatermark.ts | 72
src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue | 312
src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue | 137
src/api/member/experience-record/index.ts | 22
src/api/infra/codegen/index.ts | 112
src/views/mall/trade/delivery/pickUpStore/index.vue | 207
src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue | 198
src/api/system/oauth2/client.ts | 52
src/components/DiyEditor/components/mobile/VideoPlayer/index.vue | 30
src/components/Tooltip/index.ts | 3
src/components/AppLinkInput/AppLinkSelectDialog.vue | 211
src/views/infra/skywalking/index.vue | 27
.eslintrc.js | 75
src/components/FormCreate/src/config/useEditorRule.ts | 32
src/components/DiyEditor/components/mobile/NoticeBar/config.ts | 46
src/views/mall/trade/delivery/pickUpStore/PickUpStoreForm.vue | 262
src/api/ai/chat/message/index.ts | 104
src/views/mp/message/index.vue | 152
src/views/mall/promotion/kefu/components/message/OrderItem.vue | 181
src/views/iot/thingmodel/components/DataDefinition.vue | 73
src/views/mall/product/spu/form/DescriptionForm.vue | 81
src/views/mall/product/comment/index.vue | 259
src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue | 55
src/api/ai/chat/conversation/index.ts | 65
src/views/mp/draft/components/DraftTable.vue | 87
src/views/mall/statistics/trade/components/TradeStatisticValue.vue | 36
web-types.json | 19
src/views/system/dict/index.vue | 261
src/types/qrcode.d.ts | 9
src/api/iot/thingmodel/index.ts | 301
src/components/DiyEditor/components/mobile/UserCard/index.vue | 29
src/components/Backtop/src/Backtop.vue | 17
src/views/erp/sale/customer/index.vue | 201
.env.prod | 34
src/components/ContentWrap/src/ContentWrap.vue | 36
src/views/crm/customer/index.vue | 343
src/api/crm/contract/config/index.ts | 16
src/views/mall/promotion/point/components/PointShowcase.vue | 154
src/api/bpm/simple/index.ts | 15
src/views/ai/music/index/index.vue | 26
src/views/mall/promotion/kefu/components/asserts/lengku.png | 0
src/views/ai/knowledge/knowledge/index.vue | 221
src/api/mall/statistics/product.ts | 52
src/views/mall/promotion/diy/template/decorate.vue | 214
src/components/Form/src/components/useRenderRadio.tsx | 26
src/views/crm/business/status/index.vue | 150
src/views/iot/ota/firmware/OtaFirmwareForm.vue | 169
public/logo.gif | 0
.vscode/settings.json | 146
src/components/Backtop/index.ts | 3
src/hooks/web/useNetwork.ts | 21
src/views/infra/job/JobForm.vue | 137
src/api/system/tenantPackage/index.ts | 48
src/hooks/web/useNow.ts | 60
src/views/system/user/index.vue | 397
src/views/crm/statistics/portrait/index.vue | 156
src/views/pay/demo/withdraw/DemoWithdrawForm.vue | 129
src/views/iot/thingmodel/dataSpecs/index.ts | 11
src/views/mp/components/wx-voice-play/index.ts | 3
src/views/system/mail/template/MailTemplateSendForm.vue | 136
src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue | 73
src/api/pay/demo/withdraw/index.ts | 30
src/views/erp/purchase/return/PurchaseReturnForm.vue | 328
src/assets/imgs/avatar.jpg | 0
src/components/DiyEditor/components/mobile/Carousel/property.vue | 109
src/views/mall/promotion/combination/components/CombinationShowcase.vue | 158
src/assets/svgs/member_level.svg | 1
src/views/infra/dataSourceConfig/DataSourceConfigForm.vue | 111
src/views/mp/messageTemplate/MessageTemplateSendForm.vue | 163
.vscode/extensions.json | 18
src/api/mall/product/category.ts | 56
src/views/mall/promotion/kefu/components/asserts/xiaoku.png | 0
src/views/erp/finance/receipt/FinanceReceiptForm.vue | 278
src/views/pay/cashier/index.vue | 494
src/api/infra/demo/demo03/normal/index.ts | 81
src/views/system/user/UserAssignRoleForm.vue | 96
src/views/iot/product/category/index.vue | 169
src/views/bpm/model/CategoryDraggableModel.vue | 665
src/components/DiyEditor/components/mobile/PromotionPoint/config.ts | 96
src/views/crm/clue/detail/ClueDetailsHeader.vue | 43
src/components/DiyEditor/components/mobile/MagicCube/property.vue | 76
src/views/iot/device/device/index.vue | 529
src/views/pay/app/index.vue | 372
src/views/Login/components/RegisterForm.vue | 288
src/components/bpmnProcessDesigner/src/highlight/index.js | 5
src/views/erp/purchase/order/components/PurchaseOrderInEnableList.vue | 205
src/views/bpm/model/form/PrintTemplate/module/parse-elem-html.ts | 33
src/assets/svgs/pay/icon/alipay_wap.svg | 1
src/api/crm/product/category/index.ts | 33
src/api/iot/product/category/index.ts | 43
src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue | 103
src/views/mall/promotion/kefu/components/asserts/huaixiao.png | 0
src/views/ai/model/tool/index.vue | 178
src/components/DiyEditor/components/mobile/NavigationBar/index.vue | 93
src/components/Form/src/components/useRenderSelect.tsx | 57
src/views/mp/material/components/upload.ts | 32
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json | 1020
src/views/system/sms/log/SmsLogDetail.vue | 86
src/views/Profile/components/ResetPwd.vue | 73
src/views/mp/menu/assets/menu_foot.png | 0
src/views/mall/product/spu/components/SpuShowcase.vue | 144
src/views/crm/receivable/detail/ReceivableDetailsHeader.vue | 43
src/views/crm/receivable/plan/ReceivablePlanForm.vue | 240
src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue | 99
src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue | 198
.image/common/project-vs.png | 0
src/views/mp/material/components/ImageTable.vue | 83
src/assets/svgs/bpm/child-process.svg | 1
src/views/report/goview/index.vue | 16
src/views/erp/purchase/return/components/PurchaseReturnItemForm.vue | 300
src/views/mall/promotion/kefu/components/asserts/esi.png | 0
src/views/mp/components/wx-reply/components/TabText.vue | 22
src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue | 610
src/styles/var.css | 74
src/components/DiyEditor/components/mobile/Popover/index.vue | 38
src/views/iot/device/device/detail/DeviceDetailsThingModel.vue | 35
src/components/bpmnProcessDesigner/package/index.ts | 11
src/views/infra/apiErrorLog/ApiErrorLogDetail.vue | 81
src/plugins/animate.css/index.ts | 1
src/components/Icon/src/Icon.vue | 86
src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue | 191
src/components/Sticky/src/Sticky.vue | 143
src/views/mall/promotion/kefu/components/asserts/ziya.png | 0
src/views/system/sms/template/SmsTemplateSendForm.vue | 120
src/components/bpmnProcessDesigner/src/utils/index.js | 10
src/views/ai/music/manager/index.vue | 294
.image/通知公告.jpg | 0
src/components/DiyEditor/components/mobile/ProductCard/index.vue | 170
src/components/DiyEditor/components/mobile/MagicCube/index.vue | 76
src/views/mall/promotion/diy/page/index.vue | 191
src/api/iot/ota/firmware/index.ts | 44
src/views/mall/promotion/kefu/components/asserts/shuizhuo.png | 0
src/views/erp/stock/check/index.vue | 359
src/views/system/role/RoleForm.vue | 126
src/api/crm/contact/index.ts | 113
src/views/system/notice/index.vue | 218
src/layout/components/TabMenu/src/TabMenu.vue | 240
src/views/mall/promotion/kefu/components/asserts/tianshi.png | 0
.image/配置管理.jpg | 0
src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue | 392
src/views/mall/promotion/combination/activity/index.vue | 240
src/views/iot/device/device/detail/index.vue | 108
src/views/mall/trade/brokerage/user/BrokerageUserCreateForm.vue | 161
.image/日志中心.jpg | 0
src/views/bpm/model/form/PrintTemplate/module/index.ts | 17
src/assets/svgs/member_expenditure_balance.svg | 1
types/components.d.ts | 8
.image/代码生成.jpg | 0
src/hooks/web/useConfigGlobal.ts | 9
.image/common/mall-preview.png | 0
src/views/iot/device/device/detail/DeviceDetailsInfo.vue | 197
types/router.d.ts | 84
src/assets/svgs/member_recharge_balance.svg | 1
.image/流程模型-设计.jpg | 0
src/api/bpm/model/index.ts | 79
src/assets/svgs/pay/icon/mock.svg | 1
src/api/mall/trade/brokerage/record/index.ts | 11
src/components/DiyEditor/components/mobile/UserOrder/index.vue | 13
src/views/iot/device/device/DeviceImportForm.vue | 139
src/views/mall/promotion/kefu/components/KeFuMessageList.vue | 526
src/views/mall/promotion/kefu/components/asserts/a.png | 0
src/views/mp/material/components/VideoTable.vue | 59
src/views/system/area/AreaForm.vue | 72
src/views/mall/promotion/components/SpuAndSkuList.vue | 138
src/components/DiyEditor/components/mobile/HotZone/config.ts | 43
src/api/iot/alert/record/index.ts | 35
src/components/DiyEditor/components/mobile/SearchBar/config.ts | 43
src/views/mp/components/wx-location/index.ts | 3
src/components/DiyEditor/components/mobile/UserCoupon/index.vue | 15
src/api/system/user/index.ts | 81
src/components/DictTag/index.ts | 3
src/views/iot/device/device/detail/DeviceDetailsSimulator.vue | 420
src/directives/permission/hasRole.ts | 28
src/api/system/mail/template/index.ts | 58
src/api/mall/trade/brokerage/user/index.ts | 44
src/layout/components/ThemeSwitch/src/ThemeSwitch.vue | 46
src/views/bpm/model/form/PrintTemplate/module/plugin.ts | 28
src/api/system/tenant/index.ts | 73
src/components/DiyEditor/components/mobile/PromotionArticle/config.ts | 25
src/views/mp/messageTemplate/index.vue | 144
src/views/mall/promotion/kefu/components/asserts/yiwen.png | 0
src/components/Crontab/index.ts | 2
src/views/crm/backlog/components/ReceivableAuditList.vue | 201
src/views/mall/home/components/ComparisonCard.vue | 42
src/api/pay/wallet/transaction/index.ts | 14
.image/任务列表-待办.jpg | 0
src/views/crm/statistics/customer/components/CustomerFollowUpSummary.vue | 156
src/components/Form/index.ts | 15
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue | 265
src/views/crm/statistics/performance/components/ContractPricePerformance.vue | 236
src/views/crm/followup/components/FollowUpRecordBusinessForm.vue | 42
src/components/CountTo/index.ts | 3
src/views/mall/statistics/member/components/MemberTerminalCard.vue | 68
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue | 277
src/views/mp/components/wx-material-select/main.vue | 279
src/views/system/loginlog/LoginLogDetail.vue | 51
src/views/ai/image/index/components/dall3/index.vue | 232
src/views/mall/trade/order/components/index.ts | 3
src/api/crm/followup/index.ts | 43
src/api/system/operatelog/index.ts | 30
src/views/iot/alert/record/index.vue | 296
.image/admin-uniapp/04.png | 0
src/views/ai/knowledge/document/form/ProcessStep.vue | 146
src/views/erp/home/components/TimeSummaryChart.vue | 86
src/components/bpmnProcessDesigner/package/designer/index2.ts | 8
src/api/infra/jobLog/index.ts | 34
.image/我的流程-发起.jpg | 0
src/components/DiyEditor/components/mobile/PromotionPoint/property.vue | 154
src/layout/components/Logo/src/Logo.vue | 88
src/views/mall/promotion/rewardActivity/index.vue | 227
src/hooks/web/useI18n.ts | 53
.env.stage | 34
src/assets/svgs/bpm/copy.svg | 1
src/views/iot/product/product/ProductForm.vue | 220
src/views/mall/promotion/bargain/activity/index.vue | 233
src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue | 181
src/views/ai/image/index/components/ImageCard.vue | 131
src/views/system/user/DeptTree.vue | 79
src/api/member/group/index.ts | 38
src/components/DiyEditor/components/mobile/HotZone/index.vue | 42
src/components/DiyEditor/components/mobile/ProductCard/property.vue | 149
src/components/ColorInput/index.vue | 34
src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue | 163
src/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue | 199
src/views/ai/music/index/mode/index.vue | 35
src/views/mall/promotion/coupon/template/index.vue | 284
src/views/mp/components/wx-reply/index.ts | 7
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json | 1004
src/components/Search/src/Search.vue | 157
src/components/Icon/src/IconSelect.vue | 239
src/api/ai/model/tool/index.ts | 42
src/views/crm/statistics/rank/components/ProductSalesRank.vue | 98
src/api/ai/workflow/index.ts | 25
src/views/ai/chat/index/components/message/MessageReasoning.vue | 89
.stylelintignore | 6
src/components/DiyEditor/components/mobile/ProductList/config.ts | 64
src/views/iot/rule/data/sink/config/RocketMQConfigForm.vue | 57
src/api/bpm/processListener/index.ts | 40
src/api/erp/sale/out/index.ts | 62
src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue | 340
src/api/erp/finance/payment/index.ts | 61
src/views/erp/stock/move/StockMoveForm.vue | 148
src/views/mall/statistics/product/index.vue | 16
src/utils/permission.ts | 36
src/directives/index.ts | 24
src/components/FormCreate/src/config/useUploadImgRule.ts | 89
src/views/iot/rule/data/sink/config/HttpConfigForm.vue | 86
src/views/mall/promotion/point/components/PointTableSelect.vue | 300
src/components/Infotip/src/Infotip.vue | 54
src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts | 89
src/views/ai/knowledge/document/form/UploadStep.vue | 273
src/views/bpm/oa/leave/create.vue | 257
src/components/UploadFile/index.ts | 5
src/views/crm/backlog/components/ContractRemindList.vue | 246
src/api/ai/model/chatRole/index.ts | 83
src/views/ai/music/index/mode/lyric.vue | 83
postcss.config.js | 5
src/views/mall/promotion/kefu/components/tools/PictureSelectUpload.vue | 93
src/layout/Layout.vue | 75
src/assets/svgs/send.svg | 1
src/assets/imgs/diy/app-nav-bar-mp.png | 0
src/views/mall/promotion/kefu/components/asserts/jingya.png | 0
src/components/DiyEditor/components/mobile/PageConfig/config.ts | 23
src/views/mp/draft/index.vue | 208
src/components/DiyEditor/components/mobile/NavigationBar/config.ts | 88
src/views/infra/webSocket/index.vue | 185
src/components/DiyEditor/components/mobile/MenuGrid/index.vue | 35
src/views/mp/components/wx-reply/components/TabImage.vue | 171
src/views/pay/wallet/balance/index.vue | 156
src/views/mall/promotion/bargain/record/index.vue | 197
src/views/pay/refund/RefundDetail.vue | 95
src/views/mall/promotion/kefu/components/asserts/aixin.png | 0
src/views/pay/app/components/channel/MockChannelForm.vue | 122
src/views/crm/receivable/detail/index.vue | 100
src/views/mp/components/wx-music/main.vue | 62
src/views/erp/purchase/supplier/SupplierForm.vue | 210
src/views/iot/thingmodel/ThingModelInputOutputParam.vue | 151
src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts | 13
src/api/bpm/category/index.ts | 53
src/api/system/menu/index.ts | 49
src/layout/components/Menu/src/components/useRenderMenuItem.tsx | 50
src/styles/FormCreate/fonts/fontello.woff | 0
src/assets/map/json/china.json | 856
src/views/mall/promotion/kefu/components/asserts/hongxin.png | 0
src/utils/tsxHelper.ts | 16
src/utils/routerHelper.ts | 257
src/plugins/unocss/index.ts | 1
src/types/layout.d.ts | 1
src/views/erp/stock/out/index.vue | 378
src/views/mall/promotion/kefu/components/asserts/liukoushui.png | 0
src/api/infra/redis/types.ts | 176
src/components/Tinyflow/Tinyflow.vue | 63
src/views/crm/statistics/rank/components/ContactCountRank.vue | 98
.image/common/yudao-cloud-architecture.png | 0
.image/令牌管理.jpg | 0
src/views/mp/components/wx-account-select/main.vue | 58
src/views/bpm/processInstance/index.vue | 338
src/components/DiyEditor/components/mobile/PromotionPoint/index.vue | 202
src/plugins/vueI18n/helper.ts | 3
src/utils/tree.ts | 403
src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json | 1493
src/views/ai/knowledge/document/index.vue | 236
src/views/mall/promotion/kefu/components/asserts/outu.png | 0
src/views/Error/403.vue | 8
src/views/system/area/index.vue | 79
src/views/mall/promotion/diy/page/DiyPageForm.vue | 104
src/config/axios/index.ts | 48
src/views/crm/business/detail/BusinessDetailsInfo.vue | 61
src/views/crm/product/index.vue | 230
src/views/bpm/oa/leave/index.vue | 275
src/views/crm/receivable/plan/detail/ReceivablePlanDetailsInfo.vue | 83
src/assets/svgs/member_balance.svg | 1
src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue | 99
.image/文件管理.jpg | 0
.image/退款订单.jpg | 0
src/views/erp/sale/out/components/SaleOutItemForm.vue | 300
src/views/ai/chat/index/components/message/MessageListEmpty.vue | 36
src/api/erp/purchase/supplier/index.ts | 58
src/api/mall/promotion/coupon/couponTemplate.ts | 90
src/layout/components/ThemeSwitch/index.ts | 3
.image/common/yudao-roadmap.png | 0
src/views/mall/promotion/kefu/components/asserts/mianwubiaoqing.png | 0
src/api/mp/message/index.ts | 17
.image/首页.jpg | 0
src/components/Editor/src/Editor.vue | 294
.eslintrc-auto-import.json | 259
src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue | 285
src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js | 42
src/views/mall/promotion/banner/BannerForm.vue | 159
src/components/DiyEditor/components/mobile/UserCoupon/config.ts | 23
src/views/Login/components/index.ts | 9
src/views/system/notify/template/NotifyTemplateForm.vue | 141
src/views/bpm/task/manager/index.vue | 166
src/views/crm/product/ProductForm.vue | 212
src/views/member/user/components/UserLevelUpdateForm.vue | 101
src/views/mp/components/wx-reply/components/types.ts | 54
src/views/mp/draft/components/types.ts | 40
src/views/bpm/task/done/index.vue | 282
src/views/crm/receivable/detail/ReceivableDetailsInfo.vue | 62
src/views/mall/promotion/seckill/activity/seckillActivity.data.ts | 163
src/api/mall/statistics/pay.ts | 12
src/views/mall/promotion/kefu/components/asserts/buhaoyisi.png | 0
.image/common/ai-preview.gif | 0
src/views/crm/business/index.vue | 275
src/components/InputPassword/index.ts | 3
src/views/Login/components/ForgetPasswordForm.vue | 278
src/types/configGlobal.d.ts | 4
src/api/member/signin/record/index.ts | 13
src/assets/svgs/message.svg | 1
public/favicon.ico | 0
src/router/index.ts | 36
src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue | 48
src/views/system/notify/my/MyNotifyMessageDetail.vue | 48
src/views/bpm/model/form/PrintTemplate/module/render-elem.ts | 73
src/components/DiyEditor/components/mobile/ProductCard/config.ts | 97
src/views/mp/menu/components/types.ts | 73
src/hooks/web/useCache.ts | 41
.image/工作流设计器-simple.jpg | 0
src/views/mall/promotion/kefu/components/asserts/keshui.png | 0
src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue | 70
src/components/OperateLogV2/src/OperateLogV2.vue | 105
src/api/erp/statistics/purchase/index.ts | 28
src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/controller.ts | 143
src/utils/encrypt.ts | 231
src/api/infra/demo/demo03/inner/index.ts | 81
src/api/mp/menu/index.ts | 26
src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue | 97
src/views/mall/promotion/seckill/config/SeckillConfigForm.vue | 133
src/components/MarkdownView/index.vue | 204
src/layout/components/UserInfo/index.ts | 3
src/views/mp/menu/components/MenuPreviewer.vue | 226
src/assets/svgs/pay/icon/wx_lite.svg | 1
src/views/erp/stock/check/components/StockCheckItemForm.vue | 289
.image/admin-uniapp/02.png | 0
src/api/mall/promotion/point/index.ts | 91
src/assets/svgs/pay/icon/alipay_pc.svg | 1
src/hooks/web/usePageLoading.ts | 18
src/api/system/mail/log/index.ts | 37
src/views/mall/product/property/value/ValueForm.vue | 105
src/views/crm/customer/poolConfig/index.vue | 136
src/api/ai/model/model/index.ts | 54
src/api/pay/app/index.ts | 68
src/components/Cropper/src/CopperModal.vue | 261
src/views/mall/product/spu/components/index.ts | 54
src/views/mall/promotion/bargain/record/BargainRecordListDialog.vue | 90
src/views/mp/material/components/VoiceTable.vue | 51
src/views/infra/apiAccessLog/ApiAccessLogDetail.vue | 79
src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 | 0
src/views/system/notice/NoticeForm.vue | 132
src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue | 181
.image/我的流程-列表.jpg | 0
.image/登录.jpg | 0
src/components/Table/src/types.ts | 26
src/views/mall/statistics/member/index.vue | 313
src/views/mall/promotion/kefu/components/asserts/yun.png | 0
src/views/infra/apiErrorLog/index.vue | 252
src/api/erp/stock/record/index.ts | 32
src/views/erp/purchase/return/components/PurchaseReturnRefundEnableList.vue | 200
src/components/Cropper/index.ts | 4
.image/admin-uniapp/08.png | 0
src/views/mp/menu/assets/iphone_backImg.png | 0
src/components/DiyEditor/components/mobile/UserWallet/property.vue | 17
src/types/form.d.ts | 44
src/components/DiyEditor/components/mobile/ImageBar/config.ts | 27
src/views/iot/thingmodel/components/index.ts | 3
src/views/mall/product/spu/form/SkuForm.vue | 194
src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue | 102
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss | 825
src/views/mp/components/wx-music/index.ts | 3
src/views/crm/customer/limitConfig/index.vue | 22
src/views/iot/thingmodel/ThingModelEvent.vue | 58
src/views/iot/thingmodel/dataSpecs/ThingModelEnumDataSpecs.vue | 160
src/api/mp/account/index.ts | 46
src/views/infra/swagger/index.vue | 28
src/api/iot/device/group/index.ts | 43
src/components/DiyEditor/components/mobile/UserOrder/property.vue | 17
src/views/iot/rule/scene/form/sections/TriggerSection.vue | 222
src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue | 129
src/views/erp/finance/payment/index.vue | 394
src/views/erp/stock/in/StockInForm.vue | 170
src/components/Tinyflow/ui/index.css | 1
src/views/mall/promotion/diy/template/DiyTemplateForm.vue | 104
src/views/mp/statistics/index.vue | 357
src/views/crm/business/detail/index.vue | 146
src/api/member/point/record/index.ts | 18
src/components/UploadFile/src/UploadImg.vue | 272
src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue | 45
src/views/crm/backlog/components/CustomerTodayContactList.vue | 180
src/components/DiyEditor/components/mobile/Popover/property.vue | 38
src/views/system/user/UserImportForm.vue | 138
src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue | 201
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue | 361
src/views/crm/contract/components/ContractProductForm.vue | 183
src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue | 479
src/views/mall/trade/order/form/OrderDeliveryForm.vue | 99
src/views/crm/contract/ContractForm.vue | 369
src/views/ai/workflow/form/BasicInfo.vue | 54
src/views/mp/draft/components/NewsForm.vue | 306
src/hooks/web/useGuide.ts | 49
src/views/system/dict/data/DictDataForm.vue | 183
src/views/iot/product/product/detail/index.vue | 76
src/components/Form/src/types.ts | 17
src/components/FormCreate/src/components/useApiSelect.tsx | 270
src/components/ShortcutDateRangePicker/index.vue | 84
src/api/erp/finance/receipt/index.ts | 61
src/components/DiyEditor/components/mobile/HotZone/property.vue | 63
src/views/Login/components/useLogin.ts | 42
src/components/Search/index.ts | 3
src/assets/imgs/iot/device.png | 0
src/assets/svgs/login-box-bg.svg | 1
src/views/ai/model/apiKey/ApiKeyForm.vue | 132
src/components/IFrame/index.ts | 3
src/views/mall/promotion/diy/template/index.vue | 220
src/views/crm/product/detail/index.vue | 66
src/views/mp/components/wx-location/main.vue | 73
src/components/Descriptions/src/Descriptions.vue | 167
src/api/mall/product/brand.ts | 61
src/views/system/notify/my/index.vue | 218
.image/common/bpm-feature.png | 0
src/views/mall/promotion/seckill/components/SeckillTableSelect.vue | 343
src/utils/jsencrypt.ts | 31
src/layout/components/LocaleDropdown/index.ts | 3
src/components/Descriptions/index.ts | 4
src/views/crm/customer/detail/CustomerDetailsHeader.vue | 43
src/views/mall/promotion/coupon/components/CouponSelect.vue | 192
src/api/mall/promotion/articleCategory/index.ts | 39
src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue | 47
src/views/erp/sale/out/SaleOutForm.vue | 343
src/views/member/user/components/UserPointUpdateForm.vue | 129
src/config/axios/service.ts | 270
src/components/DiyEditor/components/mobile/UserWallet/index.vue | 15
src/views/iot/device/device/DeviceGroupForm.vue | 90
src/views/iot/product/product/index.vue | 355
.image/支付订单.jpg | 0
src/views/mall/promotion/article/ArticleForm.vue | 225
src/components/Descriptions/src/DescriptionsItemLabel.vue | 29
src/views/ai/chat/index/components/message/MessageFiles.vue | 66
src/views/mall/trade/afterSale/index.vue | 273
src/views/pay/app/components/AppForm.vue | 138
src/components/DiyEditor/components/mobile/TitleBar/config.ts | 73
src/api/pay/wallet/rechargePackage/index.ts | 34
src/views/ai/mindmap/index/components/Left.vue | 78
src/components/Error/src/Error.vue | 58
src/views/bpm/processInstance/create/index.vue | 321
src/components/Icon/src/data.ts | 1961
src/views/mall/trade/brokerage/record/index.vue | 171
src/views/mp/hooks/useUpload.ts | 50
src/views/ai/write/manager/index.vue | 227
.image/大屏设计器-编辑.jpg | 0
src/App.vue | 57
.image/角色管理.jpg | 0
prettier.config.js | 22
src/directives/permission/hasPermi.ts | 31
src/views/system/tenantPackage/TenantPackageForm.vue | 187
src/store/modules/dict.ts | 110
src/views/erp/sale/order/components/SaleOrderReturnEnableList.vue | 212
src/views/erp/purchase/return/index.vue | 443
src/views/ai/workflow/index.vue | 193
src/views/crm/business/status/BusinessStatusForm.vue | 194
src/components/Dialog/index.ts | 3
src/views/iot/device/device/DeviceForm.vue | 325
src/views/iot/product/category/ProductCategoryForm.vue | 119
src/components/Cropper/src/types.ts | 8
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue | 222
src/views/pay/wallet/transaction/WalletTransactionList.vue | 79
.image/admin-uniapp/06.png | 0
src/views/mall/trade/order/form/OrderUpdatePriceForm.vue | 95
src/api/crm/clue/index.ts | 78
src/components/DiyEditor/components/mobile/CouponCard/component.tsx | 73
src/views/ai/chat/index/components/message/MessageWebSearch.vue | 190
src/components/Infotip/index.ts | 3
src/views/crm/product/detail/ProductDetailsInfo.vue | 38
.image/菜单管理.jpg | 0
src/assets/imgs/avatar.gif | 0
src/api/member/tag/index.ts | 36
.image/MySQL.jpg | 0
src/api/mall/trade/brokerage/withdraw/index.ts | 43
src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js | 22
src/assets/svgs/bpm/finish.svg | 1
src/views/ai/chat/manager/index.vue | 22
src/api/mall/promotion/seckill/seckillConfig.ts | 53
src/components/DeptSelectForm/index.vue | 122
src/views/Home/echarts-data.ts | 308
src/views/Login/components/SSOLogin.vue | 199
src/assets/svgs/pay/icon/wx_native.svg | 1
src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue | 125
src/views/Error/500.vue | 7
src/views/mall/statistics/product/components/ProductSummary.vue | 304
.image/敏感词.jpg | 0
.image/OA请假-列表.jpg | 0
src/views/crm/statistics/rank/components/CustomerCountRank.vue | 98
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue | 39
src/components/MagicCubeEditor/index.vue | 270
src/layout/components/Logo/index.ts | 3
src/views/report/jmreport/index.vue | 15
src/api/iot/ota/task/record/index.ts | 38
src/views/erp/product/category/ProductCategoryForm.vue | 145
src/components/Tinyflow/ui/index.d.ts | 41
src/views/mp/message/MessageTable.vue | 148
src/views/mp/menu/components/MenuEditor.vue | 244
src/views/mall/promotion/kefu/components/asserts/xinsui.png | 0
src/views/crm/statistics/rank/components/ReceivablePriceRank.vue | 106
src/views/mall/promotion/article/category/ArticleCategoryForm.vue | 122
.image/工作流设计器-bpmn.jpg | 0
src/components/DiyEditor/components/mobile/CouponCard/property.vue | 119
src/components/FormCreate/src/config/useSelectRule.ts | 37
src/views/mp/account/index.vue | 195
src/assets/svgs/pay/icon/wx_bar.svg | 1
src/api/infra/config/index.ts | 53
src/views/system/dict/DictTypeForm.vue | 124
src/views/mall/statistics/product/components/ProductRank.vue | 108
src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue | 52
src/api/member/user/index.ts | 48
src/api/bpm/processInstance/index.ts | 115
src/api/system/sms/smsChannel/index.ts | 48
src/views/ai/music/index/mode/desc.vue | 55
.image/个人中心.jpg | 0
src/views/ai/image/index/components/common/index.vue | 189
src/views/mall/product/spu/form/ProductAttributes.vue | 162
src/views/mp/components/wx-news/index.ts | 3
src/views/ai/chat/index/components/role/RoleList.vue | 106
src/views/mp/draft/components/CoverSelect.vue | 166
src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue | 42
src/views/mall/promotion/coupon/formatter.ts | 59
src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue | 85
src/views/iot/rule/scene/form/RuleSceneForm.vue | 330
src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue | 184
src/views/mall/trade/delivery/express/index.vue | 189
src/views/crm/statistics/performance/components/ReceivablePricePerformance.vue | 236
.image/定时任务.jpg | 0
src/views/infra/codegen/EditTable.vue | 87
.image/任务列表-已办.jpg | 0
src/styles/global.module.scss | 6
src/assets/svgs/404.svg | 1
src/layout/components/UserInfo/src/components/LockPage.vue | 270
src/views/Login/components/QrCodeForm.vue | 30
src/assets/imgs/wechat.png | 0
src/assets/svgs/pay/icon/wx_pub.svg | 2
src/views/ai/write/index/components/Right.vue | 120
src/components/DiyEditor/components/mobile/Divider/config.ts | 29
src/types/components.d.ts | 56
src/views/ai/utils/utils.ts | 13
src/components/bpmnProcessDesigner/package/theme/process-panel.scss | 107
src/views/member/tag/components/MemberTagSelect.vue | 68
src/components/DiyEditor/components/ComponentLibrary.vue | 211
src/api/system/sms/smsTemplate/index.ts | 65
src/views/Login/SocialLogin.vue | 347
src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue | 532
src/components/Qrcode/src/Qrcode.vue | 253
src/views/pay/order/OrderDetail.vue | 113
types/global.d.ts | 58
src/api/member/level/index.ts | 42
src/views/crm/contact/detail/index.vue | 121
src/components/Verifition/src/Verify/VerifyPoints.vue | 250
src/api/ai/mindmap/index.ts | 60
src/api/mp/draft/index.ts | 35
src/views/crm/receivable/plan/components/ReceivablePlanList.vue | 173
src/components/AppLinkInput/index.vue | 43
src/api/ai/model/apiKey/index.ts | 44
src/api/member/signin/config/index.ts | 34
src/views/pay/transfer/index.vue | 283
src/views/mall/promotion/coupon/components/CouponSendForm.vue | 162
src/views/erp/purchase/in/index.vue | 443
src/views/iot/ota/task/OtaTaskList.vue | 187
src/components/DiyEditor/components/mobile/ImageBar/index.vue | 24
src/components/DiyEditor/components/mobile/MenuSwiper/property.vue | 76
src/components/Highlight/index.ts | 3
src/layout/components/Screenfull/index.ts | 3
src/api/crm/product/index.ts | 49
src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue | 106
src/views/crm/receivable/index.vue | 335
src/assets/svgs/500.svg | 1
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue | 224
src/components/SimpleProcessDesignerV2/src/index.ts | 5
src/views/mp/user/index.vue | 225
src/plugins/svgIcon/index.ts | 3
src/views/mp/material/index.vue | 159
src/styles/theme.scss | 6
src/api/crm/operateLog/index.ts | 11
.env.dev | 37
src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue | 201
src/views/erp/purchase/supplier/index.vue | 201
src/views/erp/stock/move/index.vue | 359
src/components/DiyEditor/components/mobile/NoticeBar/property.vue | 46
src/views/mall/product/spu/form/index.vue | 204
src/components/DiyEditor/components/mobile/PromotionArticle/property.vue | 56
src/views/mall/promotion/kefu/components/asserts/feiwen.png | 0
package-lock.json | 17553 ++++
.image/大屏设计器-预览.jpg | 0
src/views/mall/product/spu/components/SkuTableSelect.vue | 95
src/views/iot/rule/data/rule/index.vue | 196
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue | 308
src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue | 208
src/views/iot/utils/constants.ts | 543
src/views/crm/statistics/customer/index.vue | 214
src/views/bpm/processInstance/report/index.vue | 274
.image/任务列表-审批.jpg | 0
src/api/infra/fileConfig/index.ts | 69
src/views/crm/customer/pool/CustomerDistributeForm.vue | 85
src/views/system/dept/index.vue | 220
src/assets/ai/dall2.jpg | 0
.image/在线用户.jpg | 0
src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue | 196
src/views/mp/components/wx-msg/main.vue | 192
src/views/ai/model/tool/ToolForm.vue | 112
src/api/infra/demo/demo02/index.ts | 37
.prettierignore | 11
src/components/DiyEditor/components/mobile/NoticeBar/index.vue | 26
src/views/bpm/model/form/PrintTemplate/index.ts | 9
src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue | 198
src/views/bpm/processInstance/manager/index.vue | 259
src/views/infra/demo/demo03/normal/Demo03StudentForm.vue | 156
src/api/login/index.ts | 91
src/api/mall/trade/afterSale/index.ts | 75
src/components/DiyEditor/index.vue | 604
src/components/Tinyflow/ui/index.umd.js | 9
src/views/erp/finance/payment/components/FinancePaymentItemForm.vue | 182
.image/报表设计器-图形报表.jpg | 0
src/components/Tinyflow/ui/index.js | 16984 ++++
src/components/JsonEditor/index.ts | 3
src/views/mp/components/wx-msg/comment.scss | 126
.image/demo/vue3-ep.png | 0
src/components/Pagination/index.vue | 87
src/layout/components/UserInfo/src/UserInfo.vue | 113
src/views/mall/home/index.vue | 113
src/api/mall/promotion/bargain/bargainRecord.ts | 19
src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue | 468
src/views/mp/autoReply/components/ReplyForm.vue | 80
stylelint.config.js | 235
src/config/axios/errorCode.ts | 6
src/views/bpm/oa/leave/detail.vue | 51
src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js | 233
src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue | 70
src/api/crm/statistics/performance.ts | 33
src/views/bpm/model/form/FormDesign.vue | 129
src/api/mall/promotion/diy/template.ts | 58
src/views/mp/components/wx-msg/components/MsgEvent.vue | 52
src/types/descriptions.d.ts | 14
src/views/mall/trade/brokerage/withdraw/index.vue | 309
src/utils/download.ts | 100
.image/OA请假-发起.jpg | 0
src/views/mall/trade/brokerage/user/index.vue | 331
src/components/XButton/src/XButton.vue | 50
src/views/bpm/task/todo/index.vue | 236
src/layout/components/ContextMenu/index.ts | 10
src/api/mall/promotion/bargain/bargainHelp.ts | 14
src/views/pay/app/components/channel/WeixinChannelForm.vue | 377
src/api/erp/stock/move/index.ts | 61
src/types/theme.d.ts | 16
src/views/mall/product/spu/index.vue | 457
src/views/mp/components/wx-video-play/main.vue | 73
src/views/mall/product/property/value/index.vue | 163
src/views/mp/components/wx-msg/components/MsgList.vue | 62
src/utils/dateUtil.ts | 18
src/views/erp/finance/receipt/components/FinanceReceiptItemForm.vue | 176
src/views/mall/home/components/MemberStatisticsCard.vue | 91
src/views/system/operatelog/index.vue | 213
src/views/mall/promotion/kefu/components/asserts/liulei.png | 0
src/views/system/mail/template/MailTemplateForm.vue | 147
src/components/FormCreate/src/useFormCreateDesigner.ts | 165
src/views/system/dict/data/index.vue | 245
src/views/system/operatelog/OperateLogDetail.vue | 68
src/views/infra/demo/demo01/index.vue | 253
src/views/iot/home/components/DeviceCountCard.vue | 131
src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue | 88
src/views/mall/promotion/combination/activity/CombinationActivityForm.vue | 187
src/views/pay/demo/order/index.vue | 240
src/api/infra/redis/index.ts | 8
src/views/system/tenantPackage/index.vue | 210
src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js | 39
src/views/mp/components/wx-account-select/index.ts | 3
src/views/mall/product/brand/index.vue | 182
src/views/Login/Login.vue | 121
src/api/crm/receivable/index.ts | 73
src/assets/svgs/pay/icon/alipay_qr.svg | 2
src/components/Form/src/Form.vue | 307
src/components/Card/index.ts | 3
src/views/mall/promotion/kefu/components/message/ProductItem.vue | 116
src/api/system/permission/index.ts | 42
src/views/mall/promotion/kefu/components/asserts/jingkong.png | 0
src/views/Profile/Index.vue | 67
src/views/system/menu/index.vue | 318
src/views/bpm/processExpression/index.vue | 182
src/components/DiyEditor/components/mobile/MenuGrid/config.ts | 79
src/views/ai/chat/index/components/message/MessageLoading.vue | 6
src/utils/cron.ts | 471
src/hooks/web/useTable.ts | 223
src/store/modules/tagsView.ts | 183
src/layout/components/Breadcrumb/index.ts | 3
src/views/Home/Index.vue | 422
src/views/mp/user/UserForm.vue | 102
src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js | 24
src/api/crm/customer/limitConfig/index.ts | 49
src/views/crm/statistics/rank/index.vue | 163
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue | 357
src/views/mall/trade/delivery/express/ExpressForm.vue | 126
src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue | 164
.image/报表设计器-打印设计.jpg | 0
src/components/DiyEditor/components/mobile/VideoPlayer/config.ts | 37
src/store/modules/app.ts | 322
src/views/crm/backlog/index.vue | 177
src/views/crm/receivable/plan/index.vue | 335
src/views/mall/product/spu/components/SkuList.vue | 583
src/api/erp/purchase/in/index.ts | 64
src/hooks/web/useForm.ts | 94
pnpm-lock.yaml | 10576 ++
src/components/Form/src/componentMap.ts | 55
src/views/crm/product/detail/ProductDetailsHeader.vue | 46
src/api/crm/statistics/funnel.ts | 58
src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue | 264
src/views/iot/rule/data/sink/config/RabbitMQConfigForm.vue | 63
.image/岗位管理.jpg | 0
src/views/mall/promotion/article/category/index.vue | 199
src/views/mall/promotion/components/SpuSelect.vue | 324
.editorconfig | 12
src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue | 163
src/api/mall/trade/config/index.ts | 23
src/components/SimpleProcessDesignerV2/theme/iconfont.woff | 0
src/views/bpm/model/definition/index.vue | 174
src/components/DiyEditor/components/mobile/MagicCube/config.ts | 49
src/components/UploadFile/src/UploadImgs.vue | 329
src/views/crm/contact/components/ContactListModal.vue | 160
src/views/ai/image/index/components/midjourney/index.vue | 236
src/views/crm/statistics/customer/components/CustomerPoolSummary.vue | 154
src/components/bpmnProcessDesigner/package/designer/index.ts | 8
src/views/infra/fileConfig/index.vue | 247
src/components/Verifition/index.ts | 3
src/views/infra/config/index.vue | 257
src/layout/components/ToolHeader.vue | 103
src/views/erp/sale/order/index.vue | 407
.image/common/crm-feature.png | 0
src/assets/svgs/bpm/approve.svg | 1
src/views/crm/clue/detail/ClueDetailsInfo.vue | 72
.image/流程表单.jpg | 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue | 237
src/components/InputPassword/src/InputPassword.vue | 152
src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue | 396
src/views/crm/customer/pool/index.vue | 270
.image/任务日志.jpg | 0
src/views/ai/image/manager/index.vue | 253
src/views/crm/customer/detail/index.vue | 230
src/api/mall/promotion/seckill/seckillActivity.ts | 75
src/views/erp/finance/account/AccountForm.vue | 124
src/views/mp/draft/editor-config.ts | 75
.image/用户分组.jpg | 0
src/api/mp/messageTemplate/index.ts | 49
src/plugins/vueI18n/index.ts | 42
src/views/erp/sale/out/components/SaleOutReceiptEnableList.vue | 199
src/layout/components/Footer/src/Footer.vue | 27
src/api/mall/promotion/kefu/conversation/index.ts | 39
src/views/mp/material/components/UploadFile.vue | 77
src/api/crm/contract/index.ts | 114
src/views/crm/followup/FollowUpRecordForm.vue | 188
src/views/mall/product/spu/form/ProductPropertyAddForm.vue | 148
src/views/mall/promotion/coupon/template/CouponTemplateForm.vue | 404
src/views/erp/stock/out/components/StockOutItemForm.vue | 267
src/views/iot/rule/scene/form/selectors/ProductSelector.vue | 79
src/components/FormCreate/src/utils/index.ts | 61
src/views/ai/chat/index/components/message/MessageList.vue | 226
src/views/mall/trade/order/index.vue | 357
src/api/infra/dataSourceConfig/index.ts | 40
src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue | 70
src/views/mall/promotion/kefu/components/asserts/picture.svg | 10
src/views/system/tenant/index.vue | 304
src/api/member/address/index.ts | 15
src/assets/svgs/icon.svg | 1
src/components/Error/index.ts | 3
src/api/system/social/user/index.ts | 29
src/views/mall/promotion/seckill/config/index.vue | 211
src/api/erp/product/category/index.ts | 49
src/assets/svgs/pay/icon/wx_app.svg | 2
src/utils/color.ts | 217
src/assets/ai/qingxi.jpg | 0
src/views/member/user/detail/UserAddressList.vue | 54
src/layout/components/useRenderLayout.tsx | 294
src/views/Login/components/MobileForm.vue | 226
src/views/crm/receivable/plan/detail/index.vue | 103
src/components/DiyEditor/components/mobile/Carousel/index.vue | 43
src/components/Verifition/src/Verify/VerifyPictureWord.vue | 196
src/layout/components/Setting/index.ts | 3
src/api/crm/customer/index.ts | 132
src/views/infra/server/index.vue | 30
src/api/mall/product/favorite.ts | 12
src/api/ai/knowledge/segment/index.ts | 75
src/views/iot/rule/data/sink/DataSinkForm.vue | 188
.image/租户套餐.png | 0
src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue | 251
.image/common/infra-feature.png | 0
src/views/mall/promotion/kefu/components/asserts/dajing.png | 0
src/api/mall/promotion/discount/discountActivity.ts | 60
src/components/XButton/index.ts | 4
src/views/infra/codegen/components/BasicInfoForm.vue | 87
src/views/mall/trade/order/form/OrderPickUpForm.vue | 116
.image/admin-uniapp/01.png | 0
src/layout/components/Menu/src/components/useRenderMenuTitle.tsx | 27
src/assets/imgs/logo.png | 0
src/views/bpm/simple/SimpleModelDesign.vue | 39
src/components/DiyEditor/components/mobile/NavigationBar/property.vue | 91
src/views/bpm/model/form/PrintTemplate/module/menu/ProcessRecordMenu.ts | 42
src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue | 72
src/components/ContentWrap/index.ts | 3
src/components/DiyEditor/components/mobile/SearchBar/index.vue | 75
src/views/iot/device/device/detail/DeviceDetailConfig.vue | 134
src/views/mp/components/wx-video-play/index.ts | 3
src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue | 183
src/components/DiyEditor/components/mobile/MenuGrid/property.vue | 65
src/components/DiyEditor/components/mobile/TabBar/index.vue | 66
src/views/mp/autoReply/components/ReplyTable.vue | 115
src/views/mall/trade/order/form/OrderUpdateAddressForm.vue | 98
src/views/ai/music/index/list/audioBar/index.vue | 70
.image/common/mall-feature.png | 0
src/views/erp/product/category/index.vue | 218
src/components/UploadFile/src/useUpload.ts | 102
src/views/mall/promotion/kefu/components/asserts/bizui.png | 0
src/utils/domUtils.ts | 289
src/api/pay/notify/index.ts | 16
src/views/crm/receivable/ReceivableForm.vue | 294
src/views/erp/sale/order/components/SaleOrderOutEnableList.vue | 206
src/components/ImageViewer/src/types.ts | 9
src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue | 97
src/views/mall/promotion/banner/index.vue | 206
src/components/DiyEditor/components/mobile/ProductList/index.vue | 132
src/views/ai/write/index/index.vue | 78
src/utils/formRules.ts | 7
src/assets/audio/response.mp3 | 0
src/api/bpm/leave/index.ts | 27
src/components/OperateLogV2/index.ts | 3
src/views/mall/promotion/kefu/components/asserts/nanguo.png | 0
src/api/iot/ota/task/index.ts | 38
.image/Java监控.jpg | 0
src/views/iot/device/device/detail/DeviceDetailsHeader.vue | 74
src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue | 519
src/views/mp/account/AccountForm.vue | 160
tsconfig.json | 43
src/api/mall/product/spu.ts | 111
src/utils/dict.ts | 251
src/views/member/user/UserForm.vue | 179
src/components/ConfigGlobal/index.ts | 3
src/components/DiyEditor/components/mobile/MenuList/property.vue | 45
src/views/member/user/detail/UserCouponList.vue | 190
src/api/iot/statistics/index.ts | 60
src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue | 97
src/api/crm/business/status/index.ts | 68
src/views/iot/home/index.vue | 110
src/views/system/social/user/index.vue | 187
src/api/login/types.ts | 38
src/views/infra/codegen/PreviewCode.vue | 222
src/views/mall/promotion/kefu/components/asserts/haochi.png | 0
src/views/mp/components/wx-reply/main.vue | 208
src/components/FormCreate/src/config/useUploadFileRule.ts | 80
src/views/ai/knowledge/knowledge/retrieval/index.vue | 163
src/components/ImageViewer/index.ts | 33
src/views/bpm/model/form/PrintTemplate/MentionModal.vue | 110
src/layout/components/Footer/index.ts | 3
src/views/crm/contract/detail/ContractDetailsInfo.vue | 76
src/api/system/oauth2/token.ts | 22
src/types/elementPlus.d.ts | 3
src/api/mp/freePublish/index.ts | 23
src/views/ai/write/index/components/Tag.vue | 31
src/components/DiyEditor/components/mobile/Popover/config.ts | 26
src/views/ai/image/index/components/ImageDetail.vue | 187
src/views/mall/product/property/PropertyForm.vue | 96
src/views/member/user/detail/UserSignList.vue | 135
.env.local | 36
src/views/Profile/components/BasicInfo.vue | 121
src/api/mall/trade/delivery/expressTemplate/index.ts | 54
src/assets/svgs/bpm/transactor.svg | 1
src/views/infra/demo/demo03/inner/index.vue | 263
src/views/infra/job/logger/JobLogDetail.vue | 59
src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue | 128
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue | 244
src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue | 101
src/views/crm/statistics/performance/index.vue | 146
src/layout/components/Collapse/index.ts | 3
src/components/bpmnProcessDesigner/package/theme/index.scss | 117
src/views/crm/permission/components/PermissionList.vue | 206
src/views/erp/purchase/order/components/PurchaseOrderItemForm.vue | 276
src/assets/svgs/pay/icon/alipay_app.svg | 1
src/views/iot/device/device/components/DeviceTableSelect.vue | 303
src/api/infra/demo/demo03/erp/index.ts | 127
src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue | 1068
src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue | 44
src/views/mall/promotion/kefu/components/message/MessageItem.vue | 24
src/layout/components/LocaleDropdown/src/LocaleDropdown.vue | 52
src/assets/ai/ziran.jpg | 0
src/views/iot/rule/scene/form/selectors/PropertySelector.vue | 437
src/views/mall/promotion/kefu/components/asserts/jingshu.png | 0
src/components/DiyEditor/components/mobile/UserCard/config.ts | 21
src/views/bpm/model/index.vue | 228
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue | 240
src/views/system/role/RoleAssignMenuForm.vue | 153
src/api/pay/refund/index.ts | 116
src/views/system/mail/account/MailAccountForm.vue | 159
src/views/infra/demo/demo03/inner/Demo03StudentForm.vue | 156
src/assets/svgs/peoples.svg | 1
src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue | 153
src/views/ai/music/index/list/index.vue | 108
src/views/iot/rule/data/index.vue | 20
src/components/DiyEditor/components/mobile/CouponCard/index.vue | 149
src/components/bpmnProcessDesigner/package/penal/listeners/template.js | 178
src/components/SimpleProcessDesignerV2/src/utils.ts | 41
src/layout/components/TagsView/index.ts | 3
src/views/member/group/GroupForm.vue | 112
src/views/member/user/detail/index.vue | 160
.eslintignore | 8
src/api/iot/rule/scene/index.ts | 87
src/components/FormCreate/src/components/DictSelect.vue | 59
src/assets/svgs/bpm/delay.svg | 1
.image/租户管理.jpg | 0
src/views/mall/promotion/kefu/components/asserts/xiong.png | 0
src/api/iot/product/product/index.ts | 86
src/views/infra/codegen/components/GenerateInfoForm.vue | 385
.image/文件配置.jpg | 0
src/api/bpm/definition/index.ts | 28
src/assets/svgs/bpm/starter.svg | 1
src/views/erp/stock/warehouse/index.vue | 242
src/api/mall/statistics/trade.ts | 119
src/views/crm/contact/components/ContactList.vue | 185
src/views/mall/promotion/point/activity/index.vue | 218
src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js | 10
src/views/system/post/PostForm.vue | 125
src/views/system/post/index.vue | 232
src/api/mall/statistics/member.ts | 123
src/hooks/web/useIcon.ts | 8
src/hooks/web/useMessage.ts | 95
.image/报表设计器-数据报表.jpg | 0
src/components/IFrame/src/IFrame.vue | 47
src/views/mp/components/wx-news/main.vue | 119
src/views/crm/contract/detail/ContractDetailsHeader.vue | 45
src/views/iot/device/group/DeviceGroupForm.vue | 112
src/views/member/user/detail/UserAccountInfo.vue | 84
src/views/mall/trade/brokerage/user/BrokerageUserUpdateForm.vue | 127
src/views/member/signin/record/index.vue | 134
src/hooks/web/useLocale.ts | 35
src/views/system/sms/channel/SmsChannelForm.vue | 144
src/views/bpm/group/index.vue | 191
src/components/XButton/src/XTextButton.vue | 49
src/components/bpmnProcessDesigner/src/modules/rules/index.js | 6
src/views/erp/product/unit/index.vue | 198
build/vite/optimize.ts | 124
src/api/system/mail/account/index.ts | 47
src/components/DiyEditor/components/mobile/Divider/property.vue | 80
.image/访问日志.jpg | 0
src/views/ai/image/index/components/stableDiffusion/index.vue | 257
src/views/mall/promotion/kefu/components/tools/emoji.ts | 129
src/components/SummaryCard/index.vue | 52
src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue | 376
src/views/ai/model/chatRole/index.vue | 201
src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue | 122
src/views/system/mail/log/MailLogDetail.vue | 99
src/store/index.ts | 12
src/views/system/dept/DeptForm.vue | 172
src/views/mall/product/category/index.vue | 167
src/views/mall/promotion/discountActivity/index.vue | 239
src/views/mall/promotion/combination/record/index.vue | 276
1,700 files changed, 253,104 insertions(+), 1 deletions(-)
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..79a12ff
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+[*.{js,ts,vue}]
+charset = utf-8 # 璁剧疆鏂囦欢瀛楃闆嗕负 utf-8
+end_of_line = lf # 鎺у埗鎹㈣绫诲瀷(lf | cr | crlf)
+insert_final_newline = true # 濮嬬粓鍦ㄦ枃浠舵湯灏炬彃鍏ヤ竴涓柊琛�
+indent_style = space # 缂╄繘椋庢牸锛坱ab | space锛�
+indent_size = 2 # 缂╄繘澶у皬
+max_line_length = 100 # 鏈�澶ц闀垮害
+
+[*.md] # 浠� md 鏂囦欢閫傜敤浠ヤ笅瑙勫垯
+max_line_length = off # 鍏抽棴鏈�澶ц闀垮害闄愬埗
+trim_trailing_whitespace = false # 鍏抽棴鏈熬绌烘牸淇壀
diff --git a/.env b/.env
new file mode 100644
index 0000000..0f9c97e
--- /dev/null
+++ b/.env
@@ -0,0 +1,37 @@
+# 鏍囬
+VITE_APP_TITLE=鑺嬮亾绠$悊绯荤粺
+
+# 椤圭洰鏈湴杩愯绔彛鍙�
+VITE_PORT=80
+
+# open 杩愯 npm run dev 鏃惰嚜鍔ㄦ墦寮�娴忚鍣�
+VITE_OPEN=true
+
+# 绉熸埛寮�鍏�
+VITE_APP_TENANT_ENABLE=true
+
+# 楠岃瘉鐮佺殑寮�鍏�
+VITE_APP_CAPTCHA_ENABLE=true
+
+# 鏂囨。鍦板潃鐨勫紑鍏�
+VITE_APP_DOCALERT_ENABLE=true
+
+# 鐧惧害缁熻
+VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
+
+# 榛樿璐︽埛瀵嗙爜
+VITE_APP_DEFAULT_LOGIN_TENANT = 鑺嬮亾婧愮爜
+VITE_APP_DEFAULT_LOGIN_USERNAME = admin
+VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
+
+# API 鍔犺В瀵�
+VITE_APP_API_ENCRYPT_ENABLE = true
+VITE_APP_API_ENCRYPT_HEADER = X-Api-Encrypt
+VITE_APP_API_ENCRYPT_ALGORITHM = AES
+VITE_APP_API_ENCRYPT_REQUEST_KEY = 52549111389893486934626385991395
+VITE_APP_API_ENCRYPT_RESPONSE_KEY = 96103715984234343991809655248883
+# VITE_APP_API_ENCRYPT_REQUEST_KEY = MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCls2rIpnGdYnLFgz1XU13GbNQ5DloyPpvW00FPGjqn5Z6JpK+kDtVlnkhwR87iRrE5Vf2WNqRX6vzbLSgveIQY8e8oqGCb829myjf1MuI+ZzN4ghf/7tEYhZJGPI9AbfxFqBUzm+kR3/HByAI22GLT96WM26QiMK8n3tIP/yiLswIDAQAB
+# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
+
+# 鐧惧害鍦板浘
+VITE_BAIDU_MAP_KEY = 'efHIw2qmH8RzHPxK0z0rbCgzDVLup9LD'
\ No newline at end of file
diff --git a/.env.dev b/.env.dev
new file mode 100644
index 0000000..2c11376
--- /dev/null
+++ b/.env.dev
@@ -0,0 +1,37 @@
+# 寮�鍙戠幆澧冿細鏈湴鍙惎鍔ㄥ墠绔」鐩紝渚濊禆寮�鍙戠幆澧冿紙鍚庣銆丄PP锛�
+NODE_ENV=production
+
+VITE_DEV=true
+
+# 璇锋眰璺緞
+VITE_BASE_URL='http://101.43.143.75:48080'
+
+# 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
+VITE_UPLOAD_TYPE=server
+
+# 鎺ュ彛鍦板潃
+VITE_API_URL=/admin-api
+
+# 鏄惁鍒犻櫎debugger
+VITE_DROP_DEBUGGER=false
+
+# 鏄惁鍒犻櫎console.log
+VITE_DROP_CONSOLE=false
+
+# 鏄惁sourcemap
+VITE_SOURCEMAP=true
+
+# 鎵撳寘璺緞
+VITE_BASE_PATH=/
+
+# 杈撳嚭璺緞
+VITE_OUT_DIR=dist
+
+# 鍟嗗煄H5浼氬憳绔煙鍚�
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# 楠岃瘉鐮佺殑寮�鍏�
+VITE_APP_CAPTCHA_ENABLE=true
+
+# GoView鍩熷悕
+VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
diff --git a/.env.local b/.env.local
new file mode 100644
index 0000000..730e61f
--- /dev/null
+++ b/.env.local
@@ -0,0 +1,36 @@
+# 鏈湴寮�鍙戠幆澧冿細鏈湴鍚姩鎵�鏈夐」鐩紙鍓嶇銆佸悗绔�丄PP锛夋椂浣跨敤锛屼笉渚濊禆澶栭儴鐜
+NODE_ENV=development
+
+VITE_DEV=true
+
+# 璇锋眰璺緞
+VITE_BASE_URL='http://101.43.143.75:48080'
+# VITE_BASE_URL='https://gdkw.qxueyou.com/'
+
+
+# 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔 S3 鏈嶅姟
+VITE_UPLOAD_TYPE=server
+
+# 鎺ュ彛鍦板潃
+VITE_API_URL=/admin-api
+
+# 鏄惁鍒犻櫎debugger
+VITE_DROP_DEBUGGER=false
+
+# 鏄惁鍒犻櫎console.log
+VITE_DROP_CONSOLE=false
+
+# 鏄惁sourcemap
+VITE_SOURCEMAP=false
+
+# 鎵撳寘璺緞
+VITE_BASE_PATH=/
+
+# 鍟嗗煄H5浼氬憳绔煙鍚�
+VITE_MALL_H5_DOMAIN='http://localhost:3000'
+
+# 楠岃瘉鐮佺殑寮�鍏�
+VITE_APP_CAPTCHA_ENABLE=false
+
+# GoView鍩熷悕
+VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
diff --git a/.env.prod b/.env.prod
new file mode 100644
index 0000000..ca7cb8e
--- /dev/null
+++ b/.env.prod
@@ -0,0 +1,34 @@
+# 鐢熶骇鐜锛氬彧鍦ㄦ墦鍖呮椂浣跨敤
+NODE_ENV=production
+
+VITE_DEV=false
+
+# 璇锋眰璺緞
+VITE_BASE_URL='http://localhost:48080'
+
+# 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
+VITE_UPLOAD_TYPE=server
+
+# 鎺ュ彛鍦板潃
+VITE_API_URL=/admin-api
+
+# 鏄惁鍒犻櫎debugger
+VITE_DROP_DEBUGGER=true
+
+# 鏄惁鍒犻櫎console.log
+VITE_DROP_CONSOLE=true
+
+# 鏄惁sourcemap
+VITE_SOURCEMAP=false
+
+# 鎵撳寘璺緞
+VITE_BASE_PATH=/
+
+# 杈撳嚭璺緞
+VITE_OUT_DIR=dist-prod
+
+# 鍟嗗煄H5浼氬憳绔煙鍚�
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# GoView鍩熷悕
+VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
diff --git a/.env.stage b/.env.stage
new file mode 100644
index 0000000..084337c
--- /dev/null
+++ b/.env.stage
@@ -0,0 +1,34 @@
+# 棰勫彂甯冪幆澧冿細鍙湪鎵撳寘鏃朵娇鐢�
+NODE_ENV=production
+
+VITE_DEV=false
+
+# 璇锋眰璺緞
+VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
+
+# 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
+VITE_UPLOAD_TYPE=server
+
+# 鎺ュ彛鍦板潃
+VITE_API_URL=/admin-api
+
+# 鏄惁鍒犻櫎debugger
+VITE_DROP_DEBUGGER=true
+
+# 鏄惁鍒犻櫎console.log
+VITE_DROP_CONSOLE=true
+
+# 鏄惁sourcemap
+VITE_SOURCEMAP=false
+
+# 鎵撳寘璺緞
+VITE_BASE_PATH='http://static-vue3.yudao.iocoder.cn/'
+
+# 杈撳嚭璺緞
+VITE_OUT_DIR=dist-stage
+
+# 鍟嗗煄H5浼氬憳绔煙鍚�
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# GoView鍩熷悕
+VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
diff --git a/.env.test b/.env.test
new file mode 100644
index 0000000..2252e14
--- /dev/null
+++ b/.env.test
@@ -0,0 +1,34 @@
+# 娴嬭瘯鐜锛氬彧鍦ㄦ墦鍖呮椂浣跨敤
+NODE_ENV=production
+
+VITE_DEV=false
+
+# 璇锋眰璺緞
+VITE_BASE_URL='http://localhost:48080'
+
+# 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
+VITE_UPLOAD_TYPE=server
+
+# 鎺ュ彛鍦板潃
+VITE_API_URL=/admin-api
+
+# 鏄惁鍒犻櫎debugger
+VITE_DROP_DEBUGGER=true
+
+# 鏄惁鍒犻櫎console.log
+VITE_DROP_CONSOLE=true
+
+# 鏄惁sourcemap
+VITE_SOURCEMAP=false
+
+# 鎵撳寘璺緞
+VITE_BASE_PATH=/admin-ui-vue3/
+
+# 杈撳嚭璺緞
+VITE_OUT_DIR=dist-test
+
+# 鍟嗗煄H5浼氬憳绔煙鍚�
+VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
+
+# GoView鍩熷悕
+VITE_GOVIEW_URL='http://127.0.0.1:3000'
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..1e85c0f
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,8 @@
+/build/
+/config/
+/dist/
+/*.js
+/test/unit/coverage/
+/node_modules/*
+/dist*
+/src/main.ts
diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json
new file mode 100644
index 0000000..024c96a
--- /dev/null
+++ b/.eslintrc-auto-import.json
@@ -0,0 +1,259 @@
+{
+ "globals": {
+ "EffectScope": true,
+ "ElMessage": true,
+ "ElMessageBox": true,
+ "ElTag": true,
+ "asyncComputed": true,
+ "autoResetRef": true,
+ "computed": true,
+ "computedAsync": true,
+ "computedEager": true,
+ "computedInject": true,
+ "computedWithControl": true,
+ "controlledComputed": true,
+ "controlledRef": true,
+ "createApp": true,
+ "createEventHook": true,
+ "createGlobalState": true,
+ "createInjectionState": true,
+ "createReactiveFn": true,
+ "createSharedComposable": true,
+ "createUnrefFn": true,
+ "customRef": true,
+ "debouncedRef": true,
+ "debouncedWatch": true,
+ "defineAsyncComponent": true,
+ "defineComponent": true,
+ "eagerComputed": true,
+ "effectScope": true,
+ "extendRef": true,
+ "getCurrentInstance": true,
+ "getCurrentScope": true,
+ "h": true,
+ "ignorableWatch": true,
+ "inject": true,
+ "isDefined": true,
+ "isProxy": true,
+ "isReactive": true,
+ "isReadonly": true,
+ "isRef": true,
+ "makeDestructurable": true,
+ "markRaw": true,
+ "nextTick": true,
+ "onActivated": true,
+ "onBeforeMount": true,
+ "onBeforeUnmount": true,
+ "onBeforeUpdate": true,
+ "onClickOutside": true,
+ "onDeactivated": true,
+ "onErrorCaptured": true,
+ "onKeyStroke": true,
+ "onLongPress": true,
+ "onMounted": true,
+ "onRenderTracked": true,
+ "onRenderTriggered": true,
+ "onScopeDispose": true,
+ "onServerPrefetch": true,
+ "onStartTyping": true,
+ "onUnmounted": true,
+ "onUpdated": true,
+ "pausableWatch": true,
+ "provide": true,
+ "reactify": true,
+ "reactifyObject": true,
+ "reactive": true,
+ "reactiveComputed": true,
+ "reactiveOmit": true,
+ "reactivePick": true,
+ "readonly": true,
+ "ref": true,
+ "refAutoReset": true,
+ "refDebounced": true,
+ "refDefault": true,
+ "refThrottled": true,
+ "refWithControl": true,
+ "resolveComponent": true,
+ "resolveRef": true,
+ "resolveUnref": true,
+ "shallowReactive": true,
+ "shallowReadonly": true,
+ "shallowRef": true,
+ "syncRef": true,
+ "syncRefs": true,
+ "templateRef": true,
+ "throttledRef": true,
+ "throttledWatch": true,
+ "toRaw": true,
+ "toReactive": true,
+ "toRef": true,
+ "toRefs": true,
+ "triggerRef": true,
+ "tryOnBeforeMount": true,
+ "tryOnBeforeUnmount": true,
+ "tryOnMounted": true,
+ "tryOnScopeDispose": true,
+ "tryOnUnmounted": true,
+ "unref": true,
+ "unrefElement": true,
+ "until": true,
+ "useActiveElement": true,
+ "useArrayEvery": true,
+ "useArrayFilter": true,
+ "useArrayFind": true,
+ "useArrayFindIndex": true,
+ "useArrayJoin": true,
+ "useArrayMap": true,
+ "useArrayReduce": true,
+ "useArraySome": true,
+ "useAsyncQueue": true,
+ "useAsyncState": true,
+ "useAttrs": true,
+ "useBase64": true,
+ "useBattery": true,
+ "useBluetooth": true,
+ "useBreakpoints": true,
+ "useBroadcastChannel": true,
+ "useBrowserLocation": true,
+ "useCached": true,
+ "useClipboard": true,
+ "useColorMode": true,
+ "useConfirmDialog": true,
+ "useCounter": true,
+ "useCssModule": true,
+ "useCssVar": true,
+ "useCssVars": true,
+ "useCurrentElement": true,
+ "useCycleList": true,
+ "useDark": true,
+ "useDateFormat": true,
+ "useDebounce": true,
+ "useDebounceFn": true,
+ "useDebouncedRefHistory": true,
+ "useDeviceMotion": true,
+ "useDeviceOrientation": true,
+ "useDevicePixelRatio": true,
+ "useDevicesList": true,
+ "useDisplayMedia": true,
+ "useDocumentVisibility": true,
+ "useDraggable": true,
+ "useDropZone": true,
+ "useElementBounding": true,
+ "useElementByPoint": true,
+ "useElementHover": true,
+ "useElementSize": true,
+ "useElementVisibility": true,
+ "useEventBus": true,
+ "useEventListener": true,
+ "useEventSource": true,
+ "useEyeDropper": true,
+ "useFavicon": true,
+ "useFetch": true,
+ "useFileDialog": true,
+ "useFileSystemAccess": true,
+ "useFocus": true,
+ "useFocusWithin": true,
+ "useFps": true,
+ "useFullscreen": true,
+ "useGamepad": true,
+ "useGeolocation": true,
+ "useIdle": true,
+ "useImage": true,
+ "useInfiniteScroll": true,
+ "useIntersectionObserver": true,
+ "useInterval": true,
+ "useIntervalFn": true,
+ "useKeyModifier": true,
+ "useLastChanged": true,
+ "useLocalStorage": true,
+ "useMagicKeys": true,
+ "useManualRefHistory": true,
+ "useMediaControls": true,
+ "useMediaQuery": true,
+ "useMemoize": true,
+ "useMemory": true,
+ "useMounted": true,
+ "useMouse": true,
+ "useMouseInElement": true,
+ "useMousePressed": true,
+ "useMutationObserver": true,
+ "useNavigatorLanguage": true,
+ "useNetwork": true,
+ "useNow": true,
+ "useObjectUrl": true,
+ "useOffsetPagination": true,
+ "useOnline": true,
+ "usePageLeave": true,
+ "useParallax": true,
+ "usePermission": true,
+ "usePointer": true,
+ "usePointerSwipe": true,
+ "usePreferredColorScheme": true,
+ "usePreferredDark": true,
+ "usePreferredLanguages": true,
+ "useRafFn": true,
+ "useRefHistory": true,
+ "useResizeObserver": true,
+ "useRoute": true,
+ "useRouter": true,
+ "useScreenOrientation": true,
+ "useScreenSafeArea": true,
+ "useScriptTag": true,
+ "useScroll": true,
+ "useScrollLock": true,
+ "useSessionStorage": true,
+ "useShare": true,
+ "useSlots": true,
+ "useSpeechRecognition": true,
+ "useSpeechSynthesis": true,
+ "useStepper": true,
+ "useStorage": true,
+ "useStorageAsync": true,
+ "useStyleTag": true,
+ "useSupported": true,
+ "useSwipe": true,
+ "useTemplateRefsList": true,
+ "useTextDirection": true,
+ "useTextSelection": true,
+ "useTextareaAutosize": true,
+ "useThrottle": true,
+ "useThrottleFn": true,
+ "useThrottledRefHistory": true,
+ "useTimeAgo": true,
+ "useTimeout": true,
+ "useTimeoutFn": true,
+ "useTimeoutPoll": true,
+ "useTimestamp": true,
+ "useTitle": true,
+ "useToggle": true,
+ "useTransition": true,
+ "useUrlSearchParams": true,
+ "useUserMedia": true,
+ "useVModel": true,
+ "useVModels": true,
+ "useVibrate": true,
+ "useVirtualList": true,
+ "useWakeLock": true,
+ "useWebNotification": true,
+ "useWebSocket": true,
+ "useWebWorker": true,
+ "useWebWorkerFn": true,
+ "useWindowFocus": true,
+ "useWindowScroll": true,
+ "useWindowSize": true,
+ "watch": true,
+ "watchArray": true,
+ "watchAtMost": true,
+ "watchDebounced": true,
+ "watchEffect": true,
+ "watchIgnorable": true,
+ "watchOnce": true,
+ "watchPausable": true,
+ "watchPostEffect": true,
+ "watchSyncEffect": true,
+ "watchThrottled": true,
+ "watchTriggerable": true,
+ "watchWithFilter": true,
+ "whenever": true
+ }
+}
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..b28255c
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,75 @@
+// @ts-check
+const { defineConfig } = require('eslint-define-config')
+module.exports = defineConfig({
+ root: true,
+ env: {
+ browser: true,
+ node: true,
+ es6: true
+ },
+ parser: 'vue-eslint-parser',
+ parserOptions: {
+ parser: '@typescript-eslint/parser',
+ ecmaVersion: 2020,
+ sourceType: 'module',
+ jsxPragma: 'React',
+ ecmaFeatures: {
+ jsx: true
+ }
+ },
+ extends: [
+ 'plugin:vue/vue3-recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'prettier',
+ 'plugin:prettier/recommended',
+ '@unocss'
+ ],
+ rules: {
+ 'vue/no-setup-props-destructure': 'off',
+ 'vue/script-setup-uses-vars': 'error',
+ 'vue/no-reserved-component-names': 'off',
+ '@typescript-eslint/ban-ts-ignore': 'off',
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-var-requires': 'off',
+ '@typescript-eslint/no-empty-function': 'off',
+ 'vue/custom-event-name-casing': 'off',
+ 'no-use-before-define': 'off',
+ '@typescript-eslint/no-use-before-define': 'off',
+ '@typescript-eslint/ban-ts-comment': 'off',
+ '@typescript-eslint/ban-types': 'off',
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
+ '@typescript-eslint/no-unused-vars': 'off',
+ 'no-unused-vars': 'off',
+ 'space-before-function-paren': 'off',
+
+ 'vue/attributes-order': 'off',
+ 'vue/one-component-per-file': 'off',
+ 'vue/html-closing-bracket-newline': 'off',
+ 'vue/max-attributes-per-line': 'off',
+ 'vue/multiline-html-element-content-newline': 'off',
+ 'vue/singleline-html-element-content-newline': 'off',
+ 'vue/attribute-hyphenation': 'off',
+ 'vue/require-default-prop': 'off',
+ 'vue/require-explicit-emits': 'off',
+ 'vue/require-toggle-inside-transition': 'off',
+ 'vue/html-self-closing': [
+ 'error',
+ {
+ html: {
+ void: 'always',
+ normal: 'never',
+ component: 'always'
+ },
+ svg: 'always',
+ math: 'always'
+ }
+ ],
+ 'vue/multi-word-component-names': 'off',
+ 'vue/no-v-html': 'off',
+ 'prettier/prettier': 'off', // 鑺嬭壙锛氶粯璁ゅ叧闂� prettier 鐨� ESLint 鏍¢獙锛屽洜涓烘垜浠娇鐢ㄧ殑鏄� IDE 鐨� Prettier 鎻掍欢
+ '@unocss/order': 'off', // 鑺嬭壙锛氱鐢� unocss 銆恈ss銆戦『搴忕殑鎻愮ず锛屽洜涓烘殏鏃朵笉闇�瑕佽繖涔堜弗鏍硷紝璀﹀憡涔熸湁鐐圭箒鐞�
+ '@unocss/order-attributify': 'off' // 鑺嬭壙锛氱鐢� unocss 銆愬睘鎬с�戦『搴忕殑鎻愮ず锛屽洜涓烘殏鏃朵笉闇�瑕佽繖涔堜弗鏍硷紝璀﹀憡涔熸湁鐐圭箒鐞�
+ }
+})
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..848638a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+/dist*
+pnpm-debug
+auto-*.d.ts
+.idea
+.history
diff --git "a/.image/Java\347\233\221\346\216\247.jpg" "b/.image/Java\347\233\221\346\216\247.jpg"
new file mode 100644
index 0000000..6ad522a
--- /dev/null
+++ "b/.image/Java\347\233\221\346\216\247.jpg"
Binary files differ
diff --git a/.image/MySQL.jpg b/.image/MySQL.jpg
new file mode 100644
index 0000000..64a1940
--- /dev/null
+++ b/.image/MySQL.jpg
Binary files differ
diff --git "a/.image/OA\350\257\267\345\201\207-\345\210\227\350\241\250.jpg" "b/.image/OA\350\257\267\345\201\207-\345\210\227\350\241\250.jpg"
new file mode 100644
index 0000000..787bb73
--- /dev/null
+++ "b/.image/OA\350\257\267\345\201\207-\345\210\227\350\241\250.jpg"
Binary files differ
diff --git "a/.image/OA\350\257\267\345\201\207-\345\217\221\350\265\267.jpg" "b/.image/OA\350\257\267\345\201\207-\345\217\221\350\265\267.jpg"
new file mode 100644
index 0000000..1a7342d
--- /dev/null
+++ "b/.image/OA\350\257\267\345\201\207-\345\217\221\350\265\267.jpg"
Binary files differ
diff --git "a/.image/OA\350\257\267\345\201\207-\350\257\246\346\203\205.jpg" "b/.image/OA\350\257\267\345\201\207-\350\257\246\346\203\205.jpg"
new file mode 100644
index 0000000..a83e7c1
--- /dev/null
+++ "b/.image/OA\350\257\267\345\201\207-\350\257\246\346\203\205.jpg"
Binary files differ
diff --git a/.image/Redis.jpg b/.image/Redis.jpg
new file mode 100644
index 0000000..9569352
--- /dev/null
+++ b/.image/Redis.jpg
Binary files differ
diff --git a/.image/admin-uniapp/01.png b/.image/admin-uniapp/01.png
new file mode 100644
index 0000000..0f65d99
--- /dev/null
+++ b/.image/admin-uniapp/01.png
Binary files differ
diff --git a/.image/admin-uniapp/02.png b/.image/admin-uniapp/02.png
new file mode 100644
index 0000000..05ec781
--- /dev/null
+++ b/.image/admin-uniapp/02.png
Binary files differ
diff --git a/.image/admin-uniapp/03.png b/.image/admin-uniapp/03.png
new file mode 100644
index 0000000..f400c68
--- /dev/null
+++ b/.image/admin-uniapp/03.png
Binary files differ
diff --git a/.image/admin-uniapp/04.png b/.image/admin-uniapp/04.png
new file mode 100644
index 0000000..d5d5ea0
--- /dev/null
+++ b/.image/admin-uniapp/04.png
Binary files differ
diff --git a/.image/admin-uniapp/05.png b/.image/admin-uniapp/05.png
new file mode 100644
index 0000000..1de6d8a
--- /dev/null
+++ b/.image/admin-uniapp/05.png
Binary files differ
diff --git a/.image/admin-uniapp/06.png b/.image/admin-uniapp/06.png
new file mode 100644
index 0000000..400ae90
--- /dev/null
+++ b/.image/admin-uniapp/06.png
Binary files differ
diff --git a/.image/admin-uniapp/07.png b/.image/admin-uniapp/07.png
new file mode 100644
index 0000000..2ed8c0f
--- /dev/null
+++ b/.image/admin-uniapp/07.png
Binary files differ
diff --git a/.image/admin-uniapp/08.png b/.image/admin-uniapp/08.png
new file mode 100644
index 0000000..090e64a
--- /dev/null
+++ b/.image/admin-uniapp/08.png
Binary files differ
diff --git a/.image/admin-uniapp/09.png b/.image/admin-uniapp/09.png
new file mode 100644
index 0000000..f2032c8
--- /dev/null
+++ b/.image/admin-uniapp/09.png
Binary files differ
diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png
new file mode 100644
index 0000000..1c22dbe
--- /dev/null
+++ b/.image/common/ai-feature.png
Binary files differ
diff --git a/.image/common/ai-preview.gif b/.image/common/ai-preview.gif
new file mode 100644
index 0000000..5f13ac4
--- /dev/null
+++ b/.image/common/ai-preview.gif
Binary files differ
diff --git a/.image/common/bpm-feature.png b/.image/common/bpm-feature.png
new file mode 100644
index 0000000..23787fb
--- /dev/null
+++ b/.image/common/bpm-feature.png
Binary files differ
diff --git a/.image/common/crm-feature.png b/.image/common/crm-feature.png
new file mode 100644
index 0000000..e1c9670
--- /dev/null
+++ b/.image/common/crm-feature.png
Binary files differ
diff --git a/.image/common/erp-feature.png b/.image/common/erp-feature.png
new file mode 100644
index 0000000..d30b30e
--- /dev/null
+++ b/.image/common/erp-feature.png
Binary files differ
diff --git a/.image/common/infra-feature.png b/.image/common/infra-feature.png
new file mode 100644
index 0000000..f5cef50
--- /dev/null
+++ b/.image/common/infra-feature.png
Binary files differ
diff --git a/.image/common/mall-feature.png b/.image/common/mall-feature.png
new file mode 100644
index 0000000..cca05c0
--- /dev/null
+++ b/.image/common/mall-feature.png
Binary files differ
diff --git a/.image/common/mall-preview.png b/.image/common/mall-preview.png
new file mode 100644
index 0000000..f939214
--- /dev/null
+++ b/.image/common/mall-preview.png
Binary files differ
diff --git a/.image/common/project-vs.png b/.image/common/project-vs.png
new file mode 100644
index 0000000..561e092
--- /dev/null
+++ b/.image/common/project-vs.png
Binary files differ
diff --git a/.image/common/ruoyi-vue-pro-architecture.png b/.image/common/ruoyi-vue-pro-architecture.png
new file mode 100644
index 0000000..7bd7d59
--- /dev/null
+++ b/.image/common/ruoyi-vue-pro-architecture.png
Binary files differ
diff --git a/.image/common/ruoyi-vue-pro-biz.png b/.image/common/ruoyi-vue-pro-biz.png
new file mode 100644
index 0000000..24a385a
--- /dev/null
+++ b/.image/common/ruoyi-vue-pro-biz.png
Binary files differ
diff --git a/.image/common/system-feature.png b/.image/common/system-feature.png
new file mode 100644
index 0000000..366087c
--- /dev/null
+++ b/.image/common/system-feature.png
Binary files differ
diff --git a/.image/common/yudao-cloud-architecture.png b/.image/common/yudao-cloud-architecture.png
new file mode 100644
index 0000000..59416d8
--- /dev/null
+++ b/.image/common/yudao-cloud-architecture.png
Binary files differ
diff --git a/.image/common/yudao-roadmap.png b/.image/common/yudao-roadmap.png
new file mode 100644
index 0000000..f4becc9
--- /dev/null
+++ b/.image/common/yudao-roadmap.png
Binary files differ
diff --git a/.image/demo/vue3-ep.png b/.image/demo/vue3-ep.png
new file mode 100644
index 0000000..1bf6e8c
--- /dev/null
+++ b/.image/demo/vue3-ep.png
Binary files differ
diff --git "a/.image/\344\270\252\344\272\272\344\270\255\345\277\203.jpg" "b/.image/\344\270\252\344\272\272\344\270\255\345\277\203.jpg"
new file mode 100644
index 0000000..ce57f6e
--- /dev/null
+++ "b/.image/\344\270\252\344\272\272\344\270\255\345\277\203.jpg"
Binary files differ
diff --git "a/.image/\344\273\243\347\240\201\347\224\237\346\210\220.jpg" "b/.image/\344\273\243\347\240\201\347\224\237\346\210\220.jpg"
new file mode 100644
index 0000000..751603e
--- /dev/null
+++ "b/.image/\344\273\243\347\240\201\347\224\237\346\210\220.jpg"
Binary files differ
diff --git "a/.image/\344\273\244\347\211\214\347\256\241\347\220\206.jpg" "b/.image/\344\273\244\347\211\214\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..04abf4d
--- /dev/null
+++ "b/.image/\344\273\244\347\211\214\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\256\241\346\211\271.jpg" "b/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\256\241\346\211\271.jpg"
new file mode 100644
index 0000000..cba312a
--- /dev/null
+++ "b/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\256\241\346\211\271.jpg"
Binary files differ
diff --git "a/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\267\262\345\212\236.jpg" "b/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\267\262\345\212\236.jpg"
new file mode 100644
index 0000000..7a8d0fb
--- /dev/null
+++ "b/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\267\262\345\212\236.jpg"
Binary files differ
diff --git "a/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\276\205\345\212\236.jpg" "b/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\276\205\345\212\236.jpg"
new file mode 100644
index 0000000..a90323f
--- /dev/null
+++ "b/.image/\344\273\273\345\212\241\345\210\227\350\241\250-\345\276\205\345\212\236.jpg"
Binary files differ
diff --git "a/.image/\344\273\273\345\212\241\346\227\245\345\277\227.jpg" "b/.image/\344\273\273\345\212\241\346\227\245\345\277\227.jpg"
new file mode 100644
index 0000000..599e50a
--- /dev/null
+++ "b/.image/\344\273\273\345\212\241\346\227\245\345\277\227.jpg"
Binary files differ
diff --git "a/.image/\345\225\206\346\210\267\344\277\241\346\201\257.jpg" "b/.image/\345\225\206\346\210\267\344\277\241\346\201\257.jpg"
new file mode 100644
index 0000000..483eace
--- /dev/null
+++ "b/.image/\345\225\206\346\210\267\344\277\241\346\201\257.jpg"
Binary files differ
diff --git "a/.image/\345\234\250\347\272\277\347\224\250\346\210\267.jpg" "b/.image/\345\234\250\347\272\277\347\224\250\346\210\267.jpg"
new file mode 100644
index 0000000..b183009
--- /dev/null
+++ "b/.image/\345\234\250\347\272\277\347\224\250\346\210\267.jpg"
Binary files differ
diff --git "a/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\345\210\227\350\241\250.jpg" "b/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\345\210\227\350\241\250.jpg"
new file mode 100644
index 0000000..9a45c3b
--- /dev/null
+++ "b/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\345\210\227\350\241\250.jpg"
Binary files differ
diff --git "a/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\347\274\226\350\276\221.jpg" "b/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\347\274\226\350\276\221.jpg"
new file mode 100644
index 0000000..63298a0
--- /dev/null
+++ "b/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\347\274\226\350\276\221.jpg"
Binary files differ
diff --git "a/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\351\242\204\350\247\210.jpg" "b/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\351\242\204\350\247\210.jpg"
new file mode 100644
index 0000000..501d9ea
--- /dev/null
+++ "b/.image/\345\244\247\345\261\217\350\256\276\350\256\241\345\231\250-\351\242\204\350\247\210.jpg"
Binary files differ
diff --git "a/.image/\345\255\227\345\205\270\346\225\260\346\215\256.jpg" "b/.image/\345\255\227\345\205\270\346\225\260\346\215\256.jpg"
new file mode 100644
index 0000000..8298c89
--- /dev/null
+++ "b/.image/\345\255\227\345\205\270\346\225\260\346\215\256.jpg"
Binary files differ
diff --git "a/.image/\345\255\227\345\205\270\347\261\273\345\236\213.jpg" "b/.image/\345\255\227\345\205\270\347\261\273\345\236\213.jpg"
new file mode 100644
index 0000000..6613392
--- /dev/null
+++ "b/.image/\345\255\227\345\205\270\347\261\273\345\236\213.jpg"
Binary files differ
diff --git "a/.image/\345\256\232\346\227\266\344\273\273\345\212\241.jpg" "b/.image/\345\256\232\346\227\266\344\273\273\345\212\241.jpg"
new file mode 100644
index 0000000..d5bbd85
--- /dev/null
+++ "b/.image/\345\256\232\346\227\266\344\273\273\345\212\241.jpg"
Binary files differ
diff --git "a/.image/\345\262\227\344\275\215\347\256\241\347\220\206.jpg" "b/.image/\345\262\227\344\275\215\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..42b64d2
--- /dev/null
+++ "b/.image/\345\262\227\344\275\215\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\345\267\245\344\275\234\346\265\201\350\256\276\350\256\241\345\231\250-bpmn.jpg" "b/.image/\345\267\245\344\275\234\346\265\201\350\256\276\350\256\241\345\231\250-bpmn.jpg"
new file mode 100644
index 0000000..2a61f60
--- /dev/null
+++ "b/.image/\345\267\245\344\275\234\346\265\201\350\256\276\350\256\241\345\231\250-bpmn.jpg"
Binary files differ
diff --git "a/.image/\345\267\245\344\275\234\346\265\201\350\256\276\350\256\241\345\231\250-simple.jpg" "b/.image/\345\267\245\344\275\234\346\265\201\350\256\276\350\256\241\345\231\250-simple.jpg"
new file mode 100644
index 0000000..9ef2c9e
--- /dev/null
+++ "b/.image/\345\267\245\344\275\234\346\265\201\350\256\276\350\256\241\345\231\250-simple.jpg"
Binary files differ
diff --git "a/.image/\345\272\224\347\224\250\344\277\241\346\201\257-\345\210\227\350\241\250.jpg" "b/.image/\345\272\224\347\224\250\344\277\241\346\201\257-\345\210\227\350\241\250.jpg"
new file mode 100644
index 0000000..da419a2
--- /dev/null
+++ "b/.image/\345\272\224\347\224\250\344\277\241\346\201\257-\345\210\227\350\241\250.jpg"
Binary files differ
diff --git "a/.image/\345\272\224\347\224\250\344\277\241\346\201\257-\347\274\226\350\276\221.jpg" "b/.image/\345\272\224\347\224\250\344\277\241\346\201\257-\347\274\226\350\276\221.jpg"
new file mode 100644
index 0000000..913cfbc
--- /dev/null
+++ "b/.image/\345\272\224\347\224\250\344\277\241\346\201\257-\347\274\226\350\276\221.jpg"
Binary files differ
diff --git "a/.image/\345\272\224\347\224\250\347\256\241\347\220\206.jpg" "b/.image/\345\272\224\347\224\250\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..6e7789f
--- /dev/null
+++ "b/.image/\345\272\224\347\224\250\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\345\210\227\350\241\250.jpg" "b/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\345\210\227\350\241\250.jpg"
new file mode 100644
index 0000000..223d17a
--- /dev/null
+++ "b/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\345\210\227\350\241\250.jpg"
Binary files differ
diff --git "a/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\345\217\221\350\265\267.jpg" "b/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\345\217\221\350\265\267.jpg"
new file mode 100644
index 0000000..7a83306
--- /dev/null
+++ "b/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\345\217\221\350\265\267.jpg"
Binary files differ
diff --git "a/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\350\257\246\346\203\205.jpg" "b/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\350\257\246\346\203\205.jpg"
new file mode 100644
index 0000000..6a01541
--- /dev/null
+++ "b/.image/\346\210\221\347\232\204\346\265\201\347\250\213-\350\257\246\346\203\205.jpg"
Binary files differ
diff --git "a/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\345\233\276\345\275\242\346\212\245\350\241\250.jpg" "b/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\345\233\276\345\275\242\346\212\245\350\241\250.jpg"
new file mode 100644
index 0000000..681b318
--- /dev/null
+++ "b/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\345\233\276\345\275\242\346\212\245\350\241\250.jpg"
Binary files differ
diff --git "a/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\346\211\223\345\215\260\350\256\276\350\256\241.jpg" "b/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\346\211\223\345\215\260\350\256\276\350\256\241.jpg"
new file mode 100644
index 0000000..bb86da6
--- /dev/null
+++ "b/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\346\211\223\345\215\260\350\256\276\350\256\241.jpg"
Binary files differ
diff --git "a/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\346\225\260\346\215\256\346\212\245\350\241\250.jpg" "b/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\346\225\260\346\215\256\346\212\245\350\241\250.jpg"
new file mode 100644
index 0000000..9ca5b9b
--- /dev/null
+++ "b/.image/\346\212\245\350\241\250\350\256\276\350\256\241\345\231\250-\346\225\260\346\215\256\346\212\245\350\241\250.jpg"
Binary files differ
diff --git "a/.image/\346\223\215\344\275\234\346\227\245\345\277\227.jpg" "b/.image/\346\223\215\344\275\234\346\227\245\345\277\227.jpg"
new file mode 100644
index 0000000..4a0611a
--- /dev/null
+++ "b/.image/\346\223\215\344\275\234\346\227\245\345\277\227.jpg"
Binary files differ
diff --git "a/.image/\346\224\257\344\273\230\350\256\242\345\215\225.jpg" "b/.image/\346\224\257\344\273\230\350\256\242\345\215\225.jpg"
new file mode 100644
index 0000000..0a56dd7
--- /dev/null
+++ "b/.image/\346\224\257\344\273\230\350\256\242\345\215\225.jpg"
Binary files differ
diff --git "a/.image/\346\225\217\346\204\237\350\257\215.jpg" "b/.image/\346\225\217\346\204\237\350\257\215.jpg"
new file mode 100644
index 0000000..92a5397
--- /dev/null
+++ "b/.image/\346\225\217\346\204\237\350\257\215.jpg"
Binary files differ
diff --git "a/.image/\346\225\260\346\215\256\345\272\223\346\226\207\346\241\243.jpg" "b/.image/\346\225\260\346\215\256\345\272\223\346\226\207\346\241\243.jpg"
new file mode 100644
index 0000000..a4339d9
--- /dev/null
+++ "b/.image/\346\225\260\346\215\256\345\272\223\346\226\207\346\241\243.jpg"
Binary files differ
diff --git "a/.image/\346\226\207\344\273\266\347\256\241\347\220\206.jpg" "b/.image/\346\226\207\344\273\266\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..054b19f
--- /dev/null
+++ "b/.image/\346\226\207\344\273\266\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\346\226\207\344\273\266\347\256\241\347\220\2062.jpg" "b/.image/\346\226\207\344\273\266\347\256\241\347\220\2062.jpg"
new file mode 100644
index 0000000..b12e5c3
--- /dev/null
+++ "b/.image/\346\226\207\344\273\266\347\256\241\347\220\2062.jpg"
Binary files differ
diff --git "a/.image/\346\226\207\344\273\266\351\205\215\347\275\256.jpg" "b/.image/\346\226\207\344\273\266\351\205\215\347\275\256.jpg"
new file mode 100644
index 0000000..e618049
--- /dev/null
+++ "b/.image/\346\226\207\344\273\266\351\205\215\347\275\256.jpg"
Binary files differ
diff --git "a/.image/\346\227\245\345\277\227\344\270\255\345\277\203.jpg" "b/.image/\346\227\245\345\277\227\344\270\255\345\277\203.jpg"
new file mode 100644
index 0000000..27c1c6c
--- /dev/null
+++ "b/.image/\346\227\245\345\277\227\344\270\255\345\277\203.jpg"
Binary files differ
diff --git "a/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\345\210\227\350\241\250.jpg" "b/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\345\210\227\350\241\250.jpg"
new file mode 100644
index 0000000..ffdc584
--- /dev/null
+++ "b/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\345\210\227\350\241\250.jpg"
Binary files differ
diff --git "a/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\345\256\232\344\271\211.jpg" "b/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\345\256\232\344\271\211.jpg"
new file mode 100644
index 0000000..18b316c
--- /dev/null
+++ "b/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\345\256\232\344\271\211.jpg"
Binary files differ
diff --git "a/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\350\256\276\350\256\241.jpg" "b/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\350\256\276\350\256\241.jpg"
new file mode 100644
index 0000000..9614969
--- /dev/null
+++ "b/.image/\346\265\201\347\250\213\346\250\241\345\236\213-\350\256\276\350\256\241.jpg"
Binary files differ
diff --git "a/.image/\346\265\201\347\250\213\350\241\250\345\215\225.jpg" "b/.image/\346\265\201\347\250\213\350\241\250\345\215\225.jpg"
new file mode 100644
index 0000000..60669c1
--- /dev/null
+++ "b/.image/\346\265\201\347\250\213\350\241\250\345\215\225.jpg"
Binary files differ
diff --git "a/.image/\347\224\237\346\210\220\346\225\210\346\236\234.jpg" "b/.image/\347\224\237\346\210\220\346\225\210\346\236\234.jpg"
new file mode 100644
index 0000000..98ff2cc
--- /dev/null
+++ "b/.image/\347\224\237\346\210\220\346\225\210\346\236\234.jpg"
Binary files differ
diff --git "a/.image/\347\224\250\346\210\267\345\210\206\347\273\204.jpg" "b/.image/\347\224\250\346\210\267\345\210\206\347\273\204.jpg"
new file mode 100644
index 0000000..39af1cd
--- /dev/null
+++ "b/.image/\347\224\250\346\210\267\345\210\206\347\273\204.jpg"
Binary files differ
diff --git "a/.image/\347\224\250\346\210\267\347\256\241\347\220\206.jpg" "b/.image/\347\224\250\346\210\267\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..844604a
--- /dev/null
+++ "b/.image/\347\224\250\346\210\267\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\347\231\273\345\275\225.jpg" "b/.image/\347\231\273\345\275\225.jpg"
new file mode 100644
index 0000000..b782b98
--- /dev/null
+++ "b/.image/\347\231\273\345\275\225.jpg"
Binary files differ
diff --git "a/.image/\347\231\273\345\275\225\346\227\245\345\277\227.jpg" "b/.image/\347\231\273\345\275\225\346\227\245\345\277\227.jpg"
new file mode 100644
index 0000000..25662d9
--- /dev/null
+++ "b/.image/\347\231\273\345\275\225\346\227\245\345\277\227.jpg"
Binary files differ
diff --git "a/.image/\347\237\255\344\277\241\346\227\245\345\277\227.jpg" "b/.image/\347\237\255\344\277\241\346\227\245\345\277\227.jpg"
new file mode 100644
index 0000000..ada8e56
--- /dev/null
+++ "b/.image/\347\237\255\344\277\241\346\227\245\345\277\227.jpg"
Binary files differ
diff --git "a/.image/\347\237\255\344\277\241\346\250\241\346\235\277.jpg" "b/.image/\347\237\255\344\277\241\346\250\241\346\235\277.jpg"
new file mode 100644
index 0000000..09381cc
--- /dev/null
+++ "b/.image/\347\237\255\344\277\241\346\250\241\346\235\277.jpg"
Binary files differ
diff --git "a/.image/\347\237\255\344\277\241\346\270\240\351\201\223.jpg" "b/.image/\347\237\255\344\277\241\346\270\240\351\201\223.jpg"
new file mode 100644
index 0000000..df3a5c3
--- /dev/null
+++ "b/.image/\347\237\255\344\277\241\346\270\240\351\201\223.jpg"
Binary files differ
diff --git "a/.image/\347\247\237\346\210\267\345\245\227\351\244\220.png" "b/.image/\347\247\237\346\210\267\345\245\227\351\244\220.png"
new file mode 100644
index 0000000..9663167
--- /dev/null
+++ "b/.image/\347\247\237\346\210\267\345\245\227\351\244\220.png"
Binary files differ
diff --git "a/.image/\347\247\237\346\210\267\347\256\241\347\220\206.jpg" "b/.image/\347\247\237\346\210\267\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..647416a
--- /dev/null
+++ "b/.image/\347\247\237\346\210\267\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\347\263\273\347\273\237\346\216\245\345\217\243.jpg" "b/.image/\347\263\273\347\273\237\346\216\245\345\217\243.jpg"
new file mode 100644
index 0000000..6d39d42
--- /dev/null
+++ "b/.image/\347\263\273\347\273\237\346\216\245\345\217\243.jpg"
Binary files differ
diff --git "a/.image/\350\217\234\345\215\225\347\256\241\347\220\206.jpg" "b/.image/\350\217\234\345\215\225\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..ad3b797
--- /dev/null
+++ "b/.image/\350\217\234\345\215\225\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\350\241\250\345\215\225\346\236\204\345\273\272.jpg" "b/.image/\350\241\250\345\215\225\346\236\204\345\273\272.jpg"
new file mode 100644
index 0000000..81f0374
--- /dev/null
+++ "b/.image/\350\241\250\345\215\225\346\236\204\345\273\272.jpg"
Binary files differ
diff --git "a/.image/\350\247\222\350\211\262\347\256\241\347\220\206.jpg" "b/.image/\350\247\222\350\211\262\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..eed776e
--- /dev/null
+++ "b/.image/\350\247\222\350\211\262\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\350\256\277\351\227\256\346\227\245\345\277\227.jpg" "b/.image/\350\256\277\351\227\256\346\227\245\345\277\227.jpg"
new file mode 100644
index 0000000..ef301aa
--- /dev/null
+++ "b/.image/\350\256\277\351\227\256\346\227\245\345\277\227.jpg"
Binary files differ
diff --git "a/.image/\351\200\200\346\254\276\350\256\242\345\215\225.jpg" "b/.image/\351\200\200\346\254\276\350\256\242\345\215\225.jpg"
new file mode 100644
index 0000000..2c6c6c9
--- /dev/null
+++ "b/.image/\351\200\200\346\254\276\350\256\242\345\215\225.jpg"
Binary files differ
diff --git "a/.image/\351\200\232\347\237\245\345\205\254\345\221\212.jpg" "b/.image/\351\200\232\347\237\245\345\205\254\345\221\212.jpg"
new file mode 100644
index 0000000..97bb42f
--- /dev/null
+++ "b/.image/\351\200\232\347\237\245\345\205\254\345\221\212.jpg"
Binary files differ
diff --git "a/.image/\351\203\250\351\227\250\347\256\241\347\220\206.jpg" "b/.image/\351\203\250\351\227\250\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..6eab233
--- /dev/null
+++ "b/.image/\351\203\250\351\227\250\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\351\205\215\347\275\256\347\256\241\347\220\206.jpg" "b/.image/\351\205\215\347\275\256\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..0abaec9
--- /dev/null
+++ "b/.image/\351\205\215\347\275\256\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\351\223\276\350\267\257\350\277\275\350\270\252.jpg" "b/.image/\351\223\276\350\267\257\350\277\275\350\270\252.jpg"
new file mode 100644
index 0000000..12f7aa8
--- /dev/null
+++ "b/.image/\351\223\276\350\267\257\350\277\275\350\270\252.jpg"
Binary files differ
diff --git "a/.image/\351\224\231\350\257\257\346\227\245\345\277\227.jpg" "b/.image/\351\224\231\350\257\257\346\227\245\345\277\227.jpg"
new file mode 100644
index 0000000..eb615ea
--- /dev/null
+++ "b/.image/\351\224\231\350\257\257\346\227\245\345\277\227.jpg"
Binary files differ
diff --git "a/.image/\351\224\231\350\257\257\347\240\201\347\256\241\347\220\206.jpg" "b/.image/\351\224\231\350\257\257\347\240\201\347\256\241\347\220\206.jpg"
new file mode 100644
index 0000000..ea91dde
--- /dev/null
+++ "b/.image/\351\224\231\350\257\257\347\240\201\347\256\241\347\220\206.jpg"
Binary files differ
diff --git "a/.image/\351\246\226\351\241\265.jpg" "b/.image/\351\246\226\351\241\265.jpg"
new file mode 100644
index 0000000..10a7fde
--- /dev/null
+++ "b/.image/\351\246\226\351\241\265.jpg"
Binary files differ
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..f68ea86
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,11 @@
+/node_modules/**
+/dist/
+/dist*
+/public/*
+/docs/*
+/vite.config.ts
+/src/types/env.d.ts
+/src/types/auto-components.d.ts
+/src/types/auto-imports.d.ts
+/docs/**/*
+CHANGELOG
diff --git a/.stylelintignore b/.stylelintignore
new file mode 100644
index 0000000..aa605b4
--- /dev/null
+++ b/.stylelintignore
@@ -0,0 +1,6 @@
+/dist/*
+/public/*
+public/*
+/dist*
+/src/types/env.d.ts
+/docs/**/*
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..65288b5
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,18 @@
+{
+ "recommendations": [
+ "christian-kohler.path-intellisense",
+ "vscode-icons-team.vscode-icons",
+ "davidanson.vscode-markdownlint",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "mrmlnc.vscode-less",
+ "lokalise.i18n-ally",
+ "redhat.vscode-yaml",
+ "csstools.postcss",
+ "mikestead.dotenv",
+ "eamodio.gitlens",
+ "antfu.iconify",
+ "antfu.unocss",
+ "Vue.volar"
+ ]
+}
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..f43edc0
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,16 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "type": "msedge",
+ "request": "launch",
+ "name": "Launch Edge against localhost",
+ "url": "http://localhost",
+ "webRoot": "${workspaceFolder}/src",
+ "sourceMaps": true
+ }
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..74ab52a
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,146 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "npm.packageManager": "pnpm",
+ "editor.tabSize": 2,
+ "prettier.printWidth": 100, // 瓒呰繃鏈�澶у�兼崲琛�
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "files.eol": "\n",
+ "search.exclude": {
+ "**/node_modules": true,
+ "**/*.log": true,
+ "**/*.log*": true,
+ "**/bower_components": true,
+ "**/dist": true,
+ "**/elehukouben": true,
+ "**/.git": true,
+ "**/.gitignore": true,
+ "**/.svn": true,
+ "**/.DS_Store": true,
+ "**/.idea": true,
+ "**/.vscode": false,
+ "**/yarn.lock": true,
+ "**/tmp": true,
+ "out": true,
+ "dist": true,
+ "node_modules": true,
+ "CHANGELOG.md": true,
+ "examples": true,
+ "res": true,
+ "screenshots": true,
+ "yarn-error.log": true,
+ "**/.yarn": true
+ },
+ "files.exclude": {
+ "**/.cache": true,
+ "**/.editorconfig": true,
+ "**/.eslintcache": true,
+ "**/bower_components": true,
+ "**/.idea": true,
+ "**/tmp": true,
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/CVS": true,
+ "**/.DS_Store": true
+ },
+ "files.watcherExclude": {
+ "**/.git/objects/**": true,
+ "**/.git/subtree-cache/**": true,
+ "**/.vscode/**": true,
+ "**/node_modules/**": true,
+ "**/tmp/**": true,
+ "**/bower_components/**": true,
+ "**/dist/**": true,
+ "**/yarn.lock": true
+ },
+ "stylelint.enable": true,
+ "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
+ "path-intellisense.mappings": {
+ "@/": "${workspaceRoot}/src"
+ },
+ "[javascriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[typescriptreact]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[html]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[css]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[less]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[scss]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "[markdown]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": "explicit",
+ "source.fixAll.stylelint": "explicit"
+ },
+ "editor.formatOnSave": true,
+ "[vue]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ },
+ "i18n-ally.localesPaths": ["src/locales"],
+ "i18n-ally.keystyle": "nested",
+ "i18n-ally.sortKeys": true,
+ "i18n-ally.namespace": false,
+ "i18n-ally.enabledParsers": ["ts"],
+ "i18n-ally.sourceLanguage": "en",
+ "i18n-ally.displayLanguage": "zh-CN",
+ "i18n-ally.enabledFrameworks": ["vue", "react"],
+ "cSpell.words": [
+ "brotli",
+ "browserslist",
+ "codemirror",
+ "commitlint",
+ "cropperjs",
+ "echart",
+ "echarts",
+ "esnext",
+ "esno",
+ "iconify",
+ "INTLIFY",
+ "lintstagedrc",
+ "logicflow",
+ "nprogress",
+ "pinia",
+ "pnpm",
+ "qrcode",
+ "sider",
+ "sortablejs",
+ "stylelint",
+ "svgs",
+ "unocss",
+ "unplugin",
+ "unref",
+ "videojs",
+ "VITE",
+ "vitejs",
+ "vueuse",
+ "wangeditor",
+ "xingyu",
+ "yudao",
+ "zxcvbn"
+ ],
+ // 鎺у埗鐩稿叧鏂囦欢宓屽灞曠ず
+ "explorer.fileNesting.enabled": true,
+ "explorer.fileNesting.expand": false,
+ "explorer.fileNesting.patterns": {
+ "*.ts": "$(capture).test.ts, $(capture).test.tsx",
+ "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
+ "*.env": "$(capture).env.*",
+ "package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.eslintrc-auto-import.json,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
+ },
+ "terminal.integrated.scrollback": 10000,
+ "nuxt.isNuxtApp": false
+}
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9861118
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-present Archer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 2839aba..295946d 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,295 @@
-## app-web-examination-yudao
+**涓ヨ們澹版槑锛氱幇鍦ㄣ�佹湭鏉ラ兘涓嶄細鏈夊晢涓氱増鏈紝鎵�鏈変唬鐮佸叏閮ㄥ紑婧�!锛�**
+**銆屾垜鍠滄鍐欎唬鐮侊紝涔愭涓嶇柌銆�**
+**銆屾垜鍠滄鍋氬紑婧愶紝浠ユ涓轰箰銆�**
+鎴� 馃惗 鍦ㄤ笂娴疯壈鑻﹀鏂楋紝鏃╀腑鏅氬湪 top3 澶у巶璁ょ湡鎼爾锛屽閲屼负寮�婧愬仛璐$尞銆�
+濡傛灉杩欎釜椤圭洰璁╀綘鏈夋墍鏀惰幏锛岃寰� Star 鍏虫敞鍝︼紝杩欏鎴戞槸闈炲父涓嶉敊鐨勯紦鍔变笌鏀寔銆�
+
+## 馃惗 鏂版墜蹇呰
+
+* nodejs > 16.18.0 && pnpm > 8.6.0 (寮哄埗浣跨敤pnpm)
+* 婕旂ず鍦板潃銆怴ue3 + element-plus銆戯細<http://dashboard-vue3.yudao.iocoder.cn>
+* 婕旂ず鍦板潃銆怴ue3 + vben5.0(ant-design-vue)銆戯細<http://dashboard-vben.yudao.iocoder.cn>
+* 婕旂ず鍦板潃銆怴ue2 + element-ui銆戯細<http://dashboard.yudao.iocoder.cn>
+* 鍚姩鏂囨。锛�<https://doc.iocoder.cn/quick-start/>
+* 瑙嗛鏁欑▼锛�<https://doc.iocoder.cn/video/>
+
+## 馃惎 骞冲彴绠�浠�
+
+**鑺嬮亾**锛屼互寮�鍙戣�呬负涓績锛屾墦閫犱腑鍥界涓�娴佺殑蹇�熷紑鍙戝钩鍙帮紝鍏ㄩ儴寮�婧愶紝涓汉涓庝紒涓氬彲 100% 鍏嶈垂浣跨敤銆�
+
+* 閲囩敤 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 瀹炵幇
+* 鏀规崲 saas锛岃嚜鍔ㄥ紩鍏ョ瓑鍔熻兘
+* 浣跨敤 Element Plus 鍏嶈垂寮�婧愮殑涓悗鍙版ā鐗堬紝鍏峰濡備笅鐗规�э細
+
+
+
+* **鏈�鏂版妧鏈爤**锛氫娇鐢� Vue3銆乂ite4 绛夊墠绔墠娌挎妧鏈紑鍙�
+* **TypeScript**: 搴旂敤绋嬪簭绾� JavaScript 鐨勮瑷�
+* **涓婚**: 鍙厤缃殑涓婚
+* **鍥介檯鍖�**锛氬唴缃畬鍠勭殑鍥介檯鍖栨柟妗�
+* **鏉冮檺**锛氬唴缃畬鍠勭殑鍔ㄦ�佽矾鐢辨潈闄愮敓鎴愭柟妗�
+* **缁勪欢**锛氫簩娆″皝瑁呬簡澶氫釜甯哥敤鐨勭粍浠�
+* **绀轰緥**锛氬唴缃赴瀵岀殑绀轰緥
+
+## 鎶�鏈爤
+
+| 妗嗘灦 | 璇存槑 | 鐗堟湰 |
+|----------------------------------------------------------------------|------------------|--------|
+| [Vue](https://staging-cn.vuejs.org/) | Vue 妗嗘灦 | 3.3.8 |
+| [Vite](https://cn.vitejs.dev//) | 寮�鍙戜笌鏋勫缓宸ュ叿 | 4.5.0 |
+| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.4.2 |
+| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 鐨勮秴闆� | 5.2.2 |
+| [pinia](https://pinia.vuejs.org/) | Vue 瀛樺偍搴� 鏇夸唬 vuex5 | 2.1.7 |
+| [vueuse](https://vueuse.org/) | 甯哥敤宸ュ叿闆� | 10.6.1 |
+| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 鍥介檯鍖� | 9.6.5 |
+| [vue-router](https://router.vuejs.org/) | Vue 璺敱 | 4.2.5 |
+| [unocss](https://uno.antfu.me/) | 鍘熷瓙 css | 0.57.4 |
+| [iconify](https://icon-sets.iconify.design/) | 鍦ㄧ嚎鍥炬爣搴� | 3.1.1 |
+| [wangeditor](https://www.wangeditor.com/) | 瀵屾枃鏈紪杈戝櫒 | 5.1.23 |
+
+## 寮�鍙戝伐鍏�
+
+鎺ㄨ崘 VS Code 寮�鍙戯紝閰嶅悎鎻掍欢濡備笅锛�
+
+| 鎻掍欢鍚� | 鍔熻兘 |
+|-------------------------------|---------------------|
+| Vue - Official | Vue 涓� TypeScript 鏀寔 |
+| unocss | unocss for vscode |
+| Iconify IntelliSense | Iconify 棰勮鍜屾悳绱� |
+| i18n Ally | 鍥介檯鍖栨櫤鑳芥彁绀� |
+| Stylelint | Css 鏍煎紡鍖� |
+| Prettier | 浠g爜鏍煎紡鍖� |
+| ESLint | 鑴氭湰浠g爜妫�鏌� |
+| DotENV | env 鏂囦欢楂樹寒 |
+
+## 馃敟 鍚庣鏋舵瀯
+
+鏀寔 Spring Boot銆丼pring Cloud 涓ょ鏋舵瀯锛�
+
+鈶� Spring Boot 鍗曚綋鏋舵瀯锛�<https://doc.iocoder.cn>
+
+
+
+* 閫氱敤妯″潡锛堝繀閫夛級锛氱郴缁熷姛鑳姐�佸熀纭�璁炬柦
+* 閫氱敤妯″潡锛堝彲閫夛級锛氬伐浣滄祦绋嬨�佹敮浠樼郴缁熴�佹暟鎹姤琛ㄣ�佷細鍛樹腑蹇�
+* 涓氬姟绯荤粺锛堟寜闇�锛夛細ERP 绯荤粺銆丆RM 绯荤粺銆佸晢鍩庣郴缁熴�佸井淇″叕浼楀彿銆丄I 澶фā鍨�
+
+### 绯荤粺鍔熻兘
+
+| | 鍔熻兘 | 鎻忚堪 |
+|-----|-------|---------------------------------|
+| | 鐢ㄦ埛绠$悊 | 鐢ㄦ埛鏄郴缁熸搷浣滆�咃紝璇ュ姛鑳戒富瑕佸畬鎴愮郴缁熺敤鎴烽厤缃� |
+| 猸愶笍 | 鍦ㄧ嚎鐢ㄦ埛 | 褰撳墠绯荤粺涓椿璺冪敤鎴风姸鎬佺洃鎺э紝鏀寔鎵嬪姩韪笅绾� |
+| | 瑙掕壊绠$悊 | 瑙掕壊鑿滃崟鏉冮檺鍒嗛厤銆佽缃鑹叉寜鏈烘瀯杩涜鏁版嵁鑼冨洿鏉冮檺鍒掑垎 |
+| | 鑿滃崟绠$悊 | 閰嶇疆绯荤粺鑿滃崟銆佹搷浣滄潈闄愩�佹寜閽潈闄愭爣璇嗙瓑锛屾湰鍦扮紦瀛樻彁渚涙�ц兘 |
+| | 閮ㄩ棬绠$悊 | 閰嶇疆绯荤粺缁勭粐鏈烘瀯锛堝叕鍙搞�侀儴闂ㄣ�佸皬缁勶級锛屾爲缁撴瀯灞曠幇鏀寔鏁版嵁鏉冮檺 |
+| | 宀椾綅绠$悊 | 閰嶇疆绯荤粺鐢ㄦ埛鎵�灞炴媴浠昏亴鍔� |
+| 馃殌 | 绉熸埛绠$悊 | 閰嶇疆绯荤粺绉熸埛锛屾敮鎸� SaaS 鍦烘櫙涓嬬殑澶氱鎴峰姛鑳� |
+| 馃殌 | 绉熸埛濂楅 | 閰嶇疆绉熸埛濂楅锛岃嚜瀹氭瘡涓鎴风殑鑿滃崟銆佹搷浣溿�佹寜閽殑鏉冮檺 |
+| | 瀛楀吀绠$悊 | 瀵圭郴缁熶腑缁忓父浣跨敤鐨勪竴浜涜緝涓哄浐瀹氱殑鏁版嵁杩涜缁存姢 |
+| 馃殌 | 鐭俊绠$悊 | 鐭俊娓犻亾銆佺煭鎭ā鏉裤�佺煭淇℃棩蹇楋紝瀵规帴闃块噷浜戙�佽吘璁簯绛変富娴佺煭淇″钩鍙� |
+| 馃殌 | 閭欢绠$悊 | 閭璐﹀彿銆侀偖浠舵ā鐗堛�侀偖浠跺彂閫佹棩蹇楋紝鏀寔鎵�鏈夐偖浠跺钩鍙� |
+| 馃殌 | 绔欏唴淇� | 绯荤粺鍐呯殑娑堟伅閫氱煡锛屾彁渚涚珯鍐呬俊妯$増銆佺珯鍐呬俊娑堟伅 |
+| 馃殌 | 鎿嶄綔鏃ュ織 | 绯荤粺姝e父鎿嶄綔鏃ュ織璁板綍鍜屾煡璇紝闆嗘垚 Swagger 鐢熸垚鏃ュ織鍐呭 |
+| 猸愶笍 | 鐧诲綍鏃ュ織 | 绯荤粺鐧诲綍鏃ュ織璁板綍鏌ヨ锛屽寘鍚櫥褰曞紓甯� |
+| 馃殌 | 閿欒鐮佺鐞� | 绯荤粺鎵�鏈夐敊璇爜鐨勭鐞嗭紝鍙湪绾夸慨鏀归敊璇彁绀猴紝鏃犻渶閲嶅惎鏈嶅姟 |
+| | 閫氱煡鍏憡 | 绯荤粺閫氱煡鍏憡淇℃伅鍙戝竷缁存姢 |
+| 馃殌 | 鏁忔劅璇� | 閰嶇疆绯荤粺鏁忔劅璇嶏紝鏀寔鏍囩鍒嗙粍 |
+| 馃殌 | 搴旂敤绠$悊 | 绠$悊 SSO 鍗曠偣鐧诲綍鐨勫簲鐢紝鏀寔澶氱 OAuth2 鎺堟潈鏂瑰紡 |
+| 馃殌 | 鍦板尯绠$悊 | 灞曠ず鐪佷唤銆佸煄甯傘�佸尯闀囩瓑鍩庡競淇℃伅锛屾敮鎸� IP 瀵瑰簲鍩庡競 |
+
+ |  |
+
+> 鍘嗙粡澶撮儴浼佷笟鐢熶骇楠岃瘉锛屽伐浣滄祦寮曟搸椤绘爣閰嶄豢閽夐拤/椋炰功 + BPMN 鍙岃璁″櫒锛侊紒锛�
+>
+> 鍓嶈�呮敮鎸佽交閲忛厤缃畝鍗曟祦绋嬶紝鍚庤�呭疄鐜板鏉傚満鏅繁搴︾紪鎺�
+
+| 鍔熻兘鍒楄〃 | 鍔熻兘鎻忚堪 | 鏄惁瀹屾垚 |
+|------------|-------------------------------------------------------------------------------------|------|
+| SIMPLE 璁捐鍣� | 浠块拤閽�/椋炰功璁捐鍣紝鏀寔鎷栨嫿鎼缓琛ㄥ崟娴佺▼锛�10 鍒嗛挓蹇�熷畬鎴愬鎵规祦绋嬮厤缃� | 鉁� |
+| BPMN 璁捐鍣� | 鍩轰簬 BPMN 鏍囧噯寮�鍙戯紝閫傞厤澶嶆潅涓氬姟鍦烘櫙锛屾弧瓒冲灞傜骇瀹℃壒鍙婃祦绋嬭嚜鍔ㄥ寲闇�姹� | 鉁� |
+| 浼氱 | 鍚屼竴涓鎵硅妭鐐硅缃涓汉锛堝 A銆丅銆丆 涓変汉锛屼笁浜轰細鍚屾椂鏀跺埌寰呭姙浠诲姟锛夛紝闇�鍏ㄩ儴鍚屾剰涔嬪悗锛屽鎵规墠鍙埌涓嬩竴瀹℃壒鑺傜偣 | 鉁� |
+| 鎴栫 | 鍚屼竴涓鎵硅妭鐐硅缃涓汉锛屼换鎰忎竴涓汉澶勭悊鍚庯紝灏辫兘杩涘叆涓嬩竴涓妭鐐� | 鉁� |
+| 渚濇瀹℃壒 | 锛堥『搴忎細绛撅級鍚屼竴涓鎵硅妭鐐硅缃涓汉锛堝 A銆丅銆丆 涓変汉锛夛紝涓変汉鎸夐『搴忎緷娆℃敹鍒板緟鍔烇紝鍗� A 鍏堝鎵癸紝A 鎻愪氦鍚� B 鎵嶈兘瀹℃壒锛岄渶鍏ㄩ儴鍚屾剰涔嬪悗锛屽鎵规墠鍙埌涓嬩竴瀹℃壒鑺傜偣 | 鉁� |
+| 鎶勯�� | 灏嗗鎵圭粨鏋滈�氱煡缁欐妱閫佷汉锛屽悓涓�涓鎵归粯璁ゆ帓閲嶏紝涓嶉噸澶嶆妱閫佺粰鍚屼竴浜� | 鉁� |
+| 椹冲洖 | 锛堥��鍥烇級灏嗗鎵归噸缃彂閫佺粰鏌愯妭鐐癸紝閲嶆柊瀹℃壒銆傚彲椹冲洖鑷冲彂璧蜂汉銆佷笂涓�鑺傜偣銆佷换鎰忚妭鐐� | 鉁� |
+| 杞姙 | A 杞粰鍏� B 瀹℃壒锛孊 瀹℃壒鍚庯紝杩涘叆涓嬩竴鑺傜偣 | 鉁� |
+| 濮旀淳 | A 杞粰鍏� B 瀹℃壒锛孊 瀹℃壒鍚庯紝杞粰 A锛孉 缁х画瀹℃壒鍚庤繘鍏ヤ笅涓�鑺傜偣 | 鉁� |
+| 鍔犵 | 鍏佽褰撳墠瀹℃壒浜烘牴鎹渶瑕侊紝鑷澧炲姞褰撳墠鑺傜偣鐨勫鎵逛汉锛屾敮鎸佸悜鍓嶃�佸悜鍚庡姞绛� | 鉁� |
+| 鍑忕 | 锛堝彇娑堝姞绛撅級鍦ㄥ綋鍓嶅鎵逛汉鎿嶄綔涔嬪墠锛屽噺灏戝鎵逛汉 | 鉁� |
+| 鎾ら攢 | 锛堝彇娑堟祦绋嬶級娴佺▼鍙戣捣浜猴紝鍙互瀵规祦绋嬭繘琛屾挙閿�澶勭悊 | 鉁� |
+| 缁堟 | 绯荤粺绠$悊鍛橈紝鍦ㄤ换鎰忚妭鐐圭粓姝㈡祦绋嬪疄渚� | 鉁� |
+| 琛ㄥ崟鏉冮檺 | 鏀寔鎷栨媺鎷介厤缃〃鍗曪紝姣忎釜瀹℃壒鑺傜偣鍙厤缃彧璇汇�佺紪杈戙�侀殣钘忔潈闄� | 鉁� |
+| 瓒呮椂瀹℃壒 | 閰嶇疆瓒呮椂瀹℃壒鏃堕棿锛岃秴鏃跺悗鑷姩瑙﹀彂瀹℃壒閫氳繃銆佷笉閫氳繃銆侀┏鍥炵瓑鎿嶄綔 | 鉁� |
+| 鑷姩鎻愰啋 | 閰嶇疆鎻愰啋鏃堕棿锛屽埌杈炬椂闂村悗鑷姩瑙﹀彂鐭俊銆侀偖绠便�佺珯鍐呬俊绛夐�氱煡鎻愰啋锛屾敮鎸佽嚜瀹氫箟閲嶅鎻愰啋棰戞 | 鉁� |
+| 鐖跺瓙娴佺▼ | 涓绘祦绋嬭缃瓙娴佺▼鑺傜偣锛屽瓙娴佺▼鑺傜偣浼氳嚜鍔ㄨЕ鍙戝瓙娴佺▼銆傚瓙娴佺▼缁撴潫鍚庯紝涓绘祦绋嬫墠浼氭墽琛岋紙缁х画寰�涓嬩笅鎵ц锛夛紝鏀寔鍚屾瀛愭祦绋嬨�佸紓姝ュ瓙娴佺▼ | 鉁� |
+| 鏉′欢鍒嗘敮 | 锛堟帓瀹冨垎鏀級鐢ㄤ簬鍦ㄦ祦绋嬩腑瀹炵幇鍐崇瓥锛屽嵆鏍规嵁鏉′欢閫夋嫨涓�涓垎鏀墽琛� | 鉁� |
+| 骞惰鍒嗘敮 | 鍏佽灏嗘祦绋嬪垎鎴愬鏉″垎鏀紝涓嶈繘琛屾潯浠跺垽鏂紝鎵�鏈夊垎鏀兘浼氭墽琛� | 鉁� |
+| 鍖呭鍒嗘敮 | 锛堟潯浠跺垎鏀� + 骞惰鍒嗘敮鐨勭粨鍚堜綋锛夊厑璁稿熀浜庢潯浠堕�夋嫨澶氭潯鍒嗘敮鎵ц锛屼絾濡傛灉娌℃湁浠讳綍涓�涓垎鏀弧瓒虫潯浠讹紝鍒欏彲浠ラ�夋嫨榛樿鍒嗘敮 | 鉁� |
+| 璺敱鍒嗘敮 | 鏍规嵁鏉′欢閫夋嫨涓�涓垎鏀墽琛岋紙閲嶅畾鍚戝埌鎸囧畾閰嶇疆鑺傜偣锛夛紝涔熷彲浠ラ�夋嫨榛樿鍒嗘敮鎵ц锛堢户缁線涓嬫墽琛岋級 | 鉁� |
+| 瑙﹀彂鑺傜偣 | 鎵ц鍒拌鑺傜偣锛岃Е鍙� HTTP 璇锋眰銆丠TTP 鍥炶皟銆佹洿鏂版暟鎹�佸垹闄ゆ暟鎹瓑 | 鉁� |
+| 寤惰繜鑺傜偣 | 鎵ц鍒拌鑺傜偣锛屽鎵圭瓑寰呬竴娈垫椂闂村啀鎵ц锛屾敮鎸佸浐瀹氭椂闀裤�佸浐瀹氭棩鏈熺瓑 | 鉁� |
+| 鎷撳睍璁剧疆 | 娴佺▼鍓嶇疆/鍚庣疆閫氱煡锛岃妭鐐癸紙浠诲姟锛夊墠缃�佸悗缃�氱煡锛屾祦绋嬫姤琛紝鑷姩瀹℃壒鍘婚噸锛岃嚜瀹氭祦绋嬬紪鍙枫�佹爣棰樸�佹憳瑕侊紝娴佺▼鎶ヨ〃绛� | 鉁� |
+
+### 鏀粯绯荤粺
+
+| | 鍔熻兘 | 鎻忚堪 |
+|-----|------|---------------------------|
+| 馃殌 | 搴旂敤淇℃伅 | 閰嶇疆鍟嗘埛鐨勫簲鐢ㄤ俊鎭紝瀵规帴鏀粯瀹濄�佸井淇$瓑澶氫釜鏀粯娓犻亾 |
+| 馃殌 | 鏀粯璁㈠崟 | 鏌ョ湅鐢ㄦ埛鍙戣捣鐨勬敮浠樺疂銆佸井淇$瓑鐨勩�愭敮浠樸�戣鍗� |
+| 馃殌 | 閫�娆捐鍗� | 鏌ョ湅鐢ㄦ埛鍙戣捣鐨勬敮浠樺疂銆佸井淇$瓑鐨勩�愰��娆俱�戣鍗� |
+| 馃殌 | 鍥炶皟閫氱煡 | 鏌ョ湅鏀粯鍥炶皟涓氬姟鐨勩�愭敮浠樸�戙�愰��娆俱�戠殑閫氱煡缁撴灉 |
+| 馃殌 | 鎺ュ叆绀轰緥 | 鎻愪緵鎺ュ叆鏀粯绯荤粺鐨勩�愭敮浠樸�戙�愰��娆俱�戠殑鍔熻兘瀹炴垬 |
+
+### 鍩虹璁炬柦
+
+| | 鍔熻兘 | 鎻忚堪 |
+|-----|-----------|----------------------------------------------|
+| 馃殌 | 浠g爜鐢熸垚 | 鍓嶅悗绔唬鐮佺殑鐢熸垚锛圝ava銆乂ue銆丼QL銆佸崟鍏冩祴璇曪級锛屾敮鎸� CRUD 涓嬭浇 |
+| 馃殌 | 绯荤粺鎺ュ彛 | 鍩轰簬 Swagger 鑷姩鐢熸垚鐩稿叧鐨� RESTful API 鎺ュ彛鏂囨。 |
+| 馃殌 | 鏁版嵁搴撴枃妗� | 鍩轰簬 Screw 鑷姩鐢熸垚鏁版嵁搴撴枃妗o紝鏀寔瀵煎嚭 Word銆丠TML銆丮D 鏍煎紡 |
+| | 琛ㄥ崟鏋勫缓 | 鎷栧姩琛ㄥ崟鍏冪礌鐢熸垚鐩稿簲鐨� HTML 浠g爜锛屾敮鎸佸鍑� JSON銆乂ue 鏂囦欢 |
+| 馃殌 | 閰嶇疆绠$悊 | 瀵圭郴缁熷姩鎬侀厤缃父鐢ㄥ弬鏁帮紝鏀寔 SpringBoot 鍔犺浇 |
+| 猸愶笍 | 瀹氭椂浠诲姟 | 鍦ㄧ嚎锛堟坊鍔犮�佷慨鏀广�佸垹闄�)浠诲姟璋冨害鍖呭惈鎵ц缁撴灉鏃ュ織 |
+| 馃殌 | 鏂囦欢鏈嶅姟 | 鏀寔灏嗘枃浠跺瓨鍌ㄥ埌 S3锛圡inIO銆侀樋閲屼簯銆佽吘璁簯銆佷竷鐗涗簯锛夈�佹湰鍦般�丗TP銆佹暟鎹簱绛� |
+| 馃殌 | WebSocket | 鎻愪緵 WebSocket 鎺ュ叆绀轰緥锛屾敮鎸佷竴瀵逛竴銆佷竴瀵瑰鍙戦�佹柟寮� |
+| 馃殌 | API 鏃ュ織 | 鍖呮嫭 RESTful API 璁块棶鏃ュ織銆佸紓甯告棩蹇椾袱閮ㄥ垎锛屾柟渚挎帓鏌� API 鐩稿叧鐨勯棶棰� |
+| | MySQL 鐩戞帶 | 鐩戣褰撳墠绯荤粺鏁版嵁搴撹繛鎺ユ睜鐘舵�侊紝鍙繘琛屽垎鏋怱QL鎵惧嚭绯荤粺鎬ц兘鐡堕 |
+| | Redis 鐩戞帶 | 鐩戞帶 Redis 鏁版嵁搴撶殑浣跨敤鎯呭喌锛屼娇鐢ㄧ殑 Redis Key 绠$悊 |
+| 馃殌 | 娑堟伅闃熷垪 | 鍩轰簬 Redis 瀹炵幇娑堟伅闃熷垪锛孲tream 鎻愪緵闆嗙兢娑堣垂锛孭ub/Sub 鎻愪緵骞挎挱娑堣垂 |
+| 馃殌 | Java 鐩戞帶 | 鍩轰簬 Spring Boot Admin 瀹炵幇 Java 搴旂敤鐨勭洃鎺� |
+| 馃殌 | 閾捐矾杩借釜 | 鎺ュ叆 SkyWalking 缁勪欢锛屽疄鐜伴摼璺拷韪� |
+| 馃殌 | 鏃ュ織涓績 | 鎺ュ叆 SkyWalking 缁勪欢锛屽疄鐜版棩蹇椾腑蹇� |
+| 馃殌 | 鏈嶅姟淇濋殰 | 鍩轰簬 Redis 瀹炵幇鍒嗗竷寮忛攣銆佸箓绛夈�侀檺娴佸姛鑳斤紝婊¤冻楂樺苟鍙戝満鏅� |
+| 馃殌 | 鏃ュ織鏈嶅姟 | 杞婚噺绾ф棩蹇椾腑蹇冿紝鏌ョ湅杩滅▼鏈嶅姟鍣ㄧ殑鏃ュ織 |
+| 馃殌 | 鍗曞厓娴嬭瘯 | 鍩轰簬 JUnit + Mockito 瀹炵幇鍗曞厓娴嬭瘯锛屼繚璇佸姛鑳界殑姝g‘鎬с�佷唬鐮佺殑璐ㄩ噺绛� |
+
+ |  |  |
+| 鐢ㄦ埛 & 搴旂敤 |  |  |  |
+| 绉熸埛 & 濂楅 |  |  | - |
+| 閮ㄩ棬 & 宀椾綅 |  |  | - |
+| 鑿滃崟 & 瑙掕壊 |  |  | - |
+| 瀹¤鏃ュ織 |  |  | - |
+| 鐭俊 |  |  |  |
+| 瀛楀吀 & 鏁忔劅璇� |  |  |  | - |
+
+### 宸ヤ綔娴佺▼
+
+| 妯″潡 | biu | biu | biu |
+|---------|---------------------------------|---------------------------------|---------------------------------|
+| 娴佺▼妯″瀷 |  |  |  |
+| 琛ㄥ崟 & 鍒嗙粍 |  |  | - |
+| 鎴戠殑娴佺▼ |  |  |  |
+| 寰呭姙 & 宸插姙 |  |  |  |
+| OA 璇峰亣 |  |  |  |
+
+### 鍩虹璁炬柦
+
+| 妯″潡 | biu | biu | biu |
+|---------------|-------------------------------|-----------------------------|---------------------------|
+| 浠g爜鐢熸垚 |  |  | - |
+| 鏂囨。 |  |  |  |  |
+| 瀹氭椂浠诲姟 |  |  | - |
+| API 鏃ュ織 |  |  | - |
+| MySQL & Redis |  |  | - |
+| 鐩戞帶骞冲彴 |  |  |  |
+
+### 鏀粯绯荤粺
+
+| 妯″潡 | biu | biu | biu |
+|---------|---------------------------|---------------------------------|---------------------------------|
+| 鍟嗗 & 搴旂敤 |  |  |  |
+| 鏀粯 & 閫�娆� |  |  |  |  |
+| 澶у睆璁捐鍣� |  |  |  |
diff --git a/build/vite/index.ts b/build/vite/index.ts
new file mode 100644
index 0000000..c064cc5
--- /dev/null
+++ b/build/vite/index.ts
@@ -0,0 +1,99 @@
+import { resolve } from 'path'
+import Vue from '@vitejs/plugin-vue'
+import VueJsx from '@vitejs/plugin-vue-jsx'
+import progress from 'vite-plugin-progress'
+import EslintPlugin from 'vite-plugin-eslint'
+import PurgeIcons from 'vite-plugin-purge-icons'
+import { ViteEjsPlugin } from 'vite-plugin-ejs'
+// @ts-ignore
+import ElementPlus from 'unplugin-element-plus/vite'
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import viteCompression from 'vite-plugin-compression'
+import topLevelAwait from 'vite-plugin-top-level-await'
+import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'
+import UnoCSS from 'unocss/vite'
+
+export function createVitePlugins() {
+ const root = process.cwd()
+
+ // 璺緞鏌ユ壘
+ function pathResolve(dir: string) {
+ return resolve(root, '.', dir)
+ }
+
+ return [
+ Vue(),
+ VueJsx(),
+ UnoCSS(),
+ progress(),
+ PurgeIcons(),
+ ElementPlus({}),
+ AutoImport({
+ include: [
+ /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
+ /\.vue$/,
+ /\.vue\?vue/, // .vue
+ /\.md$/ // .md
+ ],
+ imports: [
+ 'vue',
+ 'vue-router',
+ // 鍙澶栨坊鍔犻渶瑕� autoImport 鐨勭粍浠�
+ {
+ '@/hooks/web/useI18n': ['useI18n'],
+ '@/hooks/web/useMessage': ['useMessage'],
+ '@/hooks/web/useTable': ['useTable'],
+ '@/hooks/web/useCrudSchemas': ['useCrudSchemas'],
+ '@/utils/formRules': ['required'],
+ '@/utils/dict': ['DICT_TYPE']
+ }
+ ],
+ dts: 'src/types/auto-imports.d.ts',
+ resolvers: [ElementPlusResolver()],
+ eslintrc: {
+ enabled: false, // Default `false`
+ filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
+ globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
+ }
+ }),
+ Components({
+ // 鐢熸垚鑷畾涔� `auto-components.d.ts` 鍏ㄥ眬澹版槑
+ dts: 'src/types/auto-components.d.ts',
+ // 鑷畾涔夌粍浠剁殑瑙f瀽鍣�
+ resolvers: [ElementPlusResolver()],
+ globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**']
+ }),
+ EslintPlugin({
+ cache: false,
+ include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 妫�鏌ョ殑鏂囦欢
+ }),
+ VueI18nPlugin({
+ runtimeOnly: true,
+ compositionOnly: true,
+ include: [resolve(__dirname, 'src/locales/**')]
+ }),
+ createSvgIconsPlugin({
+ iconDirs: [pathResolve('src/assets/svgs')],
+ symbolId: 'icon-[dir]-[name]',
+ }),
+ viteCompression({
+ verbose: true, // 鏄惁鍦ㄦ帶鍒跺彴杈撳嚭鍘嬬缉缁撴灉
+ disable: false, // 鏄惁绂佺敤
+ threshold: 10240, // 浣撶Н澶т簬 threshold 鎵嶄細琚帇缂�,鍗曚綅 b
+ algorithm: 'gzip', // 鍘嬬缉绠楁硶,鍙�� [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
+ ext: '.gz', // 鐢熸垚鐨勫帇缂╁寘鍚庣紑
+ deleteOriginFile: false //鍘嬬缉鍚庢槸鍚﹀垹闄ゆ簮鏂囦欢
+ }),
+ ViteEjsPlugin(),
+ topLevelAwait({
+ // https://juejin.cn/post/7152191742513512485
+ // The export name of top-level await promise for each chunk module
+ promiseExportName: '__tla',
+ // The function to generate import names of top-level await promise in each chunk module
+ promiseImportName: (i) => `__tla_${i}`
+ })
+ ]
+}
diff --git a/build/vite/optimize.ts b/build/vite/optimize.ts
new file mode 100644
index 0000000..7c47889
--- /dev/null
+++ b/build/vite/optimize.ts
@@ -0,0 +1,124 @@
+const include = [
+ 'qs',
+ 'url',
+ 'vue',
+ 'sass',
+ 'mitt',
+ 'axios',
+ 'pinia',
+ 'dayjs',
+ 'qrcode',
+ 'unocss',
+ 'vue-router',
+ 'vue-types',
+ 'vue-i18n',
+ 'crypto-js',
+ 'cropperjs',
+ 'lodash-es',
+ 'nprogress',
+ 'web-storage-cache',
+ '@iconify/iconify',
+ '@vueuse/core',
+ '@zxcvbn-ts/core',
+ 'echarts/core',
+ 'echarts/charts',
+ 'echarts/components',
+ 'echarts/renderers',
+ 'echarts-wordcloud',
+ '@wangeditor-next/editor',
+ '@wangeditor-next/editor-for-vue',
+ '@microsoft/fetch-event-source',
+ 'markdown-it',
+ 'markmap-view',
+ 'markmap-lib',
+ 'markmap-toolbar',
+ 'highlight.js',
+ 'element-plus',
+ 'element-plus/es',
+ 'element-plus/es/locale/lang/zh-cn',
+ 'element-plus/es/locale/lang/en',
+ 'element-plus/es/components/avatar/style/css',
+ 'element-plus/es/components/space/style/css',
+ 'element-plus/es/components/backtop/style/css',
+ 'element-plus/es/components/form/style/css',
+ 'element-plus/es/components/radio-group/style/css',
+ 'element-plus/es/components/radio/style/css',
+ 'element-plus/es/components/checkbox/style/css',
+ 'element-plus/es/components/checkbox-group/style/css',
+ 'element-plus/es/components/switch/style/css',
+ 'element-plus/es/components/time-picker/style/css',
+ 'element-plus/es/components/date-picker/style/css',
+ 'element-plus/es/components/descriptions/style/css',
+ 'element-plus/es/components/descriptions-item/style/css',
+ 'element-plus/es/components/link/style/css',
+ 'element-plus/es/components/tooltip/style/css',
+ 'element-plus/es/components/drawer/style/css',
+ 'element-plus/es/components/dialog/style/css',
+ 'element-plus/es/components/checkbox-button/style/css',
+ 'element-plus/es/components/option-group/style/css',
+ 'element-plus/es/components/radio-button/style/css',
+ 'element-plus/es/components/cascader/style/css',
+ 'element-plus/es/components/color-picker/style/css',
+ 'element-plus/es/components/input-number/style/css',
+ 'element-plus/es/components/rate/style/css',
+ 'element-plus/es/components/select-v2/style/css',
+ 'element-plus/es/components/tree-select/style/css',
+ 'element-plus/es/components/slider/style/css',
+ 'element-plus/es/components/time-select/style/css',
+ 'element-plus/es/components/autocomplete/style/css',
+ 'element-plus/es/components/image-viewer/style/css',
+ 'element-plus/es/components/upload/style/css',
+ 'element-plus/es/components/col/style/css',
+ 'element-plus/es/components/form-item/style/css',
+ 'element-plus/es/components/alert/style/css',
+ 'element-plus/es/components/breadcrumb/style/css',
+ 'element-plus/es/components/select/style/css',
+ 'element-plus/es/components/input/style/css',
+ 'element-plus/es/components/breadcrumb-item/style/css',
+ 'element-plus/es/components/tag/style/css',
+ 'element-plus/es/components/pagination/style/css',
+ 'element-plus/es/components/table/style/css',
+ 'element-plus/es/components/table-v2/style/css',
+ 'element-plus/es/components/table-column/style/css',
+ 'element-plus/es/components/card/style/css',
+ 'element-plus/es/components/row/style/css',
+ 'element-plus/es/components/button/style/css',
+ 'element-plus/es/components/menu/style/css',
+ 'element-plus/es/components/sub-menu/style/css',
+ 'element-plus/es/components/menu-item/style/css',
+ 'element-plus/es/components/option/style/css',
+ 'element-plus/es/components/dropdown/style/css',
+ 'element-plus/es/components/dropdown-menu/style/css',
+ 'element-plus/es/components/dropdown-item/style/css',
+ 'element-plus/es/components/skeleton/style/css',
+ 'element-plus/es/components/skeleton/style/css',
+ 'element-plus/es/components/backtop/style/css',
+ 'element-plus/es/components/menu/style/css',
+ 'element-plus/es/components/sub-menu/style/css',
+ 'element-plus/es/components/menu-item/style/css',
+ 'element-plus/es/components/dropdown/style/css',
+ 'element-plus/es/components/tree/style/css',
+ 'element-plus/es/components/dropdown-menu/style/css',
+ 'element-plus/es/components/dropdown-item/style/css',
+ 'element-plus/es/components/badge/style/css',
+ 'element-plus/es/components/breadcrumb/style/css',
+ 'element-plus/es/components/breadcrumb-item/style/css',
+ 'element-plus/es/components/image/style/css',
+ 'element-plus/es/components/collapse-transition/style/css',
+ 'element-plus/es/components/timeline/style/css',
+ 'element-plus/es/components/timeline-item/style/css',
+ 'element-plus/es/components/collapse/style/css',
+ 'element-plus/es/components/collapse-item/style/css',
+ 'element-plus/es/components/button-group/style/css',
+ 'element-plus/es/components/text/style/css',
+ 'element-plus/es/components/segmented/style/css',
+ '@element-plus/icons-vue',
+ 'element-plus/es/components/footer/style/css',
+ 'element-plus/es/components/empty/style/css',
+ 'element-plus/es/components/mention/style/css',
+ 'element-plus/es/components/progress/style/css'
+]
+
+const exclude = ['@iconify/json']
+
+export { include, exclude }
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..8cfcbef
--- /dev/null
+++ b/index.html
@@ -0,0 +1,151 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" href="/favicon.ico" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <meta
+ name="keywords"
+ content="鑺嬮亾绠$悊绯荤粺 鍩轰簬 vue3 + CompositionAPI + typescript + vite3 + element plus 鐨勫悗鍙板紑婧愬厤璐圭鐞嗙郴缁燂紒"
+ />
+ <meta
+ name="description"
+ content="鑺嬮亾绠$悊绯荤粺 鍩轰簬 vue3 + CompositionAPI + typescript + vite3 + element plus 鐨勫悗鍙板紑婧愬厤璐圭鐞嗙郴缁燂紒"
+ />
+ <title>%VITE_APP_TITLE%</title>
+ </head>
+ <body>
+ <div id="app">
+ <style>
+ .app-loading {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ background: #f0f2f5;
+ }
+
+ .app-loading .app-loading-wrap {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ display: flex;
+ -webkit-transform: translate3d(-50%, -50%, 0);
+ transform: translate3d(-50%, -50%, 0);
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ }
+
+ .app-loading .app-loading-title {
+ margin-bottom: 30px;
+ font-size: 20px;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ .app-loading .app-loading-logo {
+ width: 100px;
+ margin: 0 auto 15px auto;
+ }
+
+ .app-loading .app-loading-item {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 60px;
+ vertical-align: middle;
+ border-radius: 50%;
+ }
+
+ .app-loading .app-loading-outter {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border: 4px solid #2d8cf0;
+ border-bottom: 0;
+ border-left-color: transparent;
+ border-radius: 50%;
+ animation: loader-outter 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
+ }
+
+ .app-loading .app-loading-inner {
+ position: absolute;
+ top: calc(50% - 20px);
+ left: calc(50% - 20px);
+ width: 40px;
+ height: 40px;
+ border: 4px solid #87bdff;
+ border-right: 0;
+ border-top-color: transparent;
+ border-radius: 50%;
+ animation: loader-inner 1s cubic-bezier(0.42, 0.61, 0.58, 0.41) infinite;
+ }
+
+ @-webkit-keyframes loader-outter {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+ }
+
+ @keyframes loader-outter {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+ }
+
+ @-webkit-keyframes loader-inner {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(-360deg);
+ transform: rotate(-360deg);
+ }
+ }
+
+ @keyframes loader-inner {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ -webkit-transform: rotate(-360deg);
+ transform: rotate(-360deg);
+ }
+ }
+ </style>
+ <div class="app-loading">
+ <div class="app-loading-wrap">
+ <div class="app-loading-title">
+ <img src="/logo.gif" class="app-loading-logo" alt="Logo" />
+ <div class="app-loading-title">%VITE_APP_TITLE%</div>
+ </div>
+ <div class="app-loading-item">
+ <div class="app-loading-outter"></div>
+ <div class="app-loading-inner"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <script type="module" src="/src/main.ts"></script>
+ </body>
+</html>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..2dc52f6
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,17553 @@
+{
+ "name": "yudao-ui-admin-vue3",
+ "version": "2025.12-snapshot",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "yudao-ui-admin-vue3",
+ "version": "2025.12-snapshot",
+ "license": "MIT",
+ "dependencies": {
+ "@element-plus/icons-vue": "2.3.2",
+ "@form-create/designer": "^3.2.6",
+ "@form-create/element-ui": "^3.2.11",
+ "@iconify/iconify": "^3.1.1",
+ "@microsoft/fetch-event-source": "^2.0.1",
+ "@videojs-player/vue": "^1.0.0",
+ "@vueuse/core": "^10.9.0",
+ "@wangeditor-next/editor": "^5.6.46",
+ "@wangeditor-next/editor-for-vue": "^5.1.14",
+ "@wangeditor-next/plugin-mention": "^1.0.16",
+ "@zxcvbn-ts/core": "^3.0.4",
+ "animate.css": "^4.1.1",
+ "axios": "1.9.0",
+ "benz-amr-recorder": "^1.1.5",
+ "bpmn-js-token-simulation": "^0.36.0",
+ "camunda-bpmn-moddle": "^7.0.1",
+ "cropperjs": "^1.6.1",
+ "crypto-js": "^4.2.0",
+ "dayjs": "^1.11.10",
+ "diagram-js": "^12.8.0",
+ "driver.js": "^1.3.1",
+ "echarts": "^5.5.0",
+ "echarts-wordcloud": "^2.1.0",
+ "element-plus": "2.11.1",
+ "fast-xml-parser": "^4.3.2",
+ "highlight.js": "^11.9.0",
+ "jsencrypt": "^3.3.2",
+ "jsoneditor": "^10.1.3",
+ "lodash-es": "^4.17.21",
+ "markdown-it": "^14.1.0",
+ "markmap-common": "^0.16.0",
+ "markmap-lib": "^0.16.1",
+ "markmap-toolbar": "^0.17.0",
+ "markmap-view": "^0.16.0",
+ "min-dash": "^4.1.1",
+ "mitt": "^3.0.1",
+ "nprogress": "^0.2.0",
+ "pinia": "^2.1.7",
+ "pinia-plugin-persistedstate": "^3.2.1",
+ "qrcode": "^1.5.3",
+ "qs": "^6.12.0",
+ "snabbdom": "^3.6.2",
+ "sortablejs": "^1.15.3",
+ "steady-xml": "^0.1.0",
+ "url": "^0.11.3",
+ "video.js": "^7.21.5",
+ "vue": "3.5.12",
+ "vue-dompurify-html": "^4.1.4",
+ "vue-i18n": "9.10.2",
+ "vue-router": "4.4.5",
+ "vue-types": "^5.1.1",
+ "vue3-print-nb": "^0.1.4",
+ "vue3-signature": "^0.2.4",
+ "vuedraggable": "^4.1.0",
+ "web-storage-cache": "^1.1.1",
+ "xml-js": "^1.6.11"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^19.0.1",
+ "@commitlint/config-conventional": "^19.0.0",
+ "@iconify/json": "^2.2.187",
+ "@intlify/unplugin-vue-i18n": "^2.0.0",
+ "@purge-icons/generated": "^0.9.0",
+ "@types/jsoneditor": "^9.9.5",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^20.11.21",
+ "@types/nprogress": "^0.2.3",
+ "@types/qrcode": "^1.5.5",
+ "@types/qs": "^6.9.12",
+ "@typescript-eslint/eslint-plugin": "^7.1.0",
+ "@typescript-eslint/parser": "^7.1.0",
+ "@unocss/eslint-config": "^0.57.4",
+ "@unocss/eslint-plugin": "66.1.0-beta.5",
+ "@unocss/transformer-variant-group": "^0.58.5",
+ "@vitejs/plugin-legacy": "^5.3.1",
+ "@vitejs/plugin-vue": "^5.0.4",
+ "@vitejs/plugin-vue-jsx": "^3.1.0",
+ "autoprefixer": "^10.4.17",
+ "bpmn-js": "^17.9.2",
+ "bpmn-js-properties-panel": "5.23.0",
+ "consola": "^3.2.3",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-define-config": "^2.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-vue": "^9.22.0",
+ "lint-staged": "^15.2.2",
+ "postcss": "^8.4.35",
+ "postcss-html": "^1.6.0",
+ "postcss-scss": "^4.0.9",
+ "prettier": "^3.2.5",
+ "prettier-eslint": "^16.3.0",
+ "rimraf": "^5.0.5",
+ "rollup": "^4.12.0",
+ "sass": "^1.69.5",
+ "stylelint": "^16.2.1",
+ "stylelint-config-html": "^1.1.0",
+ "stylelint-config-recommended": "^14.0.0",
+ "stylelint-config-standard": "^36.0.0",
+ "stylelint-order": "^6.0.4",
+ "terser": "^5.28.1",
+ "typescript": "5.3.3",
+ "unocss": "^0.58.5",
+ "unplugin-auto-import": "^0.16.7",
+ "unplugin-element-plus": "^0.8.0",
+ "unplugin-vue-components": "^0.25.2",
+ "vite": "5.1.4",
+ "vite-plugin-compression": "^0.5.1",
+ "vite-plugin-ejs": "^1.7.0",
+ "vite-plugin-eslint": "^1.8.1",
+ "vite-plugin-progress": "^0.0.7",
+ "vite-plugin-purge-icons": "^0.10.0",
+ "vite-plugin-svg-icons-ng": "^1.3.1",
+ "vite-plugin-top-level-await": "^1.4.4",
+ "vue-eslint-parser": "^9.3.2",
+ "vue-tsc": "^1.8.27"
+ },
+ "engines": {
+ "node": ">= 16.0.0",
+ "pnpm": ">=8.6.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@antfu/install-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.3.0",
+ "tinyexec": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@antfu/utils": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-8.1.1.tgz",
+ "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.27.2.tgz",
+ "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.27.1.tgz",
+ "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.1",
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helpers": "^7.27.1",
+ "@babel/parser": "^7.27.1",
+ "@babel/template": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.27.1.tgz",
+ "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.1",
+ "@babel/types": "^7.27.1",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz",
+ "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz",
+ "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "regexpu-core": "^6.2.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-define-polyfill-provider": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz",
+ "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.22.6",
+ "@babel/helper-plugin-utils": "^7.22.5",
+ "debug": "^4.1.1",
+ "lodash.debounce": "^4.0.8",
+ "resolve": "^1.14.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz",
+ "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-remap-async-to-generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-wrap-function": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-wrap-function": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz",
+ "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.27.1.tgz",
+ "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.2.tgz",
+ "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz",
+ "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
+ "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
+ "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.13.0"
+ }
+ },
+ "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz",
+ "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-private-property-in-object": {
+ "version": "7.21.0-placeholder-for-preset-env.2",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
+ "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-assertions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz",
+ "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-unicode-sets-regex": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
+ "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-arrow-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz",
+ "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-generator-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz",
+ "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-remap-async-to-generator": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-async-to-generator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz",
+ "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-remap-async-to-generator": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoped-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
+ "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-block-scoping": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz",
+ "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz",
+ "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-class-static-block": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz",
+ "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.12.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-classes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz",
+ "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/traverse": "^7.27.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-computed-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz",
+ "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/template": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-destructuring": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz",
+ "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dotall-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz",
+ "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-keys": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
+ "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-dynamic-import": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
+ "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-exponentiation-operator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
+ "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-export-namespace-from": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz",
+ "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-for-of": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz",
+ "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-function-name": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz",
+ "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-json-strings": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz",
+ "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz",
+ "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-logical-assignment-operators": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz",
+ "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-member-expression-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
+ "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-amd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
+ "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-systemjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
+ "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-umd": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
+ "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-named-capturing-groups-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz",
+ "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-new-target": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
+ "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-nullish-coalescing-operator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz",
+ "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-numeric-separator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz",
+ "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-rest-spread": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.2.tgz",
+ "integrity": "sha512-AIUHD7xJ1mCrj3uPozvtngY3s0xpv7Nu7DoUSnzNY6Xam1Cy4rUznR//pvMHOhQ4AvbCexhbqXCtpxGHOGOO6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.27.1",
+ "@babel/plugin-transform-parameters": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-object-super": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
+ "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-catch-binding": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz",
+ "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-optional-chaining": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
+ "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-parameters": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz",
+ "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-methods": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz",
+ "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-private-property-in-object": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz",
+ "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-property-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
+ "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regenerator": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz",
+ "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-regexp-modifiers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz",
+ "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-reserved-words": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
+ "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-shorthand-properties": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz",
+ "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-spread": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz",
+ "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-sticky-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz",
+ "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-template-literals": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
+ "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typeof-symbol": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
+ "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.1.tgz",
+ "integrity": "sha512-Q5sT5+O4QUebHdbwKedFBEwRLb02zJ7r4A5Gg2hUoLuU3FjdMcyqcywqUrLCaDsFCxzokf7u9kuy7qz51YUuAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.1",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-escapes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
+ "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-property-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz",
+ "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz",
+ "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-unicode-sets-regex": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz",
+ "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-regexp-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/preset-env": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.27.2.tgz",
+ "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1",
+ "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
+ "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
+ "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
+ "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1",
+ "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2",
+ "@babel/plugin-syntax-import-assertions": "^7.27.1",
+ "@babel/plugin-syntax-import-attributes": "^7.27.1",
+ "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6",
+ "@babel/plugin-transform-arrow-functions": "^7.27.1",
+ "@babel/plugin-transform-async-generator-functions": "^7.27.1",
+ "@babel/plugin-transform-async-to-generator": "^7.27.1",
+ "@babel/plugin-transform-block-scoped-functions": "^7.27.1",
+ "@babel/plugin-transform-block-scoping": "^7.27.1",
+ "@babel/plugin-transform-class-properties": "^7.27.1",
+ "@babel/plugin-transform-class-static-block": "^7.27.1",
+ "@babel/plugin-transform-classes": "^7.27.1",
+ "@babel/plugin-transform-computed-properties": "^7.27.1",
+ "@babel/plugin-transform-destructuring": "^7.27.1",
+ "@babel/plugin-transform-dotall-regex": "^7.27.1",
+ "@babel/plugin-transform-duplicate-keys": "^7.27.1",
+ "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
+ "@babel/plugin-transform-dynamic-import": "^7.27.1",
+ "@babel/plugin-transform-exponentiation-operator": "^7.27.1",
+ "@babel/plugin-transform-export-namespace-from": "^7.27.1",
+ "@babel/plugin-transform-for-of": "^7.27.1",
+ "@babel/plugin-transform-function-name": "^7.27.1",
+ "@babel/plugin-transform-json-strings": "^7.27.1",
+ "@babel/plugin-transform-literals": "^7.27.1",
+ "@babel/plugin-transform-logical-assignment-operators": "^7.27.1",
+ "@babel/plugin-transform-member-expression-literals": "^7.27.1",
+ "@babel/plugin-transform-modules-amd": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-modules-systemjs": "^7.27.1",
+ "@babel/plugin-transform-modules-umd": "^7.27.1",
+ "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
+ "@babel/plugin-transform-new-target": "^7.27.1",
+ "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
+ "@babel/plugin-transform-numeric-separator": "^7.27.1",
+ "@babel/plugin-transform-object-rest-spread": "^7.27.2",
+ "@babel/plugin-transform-object-super": "^7.27.1",
+ "@babel/plugin-transform-optional-catch-binding": "^7.27.1",
+ "@babel/plugin-transform-optional-chaining": "^7.27.1",
+ "@babel/plugin-transform-parameters": "^7.27.1",
+ "@babel/plugin-transform-private-methods": "^7.27.1",
+ "@babel/plugin-transform-private-property-in-object": "^7.27.1",
+ "@babel/plugin-transform-property-literals": "^7.27.1",
+ "@babel/plugin-transform-regenerator": "^7.27.1",
+ "@babel/plugin-transform-regexp-modifiers": "^7.27.1",
+ "@babel/plugin-transform-reserved-words": "^7.27.1",
+ "@babel/plugin-transform-shorthand-properties": "^7.27.1",
+ "@babel/plugin-transform-spread": "^7.27.1",
+ "@babel/plugin-transform-sticky-regex": "^7.27.1",
+ "@babel/plugin-transform-template-literals": "^7.27.1",
+ "@babel/plugin-transform-typeof-symbol": "^7.27.1",
+ "@babel/plugin-transform-unicode-escapes": "^7.27.1",
+ "@babel/plugin-transform-unicode-property-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-regex": "^7.27.1",
+ "@babel/plugin-transform-unicode-sets-regex": "^7.27.1",
+ "@babel/preset-modules": "0.1.6-no-external-plugins",
+ "babel-plugin-polyfill-corejs2": "^0.4.10",
+ "babel-plugin-polyfill-corejs3": "^0.11.0",
+ "babel-plugin-polyfill-regenerator": "^0.6.1",
+ "core-js-compat": "^3.40.0",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-env/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/preset-modules": {
+ "version": "0.1.6-no-external-plugins",
+ "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
+ "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@babel/types": "^7.4.4",
+ "esutils": "^2.0.2"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
+ "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.1.tgz",
+ "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.27.1.tgz",
+ "integrity": "sha512-909rVuj3phpjW6y0MCXAZ5iNeORePa6ldJvp2baWGcTjwqbBDDz6xoS5JHJ7lS88NlwLYj07ImL/8IUMtDZzTA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-js-pure": "^3.30.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.27.1.tgz",
+ "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.1",
+ "@babel/parser": "^7.27.1",
+ "@babel/template": "^7.27.1",
+ "@babel/types": "^7.27.1",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.1.tgz",
+ "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bpmn-io/cm-theme": {
+ "version": "0.1.0-alpha.2",
+ "resolved": "https://registry.npmmirror.com/@bpmn-io/cm-theme/-/cm-theme-0.1.0-alpha.2.tgz",
+ "integrity": "sha512-ZILgiYzxk3KMvxplUXmdRFQo45/JehDPg5k9tWfehmzUOSE13ssyLPil8uCloMQnb3yyzyOWTjb/wzKXTHlFQw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/language": "^6.3.1",
+ "@codemirror/view": "^6.5.1",
+ "@lezer/highlight": "^1.1.4"
+ },
+ "workspaces": {
+ "packages": [
+ "preview-themes"
+ ]
+ }
+ },
+ "node_modules/@bpmn-io/diagram-js-ui": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmmirror.com/@bpmn-io/diagram-js-ui/-/diagram-js-ui-0.2.3.tgz",
+ "integrity": "sha512-OGyjZKvGK8tHSZ0l7RfeKhilGoOGtFDcoqSGYkX0uhFlo99OVZ9Jn1K7TJGzcE9BdKwvA5Y5kGqHEhdTxHvFfw==",
+ "license": "MIT",
+ "dependencies": {
+ "htm": "^3.1.1",
+ "preact": "^10.11.2"
+ }
+ },
+ "node_modules/@bpmn-io/extract-process-variables": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/@bpmn-io/extract-process-variables/-/extract-process-variables-0.8.0.tgz",
+ "integrity": "sha512-yAS7ZYX+D56K+luC36u96eRMLb4VHcPUwTUqMZ/Z/Je2gou2DJLRbuBTHAB4jjKt4wFCHSG4B8Y+TrBciEYf4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-dash": "^4.0.0"
+ }
+ },
+ "node_modules/@bpmn-io/feel-editor": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/@bpmn-io/feel-editor/-/feel-editor-1.10.0.tgz",
+ "integrity": "sha512-Unc4CSyMgDg5c2C3E3ehEbJZfyo5W9Zrq74C8cp7mjFbb3if6rTBaw3ZCZeiC06zsm881sI5P8zWHFdIhKo/vA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@bpmn-io/feel-lint": "^1.4.0",
+ "@codemirror/autocomplete": "^6.16.2",
+ "@codemirror/commands": "^6.8.0",
+ "@codemirror/language": "^6.10.2",
+ "@codemirror/lint": "^6.8.4",
+ "@codemirror/state": "^6.5.1",
+ "@codemirror/view": "^6.36.2",
+ "@lezer/highlight": "^1.2.1",
+ "lang-feel": "^2.3.0",
+ "min-dom": "^4.2.1"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/@bpmn-io/feel-lint": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/@bpmn-io/feel-lint/-/feel-lint-1.4.0.tgz",
+ "integrity": "sha512-1bsdR/9vPD7RQVqWWPk0X0tpjLsYTDrCxIzOVtN/h32o4nPGl0dZBU5m07qaFUGD4wG3eOH4Qim1wexHG8YkBw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/language": "^6.10.8",
+ "lezer-feel": "^1.7.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@bpmn-io/properties-panel": {
+ "version": "3.27.0",
+ "resolved": "https://registry.npmmirror.com/@bpmn-io/properties-panel/-/properties-panel-3.27.0.tgz",
+ "integrity": "sha512-Us1d3mP2CePMG3V0ElpPVoKuTN+ukxhb0Cjj6prxoRsEP2uWm0H62/tPsYc3SNdGcUb/YOdryBJLL+OcM/GaQw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@bpmn-io/feel-editor": "^1.10.0",
+ "@codemirror/view": "^6.28.1",
+ "classnames": "^2.3.1",
+ "feelers": "^1.4.0",
+ "focus-trap": "^7.5.2",
+ "min-dash": "^4.1.1",
+ "min-dom": "^4.0.3"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.18.6",
+ "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
+ "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmmirror.com/@codemirror/commands/-/commands-6.8.1.tgz",
+ "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.11.0",
+ "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.0.tgz",
+ "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.1.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.8.5",
+ "resolved": "https://registry.npmmirror.com/@codemirror/lint/-/lint-6.8.5.tgz",
+ "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.35.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz",
+ "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.36.8",
+ "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.36.8.tgz",
+ "integrity": "sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.5.0",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@commitlint/cli": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/cli/-/cli-19.8.1.tgz",
+ "integrity": "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/format": "^19.8.1",
+ "@commitlint/lint": "^19.8.1",
+ "@commitlint/load": "^19.8.1",
+ "@commitlint/read": "^19.8.1",
+ "@commitlint/types": "^19.8.1",
+ "tinyexec": "^1.0.0",
+ "yargs": "^17.0.0"
+ },
+ "bin": {
+ "commitlint": "cli.js"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-conventional": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-19.8.1.tgz",
+ "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^19.8.1",
+ "conventional-changelog-conventionalcommits": "^7.0.2"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/config-validator": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-19.8.1.tgz",
+ "integrity": "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^19.8.1",
+ "ajv": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/ensure": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-19.8.1.tgz",
+ "integrity": "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^19.8.1",
+ "lodash.camelcase": "^4.3.0",
+ "lodash.kebabcase": "^4.1.1",
+ "lodash.snakecase": "^4.1.1",
+ "lodash.startcase": "^4.4.0",
+ "lodash.upperfirst": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/execute-rule": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-19.8.1.tgz",
+ "integrity": "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/format": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/format/-/format-19.8.1.tgz",
+ "integrity": "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^19.8.1",
+ "chalk": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/is-ignored": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-19.8.1.tgz",
+ "integrity": "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^19.8.1",
+ "semver": "^7.6.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/lint": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/lint/-/lint-19.8.1.tgz",
+ "integrity": "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/is-ignored": "^19.8.1",
+ "@commitlint/parse": "^19.8.1",
+ "@commitlint/rules": "^19.8.1",
+ "@commitlint/types": "^19.8.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/load": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/load/-/load-19.8.1.tgz",
+ "integrity": "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^19.8.1",
+ "@commitlint/execute-rule": "^19.8.1",
+ "@commitlint/resolve-extends": "^19.8.1",
+ "@commitlint/types": "^19.8.1",
+ "chalk": "^5.3.0",
+ "cosmiconfig": "^9.0.0",
+ "cosmiconfig-typescript-loader": "^6.1.0",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.merge": "^4.6.2",
+ "lodash.uniq": "^4.5.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/message": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/message/-/message-19.8.1.tgz",
+ "integrity": "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/parse": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/parse/-/parse-19.8.1.tgz",
+ "integrity": "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/types": "^19.8.1",
+ "conventional-changelog-angular": "^7.0.0",
+ "conventional-commits-parser": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/read": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/read/-/read-19.8.1.tgz",
+ "integrity": "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/top-level": "^19.8.1",
+ "@commitlint/types": "^19.8.1",
+ "git-raw-commits": "^4.0.0",
+ "minimist": "^1.2.8",
+ "tinyexec": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/resolve-extends": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-19.8.1.tgz",
+ "integrity": "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/config-validator": "^19.8.1",
+ "@commitlint/types": "^19.8.1",
+ "global-directory": "^4.0.1",
+ "import-meta-resolve": "^4.0.0",
+ "lodash.mergewith": "^4.6.2",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/rules": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/rules/-/rules-19.8.1.tgz",
+ "integrity": "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@commitlint/ensure": "^19.8.1",
+ "@commitlint/message": "^19.8.1",
+ "@commitlint/to-lines": "^19.8.1",
+ "@commitlint/types": "^19.8.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/to-lines": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-19.8.1.tgz",
+ "integrity": "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/top-level": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-19.8.1.tgz",
+ "integrity": "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@commitlint/types": {
+ "version": "19.8.1",
+ "resolved": "https://registry.npmmirror.com/@commitlint/types/-/types-19.8.1.tgz",
+ "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/conventional-commits-parser": "^5.0.0",
+ "chalk": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=v18"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
+ "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.3"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
+ "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/media-query-list-parser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz",
+ "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+ "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@dual-bundle/import-meta-resolve": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
+ "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+ "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
+ "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
+ "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
+ "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
+ "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
+ "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
+ "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
+ "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
+ "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
+ "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
+ "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
+ "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
+ "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
+ "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
+ "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
+ "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
+ "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
+ "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
+ "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
+ "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
+ "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
+ "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
+ "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+ "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz",
+ "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.0.tgz",
+ "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.0.tgz",
+ "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.0",
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "license": "MIT"
+ },
+ "node_modules/@form-create/component-elm-checkbox": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-checkbox/-/component-elm-checkbox-3.2.18.tgz",
+ "integrity": "sha512-W8v4o+MZWPEJmIIWojKmnn87tFWpxyTbaIhWJU4Ca0S99YoXR7RdHKLt06HYwJixVLZqytNRj9BMxR4UZQ6JNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-elm-frame": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-frame/-/component-elm-frame-3.2.18.tgz",
+ "integrity": "sha512-yob3jmO1xBbKfVfFNeO/xh80o1E2IbVx8NsnpTaaK9X0ARJFvhPvW53qX6TgJxdvzCGTsm/W6P6a4SaebQJtVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-elm-group": {
+ "version": "3.2.22",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-group/-/component-elm-group-3.2.22.tgz",
+ "integrity": "sha512-hy6ZqLpDITqDoTMc4Es2twVNffjXuX0HW7aY36+iyicnJj1Y9hMRj2HPbT4DNlQWZ3ybOb/AlcYN0BwIpW40qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-elm-radio": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-radio/-/component-elm-radio-3.2.18.tgz",
+ "integrity": "sha512-kkb6xFOviqgoBRRLzsoZTnqKX9GSw2jaLCWWRPkwqEwA/aLNHRX0MuBdGNvpaaLaD1ph5g+N86GekHvvanbJlQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-elm-select": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-select/-/component-elm-select-3.2.18.tgz",
+ "integrity": "sha512-gqBzPgNGJ6GwQ/pK/qCuoxQeM/fflNv7IqibETt2IFgutsGVM1lXYic8QJ/51YkuI0afkDKF+wAEbfB6zqaKIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-elm-tree": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-tree/-/component-elm-tree-3.2.18.tgz",
+ "integrity": "sha512-s+0+NPh2t500pv4CA51dtwuWWlY2wW0qbL7ZE3steTBh8Z++7s+n/6y6joGPxTeP+7FkpfruA1TfJ7+5Ntpe1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-elm-upload": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-elm-upload/-/component-elm-upload-3.2.18.tgz",
+ "integrity": "sha512-FVFJYarlk5+/Kjg9kJ9ElwyP8bt+DR0m/pPUMqmkEoUtv0Sr6nkk596THLjfyXV1WRFeoZMjzIS6qCyTnqWksQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ }
+ },
+ "node_modules/@form-create/component-subform": {
+ "version": "3.1.34",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-subform/-/component-subform-3.1.34.tgz",
+ "integrity": "sha512-OJcFH/7MTHx7JLEjDK/weS27qfuFWAI+OK+gXTJ2jIt9aZkGWF/EWkjetiJLt5a0KMw4Z15wOS2XCY9pVK9vlA==",
+ "license": "MIT"
+ },
+ "node_modules/@form-create/component-wangeditor": {
+ "version": "3.2.14",
+ "resolved": "https://registry.npmmirror.com/@form-create/component-wangeditor/-/component-wangeditor-3.2.14.tgz",
+ "integrity": "sha512-N/U/hFBdBu2OIguxoKe1Kslq5fW6XmtyhKDImLfKLn1xI6X5WUtt3r7QTaUPcVUl2vntpM9wJ/FBdG17RzF/Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "wangeditor": "^4.6.0"
+ }
+ },
+ "node_modules/@form-create/core": {
+ "version": "3.2.22",
+ "resolved": "https://registry.npmmirror.com/@form-create/core/-/core-3.2.22.tgz",
+ "integrity": "sha512-GC3b4Yrpy9TiPLqJFL9fiUFPjEv6ZBcHnOMB+GeF6iLsMV4TpZc0o/oFBPlhZqIYeljaNuxJyO2ABCStceOrZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/utils": "^3.2.18"
+ },
+ "peerDependencies": {
+ "vue": "^3.1.0"
+ }
+ },
+ "node_modules/@form-create/designer": {
+ "version": "3.2.11",
+ "resolved": "https://registry.npmmirror.com/@form-create/designer/-/designer-3.2.11.tgz",
+ "integrity": "sha512-5mPyeHFOj8n01LOVhibjX8OujD6RYBH8TF2Ol7n8QxaSqIcAFTz9PADIiX982REPxiZ6I8BqZa2t0OtYQtETpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/component-wangeditor": "^3.1",
+ "@form-create/element-ui": "^3.2.19",
+ "@form-create/utils": "^3.2.0",
+ "codemirror": "^6.65.7",
+ "element-plus": "^2.8.4",
+ "js-beautify": "^1.15.1",
+ "vuedraggable": "4.1.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.1.5"
+ }
+ },
+ "node_modules/@form-create/element-ui": {
+ "version": "3.2.22",
+ "resolved": "https://registry.npmmirror.com/@form-create/element-ui/-/element-ui-3.2.22.tgz",
+ "integrity": "sha512-6UfJloHWwCDkei4dQjigk5JzaFQiwEISpY0Tc5plSyJg8bt7JdqCp6C9+OQYmjTYaurwzdTvgD9NfbKDFC8xEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@form-create/component-elm-checkbox": "^3.2.18",
+ "@form-create/component-elm-frame": "^3.2.18",
+ "@form-create/component-elm-group": "^3.2.22",
+ "@form-create/component-elm-radio": "^3.2.18",
+ "@form-create/component-elm-select": "^3.2.18",
+ "@form-create/component-elm-tree": "^3.2.18",
+ "@form-create/component-elm-upload": "^3.2.18",
+ "@form-create/component-subform": "^3.1.34",
+ "@form-create/core": "^3.2.22",
+ "@form-create/utils": "^3.2.18"
+ },
+ "peerDependencies": {
+ "vue": "^3.1.0"
+ }
+ },
+ "node_modules/@form-create/utils": {
+ "version": "3.2.18",
+ "resolved": "https://registry.npmmirror.com/@form-create/utils/-/utils-3.2.18.tgz",
+ "integrity": "sha512-C98bFPdFVMltiHQvEZqv4rVdhcqthJgvxMbWDlniL03HS5oyusnUvxUE8jf0I9zk5dZRDGmxKOUtzE3JDWP9nQ==",
+ "license": "MIT"
+ },
+ "node_modules/@gera2ld/jsx-dom": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmmirror.com/@gera2ld/jsx-dom/-/jsx-dom-2.2.2.tgz",
+ "integrity": "sha512-EOqf31IATRE6zS1W1EoWmXZhGfLAoO9FIlwTtHduSrBdud4npYBxYAkv8dZ5hudDPwJeeSjn40kbCL4wAzr8dA==",
+ "license": "ISC",
+ "dependencies": {
+ "@babel/runtime": "^7.21.5"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.13.0",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+ "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+ "deprecated": "Use @eslint/config-array instead",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+ "deprecated": "Use @eslint/object-schema instead",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@iconify/iconify": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/@iconify/iconify/-/iconify-3.1.1.tgz",
+ "integrity": "sha512-1nemfyD/OJzh9ALepH7YfuuP8BdEB24Skhd8DXWh0hzcOxImbb1ZizSZkpCzAwSZSGcJFmscIBaBQu+yLyWaxQ==",
+ "deprecated": "no longer maintained, switch to modern iconify-icon web component",
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/types": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/cyberalien"
+ }
+ },
+ "node_modules/@iconify/json": {
+ "version": "2.2.338",
+ "resolved": "https://registry.npmmirror.com/@iconify/json/-/json-2.2.338.tgz",
+ "integrity": "sha512-wKUTEGEQwkgbY+P84c2njj/D9iEXoKr/Gv6Abwnw+tuElkT57JsGfMFAUpZpaKDL5fTD/+noRRA4L10mFOtIYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/types": "*",
+ "pathe": "^1.1.2"
+ }
+ },
+ "node_modules/@iconify/types": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz",
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
+ "license": "MIT"
+ },
+ "node_modules/@iconify/utils": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-2.3.0.tgz",
+ "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^1.0.0",
+ "@antfu/utils": "^8.1.0",
+ "@iconify/types": "^2.0.0",
+ "debug": "^4.4.0",
+ "globals": "^15.14.0",
+ "kolorist": "^1.8.0",
+ "local-pkg": "^1.0.0",
+ "mlly": "^1.7.4"
+ }
+ },
+ "node_modules/@iconify/utils/node_modules/globals": {
+ "version": "15.15.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz",
+ "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@intlify/bundle-utils": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmmirror.com/@intlify/bundle-utils/-/bundle-utils-7.5.1.tgz",
+ "integrity": "sha512-UovJl10oBIlmYEcWw+VIHdKY5Uv5sdPG0b/b6bOYxGLln3UwB75+2dlc0F3Fsa0RhoznQ5Rp589/BZpABpE4Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/message-compiler": "^9.4.0",
+ "@intlify/shared": "^9.4.0",
+ "acorn": "^8.8.2",
+ "escodegen": "^2.1.0",
+ "estree-walker": "^2.0.2",
+ "jsonc-eslint-parser": "^2.3.0",
+ "magic-string": "^0.30.0",
+ "mlly": "^1.2.0",
+ "source-map-js": "^1.0.1",
+ "yaml-eslint-parser": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 14.16"
+ },
+ "peerDependenciesMeta": {
+ "petite-vue-i18n": {
+ "optional": true
+ },
+ "vue-i18n": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@intlify/core-base": {
+ "version": "9.10.2",
+ "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.10.2.tgz",
+ "integrity": "sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/message-compiler": "9.10.2",
+ "@intlify/shared": "9.10.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/core-base/node_modules/@intlify/message-compiler": {
+ "version": "9.10.2",
+ "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.10.2.tgz",
+ "integrity": "sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/shared": "9.10.2",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/core-base/node_modules/@intlify/shared": {
+ "version": "9.10.2",
+ "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.10.2.tgz",
+ "integrity": "sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/message-compiler": {
+ "version": "9.14.4",
+ "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
+ "integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/shared": "9.14.4",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/shared": {
+ "version": "9.14.4",
+ "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.4.tgz",
+ "integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/@intlify/unplugin-vue-i18n": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-2.0.0.tgz",
+ "integrity": "sha512-1oKvm92L9l2od2H9wKx2ZvR4tzn7gUtd7bPLI7AWUmm7U9H1iEypndt5d985ypxGsEs0gToDaKTrytbBIJwwSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/bundle-utils": "^7.4.0",
+ "@intlify/shared": "^9.4.0",
+ "@rollup/pluginutils": "^5.0.2",
+ "@vue/compiler-sfc": "^3.2.47",
+ "debug": "^4.3.3",
+ "fast-glob": "^3.2.12",
+ "js-yaml": "^4.1.0",
+ "json5": "^2.2.3",
+ "pathe": "^1.0.0",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2",
+ "unplugin": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 14.16"
+ },
+ "peerDependencies": {
+ "petite-vue-i18n": "*",
+ "vue-i18n": "*",
+ "vue-i18n-bridge": "*"
+ },
+ "peerDependenciesMeta": {
+ "petite-vue-i18n": {
+ "optional": true
+ },
+ "vue-i18n": {
+ "optional": true
+ },
+ "vue-i18n-bridge": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "29.6.3",
+ "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sinclair/typebox": "^0.27.8"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.6",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/source-map/-/source-map-0.3.6.tgz",
+ "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/@keyv/serialize/-/serialize-1.0.3.tgz",
+ "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^6.0.3"
+ }
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.2.3.tgz",
+ "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.1.tgz",
+ "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.2.tgz",
+ "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/markdown": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/@lezer/markdown/-/markdown-1.4.3.tgz",
+ "integrity": "sha512-kfw+2uMrQ/wy/+ONfrH83OkdFNM0ye5Xq96cLlaCy7h5UT9FO54DU4oRoIc0CSBh5NWmWuiIJA7NGLMJbQ+Oxg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@lezer/common": "^1.0.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
+ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@microsoft/fetch-event-source": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz",
+ "integrity": "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==",
+ "license": "MIT"
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "license": "MIT"
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz",
+ "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.5",
+ "node-addon-api": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.1",
+ "@parcel/watcher-darwin-arm64": "2.5.1",
+ "@parcel/watcher-darwin-x64": "2.5.1",
+ "@parcel/watcher-freebsd-x64": "2.5.1",
+ "@parcel/watcher-linux-arm-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm-musl": "2.5.1",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.1",
+ "@parcel/watcher-linux-arm64-musl": "2.5.1",
+ "@parcel/watcher-linux-x64-glibc": "2.5.1",
+ "@parcel/watcher-linux-x64-musl": "2.5.1",
+ "@parcel/watcher-win32-arm64": "2.5.1",
+ "@parcel/watcher-win32-ia32": "2.5.1",
+ "@parcel/watcher-win32-x64": "2.5.1"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
+ "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
+ "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
+ "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
+ "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
+ "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
+ "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
+ "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
+ "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
+ "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
+ "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
+ "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
+ "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
+ "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.1.2.tgz",
+ "integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.7",
+ "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+ "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@purge-icons/core": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmmirror.com/@purge-icons/core/-/core-0.10.0.tgz",
+ "integrity": "sha512-AtJbZv5Yy+vWX5v32DPTr+CW7AkSK8HJx52orDbrYt/9s4lGM2t4KKAmwaTQEH2HYr2HVh1mlqs54/S1s3WT1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/iconify": "2.1.2",
+ "axios": "^0.26.1",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "fs-extra": "^10.1.0"
+ }
+ },
+ "node_modules/@purge-icons/core/node_modules/@iconify/iconify": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/@iconify/iconify/-/iconify-2.1.2.tgz",
+ "integrity": "sha512-QcUzFeEWkE/mW+BVtEGmcWATClcCOIJFiYUD/PiCWuTcdEA297o8D4oN6Ra44WrNOHu1wqNW4J0ioaDIiqaFOQ==",
+ "deprecated": "no longer maintained, switch to modern iconify-icon web component",
+ "dev": true,
+ "license": "(Apache-2.0 OR GPL-2.0)",
+ "dependencies": {
+ "cross-fetch": "^3.1.5"
+ },
+ "funding": {
+ "url": "http://github.com/sponsors/cyberalien"
+ }
+ },
+ "node_modules/@purge-icons/core/node_modules/axios": {
+ "version": "0.26.1",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-0.26.1.tgz",
+ "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.14.8"
+ }
+ },
+ "node_modules/@purge-icons/generated": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmmirror.com/@purge-icons/generated/-/generated-0.9.0.tgz",
+ "integrity": "sha512-s2t+1oVtGDV6KtqfCXtUOhxfeYvOdDF90IVm+nMs/6bUP0HeGZLslguuL/AibpwtfL4FA/oCsIu/RhwapgAdJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/iconify": ">=2.0.0-rc.6"
+ }
+ },
+ "node_modules/@quansync/fs": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmmirror.com/@quansync/fs/-/fs-0.1.3.tgz",
+ "integrity": "sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "quansync": "^0.2.10"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
+ "node_modules/@rollup/plugin-virtual": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz",
+ "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
+ "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
+ "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
+ "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
+ "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
+ "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
+ "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
+ "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
+ "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
+ "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
+ "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
+ "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
+ "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
+ "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
+ "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
+ "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
+ "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
+ "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
+ "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
+ "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
+ "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
+ "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.27.8",
+ "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.8.tgz",
+ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sphinxxxx/color-conversion": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmmirror.com/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz",
+ "integrity": "sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==",
+ "license": "ISC"
+ },
+ "node_modules/@swc/core": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core/-/core-1.11.24.tgz",
+ "integrity": "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.21"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.11.24",
+ "@swc/core-darwin-x64": "1.11.24",
+ "@swc/core-linux-arm-gnueabihf": "1.11.24",
+ "@swc/core-linux-arm64-gnu": "1.11.24",
+ "@swc/core-linux-arm64-musl": "1.11.24",
+ "@swc/core-linux-x64-gnu": "1.11.24",
+ "@swc/core-linux-x64-musl": "1.11.24",
+ "@swc/core-win32-arm64-msvc": "1.11.24",
+ "@swc/core-win32-ia32-msvc": "1.11.24",
+ "@swc/core-win32-x64-msvc": "1.11.24"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.24.tgz",
+ "integrity": "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-darwin-x64/-/core-darwin-x64-1.11.24.tgz",
+ "integrity": "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.24.tgz",
+ "integrity": "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.24.tgz",
+ "integrity": "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.24.tgz",
+ "integrity": "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.24.tgz",
+ "integrity": "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.24.tgz",
+ "integrity": "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.24.tgz",
+ "integrity": "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.24.tgz",
+ "integrity": "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.11.24",
+ "resolved": "https://registry.npmmirror.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.24.tgz",
+ "integrity": "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz",
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.21",
+ "resolved": "https://registry.npmmirror.com/@swc/types/-/types-0.1.21.tgz",
+ "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@transloadit/prettier-bytes": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
+ "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==",
+ "license": "MIT"
+ },
+ "node_modules/@trysound/sax": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/@trysound/sax/-/sax-0.2.0.tgz",
+ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/@types/ace": {
+ "version": "0.0.52",
+ "resolved": "https://registry.npmmirror.com/@types/ace/-/ace-0.0.52.tgz",
+ "integrity": "sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/conventional-commits-parser": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.1.tgz",
+ "integrity": "sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmmirror.com/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.1.tgz",
+ "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz",
+ "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmmirror.com/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/eslint": {
+ "version": "8.56.12",
+ "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.56.12.tgz",
+ "integrity": "sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/jsoneditor": {
+ "version": "9.9.6",
+ "resolved": "https://registry.npmmirror.com/@types/jsoneditor/-/jsoneditor-9.9.6.tgz",
+ "integrity": "sha512-SJ29nWBIhnhtU5n72wxhPiuUVd8cnDHd7ZYMqVkzWtdRxTUdS8+oy1pg66yhmM1kcuanX3xmAAKfcyhhBnHEjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ace": "*",
+ "ajv": "^6.12.0"
+ }
+ },
+ "node_modules/@types/jsoneditor/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@types/jsoneditor/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.16",
+ "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.16.tgz",
+ "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "20.17.47",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.17.47.tgz",
+ "integrity": "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/nprogress": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmmirror.com/@types/nprogress/-/nprogress-0.2.3.tgz",
+ "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/qrcode": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.5.tgz",
+ "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.18",
+ "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.18.tgz",
+ "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/semver": {
+ "version": "7.7.0",
+ "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.7.0.tgz",
+ "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/video.js": {
+ "version": "7.3.58",
+ "resolved": "https://registry.npmmirror.com/@types/video.js/-/video.js-7.3.58.tgz",
+ "integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+ "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/type-utils": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.3.1",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^7.0.0",
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+ "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+ "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "7.18.0",
+ "@typescript-eslint/utils": "7.18.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz",
+ "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+ "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/visitor-keys": "7.18.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+ "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "7.18.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
+ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@unocss/astro": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/astro/-/astro-0.58.9.tgz",
+ "integrity": "sha512-VWfHNC0EfawFxLfb3uI+QcMGBN+ju+BYtutzeZTjilLKj31X2UpqIh8fepixL6ljgZzB3fweqg2xtUMC0gMnoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/reset": "0.58.9",
+ "@unocss/vite": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@unocss/astro/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/cli": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/cli/-/cli-0.58.9.tgz",
+ "integrity": "sha512-q7qlwX3V6UaqljWUQ5gMj36yTA9eLuuRywahdQWt1ioy4aPF/MEEfnMBZf/ntrqf5tIT5TO8fE11nvCco2Q/sA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@rollup/pluginutils": "^5.1.0",
+ "@unocss/config": "0.58.9",
+ "@unocss/core": "0.58.9",
+ "@unocss/preset-uno": "0.58.9",
+ "cac": "^6.7.14",
+ "chokidar": "^3.6.0",
+ "colorette": "^2.0.20",
+ "consola": "^3.2.3",
+ "fast-glob": "^3.3.2",
+ "magic-string": "^0.30.8",
+ "pathe": "^1.1.2",
+ "perfect-debounce": "^1.0.0"
+ },
+ "bin": {
+ "unocss": "bin/unocss.mjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/@antfu/utils": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
+ "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/@unocss/config": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-0.58.9.tgz",
+ "integrity": "sha512-90wRXIyGNI8UenWxvHUcH4l4rgq813MsTzYWsf6ZKyLLvkFjV2b2EfGXI27GPvZ7fVE1OAqx+wJNTw8CyQxwag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "unconfig": "^0.3.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/@unocss/cli/node_modules/unconfig": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-0.3.13.tgz",
+ "integrity": "sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/utils": "^0.7.7",
+ "defu": "^6.1.4",
+ "jiti": "^1.21.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/config": {
+ "version": "66.1.0-beta.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-66.1.0-beta.5.tgz",
+ "integrity": "sha512-RBty/CVvdefTpeLmluQrIQIj+Po5bTIgIgcWgw+A3dMcUN3iRv0mYbw1d3FIRa0Ladx9zKaMxRFss0xkiS13yw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "66.1.0-beta.5",
+ "unconfig": "^7.3.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/core": {
+ "version": "66.1.0-beta.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-66.1.0-beta.5.tgz",
+ "integrity": "sha512-1kZzSrB87KKd+xP+vMN7IP03j2UPEykna447aw3UaK5RYTDd/LuVtxoep6gvjN9TJiB4K+Qx0sAtgnfhPpka9Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-config": {
+ "version": "0.57.7",
+ "resolved": "https://registry.npmmirror.com/@unocss/eslint-config/-/eslint-config-0.57.7.tgz",
+ "integrity": "sha512-EJlI6rV0ZfDCphIiddHSWZVeoHdYDTVohVXGo+NfNOuRuvYWGna3n4hY3VEAiT3mWLK0/0anzHF7X0PNzCR5lQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/eslint-plugin": "0.57.7"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@antfu/utils": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
+ "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@typescript-eslint/utils": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.21.0.tgz",
+ "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@unocss/config": {
+ "version": "0.57.7",
+ "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-0.57.7.tgz",
+ "integrity": "sha512-UG8G9orWEdk/vyDvGUToXYn/RZy/Qjpx66pLsaf5wQK37hkYsBoReAU5v8Ia/6PL1ueJlkcNXLaNpN6/yVoJvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.57.7",
+ "unconfig": "^0.3.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@unocss/core": {
+ "version": "0.57.7",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.57.7.tgz",
+ "integrity": "sha512-1d36M0CV3yC80J0pqOa5rH1BX6g2iZdtKmIb3oSBN4AWnMCSrrJEPBrUikyMq2TEQTrYWJIVDzv5A9hBUat3TA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/@unocss/eslint-plugin": {
+ "version": "0.57.7",
+ "resolved": "https://registry.npmmirror.com/@unocss/eslint-plugin/-/eslint-plugin-0.57.7.tgz",
+ "integrity": "sha512-nwj7UJF7wCfPVl5B7cUB0xrSk6yuVMdMgABnsy4N5xBlds8cclrUO+boaTB9qzh8Lg9nfJVLB3+cW3po2SJoew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^6.11.0",
+ "@unocss/config": "0.57.7",
+ "@unocss/core": "0.57.7",
+ "magic-string": "^0.30.5",
+ "synckit": "^0.8.5"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/synckit": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.8.8.tgz",
+ "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/@unocss/eslint-config/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/@unocss/eslint-config/node_modules/unconfig": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-0.3.13.tgz",
+ "integrity": "sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/utils": "^0.7.7",
+ "defu": "^6.1.4",
+ "jiti": "^1.21.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin": {
+ "version": "66.1.0-beta.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/eslint-plugin/-/eslint-plugin-66.1.0-beta.5.tgz",
+ "integrity": "sha512-5BRXjE8XJ9Yrf/lmgBCCmpfXRfiaebdS0zhkbmsFJmtXzhhun0epIF2cs/nXIya9rtvne+YKUAPXxIIoHV3lKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/utils": "^8.26.1",
+ "@unocss/config": "66.1.0-beta.5",
+ "@unocss/core": "66.1.0-beta.5",
+ "@unocss/rule-utils": "66.1.0-beta.5",
+ "magic-string": "^0.30.17",
+ "synckit": "^0.9.2"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
+ "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/visitor-keys": "8.32.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.32.1.tgz",
+ "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
+ "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/visitor-keys": "8.32.1",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/@typescript-eslint/utils": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.32.1.tgz",
+ "integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.32.1",
+ "@typescript-eslint/types": "8.32.1",
+ "@typescript-eslint/typescript-estree": "8.32.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <5.9.0"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.32.1",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
+ "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.32.1",
+ "eslint-visitor-keys": "^4.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
+ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@unocss/eslint-plugin/node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/@unocss/extractor-arbitrary-variants": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-0.58.9.tgz",
+ "integrity": "sha512-M/BvPdbEEMdhcFQh/z2Bf9gylO1Ky/ZnpIvKWS1YJPLt4KA7UWXSUf+ZNTFxX+X58Is5qAb5hNh/XBQmL3gbXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/extractor-arbitrary-variants/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/inspector": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/inspector/-/inspector-0.58.9.tgz",
+ "integrity": "sha512-uRzqkCNeBmEvFePXcfIFcQPMlCXd9/bLwa5OkBthiOILwQdH1uRIW3GWAa2SWspu+kZLP0Ly3SjZ9Wqi+5ZtTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/rule-utils": "0.58.9",
+ "gzip-size": "^6.0.0",
+ "sirv": "^2.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/inspector/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/inspector/node_modules/@unocss/rule-utils": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.58.9.tgz",
+ "integrity": "sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^0.58.9",
+ "magic-string": "^0.30.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/postcss": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/postcss/-/postcss-0.58.9.tgz",
+ "integrity": "sha512-PnKmH6Qhimw35yO6u6yx9SHaX2NmvbRNPDvMDHA/1xr3M8L0o8U88tgKbWfm65NEGF3R1zJ9A8rjtZn/LPkgPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/config": "0.58.9",
+ "@unocss/core": "0.58.9",
+ "@unocss/rule-utils": "0.58.9",
+ "css-tree": "^2.3.1",
+ "fast-glob": "^3.3.2",
+ "magic-string": "^0.30.8",
+ "postcss": "^8.4.38"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/@antfu/utils": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
+ "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/@unocss/config": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-0.58.9.tgz",
+ "integrity": "sha512-90wRXIyGNI8UenWxvHUcH4l4rgq813MsTzYWsf6ZKyLLvkFjV2b2EfGXI27GPvZ7fVE1OAqx+wJNTw8CyQxwag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "unconfig": "^0.3.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/@unocss/rule-utils": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.58.9.tgz",
+ "integrity": "sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^0.58.9",
+ "magic-string": "^0.30.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/css-tree": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-2.3.1.tgz",
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.30",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/@unocss/postcss/node_modules/mdn-data": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/@unocss/postcss/node_modules/unconfig": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-0.3.13.tgz",
+ "integrity": "sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/utils": "^0.7.7",
+ "defu": "^6.1.4",
+ "jiti": "^1.21.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-attributify": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-attributify/-/preset-attributify-0.58.9.tgz",
+ "integrity": "sha512-ucP+kXRFcwmBmHohUVv31bE/SejMAMo7Hjb0QcKVLyHlzRWUJsfNR+jTAIGIUSYxN7Q8MeigYsongGo3nIeJnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-attributify/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-icons": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-icons/-/preset-icons-0.58.9.tgz",
+ "integrity": "sha512-9dS48+yAunsbS0ylOW2Wisozwpn3nGY1CqTiidkUnrMnrZK3al579A7srUX9NyPWWDjprO7eU/JkWbdDQSmFFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/utils": "^2.1.22",
+ "@unocss/core": "0.58.9",
+ "ofetch": "^1.3.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-icons/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-mini": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-mini/-/preset-mini-0.58.9.tgz",
+ "integrity": "sha512-m4aDGYtueP8QGsU3FsyML63T/w5Mtr4htme2jXy6m50+tzC1PPHaIBstMTMQfLc6h8UOregPJyGHB5iYQZGEvQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/extractor-arbitrary-variants": "0.58.9",
+ "@unocss/rule-utils": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-mini/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-mini/node_modules/@unocss/rule-utils": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.58.9.tgz",
+ "integrity": "sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^0.58.9",
+ "magic-string": "^0.30.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-tagify": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-tagify/-/preset-tagify-0.58.9.tgz",
+ "integrity": "sha512-obh75XrRmxYwrQMflzvhQUMeHwd/R9bEDhTWUW9aBTolBy4eNypmQwOhHCKh5Xi4Dg6o0xj6GWC/jcCj1SPLog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-tagify/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-typography": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-typography/-/preset-typography-0.58.9.tgz",
+ "integrity": "sha512-hrsaqKlcZni3Vh4fwXC+lP9e92FQYbqtmlZw2jpxlVwwH5aLzwk4d4MiFQGyhCfzuSDYm0Zd52putFVV02J7bA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/preset-mini": "0.58.9"
+ }
+ },
+ "node_modules/@unocss/preset-typography/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-uno": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-uno/-/preset-uno-0.58.9.tgz",
+ "integrity": "sha512-Fze+X2Z/EegCkRdDRgwwvFBmXBenNR1AG8KxAyz8iPeWbhOBaRra2sn2ScryrfH6SbJHpw26ZyJXycAdS0Fq3A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/preset-mini": "0.58.9",
+ "@unocss/preset-wind": "0.58.9",
+ "@unocss/rule-utils": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-uno/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-uno/node_modules/@unocss/rule-utils": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.58.9.tgz",
+ "integrity": "sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^0.58.9",
+ "magic-string": "^0.30.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-web-fonts": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-web-fonts/-/preset-web-fonts-0.58.9.tgz",
+ "integrity": "sha512-XtiO+Z+RYnNYomNkS2XxaQiY++CrQZKOfNGw5htgIrb32QtYVQSkyYQ3jDw7JmMiCWlZ4E72cV/zUb++WrZLxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "ofetch": "^1.3.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-web-fonts/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-wind": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/preset-wind/-/preset-wind-0.58.9.tgz",
+ "integrity": "sha512-7l+7Vx5UoN80BmJKiqDXaJJ6EUqrnUQYv8NxCThFi5lYuHzxsYWZPLU3k3XlWRUQt8XL+6rYx7mMBmD7EUSHyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/preset-mini": "0.58.9",
+ "@unocss/rule-utils": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-wind/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/preset-wind/node_modules/@unocss/rule-utils": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.58.9.tgz",
+ "integrity": "sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^0.58.9",
+ "magic-string": "^0.30.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/reset": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/reset/-/reset-0.58.9.tgz",
+ "integrity": "sha512-nA2pg3tnwlquq+FDOHyKwZvs20A6iBsKPU7Yjb48JrNnzoaXqE+O9oN6782IG2yKVW4AcnsAnAnM4cxXhGzy1w==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/rule-utils": {
+ "version": "66.1.0-beta.5",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-66.1.0-beta.5.tgz",
+ "integrity": "sha512-G757sAnQAMNRUijgOTut8UkbkncSablI6Viwcq2VP4r0Lhi6RFOv/n6AOTWsDgGeUSuWTa/p3zb3NDHY7ztE9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^66.1.0-beta.5",
+ "magic-string": "^0.30.17"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/scope": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/scope/-/scope-0.58.9.tgz",
+ "integrity": "sha512-BIwcpx0R3bE0rYa9JVDJTk0GX32EBvnbvufBpNkWfC5tb7g+B7nMkVq9ichanksYCCxrIQQo0mrIz5PNzu9sGA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@unocss/transformer-attributify-jsx": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-0.58.9.tgz",
+ "integrity": "sha512-jpL3PRwf8t43v1agUdQn2EHGgfdWfvzsMxFtoybO88xzOikzAJaaouteNtojc/fQat2T9iBduDxVj5egdKmhdQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-attributify-jsx-babel": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-attributify-jsx-babel/-/transformer-attributify-jsx-babel-0.58.9.tgz",
+ "integrity": "sha512-UGaQoGZg+3QrsPtnGHPECmsGn4EQb2KSdZ4eGEn2YssjKv+CcQhzRvpEUgnuF/F+jGPkCkS/G/YEQBHRWBY54Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.3",
+ "@babel/plugin-syntax-jsx": "^7.24.1",
+ "@babel/preset-typescript": "^7.24.1",
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-attributify-jsx-babel/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-attributify-jsx/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-compile-class": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-compile-class/-/transformer-compile-class-0.58.9.tgz",
+ "integrity": "sha512-l2VpCqelJ6Tgc1kfSODxBtg7fCGPVRr2EUzTg1LrGYKa2McbKuc/wV/2DWKHGxL6+voWi7a2C9XflqGDXXutuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-compile-class/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-directives": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-directives/-/transformer-directives-0.58.9.tgz",
+ "integrity": "sha512-pLOUsdoY2ugVntJXg0xuGjO9XZ2xCiMxTPRtpZ4TsEzUtdEzMswR06Y8VWvNciTB/Zqxcz9ta8rD0DKePOfSuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "@unocss/rule-utils": "0.58.9",
+ "css-tree": "^2.3.1"
+ }
+ },
+ "node_modules/@unocss/transformer-directives/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-directives/node_modules/@unocss/rule-utils": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.58.9.tgz",
+ "integrity": "sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "^0.58.9",
+ "magic-string": "^0.30.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-directives/node_modules/css-tree": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-2.3.1.tgz",
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.30",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/@unocss/transformer-directives/node_modules/mdn-data": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/@unocss/transformer-variant-group": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/transformer-variant-group/-/transformer-variant-group-0.58.9.tgz",
+ "integrity": "sha512-3A6voHSnFcyw6xpcZT6oxE+KN4SHRnG4z862tdtWvRGcN+jGyNr20ylEZtnbk4xj0VNMeGHHQRZ0WLvmrAwvOQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/transformer-variant-group/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/vite": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/vite/-/vite-0.58.9.tgz",
+ "integrity": "sha512-mmppBuulAHCal+sC0Qz36Y99t0HicAmznpj70Kzwl7g/yvXwm58/DW2OnpCWw+uA8/JBft/+z3zE+XvrI+T1HA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ampproject/remapping": "^2.3.0",
+ "@rollup/pluginutils": "^5.1.0",
+ "@unocss/config": "0.58.9",
+ "@unocss/core": "0.58.9",
+ "@unocss/inspector": "0.58.9",
+ "@unocss/scope": "0.58.9",
+ "@unocss/transformer-directives": "0.58.9",
+ "chokidar": "^3.6.0",
+ "fast-glob": "^3.3.2",
+ "magic-string": "^0.30.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/@antfu/utils": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
+ "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/@unocss/config": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/config/-/config-0.58.9.tgz",
+ "integrity": "sha512-90wRXIyGNI8UenWxvHUcH4l4rgq813MsTzYWsf6ZKyLLvkFjV2b2EfGXI27GPvZ7fVE1OAqx+wJNTw8CyQxwag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/core": "0.58.9",
+ "unconfig": "^0.3.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/@unocss/vite/node_modules/unconfig": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-0.3.13.tgz",
+ "integrity": "sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/utils": "^0.7.7",
+ "defu": "^6.1.4",
+ "jiti": "^1.21.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@uppy/companion-client": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
+ "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@uppy/utils": "^4.1.2",
+ "namespace-emitter": "^2.0.1"
+ }
+ },
+ "node_modules/@uppy/core": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
+ "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@transloadit/prettier-bytes": "0.0.7",
+ "@uppy/store-default": "^2.1.1",
+ "@uppy/utils": "^4.1.3",
+ "lodash.throttle": "^4.1.1",
+ "mime-match": "^1.0.2",
+ "namespace-emitter": "^2.0.1",
+ "nanoid": "^3.1.25",
+ "preact": "^10.5.13"
+ }
+ },
+ "node_modules/@uppy/store-default": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
+ "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==",
+ "license": "MIT"
+ },
+ "node_modules/@uppy/utils": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
+ "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.throttle": "^4.1.1"
+ }
+ },
+ "node_modules/@uppy/xhr-upload": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
+ "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@uppy/companion-client": "^2.2.2",
+ "@uppy/utils": "^4.1.2",
+ "nanoid": "^3.1.25"
+ },
+ "peerDependencies": {
+ "@uppy/core": "^2.3.3"
+ }
+ },
+ "node_modules/@videojs-player/vue": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/@videojs-player/vue/-/vue-1.0.0.tgz",
+ "integrity": "sha512-WonTezRfKu3fYdQLt/ta+nuKH6gMZUv8l40Jke/j4Lae7IqeO/+lLAmBnh3ni88bwR+vkFXIlZ2Ci7VKInIYJg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/video.js": "7.x",
+ "video.js": "7.x",
+ "vue": "3.x"
+ }
+ },
+ "node_modules/@videojs/http-streaming": {
+ "version": "2.16.3",
+ "resolved": "https://registry.npmmirror.com/@videojs/http-streaming/-/http-streaming-2.16.3.tgz",
+ "integrity": "sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@videojs/vhs-utils": "3.0.5",
+ "aes-decrypter": "3.1.3",
+ "global": "^4.4.0",
+ "m3u8-parser": "4.8.0",
+ "mpd-parser": "^0.22.1",
+ "mux.js": "6.0.1",
+ "video.js": "^6 || ^7"
+ },
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ },
+ "peerDependencies": {
+ "video.js": "^6 || ^7"
+ }
+ },
+ "node_modules/@videojs/vhs-utils": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmmirror.com/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
+ "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "global": "^4.4.0",
+ "url-toolkit": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ }
+ },
+ "node_modules/@videojs/xhr": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmmirror.com/@videojs/xhr/-/xhr-2.6.0.tgz",
+ "integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "global": "~4.4.0",
+ "is-function": "^1.0.1"
+ }
+ },
+ "node_modules/@vitejs/plugin-legacy": {
+ "version": "5.4.3",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-legacy/-/plugin-legacy-5.4.3.tgz",
+ "integrity": "sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.25.8",
+ "@babel/preset-env": "^7.25.8",
+ "browserslist": "^4.24.0",
+ "browserslist-to-esbuild": "^2.1.1",
+ "core-js": "^3.38.1",
+ "magic-string": "^0.30.12",
+ "regenerator-runtime": "^0.14.1",
+ "systemjs": "^6.15.1"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "peerDependencies": {
+ "terser": "^5.4.0",
+ "vite": "^5.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+ "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue-jsx": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.1.0.tgz",
+ "integrity": "sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.3",
+ "@babel/plugin-transform-typescript": "^7.23.3",
+ "@vue/babel-plugin-jsx": "^1.1.5"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.0.0 || ^5.0.0",
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz",
+ "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "1.11.1"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz",
+ "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "muggle-string": "^0.3.1"
+ }
+ },
+ "node_modules/@volar/typescript": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz",
+ "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "1.11.1",
+ "path-browserify": "^1.0.1"
+ }
+ },
+ "node_modules/@vue/babel-helper-vue-transform-on": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz",
+ "integrity": "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/babel-plugin-jsx": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.4.0.tgz",
+ "integrity": "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-plugin-utils": "^7.26.5",
+ "@babel/plugin-syntax-jsx": "^7.25.9",
+ "@babel/template": "^7.26.9",
+ "@babel/traverse": "^7.26.9",
+ "@babel/types": "^7.26.9",
+ "@vue/babel-helper-vue-transform-on": "1.4.0",
+ "@vue/babel-plugin-resolve-type": "1.4.0",
+ "@vue/shared": "^3.5.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/babel-plugin-resolve-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.4.0.tgz",
+ "integrity": "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-plugin-utils": "^7.26.5",
+ "@babel/parser": "^7.26.9",
+ "@vue/compiler-sfc": "^3.5.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.14.tgz",
+ "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.2",
+ "@vue/shared": "3.5.14",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz",
+ "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.14",
+ "@vue/shared": "3.5.14"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz",
+ "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.27.2",
+ "@vue/compiler-core": "3.5.14",
+ "@vue/compiler-dom": "3.5.14",
+ "@vue/compiler-ssr": "3.5.14",
+ "@vue/shared": "3.5.14",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.17",
+ "postcss": "^8.5.3",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz",
+ "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.14",
+ "@vue/shared": "3.5.14"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "1.8.27",
+ "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz",
+ "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "~1.11.1",
+ "@volar/source-map": "~1.11.1",
+ "@vue/compiler-dom": "^3.3.0",
+ "@vue/shared": "^3.3.0",
+ "computeds": "^0.0.1",
+ "minimatch": "^9.0.3",
+ "muggle-string": "^0.3.1",
+ "path-browserify": "^1.0.1",
+ "vue-template-compiler": "^2.7.14"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.12.tgz",
+ "integrity": "sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.12"
+ }
+ },
+ "node_modules/@vue/reactivity/node_modules/@vue/shared": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz",
+ "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.12.tgz",
+ "integrity": "sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.12",
+ "@vue/shared": "3.5.12"
+ }
+ },
+ "node_modules/@vue/runtime-core/node_modules/@vue/shared": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz",
+ "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.12.tgz",
+ "integrity": "sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.12",
+ "@vue/runtime-core": "3.5.12",
+ "@vue/shared": "3.5.12",
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz",
+ "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.12.tgz",
+ "integrity": "sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.12",
+ "@vue/shared": "3.5.12"
+ },
+ "peerDependencies": {
+ "vue": "3.5.12"
+ }
+ },
+ "node_modules/@vue/server-renderer/node_modules/@vue/compiler-core": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
+ "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.3",
+ "@vue/shared": "3.5.12",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/server-renderer/node_modules/@vue/compiler-dom": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz",
+ "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.12",
+ "@vue/shared": "3.5.12"
+ }
+ },
+ "node_modules/@vue/server-renderer/node_modules/@vue/compiler-ssr": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz",
+ "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.12",
+ "@vue/shared": "3.5.12"
+ }
+ },
+ "node_modules/@vue/server-renderer/node_modules/@vue/shared": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz",
+ "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.14",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.14.tgz",
+ "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz",
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "10.11.1",
+ "@vueuse/shared": "10.11.1",
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz",
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz",
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@wangeditor-next/editor": {
+ "version": "5.6.49",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/editor/-/editor-5.6.49.tgz",
+ "integrity": "sha512-gDh7CLzsuPvUp1n4rO//V1NTHlpGzEibL71oRcRcxpz76oNaW12u+GWWvRde4cWivaCTLzHwz7EfEVdyDkt/Ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@uppy/core": "^2.1.1",
+ "@uppy/xhr-upload": "^2.0.3",
+ "@wangeditor-next/basic-modules": "~1.5.47",
+ "@wangeditor-next/code-highlight": "~1.3.43",
+ "@wangeditor-next/core": "~1.7.45",
+ "@wangeditor-next/list-module": "~1.1.52",
+ "@wangeditor-next/table-module": "~1.6.60",
+ "@wangeditor-next/upload-image-module": "~1.1.50",
+ "@wangeditor-next/video-module": "~1.3.51",
+ "dom7": "^4.0.0",
+ "is-hotkey": "^0.2.0",
+ "lodash.camelcase": "^4.3.0",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.debounce": "^4.0.8",
+ "lodash.foreach": "^4.5.0",
+ "lodash.throttle": "^4.1.1",
+ "lodash.toarray": "^4.4.0",
+ "nanoid": "^5.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor-for-vue": {
+ "version": "5.1.14",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/editor-for-vue/-/editor-for-vue-5.1.14.tgz",
+ "integrity": "sha512-Xkrdo590AhLHvzyR+U246t6T89nIWHz1weAgMuo8jEA2HS5RiUnsA4U6+iUGaQ2E5c8mYQaeNqzHQXUp9Okbiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@wangeditor-next/editor": ">=5.1.0",
+ "vue": "^3.0.5"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/basic-modules": {
+ "version": "1.5.47",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/basic-modules/-/basic-modules-1.5.47.tgz",
+ "integrity": "sha512-FHydtBbfpsi4R4JTo5MvwWhzButwq6x36o+GoxsALdItwDW2qVgJkrlhw25aWYpg6ff1xqjivHfLBaAPWC4J+w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-url": "^1.2.4"
+ },
+ "peerDependencies": {
+ "@wangeditor-next/core": "1.7.45",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "lodash.throttle": "^4.1.1",
+ "nanoid": "^5.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/code-highlight": {
+ "version": "1.3.43",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/code-highlight/-/code-highlight-1.3.43.tgz",
+ "integrity": "sha512-22eHjYDmtTxZqZOma2ls9zWA6gsgSkWq3XtmLylA15kegVBKAy7YxYbRrdS7D4Y/igqOerSbc5oMsOdeYjRfnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prismjs": "^1.23.0"
+ },
+ "peerDependencies": {
+ "@wangeditor-next/core": "1.7.45",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/core": {
+ "version": "1.7.45",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/core/-/core-1.7.45.tgz",
+ "integrity": "sha512-5Pt8JCmdzJWk4q18zUZse+zM+mBW6jYt3npXVkLswYysx01krC3bBQq1J9JeZe4Ci+rQAs0tQj3t1imjpsmRgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/event-emitter": "^0.3.3",
+ "event-emitter": "^0.3.5",
+ "html-void-elements": "^3.0.0",
+ "i18next": "^23.0.0",
+ "scroll-into-view-if-needed": "^3.0.0",
+ "slate-history": "^0.109.0"
+ },
+ "peerDependencies": {
+ "@uppy/core": "^2.1.1",
+ "@uppy/xhr-upload": "^2.0.3",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "is-hotkey": "^0.2.0",
+ "lodash.camelcase": "^4.3.0",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.debounce": "^4.0.8",
+ "lodash.foreach": "^4.5.0",
+ "lodash.throttle": "^4.1.1",
+ "lodash.toarray": "^4.4.0",
+ "nanoid": "^5.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/list-module": {
+ "version": "1.1.52",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/list-module/-/list-module-1.1.52.tgz",
+ "integrity": "sha512-FMzvx+iXXkatFFRZZ+rbiPjZpEcPa3UtNBFs40VpZG0w7O3gQWM7B/oPec3SKvAmre/US4CC5DJEqeEY3QX4hw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@wangeditor-next/core": "1.7.45",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/table-module": {
+ "version": "1.6.60",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/table-module/-/table-module-1.6.60.tgz",
+ "integrity": "sha512-BGTG1YzPSIC4XJRafllCcynT9CkElWDSFxYBJ2svS36AvJc3ivQuj5Fhv+rCS4RqGggsN1hdeA4iP+xrtwWI4w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@wangeditor-next/core": "1.7.45",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "lodash.debounce": "^4.0.8",
+ "lodash.throttle": "^4.1.1",
+ "nanoid": "^5.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/upload-image-module": {
+ "version": "1.1.50",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/upload-image-module/-/upload-image-module-1.1.50.tgz",
+ "integrity": "sha512-KIzI1IIQA6J5Hg3/UJF/AlEsrxJ62LZZUt61tenkO8cxks2UQMvH4CEsgEU5NNfQ0PUnOeR4ErjOgyhtbZKyaQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@uppy/core": "^2.0.3",
+ "@uppy/xhr-upload": "^2.0.3",
+ "@wangeditor-next/basic-modules": "1.5.47",
+ "@wangeditor-next/core": "1.7.45",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "lodash.foreach": "^4.5.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/@wangeditor-next/video-module": {
+ "version": "1.3.51",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/video-module/-/video-module-1.3.51.tgz",
+ "integrity": "sha512-67ecZCGIY+MUsqFtmwR9QKWlzGeIXVyXHmzPuevYwEqRwg50oR2xCSuoQLhfs5CKjXDZKsZhOnD/CGgt82TU+A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@uppy/core": "^2.1.4",
+ "@uppy/xhr-upload": "^2.0.7",
+ "@wangeditor-next/core": "1.7.45",
+ "dom7": "^3.0.0 || ^4.0.0",
+ "nanoid": "^5.0.0",
+ "slate": "^0.82.0",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/compute-scroll-into-view": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+ "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+ "license": "MIT"
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/dom7": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmmirror.com/dom7/-/dom7-4.0.6.tgz",
+ "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==",
+ "license": "MIT",
+ "dependencies": {
+ "ssr-window": "^4.0.0"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/html-void-elements": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz",
+ "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/i18next": {
+ "version": "23.16.8",
+ "resolved": "https://registry.npmmirror.com/i18next/-/i18next-23.16.8.tgz",
+ "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://locize.com"
+ },
+ {
+ "type": "individual",
+ "url": "https://locize.com/i18next.html"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/nanoid": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.6.tgz",
+ "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/scroll-into-view-if-needed": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+ "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "compute-scroll-into-view": "^3.0.2"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/slate": {
+ "version": "0.82.1",
+ "resolved": "https://registry.npmmirror.com/slate/-/slate-0.82.1.tgz",
+ "integrity": "sha512-3mdRdq7U3jSEoyFrGvbeb28hgrvrr4NdFCtJX+IjaNvSFozY0VZd/CGHF0zf/JDx7aEov864xd5uj0HQxxEWTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immer": "^9.0.6",
+ "is-plain-object": "^5.0.0",
+ "tiny-warning": "^1.0.3"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/slate-history": {
+ "version": "0.109.0",
+ "resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.109.0.tgz",
+ "integrity": "sha512-DHavPwrTTAEAV66eAocB3iQHEj65N6IVtbRK98ZuqGT0S44T3zXlhzY+5SZ7EPxRcoOYVt1dioRxXYM/+PmCiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "^5.0.0"
+ },
+ "peerDependencies": {
+ "slate": ">=0.65.3"
+ }
+ },
+ "node_modules/@wangeditor-next/editor/node_modules/ssr-window": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-4.0.2.tgz",
+ "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==",
+ "license": "MIT"
+ },
+ "node_modules/@wangeditor-next/plugin-mention": {
+ "version": "1.0.19",
+ "resolved": "https://registry.npmmirror.com/@wangeditor-next/plugin-mention/-/plugin-mention-1.0.19.tgz",
+ "integrity": "sha512-aH81xDT4hZ+PdEFPsptJ/Gn4KDyIOhQdrNewLi2BKadmVBiYXlLneseodeFyv9MLhtNg2ekt+KNGJNK3kKzCsw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@wangeditor-next/editor": "5.6.49",
+ "snabbdom": "^3.6.0"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.10",
+ "resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
+ "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@zxcvbn-ts/core": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@zxcvbn-ts/core/-/core-3.0.4.tgz",
+ "integrity": "sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==",
+ "license": "MIT",
+ "dependencies": {
+ "fastest-levenshtein": "1.0.16"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ace-builds": {
+ "version": "1.41.0",
+ "resolved": "https://registry.npmmirror.com/ace-builds/-/ace-builds-1.41.0.tgz",
+ "integrity": "sha512-tiEUfw7V/FpHuI4tG7KS+muOTMIuPh6zReBAD2Uqhe9t00tLeyVGxjXu0tSqz5OIPWy7/wvuJBVXAsNWx0rYvQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/acorn": {
+ "version": "8.14.1",
+ "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz",
+ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/aes-decrypter": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/aes-decrypter/-/aes-decrypter-3.1.3.tgz",
+ "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@videojs/vhs-utils": "^3.0.5",
+ "global": "^4.4.0",
+ "pkcs7": "^1.0.4"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/animate.css": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/animate.css/-/animate.css-4.1.1.tgz",
+ "integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-escapes": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz",
+ "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "environment": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/array-ify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz",
+ "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-move": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/array-move/-/array-move-4.0.0.tgz",
+ "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autolinker": {
+ "version": "3.16.2",
+ "resolved": "https://registry.npmmirror.com/autolinker/-/autolinker-3.16.2.tgz",
+ "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ }
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-1.9.0.tgz",
+ "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2": {
+ "version": "0.4.13",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz",
+ "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.22.6",
+ "@babel/helper-define-polyfill-provider": "^0.6.4",
+ "semver": "^6.3.1"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-corejs3": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz",
+ "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.3",
+ "core-js-compat": "^3.40.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/babel-plugin-polyfill-regenerator": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz",
+ "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-define-polyfill-provider": "^0.6.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/benz-amr-recorder": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmmirror.com/benz-amr-recorder/-/benz-amr-recorder-1.1.5.tgz",
+ "integrity": "sha512-NepctcNTsZHK8NxBb5uKO5p8S+xkbm+vD6GLSkCYdJeEsriexvgumLHpDkanX4QJBcLRMVtg16buWMs+gUPB3g==",
+ "license": "MIT",
+ "dependencies": {
+ "benz-recorderjs": "^1.0.5"
+ }
+ },
+ "node_modules/benz-recorderjs": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/benz-recorderjs/-/benz-recorderjs-1.0.5.tgz",
+ "integrity": "sha512-EwedOQo9KLti7HxDi/eZY51PSRbAXnOdEZmLvJ6ro3QQSoF9Y3AXBt57MIllGvVz5vtFYMeikG+GD7qTm3+p9w==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
+ },
+ "node_modules/bpmn-js": {
+ "version": "17.11.1",
+ "resolved": "https://registry.npmmirror.com/bpmn-js/-/bpmn-js-17.11.1.tgz",
+ "integrity": "sha512-ywCeTg5kvN8lYkU+fHE+YXTGlfKc55lRBn7zW3k1//toeMNPy/PS/uQiujRWdFhMrH5dbtDvlwWukNw2pjWw8Q==",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE",
+ "dependencies": {
+ "bpmn-moddle": "^8.1.0",
+ "diagram-js": "^14.10.0",
+ "diagram-js-direct-editing": "^3.0.1",
+ "ids": "^1.0.5",
+ "inherits-browser": "^0.1.0",
+ "min-dash": "^4.1.1",
+ "min-dom": "^4.2.1",
+ "tiny-svg": "^3.1.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bpmn-js-properties-panel": {
+ "version": "5.23.0",
+ "resolved": "https://registry.npmmirror.com/bpmn-js-properties-panel/-/bpmn-js-properties-panel-5.23.0.tgz",
+ "integrity": "sha512-4B27LM8oV14A2QWRvazV17h4NxbkNERcqU+AGJmxKImMlLhu9893MWR+pCdTQCTphBdBkuD8ksWm+1wVCedJ7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bpmn-io/extract-process-variables": "^0.8.0",
+ "array-move": "^4.0.0",
+ "ids": "^1.0.5",
+ "min-dash": "^4.2.1",
+ "min-dom": "^4.2.1"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "peerDependencies": {
+ "@bpmn-io/properties-panel": ">= 3.7",
+ "bpmn-js": ">= 11.5",
+ "camunda-bpmn-js-behaviors": ">= 0.4",
+ "diagram-js": ">= 11.9"
+ }
+ },
+ "node_modules/bpmn-js-token-simulation": {
+ "version": "0.36.3",
+ "resolved": "https://registry.npmmirror.com/bpmn-js-token-simulation/-/bpmn-js-token-simulation-0.36.3.tgz",
+ "integrity": "sha512-HyiExdi+vENiStn284gIUQkQliiWly4dk2kY9PJILwwuTIoKtvg1zw8LGr9ReNUiScibNbpkt45bR25Oqfq9wA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits-browser": "^0.1.0",
+ "min-dash": "^4.2.2",
+ "min-dom": "^4.2.1",
+ "randomcolor": "^0.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/bpmn-js/node_modules/diagram-js": {
+ "version": "14.11.3",
+ "resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-14.11.3.tgz",
+ "integrity": "sha512-Seq9BHAXfzKS60L4v4Gvgvv72wOtvrfJQAyyPm9pntSZDMzjoodPSXnEUPud1G2zVCMGEUUW++s0reEdaWgkXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bpmn-io/diagram-js-ui": "^0.2.3",
+ "clsx": "^2.1.0",
+ "didi": "^10.2.2",
+ "inherits-browser": "^0.1.0",
+ "min-dash": "^4.1.0",
+ "min-dom": "^4.2.1",
+ "object-refs": "^0.4.0",
+ "path-intersection": "^3.0.0",
+ "tiny-svg": "^3.1.2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bpmn-js/node_modules/didi": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmmirror.com/didi/-/didi-10.2.2.tgz",
+ "integrity": "sha512-l8NYkYFXV1izHI65EyT8EXOjUZtKmQkHLTT89cSP7HU5J/G7AOj0dXKtLc04EXYlga99PBY18IPjOeZ+c3DI4w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/bpmn-js/node_modules/object-refs": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/object-refs/-/object-refs-0.4.0.tgz",
+ "integrity": "sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bpmn-js/node_modules/path-intersection": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/path-intersection/-/path-intersection-3.1.0.tgz",
+ "integrity": "sha512-3xS3lvv/vuwm5aH2BVvNRvnvwR2Drde7jQClKpCXTYXIMMjcw/EnMhzCgeHwqbCpzi760PEfAkU53vSIlrNr9A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.20"
+ }
+ },
+ "node_modules/bpmn-moddle": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmmirror.com/bpmn-moddle/-/bpmn-moddle-8.1.0.tgz",
+ "integrity": "sha512-yI5OAFfYVJwViKTsTsonVfCBPtB3MlefADUORwNIxxBOMp21vnoxuxsdgUWlPH/dvAEZh/+mr8UtqOBNu8NC5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-dash": "^4.0.0",
+ "moddle": "^6.2.3",
+ "moddle-xml": "^10.1.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.5",
+ "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.24.5.tgz",
+ "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001716",
+ "electron-to-chromium": "^1.5.149",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/browserslist-to-esbuild": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/browserslist-to-esbuild/-/browserslist-to-esbuild-2.1.1.tgz",
+ "integrity": "sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "meow": "^13.0.0"
+ },
+ "bin": {
+ "browserslist-to-esbuild": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "browserslist": "*"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacheable": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmmirror.com/cacheable/-/cacheable-1.9.0.tgz",
+ "integrity": "sha512-8D5htMCxPDUULux9gFzv30f04Xo3wCnik0oOxKoRTPIBoqA7HtOcJ87uBhQTs3jCfZZTrUBGsYIZOgE0ZRgMAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.8.2",
+ "keyv": "^5.3.3"
+ }
+ },
+ "node_modules/cacheable/node_modules/keyv": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmmirror.com/keyv/-/keyv-5.3.3.tgz",
+ "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.0.3"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camunda-bpmn-js-behaviors": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/camunda-bpmn-js-behaviors/-/camunda-bpmn-js-behaviors-1.10.0.tgz",
+ "integrity": "sha512-WQ4S/IcNjtRSZrEzhI9r1mk+57y0PAAZ+Xz/a5srGaCWv8dNHyXk66YRZDaomFSc67jS6toFO2HpQX9S0ZdQFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ids": "^1.0.0",
+ "min-dash": "^4.0.0"
+ },
+ "peerDependencies": {
+ "bpmn-js": ">= 9",
+ "camunda-bpmn-moddle": ">= 7",
+ "zeebe-bpmn-moddle": ">= 0.18"
+ }
+ },
+ "node_modules/camunda-bpmn-moddle": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/camunda-bpmn-moddle/-/camunda-bpmn-moddle-7.0.1.tgz",
+ "integrity": "sha512-Br8Diu6roMpziHdpl66Dhnm0DTnCFMrSD9zwLV08LpD52QA0UsXxU87XfHf08HjuB7ly0Hd1bvajZRpf9hbmYQ==",
+ "license": "MIT"
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001718",
+ "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
+ "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz",
+ "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/cheerio": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmmirror.com/cheerio/-/cheerio-1.0.0-rc.12.tgz",
+ "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
+ "license": "MIT",
+ "dependencies": {
+ "cheerio-select": "^2.1.0",
+ "dom-serializer": "^2.0.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "htmlparser2": "^8.0.1",
+ "parse5": "^7.0.0",
+ "parse5-htmlparser2-tree-adapter": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/cheerio?sponsor=1"
+ }
+ },
+ "node_modules/cheerio-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/cheerio-select/-/cheerio-select-2.1.0.tgz",
+ "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-select": "^5.1.0",
+ "css-what": "^6.1.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/codemirror": {
+ "version": "6.65.7",
+ "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.65.7.tgz",
+ "integrity": "sha512-HcfnUFJwI2FvH73YWVbbMh7ObWxZiHIycEhv9ZEXy6e8ZKDjtZKbbYFUtsLN46HFXPvU5V2Uvc2d55Z//oFW5A==",
+ "deprecated": "This is an accidentally mis-tagged instance of 5.65.7",
+ "license": "MIT"
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmmirror.com/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/common-tags": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmmirror.com/common-tags/-/common-tags-1.8.2.tgz",
+ "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/compare-func": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz",
+ "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-ify": "^1.0.0",
+ "dot-prop": "^5.1.0"
+ }
+ },
+ "node_modules/component-event": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/component-event/-/component-event-0.2.1.tgz",
+ "integrity": "sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw==",
+ "license": "MIT"
+ },
+ "node_modules/computeds": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz",
+ "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/config-chain/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/conventional-changelog-angular": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz",
+ "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/conventional-changelog-conventionalcommits": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmmirror.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz",
+ "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "compare-func": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/conventional-commits-parser": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz",
+ "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-text-path": "^2.0.0",
+ "JSONStream": "^1.3.5",
+ "meow": "^12.0.1",
+ "split2": "^4.0.0"
+ },
+ "bin": {
+ "conventional-commits-parser": "cli.mjs"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/conventional-commits-parser/node_modules/meow": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmmirror.com/meow/-/meow-12.1.1.tgz",
+ "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/core-js": {
+ "version": "3.42.0",
+ "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.42.0.tgz",
+ "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-compat": {
+ "version": "3.42.0",
+ "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.42.0.tgz",
+ "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.42.0",
+ "resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.42.0.tgz",
+ "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cosmiconfig-typescript-loader": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.1.0.tgz",
+ "integrity": "sha512-tJ1w35ZRUiM5FeTzT7DtYWAFFv37ZLqSRkGi2oeCK1gPhvaWjkAtfXvLmvE1pRfxxp9aQo6ba/Pvg1dKj05D4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jiti": "^2.4.1"
+ },
+ "engines": {
+ "node": ">=v18"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "cosmiconfig": ">=9",
+ "typescript": ">=5"
+ }
+ },
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/cropperjs": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.6.2.tgz",
+ "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
+ "license": "MIT"
+ },
+ "node_modules/cross-fetch": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/cross-fetch/-/cross-fetch-3.2.0.tgz",
+ "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-fetch": "^2.7.0"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypto-js": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+ "license": "MIT"
+ },
+ "node_modules/css-functions-list": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/css-functions-list/-/css-functions-list-3.2.3.tgz",
+ "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12 || >=16"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/css-select/-/css-select-5.1.0.tgz",
+ "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.1.0.tgz",
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.12.2",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csso": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmmirror.com/csso/-/csso-5.0.5.tgz",
+ "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-tree": "~2.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/css-tree": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-2.2.1.tgz",
+ "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.28",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
+ "npm": ">=7.0.0"
+ }
+ },
+ "node_modules/csso/node_modules/mdn-data": {
+ "version": "2.0.28",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.28.tgz",
+ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/d": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
+ "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
+ "license": "ISC",
+ "dependencies": {
+ "es5-ext": "^0.10.64",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmmirror.com/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmmirror.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-flextree": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/d3-flextree/-/d3-flextree-2.1.2.tgz",
+ "integrity": "sha512-gJiHrx5uTTHq44bjyIb3xpbmmdZcWLYPKeO9EPVOq8EylMFOiH2+9sWqKAiQ4DcFuOZTAxPOQyv0Rnmji/g15A==",
+ "license": "WTFPL",
+ "dependencies": {
+ "d3-hierarchy": "^1.1.5"
+ }
+ },
+ "node_modules/d3-flextree/node_modules/d3-hierarchy": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
+ "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/dargs": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmmirror.com/dargs/-/dargs-8.1.0.tgz",
+ "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+ "license": "MIT"
+ },
+ "node_modules/de-indent": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
+ "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/default-passive-events": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/default-passive-events/-/default-passive-events-2.0.0.tgz",
+ "integrity": "sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==",
+ "license": "MIT"
+ },
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delaunator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz",
+ "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/destr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz",
+ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/diagram-js": {
+ "version": "12.8.1",
+ "resolved": "https://registry.npmmirror.com/diagram-js/-/diagram-js-12.8.1.tgz",
+ "integrity": "sha512-LF9BiwjbOPpZd0ez5VSlYRbdbEA59YQX43bWvNDp1rLMv0xwZ5yIg4oaYDK82nIQ0kH1tjvoQRpNevMTCgQVyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@bpmn-io/diagram-js-ui": "^0.2.2",
+ "clsx": "^2.0.0",
+ "didi": "^9.0.2",
+ "hammerjs": "^2.0.1",
+ "inherits-browser": "^0.1.0",
+ "min-dash": "^4.1.0",
+ "min-dom": "^4.1.0",
+ "object-refs": "^0.3.0",
+ "path-intersection": "^2.2.1",
+ "tiny-svg": "^3.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/diagram-js-direct-editing": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/diagram-js-direct-editing/-/diagram-js-direct-editing-3.2.0.tgz",
+ "integrity": "sha512-+pyxeQGBSdLiZX0/tmmsm2qZSvm9YtVzod5W3RMHSTR7VrkUMD6E7EX/W9JQv3ebxO7oIdqFmytmNDDpSHnYEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-dash": "^4.0.0",
+ "min-dom": "^4.2.1"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "peerDependencies": {
+ "diagram-js": "*"
+ }
+ },
+ "node_modules/didi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmmirror.com/didi/-/didi-9.0.2.tgz",
+ "integrity": "sha512-q2+aj+lnJcUweV7A9pdUrwFr4LHVmRPwTmQLtHPFz4aT7IBoryN6Iy+jmFku+oIzr5ebBkvtBCOb87+dJhb7bg==",
+ "license": "MIT"
+ },
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/dom-walk": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/dom-walk/-/dom-walk-0.1.2.tgz",
+ "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domify": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmmirror.com/domify/-/domify-1.4.2.tgz",
+ "integrity": "sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.2.5.tgz",
+ "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz",
+ "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/driver.js": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmmirror.com/driver.js/-/driver.js-1.3.6.tgz",
+ "integrity": "sha512-g2nNuu+tWmPpuoyk3ffpT9vKhjPz4NrJzq6mkRDZIwXCrFhrKdDJ9TX5tJOBpvCTBrBYjgRQ17XlcQB15q4gMg==",
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
+ "node_modules/echarts-wordcloud": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/echarts-wordcloud/-/echarts-wordcloud-2.1.0.tgz",
+ "integrity": "sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "echarts": "^5.0.1"
+ }
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.4.tgz",
+ "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/editorconfig/node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ejs": {
+ "version": "3.1.10",
+ "resolved": "https://registry.npmmirror.com/ejs/-/ejs-3.1.10.tgz",
+ "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "jake": "^10.8.5"
+ },
+ "bin": {
+ "ejs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.155",
+ "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
+ "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/element-plus": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.1.tgz",
+ "integrity": "sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^3.4.1",
+ "@element-plus/icons-vue": "^2.3.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+ "@types/lodash": "^4.14.182",
+ "@types/lodash-es": "^4.17.6",
+ "@vueuse/core": "^9.1.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.13",
+ "escape-html": "^1.0.3",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "lodash-unified": "^1.0.2",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/element-plus/node_modules/@types/web-bluetooth": {
+ "version": "0.0.16",
+ "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
+ "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
+ "license": "MIT"
+ },
+ "node_modules/element-plus/node_modules/@vueuse/core": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
+ "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.16",
+ "@vueuse/metadata": "9.13.0",
+ "@vueuse/shared": "9.13.0",
+ "vue-demi": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/element-plus/node_modules/@vueuse/metadata": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
+ "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/element-plus/node_modules/@vueuse/shared": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
+ "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/environment": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz",
+ "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es5-ext": {
+ "version": "0.10.64",
+ "resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
+ "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
+ "hasInstallScript": true,
+ "license": "ISC",
+ "dependencies": {
+ "es6-iterator": "^2.0.3",
+ "es6-symbol": "^3.1.3",
+ "esniff": "^2.0.1",
+ "next-tick": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/es6-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
+ "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "^0.10.35",
+ "es6-symbol": "^3.1.1"
+ }
+ },
+ "node_modules/es6-symbol": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
+ "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.2",
+ "ext": "^1.7.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.19.12",
+ "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.19.12.tgz",
+ "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.19.12",
+ "@esbuild/android-arm": "0.19.12",
+ "@esbuild/android-arm64": "0.19.12",
+ "@esbuild/android-x64": "0.19.12",
+ "@esbuild/darwin-arm64": "0.19.12",
+ "@esbuild/darwin-x64": "0.19.12",
+ "@esbuild/freebsd-arm64": "0.19.12",
+ "@esbuild/freebsd-x64": "0.19.12",
+ "@esbuild/linux-arm": "0.19.12",
+ "@esbuild/linux-arm64": "0.19.12",
+ "@esbuild/linux-ia32": "0.19.12",
+ "@esbuild/linux-loong64": "0.19.12",
+ "@esbuild/linux-mips64el": "0.19.12",
+ "@esbuild/linux-ppc64": "0.19.12",
+ "@esbuild/linux-riscv64": "0.19.12",
+ "@esbuild/linux-s390x": "0.19.12",
+ "@esbuild/linux-x64": "0.19.12",
+ "@esbuild/netbsd-x64": "0.19.12",
+ "@esbuild/openbsd-x64": "0.19.12",
+ "@esbuild/sunos-x64": "0.19.12",
+ "@esbuild/win32-arm64": "0.19.12",
+ "@esbuild/win32-ia32": "0.19.12",
+ "@esbuild/win32-x64": "0.19.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.57.1",
+ "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz",
+ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.1",
+ "@humanwhocodes/config-array": "^0.13.0",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-define-config": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/eslint-define-config/-/eslint-define-config-2.1.0.tgz",
+ "integrity": "sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/Shinigami92"
+ },
+ {
+ "type": "paypal",
+ "url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=9.0.0",
+ "pnpm": ">=8.6.0"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
+ "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.11.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-prettier/node_modules/@pkgr/core": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.4.tgz",
+ "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/eslint-plugin-prettier/node_modules/synckit": {
+ "version": "0.11.5",
+ "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.5.tgz",
+ "integrity": "sha512-frqvfWyDA5VPVdrWfH24uM6SI/O8NLpVbIIJxb8t/a3YGsp4AW9CYgSKC0OaSEfexnp7Y1pVh2Y6IHO8ggGDmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.2.4",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/eslint-plugin-prettier/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/eslint-plugin-vue": {
+ "version": "9.33.0",
+ "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz",
+ "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "globals": "^13.24.0",
+ "natural-compare": "^1.4.0",
+ "nth-check": "^2.1.1",
+ "postcss-selector-parser": "^6.0.15",
+ "semver": "^7.6.3",
+ "vue-eslint-parser": "^9.4.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-vue/node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/eslint/node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/esniff": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
+ "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
+ "license": "ISC",
+ "dependencies": {
+ "d": "^1.0.1",
+ "es5-ext": "^0.10.62",
+ "event-emitter": "^0.3.5",
+ "type": "^2.7.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/event-emitter": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
+ "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
+ "license": "MIT",
+ "dependencies": {
+ "d": "1",
+ "es5-ext": "~0.10.14"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
+ "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/execa": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz",
+ "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^8.0.1",
+ "human-signals": "^5.0.0",
+ "is-stream": "^3.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^5.1.0",
+ "onetime": "^6.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-final-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.17"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.5.tgz",
+ "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ext": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
+ "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
+ "license": "ISC",
+ "dependencies": {
+ "type": "^2.7.2"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.0.6.tgz",
+ "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "4.5.3",
+ "resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
+ "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^1.1.1"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmmirror.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/feelers": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/feelers/-/feelers-1.4.0.tgz",
+ "integrity": "sha512-CGa/7ILuqoqTaeYeoKsg/4tzu2es9sEEJTmSjdu0lousZBw4V9gcYhHYFNmbrSrKmbAVfOzj6/DsymGJWFIOeg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@bpmn-io/cm-theme": "^0.1.0-alpha.2",
+ "@bpmn-io/feel-lint": "^1.2.0",
+ "@codemirror/autocomplete": "^6.10.1",
+ "@codemirror/commands": "^6.3.0",
+ "@codemirror/language": "^6.9.1",
+ "@codemirror/lint": "^6.4.2",
+ "@codemirror/state": "^6.3.0",
+ "@codemirror/view": "^6.21.3",
+ "@lezer/common": "^1.1.0",
+ "@lezer/highlight": "^1.1.6",
+ "@lezer/lr": "^1.3.13",
+ "@lezer/markdown": "^1.1.0",
+ "feelin": "^3.0.1",
+ "lezer-feel": "^1.2.4",
+ "min-dom": "^5.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "workspaces": {
+ "packages": [
+ "feelers-playground"
+ ]
+ }
+ },
+ "node_modules/feelers/node_modules/domify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/domify/-/domify-2.0.0.tgz",
+ "integrity": "sha512-rmvrrmWQPD/X1A/nPBfIVg4r05792QdG9Z4Prk6oQG0F9zBMDkr0GKAdds1wjb2dq1rTz/ywc4ZxpZbgz0tttg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/feelers/node_modules/min-dom": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-5.1.1.tgz",
+ "integrity": "sha512-GaKUlguMAofd3OJsB0OkP17i5kucKqErgVCJxPawO9l5NwIPnr28SAr99zzlzMCWWljISBYrnZVWdE2Q92YGFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "domify": "^2.0.0",
+ "min-dash": "^4.2.1"
+ }
+ },
+ "node_modules/feelin": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/feelin/-/feelin-3.2.0.tgz",
+ "integrity": "sha512-GFDbHsTYk7YXO1tyw1dOjb7IODeAZvNIosdGZThUwPx5XcD/XhO0hnPZXsIbAzSsIdrgGlTEEdby9fZ2gixysA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@lezer/lr": "^1.4.2",
+ "lezer-feel": "^1.4.0",
+ "luxon": "^3.5.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/filelist": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/filelist/-/filelist-1.0.4.tgz",
+ "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "minimatch": "^5.0.1"
+ }
+ },
+ "node_modules/filelist/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-7.0.0.tgz",
+ "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^7.2.0",
+ "path-exists": "^5.0.0",
+ "unicorn-magic": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flat-cache/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/flat-cache/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/flat-cache/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/flat-cache/node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/focus-trap": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmmirror.com/focus-trap/-/focus-trap-7.6.4.tgz",
+ "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tabbable": "^6.2.0"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz",
+ "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz",
+ "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/git-raw-commits": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/git-raw-commits/-/git-raw-commits-4.0.0.tgz",
+ "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dargs": "^8.0.0",
+ "meow": "^12.0.1",
+ "split2": "^4.0.0"
+ },
+ "bin": {
+ "git-raw-commits": "cli.mjs"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/git-raw-commits/node_modules/meow": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmmirror.com/meow/-/meow-12.1.1.tgz",
+ "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/global": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmmirror.com/global/-/global-4.4.0.tgz",
+ "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+ "license": "MIT",
+ "dependencies": {
+ "min-document": "^2.19.0",
+ "process": "^0.11.10"
+ }
+ },
+ "node_modules/global-directory": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/global-directory/-/global-directory-4.0.1.tgz",
+ "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "4.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/global-modules/-/global-modules-2.0.0.tgz",
+ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "global-prefix": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/global-prefix/-/global-prefix-3.0.0.tgz",
+ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.5",
+ "kind-of": "^6.0.2",
+ "which": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmmirror.com/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globjoin": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/globjoin/-/globjoin-0.1.4.tgz",
+ "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/gzip-size": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/gzip-size/-/gzip-size-6.0.0.tgz",
+ "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/hammerjs": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmmirror.com/hammerjs/-/hammerjs-2.0.8.tgz",
+ "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-ansi/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/hookified": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmmirror.com/hookified/-/hookified-1.9.0.tgz",
+ "integrity": "sha512-2yEEGqphImtKIe1NXWEhu6yD3hlFR4Mxk4Mtp3XEyScpSt4pQ4ymmXA1zzxZpj99QkFK+nN0nzjeb2+RUi/6CQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/htm": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/htm/-/htm-3.1.1.tgz",
+ "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/html-tags": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/html-tags/-/html-tags-3.3.1.tgz",
+ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-8.0.2.tgz",
+ "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.0.1",
+ "entities": "^4.4.0"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz",
+ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=16.17.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ids": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/ids/-/ids-1.0.5.tgz",
+ "integrity": "sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "9.0.21",
+ "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
+ "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.2.tgz",
+ "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-meta-resolve": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz",
+ "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/individual": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/individual/-/individual-2.0.0.tgz",
+ "integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g=="
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits-browser": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/inherits-browser/-/inherits-browser-0.1.0.tgz",
+ "integrity": "sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/ini/-/ini-4.1.1.tgz",
+ "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-function": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/is-function/-/is-function-1.0.2.tgz",
+ "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hotkey": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
+ "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
+ "license": "MIT"
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz",
+ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-text-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/is-text-path/-/is-text-path-2.0.0.tgz",
+ "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "text-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-url": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
+ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jake": {
+ "version": "10.9.2",
+ "resolved": "https://registry.npmmirror.com/jake/-/jake-10.9.2.tgz",
+ "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "async": "^3.2.3",
+ "chalk": "^4.0.2",
+ "filelist": "^1.0.4",
+ "minimatch": "^3.1.2"
+ },
+ "bin": {
+ "jake": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jake/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/jake/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/jake/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/jake/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmmirror.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
+ "license": "MIT"
+ },
+ "node_modules/jiti": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.4.2.tgz",
+ "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/jmespath": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmmirror.com/jmespath/-/jmespath-0.16.0.tgz",
+ "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.4",
+ "resolved": "https://registry.npmmirror.com/js-beautify/-/js-beautify-1.15.4.tgz",
+ "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "license": "MIT",
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.4.2",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.1"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsencrypt": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.3.2.tgz",
+ "integrity": "sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmmirror.com/json-source-map/-/json-source-map-0.6.1.tgz",
+ "integrity": "sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==",
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonc-eslint-parser": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmmirror.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz",
+ "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.5.0",
+ "eslint-visitor-keys": "^3.0.0",
+ "espree": "^9.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
+ }
+ },
+ "node_modules/jsoneditor": {
+ "version": "10.4.2",
+ "resolved": "https://registry.npmmirror.com/jsoneditor/-/jsoneditor-10.4.2.tgz",
+ "integrity": "sha512-SQPCXlanU4PqdVsYuj2X7yfbLiiJYjklbksGfMKPsuwLhAIPxDlG43jYfXieGXvxpuq1fkw08YoRbkKXKabcLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "ace-builds": "^1.36.2",
+ "ajv": "^6.12.6",
+ "javascript-natural-sort": "^0.7.1",
+ "jmespath": "^0.16.0",
+ "json-source-map": "^0.6.1",
+ "jsonrepair": "^3.8.1",
+ "picomodal": "^3.0.0",
+ "vanilla-picker": "^2.12.3"
+ }
+ },
+ "node_modules/jsoneditor/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/jsoneditor/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "license": "MIT"
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmmirror.com/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "dev": true,
+ "engines": [
+ "node >= 0.2.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/jsonrepair": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmmirror.com/jsonrepair/-/jsonrepair-3.13.1.tgz",
+ "integrity": "sha512-WJeiE0jGfxYmtLwBTEk8+y/mYcaleyLXWaqp5bJu0/ZTSeG0KQq/wWQ8pmnkKenEdN6pdnn6QtcoSUkbqDHWNw==",
+ "license": "ISC",
+ "bin": {
+ "jsonrepair": "bin/cli.js"
+ }
+ },
+ "node_modules/JSONStream": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmmirror.com/JSONStream/-/JSONStream-1.3.5.tgz",
+ "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
+ "dev": true,
+ "license": "(MIT OR Apache-2.0)",
+ "dependencies": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ },
+ "bin": {
+ "JSONStream": "bin.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/katex": {
+ "version": "0.16.22",
+ "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.22.tgz",
+ "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^8.3.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "node_modules/katex/node_modules/commander": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz",
+ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/keycode": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/keycode/-/keycode-2.2.1.tgz",
+ "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==",
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmmirror.com/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/known-css-properties": {
+ "version": "0.36.0",
+ "resolved": "https://registry.npmmirror.com/known-css-properties/-/known-css-properties-0.36.0.tgz",
+ "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/kolorist": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz",
+ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lang-feel": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/lang-feel/-/lang-feel-2.3.0.tgz",
+ "integrity": "sha512-cotBfyBP710udy3Tm7s4NyNZPSSLXkVV/rrfmM4NVbuzB9WGL7CbMWUzfSn6GZ+qFnh8/xbkeDHfAvPM90oENA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.18.4",
+ "@codemirror/language": "^6.10.8",
+ "@lezer/common": "^1.2.3",
+ "lezer-feel": "^1.7.0"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lezer-feel": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/lezer-feel/-/lezer-feel-1.7.0.tgz",
+ "integrity": "sha512-UC8h3Nu4llRPISEUhv+Ne7bNkdxjf4+/DcU4KfO8zKxycWxev8d2BoVnGlG17zbQDtQJBD39ZQvWtjCeTFm69g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@lezer/highlight": "^1.2.1",
+ "@lezer/lr": "^1.4.2",
+ "min-dash": "^4.2.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/lint-staged": {
+ "version": "15.5.2",
+ "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-15.5.2.tgz",
+ "integrity": "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.4.1",
+ "commander": "^13.1.0",
+ "debug": "^4.4.0",
+ "execa": "^8.0.1",
+ "lilconfig": "^3.1.3",
+ "listr2": "^8.2.5",
+ "micromatch": "^4.0.8",
+ "pidtree": "^0.6.0",
+ "string-argv": "^0.3.2",
+ "yaml": "^2.7.0"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/commander": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-13.1.0.tgz",
+ "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "8.3.3",
+ "resolved": "https://registry.npmmirror.com/listr2/-/listr2-8.3.3.tgz",
+ "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cli-truncate": "^4.0.0",
+ "colorette": "^2.0.20",
+ "eventemitter3": "^5.0.1",
+ "log-update": "^6.1.0",
+ "rfdc": "^1.4.1",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/listr2/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/listr2/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/listr2/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/local-pkg": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.1.tgz",
+ "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mlly": "^1.7.4",
+ "pkg-types": "^2.0.1",
+ "quansync": "^0.2.8"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/local-pkg/node_modules/confbox": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz",
+ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/local-pkg/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/local-pkg/node_modules/pkg-types": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.1.0.tgz",
+ "integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.1",
+ "exsolve": "^1.0.1",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-7.2.0.tgz",
+ "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.foreach": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
+ "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.kebabcase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+ "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.mergewith": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmmirror.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.snakecase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.startcase": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmmirror.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz",
+ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.throttle": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+ "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.toarray": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
+ "integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmmirror.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.upperfirst": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmmirror.com/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz",
+ "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-update": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz",
+ "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^7.0.0",
+ "cli-cursor": "^5.0.0",
+ "slice-ansi": "^7.1.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-update/node_modules/is-fullwidth-code-point": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz",
+ "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/slice-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.0.tgz",
+ "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "is-fullwidth-code-point": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-update/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/log-update/node_modules/wrap-ansi": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz",
+ "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/loglevel": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmmirror.com/loglevel/-/loglevel-1.9.2.tgz",
+ "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ },
+ "funding": {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/loglevel"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz",
+ "integrity": "sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^1.1.3",
+ "loglevel": "^1.4.1"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix/node_modules/ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix/node_modules/ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix/node_modules/chalk": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix/node_modules/strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/loglevel-colored-level-prefix/node_modules/supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/luxon": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmmirror.com/luxon/-/luxon-3.6.1.tgz",
+ "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/m3u8-parser": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmmirror.com/m3u8-parser/-/m3u8-parser-4.8.0.tgz",
+ "integrity": "sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@videojs/vhs-utils": "^3.0.5",
+ "global": "^4.4.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markmap-common": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmmirror.com/markmap-common/-/markmap-common-0.16.0.tgz",
+ "integrity": "sha512-q3nlNDMKuWXTm3VwZFY9V5zteL/+iBLZanUK5vS+e26bUbzTSG5VtAzsyJbmgJm1WhwmIIAxbXEnp6JdvtTduA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@gera2ld/jsx-dom": "^2.2.2",
+ "npm2url": "^0.2.4"
+ }
+ },
+ "node_modules/markmap-html-parser": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmmirror.com/markmap-html-parser/-/markmap-html-parser-0.16.1.tgz",
+ "integrity": "sha512-/Mgm4g1qMQ8uEOz8h8K+jPspdgjfw29NqmfTLZSt8yG+vW7fWWduPjGRFc5axAZxCzP7PTzZLEuOxAqOwEg8Bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "cheerio": "1.0.0-rc.12"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/markmap-lib": {
+ "version": "0.16.1",
+ "resolved": "https://registry.npmmirror.com/markmap-lib/-/markmap-lib-0.16.1.tgz",
+ "integrity": "sha512-jD8VsB67m677IRehGSwwVJDlC6PS+xzDKsJOwdvjZ+ndfXrHa1lyqfvR6mIwvGGUIciF86YEITSKL9hQTHE4Rw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "highlight.js": "^11.8.0",
+ "js-yaml": "^4.1.0",
+ "katex": "^0.16.8",
+ "markmap-html-parser": "0.16.1",
+ "markmap-view": "0.16.0",
+ "prismjs": "^1.29.0",
+ "remarkable": "^2.0.1",
+ "remarkable-katex": "^1.2.1"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/markmap-toolbar": {
+ "version": "0.17.2",
+ "resolved": "https://registry.npmmirror.com/markmap-toolbar/-/markmap-toolbar-0.17.2.tgz",
+ "integrity": "sha512-WQ05P2xvQmZT0ybRUE0uRzrs30aXlJ6/yEUsA6A9nYEwm8T9jSwBxIM/5zYlkH/XzUcsRRxtCa4k1IWR74gkpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@gera2ld/jsx-dom": "^2.2.2"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/markmap-view": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmmirror.com/markmap-view/-/markmap-view-0.16.0.tgz",
+ "integrity": "sha512-JOiSEThs8B4bAP9E6rcCWOz2SsMwCBFaR76wLARRVb04C/qLiLmvrm675kNPq4lRBAwtugHCYvjG0otpSlB4Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.22.6",
+ "@gera2ld/jsx-dom": "^2.2.2",
+ "@types/d3": "^7.4.0",
+ "d3": "^7.8.5",
+ "d3-flextree": "^2.1.2"
+ },
+ "peerDependencies": {
+ "markmap-common": "*"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mathml-tag-names": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
+ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/meow": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
+ "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
+ "license": "ISC",
+ "dependencies": {
+ "wildcard": "^1.1.0"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz",
+ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/min-dash": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/min-dash/-/min-dash-4.2.3.tgz",
+ "integrity": "sha512-VLMYQI5+FcD9Ad24VcB08uA83B07OhueAlZ88jBK6PyupTvEJwllTMUqMy0wPGYs7pZUEtEEMWdHB63m3LtEcg==",
+ "license": "MIT"
+ },
+ "node_modules/min-document": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmmirror.com/min-document/-/min-document-2.19.0.tgz",
+ "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
+ "dependencies": {
+ "dom-walk": "^0.1.0"
+ }
+ },
+ "node_modules/min-dom": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmmirror.com/min-dom/-/min-dom-4.2.1.tgz",
+ "integrity": "sha512-TMoL8SEEIhUWYgkj7XMSgxmwSyGI+4fP2KFFGnN3FbHfbGHVdsLYSz8LoIsgPhz4dWRmLvxWWSMgzZMJW5sZuA==",
+ "license": "MIT",
+ "dependencies": {
+ "component-event": "^0.2.1",
+ "domify": "^1.4.1",
+ "min-dash": "^4.2.1"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
+ "node_modules/mlly": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.7.4.tgz",
+ "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "pathe": "^2.0.1",
+ "pkg-types": "^1.3.0",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/mlly/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/moddle": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmmirror.com/moddle/-/moddle-6.2.3.tgz",
+ "integrity": "sha512-bLVN+ZHL3aKnhxc19XtjUfvdJsS3EsiEJC7bT6YPD11qYmTzvsxrGgyYz1Ouof7TZuGw0lDJ1OLmEnxcpQWk3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-dash": "^4.0.0"
+ }
+ },
+ "node_modules/moddle-xml": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmmirror.com/moddle-xml/-/moddle-xml-10.1.0.tgz",
+ "integrity": "sha512-erWckwLt+dYskewKXJso9u+aAZ5172lOiYxSOqKCPTy7L/xmqH1PoeoA7eVC7oJTt3PqF5TkZzUmbjGH6soQBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-dash": "^4.0.0",
+ "moddle": "^6.0.0",
+ "saxen": "^8.1.2"
+ }
+ },
+ "node_modules/mpd-parser": {
+ "version": "0.22.1",
+ "resolved": "https://registry.npmmirror.com/mpd-parser/-/mpd-parser-0.22.1.tgz",
+ "integrity": "sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@videojs/vhs-utils": "^3.0.5",
+ "@xmldom/xmldom": "^0.8.3",
+ "global": "^4.4.0"
+ },
+ "bin": {
+ "mpd-to-m3u8-json": "bin/parse.js"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/muggle-string": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz",
+ "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mux.js": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/mux.js/-/mux.js-6.0.1.tgz",
+ "integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.11.2",
+ "global": "^4.4.0"
+ },
+ "bin": {
+ "muxjs-transmux": "bin/transmux.js"
+ },
+ "engines": {
+ "node": ">=8",
+ "npm": ">=5"
+ }
+ },
+ "node_modules/namespace-emitter": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
+ "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/next-tick": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
+ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
+ "license": "ISC"
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-fetch-native": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz",
+ "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-html-parser": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/node-html-parser/-/node-html-parser-7.0.1.tgz",
+ "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "he": "1.2.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmmirror.com/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmmirror.com/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/npm-run-path": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz",
+ "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm2url": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmmirror.com/npm2url/-/npm2url-0.2.4.tgz",
+ "integrity": "sha512-arzGp/hQz0Ey+ZGhF64XVH7Xqwd+1Q/po5uGiBbzph8ebX6T0uvt3N7c1nBHQNsQVykQgHhqoRTX7JFcHecGuw==",
+ "license": "MIT"
+ },
+ "node_modules/nprogress": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz",
+ "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
+ "license": "MIT"
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-refs": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmmirror.com/object-refs/-/object-refs-0.3.0.tgz",
+ "integrity": "sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==",
+ "license": "MIT"
+ },
+ "node_modules/ofetch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/ofetch/-/ofetch-1.4.1.tgz",
+ "integrity": "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "destr": "^2.0.3",
+ "node-fetch-native": "^1.6.4",
+ "ufo": "^1.5.4"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz",
+ "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-4.0.0.tgz",
+ "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-6.0.0.tgz",
+ "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/package-manager-detector": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz",
+ "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
+ "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "^5.0.3",
+ "parse5": "^7.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.0.tgz",
+ "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-5.0.0.tgz",
+ "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/path-intersection": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmmirror.com/path-intersection/-/path-intersection-2.2.1.tgz",
+ "integrity": "sha512-9u8xvMcSfuOiStv9bPdnRJQhGQXLKurew94n4GPQCdH1nj9QKC9ObbNoIpiRq8skiOBxKkt277PgOoFgAt3/rA==",
+ "license": "MIT"
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz",
+ "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz",
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/picomodal": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/picomodal/-/picomodal-3.0.0.tgz",
+ "integrity": "sha512-FoR3TDfuLlqUvcEeK5ifpKSVVns6B4BQvc8SDF6THVMuadya6LLtji0QgUDSStw0ZR2J7I6UGi5V2V23rnPWTw==",
+ "license": "MIT"
+ },
+ "node_modules/pidtree": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz",
+ "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pinia": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz",
+ "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.3",
+ "vue-demi": "^0.14.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pinia-plugin-persistedstate": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.3.tgz",
+ "integrity": "sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pinia": "^2.0.0"
+ }
+ },
+ "node_modules/pinia/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pkcs7": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/pkcs7/-/pkcs7-1.0.4.tgz",
+ "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5"
+ },
+ "bin": {
+ "pkcs7": "bin/cli.js"
+ }
+ },
+ "node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
+ },
+ "node_modules/pkg-types/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.3.tgz",
+ "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-html": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/postcss-html/-/postcss-html-1.8.0.tgz",
+ "integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "htmlparser2": "^8.0.0",
+ "js-tokens": "^9.0.0",
+ "postcss": "^8.5.0",
+ "postcss-safe-parser": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12 || >=14"
+ }
+ },
+ "node_modules/postcss-html/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss-resolve-nested-selector": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmmirror.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz",
+ "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/postcss-safe-parser": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz",
+ "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3"
+ }
+ },
+ "node_modules/postcss-scss": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmmirror.com/postcss-scss/-/postcss-scss-4.0.9.tgz",
+ "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-scss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.29"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-sorting": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmmirror.com/postcss-sorting/-/postcss-sorting-8.0.2.tgz",
+ "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "postcss": "^8.4.20"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/preact": {
+ "version": "10.26.6",
+ "resolved": "https://registry.npmmirror.com/preact/-/preact-10.26.6.tgz",
+ "integrity": "sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/preact"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.5.3.tgz",
+ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-eslint": {
+ "version": "16.4.2",
+ "resolved": "https://registry.npmmirror.com/prettier-eslint/-/prettier-eslint-16.4.2.tgz",
+ "integrity": "sha512-vtJAQEkaN8fW5QKl08t7A5KCjlZuDUNeIlr9hgolMS5s3+uzbfRHDwaRnzrdqnY2YpHDmeDS/8zY0MKQHXJtaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/parser": "^6.21.0",
+ "common-tags": "^1.8.2",
+ "dlv": "^1.1.3",
+ "eslint": "^8.57.1",
+ "indent-string": "^4.0.0",
+ "lodash.merge": "^4.6.2",
+ "loglevel-colored-level-prefix": "^1.0.0",
+ "prettier": "^3.5.3",
+ "pretty-format": "^29.7.0",
+ "require-relative": "^0.8.7",
+ "tslib": "^2.8.1",
+ "vue-eslint-parser": "^9.4.3"
+ },
+ "engines": {
+ "node": ">=16.10.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/prettier-eslint"
+ },
+ "peerDependencies": {
+ "prettier-plugin-svelte": "^3.0.0",
+ "svelte-eslint-parser": "*"
+ },
+ "peerDependenciesMeta": {
+ "prettier-plugin-svelte": {
+ "optional": true
+ },
+ "svelte-eslint-parser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/@typescript-eslint/parser": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.21.0.tgz",
+ "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "6.21.0",
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/typescript-estree": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz",
+ "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/@typescript-eslint/types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.21.0.tgz",
+ "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz",
+ "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "@typescript-eslint/visitor-keys": "6.21.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "9.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz",
+ "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "6.21.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/minimatch": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz",
+ "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/prettier-eslint/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/schemas": "^29.6.3",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/prismjs": {
+ "version": "1.30.0",
+ "resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
+ "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmmirror.com/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "license": "ISC"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+ "license": "MIT"
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/qrcode/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/qrcode/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/qrcode/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/qrcode/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/quansync": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.10.tgz",
+ "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/randomcolor": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmmirror.com/randomcolor/-/randomcolor-0.6.2.tgz",
+ "integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==",
+ "license": "CC0"
+ },
+ "node_modules/rd": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/rd/-/rd-2.0.1.tgz",
+ "integrity": "sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "^10.3.6"
+ }
+ },
+ "node_modules/rd/node_modules/@types/node": {
+ "version": "10.17.60",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-10.17.60.tgz",
+ "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/regenerate": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz",
+ "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regenerate-unicode-properties": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz",
+ "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.14.1",
+ "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regexpu-core": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.2.0.tgz",
+ "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerate": "^1.4.2",
+ "regenerate-unicode-properties": "^10.2.0",
+ "regjsgen": "^0.8.0",
+ "regjsparser": "^0.12.0",
+ "unicode-match-property-ecmascript": "^2.0.0",
+ "unicode-match-property-value-ecmascript": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/regjsgen": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz",
+ "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regjsparser": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.12.0.tgz",
+ "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "jsesc": "~3.0.2"
+ },
+ "bin": {
+ "regjsparser": "bin/parser"
+ }
+ },
+ "node_modules/regjsparser/node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/remarkable": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/remarkable/-/remarkable-2.0.1.tgz",
+ "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.10",
+ "autolinker": "^3.11.0"
+ },
+ "bin": {
+ "remarkable": "bin/remarkable.js"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/remarkable-katex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/remarkable-katex/-/remarkable-katex-1.2.1.tgz",
+ "integrity": "sha512-Y1VquJBZnaVsfsVcKW2hmjT+pDL7mp8l5WAVlvuvViltrdok2m1AIKmJv8SsH+mBY84PoMw67t3kTWw1dIm8+g==",
+ "license": "MIT"
+ },
+ "node_modules/remarkable/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/require-relative": {
+ "version": "0.8.7",
+ "resolved": "https://registry.npmmirror.com/require-relative/-/require-relative-0.8.7.tgz",
+ "integrity": "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/restore-cursor/node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rimraf": {
+ "version": "5.0.10",
+ "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-5.0.10.tgz",
+ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^10.3.7"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/robust-predicates": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz",
+ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
+ "license": "Unlicense"
+ },
+ "node_modules/rollup": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.40.2.tgz",
+ "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.40.2",
+ "@rollup/rollup-android-arm64": "4.40.2",
+ "@rollup/rollup-darwin-arm64": "4.40.2",
+ "@rollup/rollup-darwin-x64": "4.40.2",
+ "@rollup/rollup-freebsd-arm64": "4.40.2",
+ "@rollup/rollup-freebsd-x64": "4.40.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.40.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.40.2",
+ "@rollup/rollup-linux-arm64-musl": "4.40.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.40.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-musl": "4.40.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.40.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.40.2",
+ "@rollup/rollup-win32-x64-msvc": "4.40.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup-plugin-purge-icons": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmmirror.com/rollup-plugin-purge-icons/-/rollup-plugin-purge-icons-0.10.0.tgz",
+ "integrity": "sha512-GD2ftg4L9G/sagIhtCmBn5vdyzePOisniythubpbywP0Q3ix9rZuDeFvgXTPemOsc22pvH7t22ryYQIl0rwGog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@purge-icons/core": "^0.10.0",
+ "@purge-icons/generated": "^0.10.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/rollup-plugin-purge-icons/node_modules/@purge-icons/generated": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmmirror.com/@purge-icons/generated/-/generated-0.10.0.tgz",
+ "integrity": "sha512-I+1yN7/yDy/eZzfhAZqKF8Z6FM8D/O1vempbPrHJ0m9HlZwvf8sWXOArPJ2qRQGB6mJUVSpaXkoGBuoz1GQX5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/iconify": ">=3.1.1"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rust-result": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/rust-result/-/rust-result-1.0.0.tgz",
+ "integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==",
+ "license": "MIT",
+ "dependencies": {
+ "individual": "^2.0.0"
+ }
+ },
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safe-json-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
+ "integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==",
+ "dependencies": {
+ "rust-result": "^1.0.0"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/sass": {
+ "version": "1.89.0",
+ "resolved": "https://registry.npmmirror.com/sass/-/sass-1.89.0.tgz",
+ "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.0.2",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmmirror.com/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+ "license": "ISC"
+ },
+ "node_modules/saxen": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmmirror.com/saxen/-/saxen-8.1.2.tgz",
+ "integrity": "sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/scule": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
+ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/signature_pad": {
+ "version": "3.0.0-beta.4",
+ "resolved": "https://registry.npmmirror.com/signature_pad/-/signature_pad-3.0.0-beta.4.tgz",
+ "integrity": "sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw==",
+ "license": "MIT"
+ },
+ "node_modules/sirv": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmmirror.com/sirv/-/sirv-2.0.4.tgz",
+ "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/slice-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/snabbdom": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.2.tgz",
+ "integrity": "sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.17.0"
+ }
+ },
+ "node_modules/sortablejs": {
+ "version": "1.15.6",
+ "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.6.tgz",
+ "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
+ "license": "MIT"
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/steady-xml": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/steady-xml/-/steady-xml-0.1.0.tgz",
+ "integrity": "sha512-5sk17qO2wWRtonTNoBhoKAB35OSsGJOa3+NEa6D+1GS+de+ujDWxnflMkXBrviOfkNrPTUqduAdXhrMJs89nAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/string-argv": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz",
+ "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+ "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz",
+ "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/strnum": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/strnum/-/strnum-1.1.2.tgz",
+ "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/style-mod/-/style-mod-4.1.2.tgz",
+ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/stylelint": {
+ "version": "16.19.1",
+ "resolved": "https://registry.npmmirror.com/stylelint/-/stylelint-16.19.1.tgz",
+ "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "@csstools/media-query-list-parser": "^4.0.2",
+ "@csstools/selector-specificity": "^5.0.0",
+ "@dual-bundle/import-meta-resolve": "^4.1.0",
+ "balanced-match": "^2.0.0",
+ "colord": "^2.9.3",
+ "cosmiconfig": "^9.0.0",
+ "css-functions-list": "^3.2.3",
+ "css-tree": "^3.1.0",
+ "debug": "^4.3.7",
+ "fast-glob": "^3.3.3",
+ "fastest-levenshtein": "^1.0.16",
+ "file-entry-cache": "^10.0.8",
+ "global-modules": "^2.0.0",
+ "globby": "^11.1.0",
+ "globjoin": "^0.1.4",
+ "html-tags": "^3.3.1",
+ "ignore": "^7.0.3",
+ "imurmurhash": "^0.1.4",
+ "is-plain-object": "^5.0.0",
+ "known-css-properties": "^0.36.0",
+ "mathml-tag-names": "^2.1.3",
+ "meow": "^13.2.0",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.5.3",
+ "postcss-resolve-nested-selector": "^0.1.6",
+ "postcss-safe-parser": "^7.0.1",
+ "postcss-selector-parser": "^7.1.0",
+ "postcss-value-parser": "^4.2.0",
+ "resolve-from": "^5.0.0",
+ "string-width": "^4.2.3",
+ "supports-hyperlinks": "^3.2.0",
+ "svg-tags": "^1.0.0",
+ "table": "^6.9.0",
+ "write-file-atomic": "^5.0.1"
+ },
+ "bin": {
+ "stylelint": "bin/stylelint.mjs"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ }
+ },
+ "node_modules/stylelint-config-html": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/stylelint-config-html/-/stylelint-config-html-1.1.0.tgz",
+ "integrity": "sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12 || >=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
+ },
+ "peerDependencies": {
+ "postcss-html": "^1.0.0",
+ "stylelint": ">=14.0.0"
+ }
+ },
+ "node_modules/stylelint-config-recommended": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmmirror.com/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz",
+ "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.1.0"
+ }
+ },
+ "node_modules/stylelint-config-standard": {
+ "version": "36.0.1",
+ "resolved": "https://registry.npmmirror.com/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz",
+ "integrity": "sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/stylelint"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "stylelint-config-recommended": "^14.0.1"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.1.0"
+ }
+ },
+ "node_modules/stylelint-order": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmmirror.com/stylelint-order/-/stylelint-order-6.0.4.tgz",
+ "integrity": "sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss": "^8.4.32",
+ "postcss-sorting": "^8.0.2"
+ },
+ "peerDependencies": {
+ "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.1"
+ }
+ },
+ "node_modules/stylelint/node_modules/@csstools/selector-specificity": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz",
+ "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^7.0.0"
+ }
+ },
+ "node_modules/stylelint/node_modules/balanced-match": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-2.0.0.tgz",
+ "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stylelint/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stylelint/node_modules/file-entry-cache": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-10.1.0.tgz",
+ "integrity": "sha512-Et/ex6smi3wOOB+n5mek+Grf7P2AxZR5ueqRUvAAn4qkyatXi3cUC1cuQXVkX0VlzBVsN4BkWJFmY/fYiRTdww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.9"
+ }
+ },
+ "node_modules/stylelint/node_modules/flat-cache": {
+ "version": "6.1.9",
+ "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-6.1.9.tgz",
+ "integrity": "sha512-DUqiKkTlAfhtl7g78IuwqYM+YqvT+as0mY+EVk6mfimy19U79pJCzDZQsnqk3Ou/T6hFXWLGbwbADzD/c8Tydg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^1.9.0",
+ "flatted": "^3.3.3",
+ "hookified": "^1.8.2"
+ }
+ },
+ "node_modules/stylelint/node_modules/ignore": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.4.tgz",
+ "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/stylelint/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stylelint/node_modules/postcss-safe-parser": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz",
+ "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.31"
+ }
+ },
+ "node_modules/stylelint/node_modules/postcss-selector-parser": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
+ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/stylelint/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz",
+ "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/svg-tags": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/svg-tags/-/svg-tags-1.0.0.tgz",
+ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
+ "dev": true
+ },
+ "node_modules/svgo": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmmirror.com/svgo/-/svgo-3.3.2.tgz",
+ "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@trysound/sax": "0.2.0",
+ "commander": "^7.2.0",
+ "css-select": "^5.1.0",
+ "css-tree": "^2.3.1",
+ "css-what": "^6.1.0",
+ "csso": "^5.0.5",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "svgo": "bin/svgo"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/svgo"
+ }
+ },
+ "node_modules/svgo/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/svgo/node_modules/css-tree": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-2.3.1.tgz",
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.30",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/svgo/node_modules/mdn-data": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/synckit": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.9.2.tgz",
+ "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
+ "node_modules/synckit/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/systemjs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmmirror.com/systemjs/-/systemjs-6.15.1.tgz",
+ "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmmirror.com/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/table": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmmirror.com/table/-/table-6.9.0.tgz",
+ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/table/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/table/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/table/node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/table/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.39.2",
+ "resolved": "https://registry.npmmirror.com/terser/-/terser-5.39.2.tgz",
+ "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.14.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/text-extensions": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmmirror.com/text-extensions/-/text-extensions-2.4.0.tgz",
+ "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tiny-svg": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/tiny-svg/-/tiny-svg-3.1.3.tgz",
+ "integrity": "sha512-9mwnPqXInRsBmH/DO6NMxBE++9LsqpVXQSSTZGc5bomoKKvL5OX/Hlotw7XVXP6XLRcHWIzZpxfovGqWKgCypQ==",
+ "license": "MIT"
+ },
+ "node_modules/tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.1.tgz",
+ "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-api-utils": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
+ "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
+ "license": "0BSD"
+ },
+ "node_modules/type": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
+ "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
+ "license": "ISC"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.3.3.tgz",
+ "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
+ "node_modules/ufo": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz",
+ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unconfig": {
+ "version": "7.3.2",
+ "resolved": "https://registry.npmmirror.com/unconfig/-/unconfig-7.3.2.tgz",
+ "integrity": "sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@quansync/fs": "^0.1.1",
+ "defu": "^6.1.4",
+ "jiti": "^2.4.2",
+ "quansync": "^0.2.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unicode-canonical-property-names-ecmascript": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz",
+ "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-ecmascript": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz",
+ "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "unicode-canonical-property-names-ecmascript": "^2.0.0",
+ "unicode-property-aliases-ecmascript": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-match-property-value-ecmascript": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz",
+ "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicode-property-aliases-ecmascript": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz",
+ "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unimport": {
+ "version": "3.14.6",
+ "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz",
+ "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.1.4",
+ "acorn": "^8.14.0",
+ "escape-string-regexp": "^5.0.0",
+ "estree-walker": "^3.0.3",
+ "fast-glob": "^3.3.3",
+ "local-pkg": "^1.0.0",
+ "magic-string": "^0.30.17",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1",
+ "picomatch": "^4.0.2",
+ "pkg-types": "^1.3.0",
+ "scule": "^1.3.0",
+ "strip-literal": "^2.1.1",
+ "unplugin": "^1.16.1"
+ }
+ },
+ "node_modules/unimport/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unimport/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/unimport/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unocss": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/unocss/-/unocss-0.58.9.tgz",
+ "integrity": "sha512-aqANXXP0RrtN4kSaTLn/7I6wh8o45LUdVgPzGu7Fan2DfH2+wpIs6frlnlHlOymnb+52dp6kXluQinddaUKW1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@unocss/astro": "0.58.9",
+ "@unocss/cli": "0.58.9",
+ "@unocss/core": "0.58.9",
+ "@unocss/extractor-arbitrary-variants": "0.58.9",
+ "@unocss/postcss": "0.58.9",
+ "@unocss/preset-attributify": "0.58.9",
+ "@unocss/preset-icons": "0.58.9",
+ "@unocss/preset-mini": "0.58.9",
+ "@unocss/preset-tagify": "0.58.9",
+ "@unocss/preset-typography": "0.58.9",
+ "@unocss/preset-uno": "0.58.9",
+ "@unocss/preset-web-fonts": "0.58.9",
+ "@unocss/preset-wind": "0.58.9",
+ "@unocss/reset": "0.58.9",
+ "@unocss/transformer-attributify-jsx": "0.58.9",
+ "@unocss/transformer-attributify-jsx-babel": "0.58.9",
+ "@unocss/transformer-compile-class": "0.58.9",
+ "@unocss/transformer-directives": "0.58.9",
+ "@unocss/transformer-variant-group": "0.58.9",
+ "@unocss/vite": "0.58.9"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@unocss/webpack": "0.58.9",
+ "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@unocss/webpack": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/unocss/node_modules/@unocss/core": {
+ "version": "0.58.9",
+ "resolved": "https://registry.npmmirror.com/@unocss/core/-/core-0.58.9.tgz",
+ "integrity": "sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unplugin": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz",
+ "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.14.0",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/unplugin-auto-import": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.7.tgz",
+ "integrity": "sha512-w7XmnRlchq6YUFJVFGSvG1T/6j8GrdYN6Em9Wf0Ye+HXgD/22kont+WnuCAA0UaUoxtuvRR1u/mXKy63g/hfqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/utils": "^0.7.6",
+ "@rollup/pluginutils": "^5.0.5",
+ "fast-glob": "^3.3.1",
+ "local-pkg": "^0.5.0",
+ "magic-string": "^0.30.5",
+ "minimatch": "^9.0.3",
+ "unimport": "^3.4.0",
+ "unplugin": "^1.5.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@nuxt/kit": "^3.2.2",
+ "@vueuse/core": "*"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ },
+ "@vueuse/core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/unplugin-auto-import/node_modules/@antfu/utils": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
+ "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unplugin-auto-import/node_modules/local-pkg": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz",
+ "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mlly": "^1.7.3",
+ "pkg-types": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unplugin-element-plus": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/unplugin-element-plus/-/unplugin-element-plus-0.8.0.tgz",
+ "integrity": "sha512-jByUGY3FG2B8RJKFryqxx4eNtSTj+Hjlo8edcOdJymewndDQjThZ1pRUQHRjQsbKhTV2jEctJV7t7RJ405UL4g==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.2",
+ "es-module-lexer": "^1.3.0",
+ "magic-string": "^0.30.1",
+ "unplugin": "^1.3.2"
+ },
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
+ "node_modules/unplugin-vue-components": {
+ "version": "0.25.2",
+ "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.2.tgz",
+ "integrity": "sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/utils": "^0.7.5",
+ "@rollup/pluginutils": "^5.0.2",
+ "chokidar": "^3.5.3",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.0",
+ "local-pkg": "^0.4.3",
+ "magic-string": "^0.30.1",
+ "minimatch": "^9.0.3",
+ "resolve": "^1.22.2",
+ "unplugin": "^1.4.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@babel/parser": "^7.15.8",
+ "@nuxt/kit": "^3.2.2",
+ "vue": "2 || 3"
+ },
+ "peerDependenciesMeta": {
+ "@babel/parser": {
+ "optional": true
+ },
+ "@nuxt/kit": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/unplugin-vue-components/node_modules/@antfu/utils": {
+ "version": "0.7.10",
+ "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz",
+ "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unplugin-vue-components/node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/unplugin-vue-components/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/unplugin-vue-components/node_modules/local-pkg": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz",
+ "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/unplugin-vue-components/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/unplugin-vue-components/node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/uri-js/node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/url": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmmirror.com/url/-/url-0.11.4.tgz",
+ "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^1.4.1",
+ "qs": "^6.12.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/url-toolkit": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmmirror.com/url-toolkit/-/url-toolkit-2.2.5.tgz",
+ "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vanilla-picker": {
+ "version": "2.12.3",
+ "resolved": "https://registry.npmmirror.com/vanilla-picker/-/vanilla-picker-2.12.3.tgz",
+ "integrity": "sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==",
+ "license": "ISC",
+ "dependencies": {
+ "@sphinxxxx/color-conversion": "^2.2.2"
+ }
+ },
+ "node_modules/video.js": {
+ "version": "7.21.7",
+ "resolved": "https://registry.npmmirror.com/video.js/-/video.js-7.21.7.tgz",
+ "integrity": "sha512-T2s3WFAht7Zjr2OSJamND9x9Dn2O+Z5WuHGdh8jI5SYh5mkMdVTQ7vSRmA5PYpjXJ2ycch6jpMjkJEIEU2xxqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@videojs/http-streaming": "2.16.3",
+ "@videojs/vhs-utils": "^3.0.4",
+ "@videojs/xhr": "2.6.0",
+ "aes-decrypter": "3.1.3",
+ "global": "^4.4.0",
+ "keycode": "^2.2.0",
+ "m3u8-parser": "4.8.0",
+ "mpd-parser": "0.22.1",
+ "mux.js": "6.0.1",
+ "safe-json-parse": "4.0.0",
+ "videojs-font": "3.2.0",
+ "videojs-vtt.js": "^0.15.5"
+ }
+ },
+ "node_modules/videojs-font": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/videojs-font/-/videojs-font-3.2.0.tgz",
+ "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/videojs-vtt.js": {
+ "version": "0.15.5",
+ "resolved": "https://registry.npmmirror.com/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
+ "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "global": "^4.3.1"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmmirror.com/vite/-/vite-5.1.4.tgz",
+ "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.19.3",
+ "postcss": "^8.4.35",
+ "rollup": "^4.2.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-compression": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz",
+ "integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "debug": "^4.3.3",
+ "fs-extra": "^10.0.0"
+ },
+ "peerDependencies": {
+ "vite": ">=2.0.0"
+ }
+ },
+ "node_modules/vite-plugin-compression/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/vite-plugin-compression/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/vite-plugin-ejs": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-ejs/-/vite-plugin-ejs-1.7.0.tgz",
+ "integrity": "sha512-JNP3zQDC4mSbfoJ3G73s5mmZITD8NGjUmLkq4swxyahy/W0xuokK9U9IJGXw7KCggq6UucT6hJ0p+tQrNtqTZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ejs": "^3.1.9"
+ },
+ "peerDependencies": {
+ "vite": ">=5.0.0"
+ }
+ },
+ "node_modules/vite-plugin-eslint": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz",
+ "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^4.2.1",
+ "@types/eslint": "^8.4.5",
+ "rollup": "^2.77.2"
+ },
+ "peerDependencies": {
+ "eslint": ">=7",
+ "vite": ">=2"
+ }
+ },
+ "node_modules/vite-plugin-eslint/node_modules/@rollup/pluginutils": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
+ "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "estree-walker": "^2.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/vite-plugin-eslint/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vite-plugin-eslint/node_modules/rollup": {
+ "version": "2.79.2",
+ "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.79.2.tgz",
+ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/vite-plugin-progress": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-progress/-/vite-plugin-progress-0.0.7.tgz",
+ "integrity": "sha512-zyvKdcc/X+6hnw3J1HVV1TKrlFKC4Rh8GnDnWG/2qhRXjqytTcM++xZ+SAPnoDsSyWl8O93ymK0wZRgHAoglEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.0.0",
+ "progress": "^2.0.3",
+ "rd": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14",
+ "pnpm": ">=7.0.0"
+ },
+ "peerDependencies": {
+ "vite": ">2.0.0-0"
+ }
+ },
+ "node_modules/vite-plugin-purge-icons": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-purge-icons/-/vite-plugin-purge-icons-0.10.0.tgz",
+ "integrity": "sha512-4fMJKQuBu9lAPJWjqGEytRaxty1pP9bWgQLA68dwbbaCXu6NBrOUb/3kMaUc7TP09kerEk+qTriCk05OZXpjwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@purge-icons/core": "^0.10.0",
+ "@purge-icons/generated": "^0.10.0",
+ "rollup-plugin-purge-icons": "^0.10.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "vite": ">=2"
+ }
+ },
+ "node_modules/vite-plugin-purge-icons/node_modules/@purge-icons/generated": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmmirror.com/@purge-icons/generated/-/generated-0.10.0.tgz",
+ "integrity": "sha512-I+1yN7/yDy/eZzfhAZqKF8Z6FM8D/O1vempbPrHJ0m9HlZwvf8sWXOArPJ2qRQGB6mJUVSpaXkoGBuoz1GQX5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@iconify/iconify": ">=3.1.1"
+ }
+ },
+ "node_modules/vite-plugin-svg-icons-ng": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-svg-icons-ng/-/vite-plugin-svg-icons-ng-1.4.0.tgz",
+ "integrity": "sha512-DWJZMUyK6/rq0qeFRDm/Ah82ePblN8EADk0e0OpPXF8M7rx+dAWNERtItstisBgVoczpaGOlxgL2AXT/G3zInQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.3.3",
+ "fs-extra": "^11.3.0",
+ "node-html-parser": "^7.0.1",
+ "pathe": "^2.0.3",
+ "svgo": "^3.3.2"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": ">=5.0.0"
+ }
+ },
+ "node_modules/vite-plugin-svg-icons-ng/node_modules/fs-extra": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.0.tgz",
+ "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/vite-plugin-svg-icons-ng/node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite-plugin-top-level-await": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.5.0.tgz",
+ "integrity": "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/plugin-virtual": "^3.0.2",
+ "@swc/core": "^1.10.16",
+ "uuid": "^10.0.0"
+ },
+ "peerDependencies": {
+ "vite": ">=2.8"
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.12.tgz",
+ "integrity": "sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.12",
+ "@vue/compiler-sfc": "3.5.12",
+ "@vue/runtime-dom": "3.5.12",
+ "@vue/server-renderer": "3.5.12",
+ "@vue/shared": "3.5.12"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-dompurify-html": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmmirror.com/vue-dompurify-html/-/vue-dompurify-html-4.1.4.tgz",
+ "integrity": "sha512-K0XDSZA4dmMMvAgW8yaCx1kAYQldmgXeHJaLPS0mlSKOu8B+onE06X4KfB5LGyX4jR3rlVosyWJczRBzR0sZ/g==",
+ "license": "MIT",
+ "dependencies": {
+ "dompurify": "^3.0.0",
+ "vue-demi": "^0.14.0"
+ },
+ "peerDependencies": {
+ "vue": "^2.7.0 || ^3.0.0"
+ }
+ },
+ "node_modules/vue-dompurify-html/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-eslint-parser": {
+ "version": "9.4.3",
+ "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
+ "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "eslint-scope": "^7.1.1",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.1",
+ "esquery": "^1.4.0",
+ "lodash": "^4.17.21",
+ "semver": "^7.3.6"
+ },
+ "engines": {
+ "node": "^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=6.0.0"
+ }
+ },
+ "node_modules/vue-i18n": {
+ "version": "9.10.2",
+ "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.10.2.tgz",
+ "integrity": "sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@intlify/core-base": "9.10.2",
+ "@intlify/shared": "9.10.2",
+ "@vue/devtools-api": "^6.5.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/vue-i18n/node_modules/@intlify/shared": {
+ "version": "9.10.2",
+ "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.10.2.tgz",
+ "integrity": "sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.4.5.tgz",
+ "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/vue-template-compiler": {
+ "version": "2.7.16",
+ "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
+ "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "de-indent": "^1.0.2",
+ "he": "^1.2.0"
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "1.8.27",
+ "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz",
+ "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "~1.11.1",
+ "@vue/language-core": "1.8.27",
+ "semver": "^7.5.4"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ }
+ },
+ "node_modules/vue-types": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-5.1.3.tgz",
+ "integrity": "sha512-3Wy6QcZl0VusCCHX3vYrWSILFlrOB2EQDoySnuYmASM5cUp1FivJGfkS5lp1CutDgyRb41g32r/1QCmiBj5i1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-object": "5.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "vue": "^2.0.0 || ^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue/node_modules/@vue/compiler-core": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz",
+ "integrity": "sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.3",
+ "@vue/shared": "3.5.12",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/vue/node_modules/@vue/compiler-dom": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz",
+ "integrity": "sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.12",
+ "@vue/shared": "3.5.12"
+ }
+ },
+ "node_modules/vue/node_modules/@vue/compiler-sfc": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.12.tgz",
+ "integrity": "sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.3",
+ "@vue/compiler-core": "3.5.12",
+ "@vue/compiler-dom": "3.5.12",
+ "@vue/compiler-ssr": "3.5.12",
+ "@vue/shared": "3.5.12",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.11",
+ "postcss": "^8.4.47",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/vue/node_modules/@vue/compiler-ssr": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz",
+ "integrity": "sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.12",
+ "@vue/shared": "3.5.12"
+ }
+ },
+ "node_modules/vue/node_modules/@vue/shared": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.12.tgz",
+ "integrity": "sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==",
+ "license": "MIT"
+ },
+ "node_modules/vue3-print-nb": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/vue3-print-nb/-/vue3-print-nb-0.1.4.tgz",
+ "integrity": "sha512-LExI7viEzplR6ZKQ2b+V4U0cwGYbVD4fut/XHvk3UPGlT5CcvIGs6VlwGp107aKgk6P8Pgx4rco3Rehv2lti3A==",
+ "dependencies": {
+ "vue": "^3.0.5"
+ }
+ },
+ "node_modules/vue3-signature": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmmirror.com/vue3-signature/-/vue3-signature-0.2.4.tgz",
+ "integrity": "sha512-XFwwFVK9OG3F085pKIq2SlNVqx32WdFH+TXbGEWc5FfEKpx8oMmZuAwZZ50K/pH2FgmJSE8IRwU9DDhrLpd6iA==",
+ "license": "MIT",
+ "dependencies": {
+ "default-passive-events": "^2.0.0",
+ "signature_pad": "^3.0.0-beta.4",
+ "vue": "^3.2.37"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/vuedraggable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz",
+ "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
+ "license": "MIT",
+ "dependencies": {
+ "sortablejs": "1.14.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.1"
+ }
+ },
+ "node_modules/vuedraggable/node_modules/sortablejs": {
+ "version": "1.14.0",
+ "resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz",
+ "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
+ "license": "MIT"
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/wangeditor": {
+ "version": "4.7.15",
+ "resolved": "https://registry.npmmirror.com/wangeditor/-/wangeditor-4.7.15.tgz",
+ "integrity": "sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.11.2",
+ "@babel/runtime-corejs3": "^7.11.2",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/web-storage-cache": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/web-storage-cache/-/web-storage-cache-1.1.1.tgz",
+ "integrity": "sha512-D0MieGooOs8RpsrK+vnejXnvh4OOv/+lTFB35JRkJJQt+uOjPE08XpaE0QBLMTRu47B1KGT/Nq3Gbag3Orinzw==",
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
+ "node_modules/wildcard": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
+ "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
+ "license": "MIT"
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
+ "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/xml-js": {
+ "version": "1.6.11",
+ "resolved": "https://registry.npmmirror.com/xml-js/-/xml-js-1.6.11.tgz",
+ "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": "^1.2.4"
+ },
+ "bin": {
+ "xml-js": "bin/cli.js"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.0.tgz",
+ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yaml-eslint-parser": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/yaml-eslint-parser/-/yaml-eslint-parser-1.3.0.tgz",
+ "integrity": "sha512-E/+VitOorXSLiAqtTd7Yqax0/pAS3xaYMP+AUUJGOK1OZG3rhcj9fcJOM5HJ2VrP1FrStVCWr1muTfQCdj4tAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.0.0",
+ "yaml": "^2.0.0"
+ },
+ "engines": {
+ "node": "^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.2.1.tgz",
+ "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zeebe-bpmn-moddle": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmmirror.com/zeebe-bpmn-moddle/-/zeebe-bpmn-moddle-1.9.0.tgz",
+ "integrity": "sha512-Y9ncIdP4m1PKbIBDqSghwZud2eiiBpfygE0bTApGqtnGlJMA/6Xanl/J7ujxG5zREoAliwf6rJyJFk3FZ75AYg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1a12256
--- /dev/null
+++ b/package.json
@@ -0,0 +1,159 @@
+{
+ "name": "yudao-ui-admin-vue3",
+ "version": "2025.12-snapshot",
+ "description": "鍩轰簬vue3銆乿ite4銆乪lement-plus銆乼ypesScript",
+ "author": "xingyu",
+ "private": false,
+ "scripts": {
+ "i": "pnpm install",
+ "dev": "vite --mode env.local",
+ "dev-server": "vite --mode dev",
+ "ts:check": "vue-tsc --noEmit",
+ "build:local": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build",
+ "build:dev": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode dev",
+ "build:test": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode test",
+ "build:stage": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode stage",
+ "build:prod": "node --max_old_space_size=4096 ./node_modules/vite/bin/vite.js build --mode prod",
+ "serve:dev": "vite preview --mode dev",
+ "serve:prod": "vite preview --mode prod",
+ "preview": "pnpm build:local && vite preview",
+ "clean": "npx rimraf node_modules",
+ "clean:cache": "npx rimraf node_modules/.cache",
+ "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
+ "lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
+ "lint:style": "stylelint --fix \"./src/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
+ "lint:lint-staged": "lint-staged -c "
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "2.3.2",
+ "@form-create/designer": "^3.2.6",
+ "@form-create/element-ui": "^3.2.11",
+ "@iconify/iconify": "^3.1.1",
+ "@microsoft/fetch-event-source": "^2.0.1",
+ "@videojs-player/vue": "^1.0.0",
+ "@vueuse/core": "^10.9.0",
+ "@wangeditor-next/editor": "^5.6.46",
+ "@wangeditor-next/editor-for-vue": "^5.1.14",
+ "@wangeditor-next/plugin-mention": "^1.0.16",
+ "@zxcvbn-ts/core": "^3.0.4",
+ "animate.css": "^4.1.1",
+ "axios": "1.9.0",
+ "benz-amr-recorder": "^1.1.5",
+ "bpmn-js-token-simulation": "^0.36.0",
+ "camunda-bpmn-moddle": "^7.0.1",
+ "cropperjs": "^1.6.1",
+ "crypto-js": "^4.2.0",
+ "dayjs": "^1.11.10",
+ "diagram-js": "^12.8.0",
+ "driver.js": "^1.3.1",
+ "echarts": "^5.5.0",
+ "echarts-wordcloud": "^2.1.0",
+ "element-plus": "2.11.1",
+ "fast-xml-parser": "^4.3.2",
+ "highlight.js": "^11.9.0",
+ "jsencrypt": "^3.3.2",
+ "jsoneditor": "^10.1.3",
+ "lodash-es": "^4.17.21",
+ "markdown-it": "^14.1.0",
+ "markmap-common": "^0.16.0",
+ "markmap-lib": "^0.16.1",
+ "markmap-toolbar": "^0.17.0",
+ "markmap-view": "^0.16.0",
+ "min-dash": "^4.1.1",
+ "mitt": "^3.0.1",
+ "nprogress": "^0.2.0",
+ "pinia": "^2.1.7",
+ "pinia-plugin-persistedstate": "^3.2.1",
+ "qrcode": "^1.5.3",
+ "qs": "^6.12.0",
+ "snabbdom": "^3.6.2",
+ "sortablejs": "^1.15.3",
+ "steady-xml": "^0.1.0",
+ "url": "^0.11.3",
+ "video.js": "^7.21.5",
+ "vue": "3.5.12",
+ "vue-dompurify-html": "^4.1.4",
+ "vue-i18n": "9.10.2",
+ "vue-router": "4.4.5",
+ "vue-types": "^5.1.1",
+ "vue3-print-nb": "^0.1.4",
+ "vue3-signature": "^0.2.4",
+ "vuedraggable": "^4.1.0",
+ "web-storage-cache": "^1.1.1",
+ "xml-js": "^1.6.11"
+ },
+ "devDependencies": {
+ "@commitlint/cli": "^19.0.1",
+ "@commitlint/config-conventional": "^19.0.0",
+ "@iconify/json": "^2.2.187",
+ "@intlify/unplugin-vue-i18n": "^2.0.0",
+ "@purge-icons/generated": "^0.9.0",
+ "@types/jsoneditor": "^9.9.5",
+ "@types/lodash-es": "^4.17.12",
+ "@types/node": "^20.11.21",
+ "@types/nprogress": "^0.2.3",
+ "@types/qrcode": "^1.5.5",
+ "@types/qs": "^6.9.12",
+ "@typescript-eslint/eslint-plugin": "^7.1.0",
+ "@typescript-eslint/parser": "^7.1.0",
+ "@unocss/eslint-config": "^0.57.4",
+ "@unocss/eslint-plugin": "66.1.0-beta.5",
+ "@unocss/transformer-variant-group": "^0.58.5",
+ "@vitejs/plugin-legacy": "^5.3.1",
+ "@vitejs/plugin-vue": "^5.0.4",
+ "@vitejs/plugin-vue-jsx": "^3.1.0",
+ "autoprefixer": "^10.4.17",
+ "bpmn-js": "^17.9.2",
+ "bpmn-js-properties-panel": "5.23.0",
+ "consola": "^3.2.3",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-define-config": "^2.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-vue": "^9.22.0",
+ "lint-staged": "^15.2.2",
+ "postcss": "^8.4.35",
+ "postcss-html": "^1.6.0",
+ "postcss-scss": "^4.0.9",
+ "prettier": "^3.2.5",
+ "prettier-eslint": "^16.3.0",
+ "rimraf": "^5.0.5",
+ "rollup": "^4.12.0",
+ "sass": "^1.69.5",
+ "stylelint": "^16.2.1",
+ "stylelint-config-html": "^1.1.0",
+ "stylelint-config-recommended": "^14.0.0",
+ "stylelint-config-standard": "^36.0.0",
+ "stylelint-order": "^6.0.4",
+ "terser": "^5.28.1",
+ "typescript": "5.3.3",
+ "unocss": "^0.58.5",
+ "unplugin-auto-import": "^0.16.7",
+ "unplugin-element-plus": "^0.8.0",
+ "unplugin-vue-components": "^0.25.2",
+ "vite": "5.1.4",
+ "vite-plugin-compression": "^0.5.1",
+ "vite-plugin-ejs": "^1.7.0",
+ "vite-plugin-eslint": "^1.8.1",
+ "vite-plugin-progress": "^0.0.7",
+ "vite-plugin-purge-icons": "^0.10.0",
+ "vite-plugin-svg-icons-ng": "^1.3.1",
+ "vite-plugin-top-level-await": "^1.4.4",
+ "vue-eslint-parser": "^9.3.2",
+ "vue-tsc": "^1.8.27"
+ },
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://gitee.com/yudaocode/yudao-ui-admin-vue3"
+ },
+ "bugs": {
+ "url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
+ },
+ "homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
+ "web-types": "./web-types.json",
+ "engines": {
+ "node": ">= 16.0.0",
+ "pnpm": ">=8.6.0"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..ac46ccb
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,10576 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@element-plus/icons-vue':
+ specifier: 2.3.2
+ version: 2.3.2(vue@3.5.12(typescript@5.3.3))
+ '@form-create/designer':
+ specifier: ^3.2.6
+ version: 3.2.8(vue@3.5.12(typescript@5.3.3))
+ '@form-create/element-ui':
+ specifier: ^3.2.11
+ version: 3.2.14(vue@3.5.12(typescript@5.3.3))
+ '@iconify/iconify':
+ specifier: ^3.1.1
+ version: 3.1.1
+ '@microsoft/fetch-event-source':
+ specifier: ^2.0.1
+ version: 2.0.1
+ '@videojs-player/vue':
+ specifier: ^1.0.0
+ version: 1.0.0(@types/video.js@7.3.58)(video.js@7.21.6)(vue@3.5.12(typescript@5.3.3))
+ '@vueuse/core':
+ specifier: ^10.9.0
+ version: 10.11.1(vue@3.5.12(typescript@5.3.3))
+ '@wangeditor-next/editor':
+ specifier: ^5.6.46
+ version: 5.6.46
+ '@wangeditor-next/editor-for-vue':
+ specifier: ^5.1.14
+ version: 5.1.14(@wangeditor-next/editor@5.6.46)(vue@3.5.12(typescript@5.3.3))
+ '@wangeditor-next/plugin-mention':
+ specifier: ^1.0.16
+ version: 1.0.16(@wangeditor-next/editor@5.6.46)(snabbdom@3.6.2)
+ '@zxcvbn-ts/core':
+ specifier: ^3.0.4
+ version: 3.0.4
+ animate.css:
+ specifier: ^4.1.1
+ version: 4.1.1
+ axios:
+ specifier: 1.9.0
+ version: 1.9.0
+ benz-amr-recorder:
+ specifier: ^1.1.5
+ version: 1.1.5
+ bpmn-js-token-simulation:
+ specifier: ^0.36.0
+ version: 0.36.2
+ camunda-bpmn-moddle:
+ specifier: ^7.0.1
+ version: 7.0.1
+ cropperjs:
+ specifier: ^1.6.1
+ version: 1.6.2
+ crypto-js:
+ specifier: ^4.2.0
+ version: 4.2.0
+ dayjs:
+ specifier: ^1.11.10
+ version: 1.11.13
+ diagram-js:
+ specifier: ^12.8.0
+ version: 12.8.1
+ driver.js:
+ specifier: ^1.3.1
+ version: 1.3.1
+ echarts:
+ specifier: ^5.5.0
+ version: 5.5.1
+ echarts-wordcloud:
+ specifier: ^2.1.0
+ version: 2.1.0(echarts@5.5.1)
+ element-plus:
+ specifier: 2.11.1
+ version: 2.11.1(vue@3.5.12(typescript@5.3.3))
+ fast-xml-parser:
+ specifier: ^4.3.2
+ version: 4.5.0
+ highlight.js:
+ specifier: ^11.9.0
+ version: 11.10.0
+ jsencrypt:
+ specifier: ^3.3.2
+ version: 3.3.2
+ jsoneditor:
+ specifier: ^10.1.3
+ version: 10.4.1
+ lodash-es:
+ specifier: ^4.17.21
+ version: 4.17.21
+ markdown-it:
+ specifier: ^14.1.0
+ version: 14.1.0
+ markmap-common:
+ specifier: ^0.16.0
+ version: 0.16.0
+ markmap-lib:
+ specifier: ^0.16.1
+ version: 0.16.1(markmap-common@0.16.0)
+ markmap-toolbar:
+ specifier: ^0.17.0
+ version: 0.17.2(markmap-common@0.16.0)
+ markmap-view:
+ specifier: ^0.16.0
+ version: 0.16.0(markmap-common@0.16.0)
+ min-dash:
+ specifier: ^4.1.1
+ version: 4.2.2
+ mitt:
+ specifier: ^3.0.1
+ version: 3.0.1
+ nprogress:
+ specifier: ^0.2.0
+ version: 0.2.0
+ pinia:
+ specifier: ^2.1.7
+ version: 2.2.8(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3))
+ pinia-plugin-persistedstate:
+ specifier: ^3.2.1
+ version: 3.2.3(pinia@2.2.8(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3)))
+ qrcode:
+ specifier: ^1.5.3
+ version: 1.5.4
+ qs:
+ specifier: ^6.12.0
+ version: 6.13.1
+ snabbdom:
+ specifier: ^3.6.2
+ version: 3.6.2
+ sortablejs:
+ specifier: ^1.15.3
+ version: 1.15.6
+ steady-xml:
+ specifier: ^0.1.0
+ version: 0.1.0
+ url:
+ specifier: ^0.11.3
+ version: 0.11.4
+ video.js:
+ specifier: ^7.21.5
+ version: 7.21.6
+ vue:
+ specifier: 3.5.12
+ version: 3.5.12(typescript@5.3.3)
+ vue-dompurify-html:
+ specifier: ^4.1.4
+ version: 4.1.4(vue@3.5.12(typescript@5.3.3))
+ vue-i18n:
+ specifier: 9.10.2
+ version: 9.10.2(vue@3.5.12(typescript@5.3.3))
+ vue-router:
+ specifier: 4.4.5
+ version: 4.4.5(vue@3.5.12(typescript@5.3.3))
+ vue-types:
+ specifier: ^5.1.1
+ version: 5.1.3(vue@3.5.12(typescript@5.3.3))
+ vue3-print-nb:
+ specifier: ^0.1.4
+ version: 0.1.4(typescript@5.3.3)
+ vue3-signature:
+ specifier: ^0.2.4
+ version: 0.2.4(vue@3.5.12(typescript@5.3.3))
+ vuedraggable:
+ specifier: ^4.1.0
+ version: 4.1.0(vue@3.5.12(typescript@5.3.3))
+ web-storage-cache:
+ specifier: ^1.1.1
+ version: 1.1.1
+ xml-js:
+ specifier: ^1.6.11
+ version: 1.6.11
+ devDependencies:
+ '@commitlint/cli':
+ specifier: ^19.0.1
+ version: 19.6.0(@types/node@20.17.9)(typescript@5.3.3)
+ '@commitlint/config-conventional':
+ specifier: ^19.0.0
+ version: 19.6.0
+ '@iconify/json':
+ specifier: ^2.2.187
+ version: 2.2.277
+ '@intlify/unplugin-vue-i18n':
+ specifier: ^2.0.0
+ version: 2.0.0(rollup@4.27.4)(vue-i18n@9.10.2(vue@3.5.12(typescript@5.3.3)))
+ '@purge-icons/generated':
+ specifier: ^0.9.0
+ version: 0.9.0
+ '@types/jsoneditor':
+ specifier: ^9.9.5
+ version: 9.9.6
+ '@types/lodash-es':
+ specifier: ^4.17.12
+ version: 4.17.12
+ '@types/node':
+ specifier: ^20.11.21
+ version: 20.17.9
+ '@types/nprogress':
+ specifier: ^0.2.3
+ version: 0.2.3
+ '@types/qrcode':
+ specifier: ^1.5.5
+ version: 1.5.5
+ '@types/qs':
+ specifier: ^6.9.12
+ version: 6.9.17
+ '@typescript-eslint/eslint-plugin':
+ specifier: ^7.1.0
+ version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3)
+ '@typescript-eslint/parser':
+ specifier: ^7.1.0
+ version: 7.18.0(eslint@8.57.1)(typescript@5.3.3)
+ '@unocss/eslint-config':
+ specifier: ^0.57.4
+ version: 0.57.7(eslint@8.57.1)(typescript@5.3.3)
+ '@unocss/eslint-plugin':
+ specifier: 66.1.0-beta.5
+ version: 66.1.0-beta.5(eslint@8.57.1)(typescript@5.3.3)
+ '@unocss/transformer-variant-group':
+ specifier: ^0.58.5
+ version: 0.58.9
+ '@vitejs/plugin-legacy':
+ specifier: ^5.3.1
+ version: 5.4.3(terser@5.36.0)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ '@vitejs/plugin-vue':
+ specifier: ^5.0.4
+ version: 5.2.1(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))(vue@3.5.12(typescript@5.3.3))
+ '@vitejs/plugin-vue-jsx':
+ specifier: ^3.1.0
+ version: 3.1.0(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))(vue@3.5.12(typescript@5.3.3))
+ autoprefixer:
+ specifier: ^10.4.17
+ version: 10.4.20(postcss@8.4.49)
+ bpmn-js:
+ specifier: ^17.9.2
+ version: 17.11.1
+ bpmn-js-properties-panel:
+ specifier: 5.23.0
+ version: 5.23.0(@bpmn-io/properties-panel@3.25.0(@lezer/common@1.2.3))(bpmn-js@17.11.1)(camunda-bpmn-js-behaviors@1.7.2(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.7.0))(diagram-js@12.8.1)
+ consola:
+ specifier: ^3.2.3
+ version: 3.2.3
+ eslint:
+ specifier: ^8.57.0
+ version: 8.57.1
+ eslint-config-prettier:
+ specifier: ^9.1.0
+ version: 9.1.0(eslint@8.57.1)
+ eslint-define-config:
+ specifier: ^2.1.0
+ version: 2.1.0
+ eslint-plugin-prettier:
+ specifier: ^5.1.3
+ version: 5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.1)
+ eslint-plugin-vue:
+ specifier: ^9.22.0
+ version: 9.31.0(eslint@8.57.1)
+ lint-staged:
+ specifier: ^15.2.2
+ version: 15.2.10
+ postcss:
+ specifier: ^8.4.35
+ version: 8.4.49
+ postcss-html:
+ specifier: ^1.6.0
+ version: 1.7.0
+ postcss-scss:
+ specifier: ^4.0.9
+ version: 4.0.9(postcss@8.4.49)
+ prettier:
+ specifier: ^3.2.5
+ version: 3.4.1
+ prettier-eslint:
+ specifier: ^16.3.0
+ version: 16.3.0
+ rimraf:
+ specifier: ^5.0.5
+ version: 5.0.10
+ rollup:
+ specifier: ^4.12.0
+ version: 4.27.4
+ sass:
+ specifier: ^1.69.5
+ version: 1.81.0
+ stylelint:
+ specifier: ^16.2.1
+ version: 16.11.0(typescript@5.3.3)
+ stylelint-config-html:
+ specifier: ^1.1.0
+ version: 1.1.0(postcss-html@1.7.0)(stylelint@16.11.0(typescript@5.3.3))
+ stylelint-config-recommended:
+ specifier: ^14.0.0
+ version: 14.0.1(stylelint@16.11.0(typescript@5.3.3))
+ stylelint-config-standard:
+ specifier: ^36.0.0
+ version: 36.0.1(stylelint@16.11.0(typescript@5.3.3))
+ stylelint-order:
+ specifier: ^6.0.4
+ version: 6.0.4(stylelint@16.11.0(typescript@5.3.3))
+ terser:
+ specifier: ^5.28.1
+ version: 5.36.0
+ typescript:
+ specifier: 5.3.3
+ version: 5.3.3
+ unocss:
+ specifier: ^0.58.5
+ version: 0.58.9(postcss@8.4.49)(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ unplugin-auto-import:
+ specifier: ^0.16.7
+ version: 0.16.7(@vueuse/core@10.11.1(vue@3.5.12(typescript@5.3.3)))(rollup@4.27.4)
+ unplugin-element-plus:
+ specifier: ^0.8.0
+ version: 0.8.0(rollup@4.27.4)
+ unplugin-vue-components:
+ specifier: ^0.25.2
+ version: 0.25.2(@babel/parser@7.26.2)(rollup@4.27.4)(vue@3.5.12(typescript@5.3.3))
+ vite:
+ specifier: 5.1.4
+ version: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ vite-plugin-compression:
+ specifier: ^0.5.1
+ version: 0.5.1(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vite-plugin-ejs:
+ specifier: ^1.7.0
+ version: 1.7.0(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vite-plugin-eslint:
+ specifier: ^1.8.1
+ version: 1.8.1(eslint@8.57.1)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vite-plugin-progress:
+ specifier: ^0.0.7
+ version: 0.0.7(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vite-plugin-purge-icons:
+ specifier: ^0.10.0
+ version: 0.10.0(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vite-plugin-svg-icons-ng:
+ specifier: ^1.3.1
+ version: 1.3.1(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vite-plugin-top-level-await:
+ specifier: ^1.4.4
+ version: 1.4.4(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ vue-eslint-parser:
+ specifier: ^9.3.2
+ version: 9.4.3(eslint@8.57.1)
+ vue-tsc:
+ specifier: ^1.8.27
+ version: 1.8.27(typescript@5.3.3)
+
+packages:
+
+ '@ampproject/remapping@2.3.0':
+ resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
+ engines: {node: '>=6.0.0'}
+
+ '@antfu/install-pkg@0.4.1':
+ resolution: {integrity: sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==}
+
+ '@antfu/utils@0.7.10':
+ resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
+
+ '@babel/code-frame@7.26.2':
+ resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/compat-data@7.26.2':
+ resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/core@7.26.0':
+ resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/generator@7.26.2':
+ resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-annotate-as-pure@7.25.9':
+ resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9':
+ resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-compilation-targets@7.25.9':
+ resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-create-class-features-plugin@7.25.9':
+ resolution: {integrity: sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-create-regexp-features-plugin@7.25.9':
+ resolution: {integrity: sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-define-polyfill-provider@0.6.3':
+ resolution: {integrity: sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ '@babel/helper-member-expression-to-functions@7.25.9':
+ resolution: {integrity: sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-imports@7.25.9':
+ resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-module-transforms@7.26.0':
+ resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-optimise-call-expression@7.25.9':
+ resolution: {integrity: sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-plugin-utils@7.25.9':
+ resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-remap-async-to-generator@7.25.9':
+ resolution: {integrity: sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-replace-supers@7.25.9':
+ resolution: {integrity: sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/helper-simple-access@7.25.9':
+ resolution: {integrity: sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.25.9':
+ resolution: {integrity: sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-string-parser@7.25.9':
+ resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.25.9':
+ resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-option@7.25.9':
+ resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-wrap-function@7.25.9':
+ resolution: {integrity: sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helpers@7.26.0':
+ resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.26.2':
+ resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9':
+ resolution: {integrity: sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9':
+ resolution: {integrity: sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9':
+ resolution: {integrity: sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9':
+ resolution: {integrity: sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.13.0
+
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9':
+ resolution: {integrity: sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2':
+ resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-assertions@7.26.0':
+ resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-import-attributes@7.26.0':
+ resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-jsx@7.25.9':
+ resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-typescript@7.25.9':
+ resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6':
+ resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-arrow-functions@7.25.9':
+ resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-generator-functions@7.25.9':
+ resolution: {integrity: sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-async-to-generator@7.25.9':
+ resolution: {integrity: sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoped-functions@7.25.9':
+ resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-block-scoping@7.25.9':
+ resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-properties@7.25.9':
+ resolution: {integrity: sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-class-static-block@7.26.0':
+ resolution: {integrity: sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.12.0
+
+ '@babel/plugin-transform-classes@7.25.9':
+ resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-computed-properties@7.25.9':
+ resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-destructuring@7.25.9':
+ resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-dotall-regex@7.25.9':
+ resolution: {integrity: sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-duplicate-keys@7.25.9':
+ resolution: {integrity: sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9':
+ resolution: {integrity: sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-dynamic-import@7.25.9':
+ resolution: {integrity: sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-exponentiation-operator@7.25.9':
+ resolution: {integrity: sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-export-namespace-from@7.25.9':
+ resolution: {integrity: sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-for-of@7.25.9':
+ resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-function-name@7.25.9':
+ resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-json-strings@7.25.9':
+ resolution: {integrity: sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-literals@7.25.9':
+ resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-logical-assignment-operators@7.25.9':
+ resolution: {integrity: sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-member-expression-literals@7.25.9':
+ resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-amd@7.25.9':
+ resolution: {integrity: sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-commonjs@7.25.9':
+ resolution: {integrity: sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-systemjs@7.25.9':
+ resolution: {integrity: sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-modules-umd@7.25.9':
+ resolution: {integrity: sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.25.9':
+ resolution: {integrity: sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-new-target@7.25.9':
+ resolution: {integrity: sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.25.9':
+ resolution: {integrity: sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-numeric-separator@7.25.9':
+ resolution: {integrity: sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-rest-spread@7.25.9':
+ resolution: {integrity: sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-object-super@7.25.9':
+ resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-catch-binding@7.25.9':
+ resolution: {integrity: sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-optional-chaining@7.25.9':
+ resolution: {integrity: sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-parameters@7.25.9':
+ resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-methods@7.25.9':
+ resolution: {integrity: sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-private-property-in-object@7.25.9':
+ resolution: {integrity: sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-property-literals@7.25.9':
+ resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regenerator@7.25.9':
+ resolution: {integrity: sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-regexp-modifiers@7.26.0':
+ resolution: {integrity: sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/plugin-transform-reserved-words@7.25.9':
+ resolution: {integrity: sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-shorthand-properties@7.25.9':
+ resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-spread@7.25.9':
+ resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-sticky-regex@7.25.9':
+ resolution: {integrity: sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-template-literals@7.25.9':
+ resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typeof-symbol@7.25.9':
+ resolution: {integrity: sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-typescript@7.25.9':
+ resolution: {integrity: sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-escapes@7.25.9':
+ resolution: {integrity: sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-property-regex@7.25.9':
+ resolution: {integrity: sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-regex@7.25.9':
+ resolution: {integrity: sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/plugin-transform-unicode-sets-regex@7.25.9':
+ resolution: {integrity: sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0
+
+ '@babel/preset-env@7.26.0':
+ resolution: {integrity: sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/preset-modules@0.1.6-no-external-plugins':
+ resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0
+
+ '@babel/preset-typescript@7.26.0':
+ resolution: {integrity: sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg==}
+ engines: {node: '>=6.9.0'}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@babel/runtime-corejs3@7.26.0':
+ resolution: {integrity: sha512-YXHu5lN8kJCb1LOb9PgV6pvak43X2h4HvRApcN5SdWeaItQOzfn1hgP6jasD6KWQyJDBxrVmA9o9OivlnNJK/w==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/runtime@7.26.0':
+ resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/template@7.25.9':
+ resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/traverse@7.25.9':
+ resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/types@7.26.0':
+ resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==}
+ engines: {node: '>=6.9.0'}
+
+ '@bpmn-io/cm-theme@0.1.0-alpha.2':
+ resolution: {integrity: sha512-ZILgiYzxk3KMvxplUXmdRFQo45/JehDPg5k9tWfehmzUOSE13ssyLPil8uCloMQnb3yyzyOWTjb/wzKXTHlFQw==}
+
+ '@bpmn-io/diagram-js-ui@0.2.3':
+ resolution: {integrity: sha512-OGyjZKvGK8tHSZ0l7RfeKhilGoOGtFDcoqSGYkX0uhFlo99OVZ9Jn1K7TJGzcE9BdKwvA5Y5kGqHEhdTxHvFfw==}
+
+ '@bpmn-io/extract-process-variables@0.8.0':
+ resolution: {integrity: sha512-yAS7ZYX+D56K+luC36u96eRMLb4VHcPUwTUqMZ/Z/Je2gou2DJLRbuBTHAB4jjKt4wFCHSG4B8Y+TrBciEYf4w==}
+
+ '@bpmn-io/feel-editor@1.9.1':
+ resolution: {integrity: sha512-UxSORdh5cwKM4fib4f9ov6J1/BHGpQVNtA+wPyEdKQyCyz3wqwE2/xe5wneVR1j5QFC5m2Na8nTy4a1TDFvZTw==}
+ engines: {node: '>= 16'}
+
+ '@bpmn-io/feel-lint@1.3.1':
+ resolution: {integrity: sha512-wcFkJKhOm/iqCt5bzkKvxL5Dr9wKwUD+t164bQYbJsTYouAqmkkxiGsoqck42hXwdIhMSguZ+vqQ3hj5QdiYCA==}
+
+ '@bpmn-io/properties-panel@3.25.0':
+ resolution: {integrity: sha512-SRGgj8uJc1Yyjcht2g36Q+xKR7sTx5VZXvcwDrdmQKlx5Y3nRmvmMjDGzeGDJDb7pNU1DSlaBJic84uISDBMWg==}
+
+ '@codemirror/autocomplete@6.18.3':
+ resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==}
+ peerDependencies:
+ '@codemirror/language': ^6.0.0
+ '@codemirror/state': ^6.0.0
+ '@codemirror/view': ^6.0.0
+ '@lezer/common': ^1.0.0
+
+ '@codemirror/commands@6.7.1':
+ resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==}
+
+ '@codemirror/language@6.10.6':
+ resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==}
+
+ '@codemirror/lint@6.8.4':
+ resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==}
+
+ '@codemirror/state@6.4.1':
+ resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==}
+
+ '@codemirror/view@6.35.0':
+ resolution: {integrity: sha512-I0tYy63q5XkaWsJ8QRv5h6ves7kvtrBWjBcnf/bzohFJQc5c14a1AQRdE8QpPF9eMp5Mq2FMm59TCj1gDfE7kw==}
+
+ '@commitlint/cli@19.6.0':
+ resolution: {integrity: sha512-v17BgGD9w5KnthaKxXnEg6KLq6DYiAxyiN44TpiRtqyW8NSq+Kx99mkEG8Qo6uu6cI5eMzMojW2muJxjmPnF8w==}
+ engines: {node: '>=v18'}
+ hasBin: true
+
+ '@commitlint/config-conventional@19.6.0':
+ resolution: {integrity: sha512-DJT40iMnTYtBtUfw9ApbsLZFke1zKh6llITVJ+x9mtpHD08gsNXaIRqHTmwTZL3dNX5+WoyK7pCN/5zswvkBCQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/config-validator@19.5.0':
+ resolution: {integrity: sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/ensure@19.5.0':
+ resolution: {integrity: sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/execute-rule@19.5.0':
+ resolution: {integrity: sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/format@19.5.0':
+ resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/is-ignored@19.6.0':
+ resolution: {integrity: sha512-Ov6iBgxJQFR9koOupDPHvcHU9keFupDgtB3lObdEZDroiG4jj1rzky60fbQozFKVYRTUdrBGICHG0YVmRuAJmw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/lint@19.6.0':
+ resolution: {integrity: sha512-LRo7zDkXtcIrpco9RnfhOKeg8PAnE3oDDoalnrVU/EVaKHYBWYL1DlRR7+3AWn0JiBqD8yKOfetVxJGdEtZ0tg==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/load@19.5.0':
+ resolution: {integrity: sha512-INOUhkL/qaKqwcTUvCE8iIUf5XHsEPCLY9looJ/ipzi7jtGhgmtH7OOFiNvwYgH7mA8osUWOUDV8t4E2HAi4xA==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/message@19.5.0':
+ resolution: {integrity: sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/parse@19.5.0':
+ resolution: {integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/read@19.5.0':
+ resolution: {integrity: sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/resolve-extends@19.5.0':
+ resolution: {integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/rules@19.6.0':
+ resolution: {integrity: sha512-1f2reW7lbrI0X0ozZMesS/WZxgPa4/wi56vFuJENBmed6mWq5KsheN/nxqnl/C23ioxpPO/PL6tXpiiFy5Bhjw==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/to-lines@19.5.0':
+ resolution: {integrity: sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/top-level@19.5.0':
+ resolution: {integrity: sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==}
+ engines: {node: '>=v18'}
+
+ '@commitlint/types@19.5.0':
+ resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==}
+ engines: {node: '>=v18'}
+
+ '@csstools/css-parser-algorithms@3.0.4':
+ resolution: {integrity: sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.3
+
+ '@csstools/css-tokenizer@3.0.3':
+ resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==}
+ engines: {node: '>=18'}
+
+ '@csstools/media-query-list-parser@4.0.2':
+ resolution: {integrity: sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.4
+ '@csstools/css-tokenizer': ^3.0.3
+
+ '@csstools/selector-specificity@5.0.0':
+ resolution: {integrity: sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ postcss-selector-parser: ^7.0.0
+
+ '@ctrl/tinycolor@3.6.1':
+ resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==}
+ engines: {node: '>=10'}
+
+ '@dual-bundle/import-meta-resolve@4.1.0':
+ resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==}
+
+ '@element-plus/icons-vue@2.3.2':
+ resolution: {integrity: sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ '@esbuild/aix-ppc64@0.19.12':
+ resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.19.12':
+ resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.19.12':
+ resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.19.12':
+ resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.19.12':
+ resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.19.12':
+ resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.19.12':
+ resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.19.12':
+ resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.19.12':
+ resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.19.12':
+ resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
+ engines: {node: '>=12'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.19.12':
+ resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.19.12':
+ resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
+ engines: {node: '>=12'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.19.12':
+ resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
+ engines: {node: '>=12'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.19.12':
+ resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
+ engines: {node: '>=12'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.19.12':
+ resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
+ engines: {node: '>=12'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.19.12':
+ resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
+ engines: {node: '>=12'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.19.12':
+ resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-x64@0.19.12':
+ resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-x64@0.19.12':
+ resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/sunos-x64@0.19.12':
+ resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.19.12':
+ resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
+ engines: {node: '>=12'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.19.12':
+ resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
+ engines: {node: '>=12'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.19.12':
+ resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
+ engines: {node: '>=12'}
+ cpu: [x64]
+ os: [win32]
+
+ '@eslint-community/eslint-utils@4.4.1':
+ resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+ '@eslint-community/regexpp@4.12.1':
+ resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+ engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+ '@eslint/eslintrc@2.1.4':
+ resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ '@eslint/js@8.57.1':
+ resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ '@floating-ui/core@1.6.8':
+ resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
+
+ '@floating-ui/dom@1.6.12':
+ resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==}
+
+ '@floating-ui/utils@0.2.8':
+ resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
+
+ '@form-create/component-elm-checkbox@3.2.14':
+ resolution: {integrity: sha512-VtjRvNxbKpjp0bNYQ2BuLRVIQHZdPpYn3Hy0xSuzv6QjEDnffMdmawHImPSlp8wAW3b94wZdHMLMqpyMew8bBw==}
+
+ '@form-create/component-elm-frame@3.2.14':
+ resolution: {integrity: sha512-JR7F8rEK4rC87ofjndRWaCNirUJTBGIINkP2GGbB5n8dH5wrkXi1QPemXBGaEgXoaxOQPRgMdc/GgZERgl6l/w==}
+
+ '@form-create/component-elm-group@3.2.14':
+ resolution: {integrity: sha512-fK0Gw+mTuypFqOqXLT3PK+/lhUd/Qv8EJzjgl0hfy9A2SoR1g1t2HBz2E70MK9CtZ5i4Zcf118NjQM0cDAPkIw==}
+
+ '@form-create/component-elm-radio@3.2.14':
+ resolution: {integrity: sha512-bNtMhDlWMpBHBFjkITGwDpYH/hZQDJ/q1SqsO5aWw+fxonWEod9ZgFaxUfNeqCKyo8loqu3tzivd5ZL77TsGFw==}
+
+ '@form-create/component-elm-select@3.2.14':
+ resolution: {integrity: sha512-yUX0uZQHakIVngV/0D54tchhytApKsuuJcsxSrdIqTRBd83XtEC9UO4fPDX8O+M53DpSX6YEUduRvqSPmUfKgQ==}
+
+ '@form-create/component-elm-tree@3.2.14':
+ resolution: {integrity: sha512-zZWsSmPqVzA8p31di1QmpPaknd7NXuyNDMJ8L6kwCo/ipzJwvToAVtj0fnTbQbdMVvGQlREs+Hwy9gJBkCoiFA==}
+
+ '@form-create/component-elm-upload@3.2.14':
+ resolution: {integrity: sha512-QtfzjPdSDuEUh4gfIInnNBFQB+qZvIJ/mKTz0r7wTVvZUOJbvnnEiaB0/1QzJ4z9ZfqYswdlahO9+hBW18ioCA==}
+
+ '@form-create/component-subform@3.1.34':
+ resolution: {integrity: sha512-OJcFH/7MTHx7JLEjDK/weS27qfuFWAI+OK+gXTJ2jIt9aZkGWF/EWkjetiJLt5a0KMw4Z15wOS2XCY9pVK9vlA==}
+
+ '@form-create/component-wangeditor@3.2.14':
+ resolution: {integrity: sha512-N/U/hFBdBu2OIguxoKe1Kslq5fW6XmtyhKDImLfKLn1xI6X5WUtt3r7QTaUPcVUl2vntpM9wJ/FBdG17RzF/Dg==}
+
+ '@form-create/core@3.2.14':
+ resolution: {integrity: sha512-z2YFhsru4PP/5AIwW2uBWW/Abn0ZtTMb52MqpJOedWulGRSS+zSvzsMMXB18EZPsug2OG1plQUkK79wlR6Y5JA==}
+ peerDependencies:
+ vue: ^3.1.0
+
+ '@form-create/designer@3.2.8':
+ resolution: {integrity: sha512-SgrGiWOFaQTARAmysepHDtFyRi97rERrlkv1joz+DCOAzZME3RKRTXVqA7ALzJ2jI3psiCosGAK4rPSLh6EvgA==}
+ peerDependencies:
+ vue: ^3.1.5
+
+ '@form-create/element-ui@3.2.14':
+ resolution: {integrity: sha512-xd+DNxS4ZBuE0gH/o+br/Lyn5kJQq7RonTykUXagfSxPq+iMnN2vmOSqHYQ0+uXNNu151PfRlZcsujNXgK1t/w==}
+ peerDependencies:
+ vue: ^3.1.0
+
+ '@form-create/utils@3.2.14':
+ resolution: {integrity: sha512-LDr2uao4qM68C4BXXAQkaMErxRvy3ZFda9992n1frXG8Ry2sbXXxOaY20ZWQoFY6HQP8ABJuJFVfM9p0KVSFLQ==}
+
+ '@gera2ld/jsx-dom@2.2.2':
+ resolution: {integrity: sha512-EOqf31IATRE6zS1W1EoWmXZhGfLAoO9FIlwTtHduSrBdud4npYBxYAkv8dZ5hudDPwJeeSjn40kbCL4wAzr8dA==}
+
+ '@humanwhocodes/config-array@0.13.0':
+ resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
+ engines: {node: '>=10.10.0'}
+ deprecated: Use @eslint/config-array instead
+
+ '@humanwhocodes/module-importer@1.0.1':
+ resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+ engines: {node: '>=12.22'}
+
+ '@humanwhocodes/object-schema@2.0.3':
+ resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
+ deprecated: Use @eslint/object-schema instead
+
+ '@iconify/iconify@2.1.2':
+ resolution: {integrity: sha512-QcUzFeEWkE/mW+BVtEGmcWATClcCOIJFiYUD/PiCWuTcdEA297o8D4oN6Ra44WrNOHu1wqNW4J0ioaDIiqaFOQ==}
+ deprecated: no longer maintained, switch to modern iconify-icon web component
+
+ '@iconify/iconify@3.1.1':
+ resolution: {integrity: sha512-1nemfyD/OJzh9ALepH7YfuuP8BdEB24Skhd8DXWh0hzcOxImbb1ZizSZkpCzAwSZSGcJFmscIBaBQu+yLyWaxQ==}
+ deprecated: no longer maintained, switch to modern iconify-icon web component
+
+ '@iconify/json@2.2.277':
+ resolution: {integrity: sha512-hNBnGD2djNgsdB4Yq5dBhP2CI0PLt+4EamozKSAD+hsbFAzVUN6sMj5FUiBFu8BKUOBIYcrX8ri7C7Qe3K10ew==}
+
+ '@iconify/types@2.0.0':
+ resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
+
+ '@iconify/utils@2.1.33':
+ resolution: {integrity: sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==}
+
+ '@intlify/bundle-utils@7.5.1':
+ resolution: {integrity: sha512-UovJl10oBIlmYEcWw+VIHdKY5Uv5sdPG0b/b6bOYxGLln3UwB75+2dlc0F3Fsa0RhoznQ5Rp589/BZpABpE4Xw==}
+ engines: {node: '>= 14.16'}
+ peerDependencies:
+ petite-vue-i18n: '*'
+ vue-i18n: '*'
+ peerDependenciesMeta:
+ petite-vue-i18n:
+ optional: true
+ vue-i18n:
+ optional: true
+
+ '@intlify/core-base@9.10.2':
+ resolution: {integrity: sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg==}
+ engines: {node: '>= 16'}
+
+ '@intlify/message-compiler@9.10.2':
+ resolution: {integrity: sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA==}
+ engines: {node: '>= 16'}
+
+ '@intlify/message-compiler@9.14.2':
+ resolution: {integrity: sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==}
+ engines: {node: '>= 16'}
+
+ '@intlify/shared@9.10.2':
+ resolution: {integrity: sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q==}
+ engines: {node: '>= 16'}
+
+ '@intlify/shared@9.14.2':
+ resolution: {integrity: sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==}
+ engines: {node: '>= 16'}
+
+ '@intlify/unplugin-vue-i18n@2.0.0':
+ resolution: {integrity: sha512-1oKvm92L9l2od2H9wKx2ZvR4tzn7gUtd7bPLI7AWUmm7U9H1iEypndt5d985ypxGsEs0gToDaKTrytbBIJwwSg==}
+ engines: {node: '>= 14.16'}
+ peerDependencies:
+ petite-vue-i18n: '*'
+ vue-i18n: '*'
+ vue-i18n-bridge: '*'
+ peerDependenciesMeta:
+ petite-vue-i18n:
+ optional: true
+ vue-i18n:
+ optional: true
+ vue-i18n-bridge:
+ optional: true
+
+ '@isaacs/cliui@8.0.2':
+ resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
+ engines: {node: '>=12'}
+
+ '@jest/schemas@29.6.3':
+ resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ '@jridgewell/gen-mapping@0.3.5':
+ resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/resolve-uri@3.1.2':
+ resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/set-array@1.2.1':
+ resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==}
+ engines: {node: '>=6.0.0'}
+
+ '@jridgewell/source-map@0.3.6':
+ resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==}
+
+ '@jridgewell/sourcemap-codec@1.5.0':
+ resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
+
+ '@lezer/common@1.2.3':
+ resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
+
+ '@lezer/highlight@1.2.1':
+ resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
+
+ '@lezer/lr@1.4.2':
+ resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
+
+ '@lezer/markdown@1.3.2':
+ resolution: {integrity: sha512-Wu7B6VnrKTbBEohqa63h5vxXjiC4pO5ZQJ/TDbhJxPQaaIoRD/6UVDhSDtVsCwVZV12vvN9KxuLL3ATMnlG0oQ==}
+
+ '@microsoft/fetch-event-source@2.0.1':
+ resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==}
+
+ '@nodelib/fs.scandir@2.1.5':
+ resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.stat@2.0.5':
+ resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+ engines: {node: '>= 8'}
+
+ '@nodelib/fs.walk@1.2.8':
+ resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+ engines: {node: '>= 8'}
+
+ '@parcel/watcher-android-arm64@2.5.0':
+ resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ '@parcel/watcher-darwin-arm64@2.5.0':
+ resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@parcel/watcher-darwin-x64@2.5.0':
+ resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@parcel/watcher-freebsd-x64@2.5.0':
+ resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@parcel/watcher-linux-arm-glibc@2.5.0':
+ resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-arm-musl@2.5.0':
+ resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.0':
+ resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-arm64-musl@2.5.0':
+ resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-linux-x64-glibc@2.5.0':
+ resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@parcel/watcher-linux-x64-musl@2.5.0':
+ resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@parcel/watcher-win32-arm64@2.5.0':
+ resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@parcel/watcher-win32-ia32@2.5.0':
+ resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@parcel/watcher-win32-x64@2.5.0':
+ resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==}
+ engines: {node: '>= 10.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@parcel/watcher@2.5.0':
+ resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==}
+ engines: {node: '>= 10.0.0'}
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
+ '@pkgr/core@0.1.1':
+ resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
+ engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+
+ '@polka/url@1.0.0-next.28':
+ resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==}
+
+ '@purge-icons/core@0.10.0':
+ resolution: {integrity: sha512-AtJbZv5Yy+vWX5v32DPTr+CW7AkSK8HJx52orDbrYt/9s4lGM2t4KKAmwaTQEH2HYr2HVh1mlqs54/S1s3WT1g==}
+
+ '@purge-icons/generated@0.10.0':
+ resolution: {integrity: sha512-I+1yN7/yDy/eZzfhAZqKF8Z6FM8D/O1vempbPrHJ0m9HlZwvf8sWXOArPJ2qRQGB6mJUVSpaXkoGBuoz1GQX5A==}
+
+ '@purge-icons/generated@0.9.0':
+ resolution: {integrity: sha512-s2t+1oVtGDV6KtqfCXtUOhxfeYvOdDF90IVm+nMs/6bUP0HeGZLslguuL/AibpwtfL4FA/oCsIu/RhwapgAdJw==}
+
+ '@quansync/fs@0.1.1':
+ resolution: {integrity: sha512-sx8J1O/+j2lqs8MvsEz6rs/6UAUpCb4fu7C6EqtMqzbS3CmqLkTDTOMK+DrWukvyUuHzl8DhMjfNJzQDTqfGJg==}
+ engines: {node: '>=20.18.0'}
+
+ '@rollup/plugin-virtual@3.0.2':
+ resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ '@rollup/pluginutils@4.2.1':
+ resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
+ engines: {node: '>= 8.0.0'}
+
+ '@rollup/pluginutils@5.1.3':
+ resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+ peerDependenciesMeta:
+ rollup:
+ optional: true
+
+ '@rollup/rollup-android-arm-eabi@4.27.4':
+ resolution: {integrity: sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==}
+ cpu: [arm]
+ os: [android]
+
+ '@rollup/rollup-android-arm64@4.27.4':
+ resolution: {integrity: sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==}
+ cpu: [arm64]
+ os: [android]
+
+ '@rollup/rollup-darwin-arm64@4.27.4':
+ resolution: {integrity: sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rollup/rollup-darwin-x64@4.27.4':
+ resolution: {integrity: sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rollup/rollup-freebsd-arm64@4.27.4':
+ resolution: {integrity: sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@rollup/rollup-freebsd-x64@4.27.4':
+ resolution: {integrity: sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.27.4':
+ resolution: {integrity: sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm-musleabihf@4.27.4':
+ resolution: {integrity: sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==}
+ cpu: [arm]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-arm64-gnu@4.27.4':
+ resolution: {integrity: sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-arm64-musl@4.27.4':
+ resolution: {integrity: sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.27.4':
+ resolution: {integrity: sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-riscv64-gnu@4.27.4':
+ resolution: {integrity: sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-s390x-gnu@4.27.4':
+ resolution: {integrity: sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-gnu@4.27.4':
+ resolution: {integrity: sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rollup/rollup-linux-x64-musl@4.27.4':
+ resolution: {integrity: sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rollup/rollup-win32-arm64-msvc@4.27.4':
+ resolution: {integrity: sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rollup/rollup-win32-ia32-msvc@4.27.4':
+ resolution: {integrity: sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==}
+ cpu: [ia32]
+ os: [win32]
+
+ '@rollup/rollup-win32-x64-msvc@4.27.4':
+ resolution: {integrity: sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==}
+ cpu: [x64]
+ os: [win32]
+
+ '@sinclair/typebox@0.27.8':
+ resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
+
+ '@sphinxxxx/color-conversion@2.2.2':
+ resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==}
+
+ '@swc/core-darwin-arm64@1.9.3':
+ resolution: {integrity: sha512-hGfl/KTic/QY4tB9DkTbNuxy5cV4IeejpPD4zo+Lzt4iLlDWIeANL4Fkg67FiVceNJboqg48CUX+APhDHO5G1w==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@swc/core-darwin-x64@1.9.3':
+ resolution: {integrity: sha512-IaRq05ZLdtgF5h9CzlcgaNHyg4VXuiStnOFpfNEMuI5fm5afP2S0FHq8WdakUz5WppsbddTdplL+vpeApt/WCQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@swc/core-linux-arm-gnueabihf@1.9.3':
+ resolution: {integrity: sha512-Pbwe7xYprj/nEnZrNBvZfjnTxlBIcfApAGdz2EROhjpPj+FBqBa3wOogqbsuGGBdCphf8S+KPprL1z+oDWkmSQ==}
+ engines: {node: '>=10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@swc/core-linux-arm64-gnu@1.9.3':
+ resolution: {integrity: sha512-AQ5JZiwNGVV/2K2TVulg0mw/3LYfqpjZO6jDPtR2evNbk9Yt57YsVzS+3vHSlUBQDRV9/jqMuZYVU3P13xrk+g==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@swc/core-linux-arm64-musl@1.9.3':
+ resolution: {integrity: sha512-tzVH480RY6RbMl/QRgh5HK3zn1ZTFsThuxDGo6Iuk1MdwIbdFYUY034heWUTI4u3Db97ArKh0hNL0xhO3+PZdg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@swc/core-linux-x64-gnu@1.9.3':
+ resolution: {integrity: sha512-ivXXBRDXDc9k4cdv10R21ccBmGebVOwKXT/UdH1PhxUn9m/h8erAWjz5pcELwjiMf27WokqPgaWVfaclDbgE+w==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@swc/core-linux-x64-musl@1.9.3':
+ resolution: {integrity: sha512-ILsGMgfnOz1HwdDz+ZgEuomIwkP1PHT6maigZxaCIuC6OPEhKE8uYna22uU63XvYcLQvZYDzpR3ms47WQPuNEg==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@swc/core-win32-arm64-msvc@1.9.3':
+ resolution: {integrity: sha512-e+XmltDVIHieUnNJHtspn6B+PCcFOMYXNJB1GqoCcyinkEIQNwC8KtWgMqUucUbEWJkPc35NHy9k8aCXRmw9Kg==}
+ engines: {node: '>=10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@swc/core-win32-ia32-msvc@1.9.3':
+ resolution: {integrity: sha512-rqpzNfpAooSL4UfQnHhkW8aL+oyjqJniDP0qwZfGnjDoJSbtPysHg2LpcOBEdSnEH+uIZq6J96qf0ZFD8AGfXA==}
+ engines: {node: '>=10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@swc/core-win32-x64-msvc@1.9.3':
+ resolution: {integrity: sha512-3YJJLQ5suIEHEKc1GHtqVq475guiyqisKSoUnoaRtxkDaW5g1yvPt9IoSLOe2mRs7+FFhGGU693RsBUSwOXSdQ==}
+ engines: {node: '>=10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/core@1.9.3':
+ resolution: {integrity: sha512-oRj0AFePUhtatX+BscVhnzaAmWjpfAeySpM1TCbxA1rtBDeH/JDhi5yYzAKneDYtVtBvA7ApfeuzhMC9ye4xSg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@swc/helpers': '*'
+ peerDependenciesMeta:
+ '@swc/helpers':
+ optional: true
+
+ '@swc/counter@0.1.3':
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+ '@swc/types@0.1.17':
+ resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==}
+
+ '@sxzz/popperjs-es@2.11.7':
+ resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
+
+ '@transloadit/prettier-bytes@0.0.7':
+ resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
+
+ '@trysound/sax@0.2.0':
+ resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
+ engines: {node: '>=10.13.0'}
+
+ '@types/ace@0.0.52':
+ resolution: {integrity: sha512-YPF9S7fzpuyrxru+sG/rrTpZkC6gpHBPF14W3x70kqVOD+ks6jkYLapk4yceh36xej7K4HYxcyz9ZDQ2lTvwgQ==}
+
+ '@types/conventional-commits-parser@5.0.1':
+ resolution: {integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ==}
+
+ '@types/d3-array@3.2.1':
+ resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
+
+ '@types/d3-axis@3.0.6':
+ resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
+
+ '@types/d3-brush@3.0.6':
+ resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
+
+ '@types/d3-chord@3.0.6':
+ resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-contour@3.0.6':
+ resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
+
+ '@types/d3-delaunay@6.0.4':
+ resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
+
+ '@types/d3-dispatch@3.0.6':
+ resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==}
+
+ '@types/d3-drag@3.0.7':
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
+
+ '@types/d3-dsv@3.0.7':
+ resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-fetch@3.0.7':
+ resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
+
+ '@types/d3-force@3.0.10':
+ resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
+
+ '@types/d3-format@3.0.4':
+ resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
+
+ '@types/d3-geo@3.1.0':
+ resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
+
+ '@types/d3-hierarchy@3.1.7':
+ resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.0':
+ resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
+
+ '@types/d3-polygon@3.0.2':
+ resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
+
+ '@types/d3-quadtree@3.0.6':
+ resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
+
+ '@types/d3-random@3.0.3':
+ resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
+
+ '@types/d3-scale-chromatic@3.1.0':
+ resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
+
+ '@types/d3-scale@4.0.8':
+ resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
+
+ '@types/d3-selection@3.0.11':
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
+
+ '@types/d3-shape@3.1.6':
+ resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
+
+ '@types/d3-time-format@4.0.3':
+ resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
+ '@types/d3-transition@3.0.9':
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
+
+ '@types/d3-zoom@3.0.8':
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
+
+ '@types/d3@7.4.3':
+ resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+
+ '@types/eslint@8.56.12':
+ resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==}
+
+ '@types/estree@1.0.6':
+ resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+
+ '@types/event-emitter@0.3.5':
+ resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
+
+ '@types/geojson@7946.0.14':
+ resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==}
+
+ '@types/json-schema@7.0.15':
+ resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+ '@types/jsoneditor@9.9.6':
+ resolution: {integrity: sha512-SJ29nWBIhnhtU5n72wxhPiuUVd8cnDHd7ZYMqVkzWtdRxTUdS8+oy1pg66yhmM1kcuanX3xmAAKfcyhhBnHEjQ==}
+
+ '@types/lodash-es@4.17.12':
+ resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
+
+ '@types/lodash@4.17.13':
+ resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
+
+ '@types/node@10.17.60':
+ resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
+
+ '@types/node@20.17.9':
+ resolution: {integrity: sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==}
+
+ '@types/nprogress@0.2.3':
+ resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
+
+ '@types/qrcode@1.5.5':
+ resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
+
+ '@types/qs@6.9.17':
+ resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==}
+
+ '@types/semver@7.5.8':
+ resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
+
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
+ '@types/video.js@7.3.58':
+ resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==}
+
+ '@types/web-bluetooth@0.0.16':
+ resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
+
+ '@types/web-bluetooth@0.0.20':
+ resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
+
+ '@typescript-eslint/eslint-plugin@7.18.0':
+ resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+ peerDependencies:
+ '@typescript-eslint/parser': ^7.0.0
+ eslint: ^8.56.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/parser@6.21.0':
+ resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/parser@7.18.0':
+ resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+ peerDependencies:
+ eslint: ^8.56.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/scope-manager@6.21.0':
+ resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+
+ '@typescript-eslint/scope-manager@7.18.0':
+ resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+
+ '@typescript-eslint/scope-manager@8.26.1':
+ resolution: {integrity: sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/type-utils@7.18.0':
+ resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+ peerDependencies:
+ eslint: ^8.56.0
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/types@6.21.0':
+ resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+
+ '@typescript-eslint/types@7.18.0':
+ resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+
+ '@typescript-eslint/types@8.26.1':
+ resolution: {integrity: sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@typescript-eslint/typescript-estree@6.21.0':
+ resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/typescript-estree@7.18.0':
+ resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@typescript-eslint/typescript-estree@8.26.1':
+ resolution: {integrity: sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/utils@6.21.0':
+ resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+
+ '@typescript-eslint/utils@7.18.0':
+ resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+ peerDependencies:
+ eslint: ^8.56.0
+
+ '@typescript-eslint/utils@8.26.1':
+ resolution: {integrity: sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0
+ typescript: '>=4.8.4 <5.9.0'
+
+ '@typescript-eslint/visitor-keys@6.21.0':
+ resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==}
+ engines: {node: ^16.0.0 || >=18.0.0}
+
+ '@typescript-eslint/visitor-keys@7.18.0':
+ resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==}
+ engines: {node: ^18.18.0 || >=20.0.0}
+
+ '@typescript-eslint/visitor-keys@8.26.1':
+ resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ '@ungap/structured-clone@1.2.0':
+ resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
+
+ '@unocss/astro@0.58.9':
+ resolution: {integrity: sha512-VWfHNC0EfawFxLfb3uI+QcMGBN+ju+BYtutzeZTjilLKj31X2UpqIh8fepixL6ljgZzB3fweqg2xtUMC0gMnoQ==}
+ peerDependencies:
+ vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ '@unocss/cli@0.58.9':
+ resolution: {integrity: sha512-q7qlwX3V6UaqljWUQ5gMj36yTA9eLuuRywahdQWt1ioy4aPF/MEEfnMBZf/ntrqf5tIT5TO8fE11nvCco2Q/sA==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ '@unocss/config@0.57.7':
+ resolution: {integrity: sha512-UG8G9orWEdk/vyDvGUToXYn/RZy/Qjpx66pLsaf5wQK37hkYsBoReAU5v8Ia/6PL1ueJlkcNXLaNpN6/yVoJvg==}
+ engines: {node: '>=14'}
+
+ '@unocss/config@0.58.9':
+ resolution: {integrity: sha512-90wRXIyGNI8UenWxvHUcH4l4rgq813MsTzYWsf6ZKyLLvkFjV2b2EfGXI27GPvZ7fVE1OAqx+wJNTw8CyQxwag==}
+ engines: {node: '>=14'}
+
+ '@unocss/config@66.1.0-beta.5':
+ resolution: {integrity: sha512-RBty/CVvdefTpeLmluQrIQIj+Po5bTIgIgcWgw+A3dMcUN3iRv0mYbw1d3FIRa0Ladx9zKaMxRFss0xkiS13yw==}
+ engines: {node: '>=14'}
+
+ '@unocss/core@0.57.7':
+ resolution: {integrity: sha512-1d36M0CV3yC80J0pqOa5rH1BX6g2iZdtKmIb3oSBN4AWnMCSrrJEPBrUikyMq2TEQTrYWJIVDzv5A9hBUat3TA==}
+
+ '@unocss/core@0.58.9':
+ resolution: {integrity: sha512-wYpPIPPsOIbIoMIDuH8ihehJk5pAZmyFKXIYO/Kro98GEOFhz6lJoLsy6/PZuitlgp2/TSlubUuWGjHWvp5osw==}
+
+ '@unocss/core@66.1.0-beta.5':
+ resolution: {integrity: sha512-1kZzSrB87KKd+xP+vMN7IP03j2UPEykna447aw3UaK5RYTDd/LuVtxoep6gvjN9TJiB4K+Qx0sAtgnfhPpka9Q==}
+
+ '@unocss/eslint-config@0.57.7':
+ resolution: {integrity: sha512-EJlI6rV0ZfDCphIiddHSWZVeoHdYDTVohVXGo+NfNOuRuvYWGna3n4hY3VEAiT3mWLK0/0anzHF7X0PNzCR5lQ==}
+ engines: {node: '>=14'}
+
+ '@unocss/eslint-plugin@0.57.7':
+ resolution: {integrity: sha512-nwj7UJF7wCfPVl5B7cUB0xrSk6yuVMdMgABnsy4N5xBlds8cclrUO+boaTB9qzh8Lg9nfJVLB3+cW3po2SJoew==}
+ engines: {node: '>=14'}
+
+ '@unocss/eslint-plugin@66.1.0-beta.5':
+ resolution: {integrity: sha512-5BRXjE8XJ9Yrf/lmgBCCmpfXRfiaebdS0zhkbmsFJmtXzhhun0epIF2cs/nXIya9rtvne+YKUAPXxIIoHV3lKA==}
+ engines: {node: '>=14'}
+
+ '@unocss/extractor-arbitrary-variants@0.58.9':
+ resolution: {integrity: sha512-M/BvPdbEEMdhcFQh/z2Bf9gylO1Ky/ZnpIvKWS1YJPLt4KA7UWXSUf+ZNTFxX+X58Is5qAb5hNh/XBQmL3gbXg==}
+
+ '@unocss/inspector@0.58.9':
+ resolution: {integrity: sha512-uRzqkCNeBmEvFePXcfIFcQPMlCXd9/bLwa5OkBthiOILwQdH1uRIW3GWAa2SWspu+kZLP0Ly3SjZ9Wqi+5ZtTw==}
+
+ '@unocss/postcss@0.58.9':
+ resolution: {integrity: sha512-PnKmH6Qhimw35yO6u6yx9SHaX2NmvbRNPDvMDHA/1xr3M8L0o8U88tgKbWfm65NEGF3R1zJ9A8rjtZn/LPkgPA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ postcss: ^8.4.21
+
+ '@unocss/preset-attributify@0.58.9':
+ resolution: {integrity: sha512-ucP+kXRFcwmBmHohUVv31bE/SejMAMo7Hjb0QcKVLyHlzRWUJsfNR+jTAIGIUSYxN7Q8MeigYsongGo3nIeJnQ==}
+
+ '@unocss/preset-icons@0.58.9':
+ resolution: {integrity: sha512-9dS48+yAunsbS0ylOW2Wisozwpn3nGY1CqTiidkUnrMnrZK3al579A7srUX9NyPWWDjprO7eU/JkWbdDQSmFFA==}
+
+ '@unocss/preset-mini@0.58.9':
+ resolution: {integrity: sha512-m4aDGYtueP8QGsU3FsyML63T/w5Mtr4htme2jXy6m50+tzC1PPHaIBstMTMQfLc6h8UOregPJyGHB5iYQZGEvQ==}
+
+ '@unocss/preset-tagify@0.58.9':
+ resolution: {integrity: sha512-obh75XrRmxYwrQMflzvhQUMeHwd/R9bEDhTWUW9aBTolBy4eNypmQwOhHCKh5Xi4Dg6o0xj6GWC/jcCj1SPLog==}
+
+ '@unocss/preset-typography@0.58.9':
+ resolution: {integrity: sha512-hrsaqKlcZni3Vh4fwXC+lP9e92FQYbqtmlZw2jpxlVwwH5aLzwk4d4MiFQGyhCfzuSDYm0Zd52putFVV02J7bA==}
+
+ '@unocss/preset-uno@0.58.9':
+ resolution: {integrity: sha512-Fze+X2Z/EegCkRdDRgwwvFBmXBenNR1AG8KxAyz8iPeWbhOBaRra2sn2ScryrfH6SbJHpw26ZyJXycAdS0Fq3A==}
+
+ '@unocss/preset-web-fonts@0.58.9':
+ resolution: {integrity: sha512-XtiO+Z+RYnNYomNkS2XxaQiY++CrQZKOfNGw5htgIrb32QtYVQSkyYQ3jDw7JmMiCWlZ4E72cV/zUb++WrZLxg==}
+
+ '@unocss/preset-wind@0.58.9':
+ resolution: {integrity: sha512-7l+7Vx5UoN80BmJKiqDXaJJ6EUqrnUQYv8NxCThFi5lYuHzxsYWZPLU3k3XlWRUQt8XL+6rYx7mMBmD7EUSHyw==}
+
+ '@unocss/reset@0.58.9':
+ resolution: {integrity: sha512-nA2pg3tnwlquq+FDOHyKwZvs20A6iBsKPU7Yjb48JrNnzoaXqE+O9oN6782IG2yKVW4AcnsAnAnM4cxXhGzy1w==}
+
+ '@unocss/rule-utils@0.58.9':
+ resolution: {integrity: sha512-45bDa+elmlFLthhJmKr2ltKMAB0yoXnDMQ6Zp5j3OiRB7dDMBkwYRPvHLvIe+34Ey7tDt/kvvDPtWMpPl2quUQ==}
+ engines: {node: '>=14'}
+
+ '@unocss/rule-utils@66.1.0-beta.5':
+ resolution: {integrity: sha512-G757sAnQAMNRUijgOTut8UkbkncSablI6Viwcq2VP4r0Lhi6RFOv/n6AOTWsDgGeUSuWTa/p3zb3NDHY7ztE9g==}
+ engines: {node: '>=14'}
+
+ '@unocss/scope@0.58.9':
+ resolution: {integrity: sha512-BIwcpx0R3bE0rYa9JVDJTk0GX32EBvnbvufBpNkWfC5tb7g+B7nMkVq9ichanksYCCxrIQQo0mrIz5PNzu9sGA==}
+
+ '@unocss/transformer-attributify-jsx-babel@0.58.9':
+ resolution: {integrity: sha512-UGaQoGZg+3QrsPtnGHPECmsGn4EQb2KSdZ4eGEn2YssjKv+CcQhzRvpEUgnuF/F+jGPkCkS/G/YEQBHRWBY54Q==}
+
+ '@unocss/transformer-attributify-jsx@0.58.9':
+ resolution: {integrity: sha512-jpL3PRwf8t43v1agUdQn2EHGgfdWfvzsMxFtoybO88xzOikzAJaaouteNtojc/fQat2T9iBduDxVj5egdKmhdQ==}
+
+ '@unocss/transformer-compile-class@0.58.9':
+ resolution: {integrity: sha512-l2VpCqelJ6Tgc1kfSODxBtg7fCGPVRr2EUzTg1LrGYKa2McbKuc/wV/2DWKHGxL6+voWi7a2C9XflqGDXXutuQ==}
+
+ '@unocss/transformer-directives@0.58.9':
+ resolution: {integrity: sha512-pLOUsdoY2ugVntJXg0xuGjO9XZ2xCiMxTPRtpZ4TsEzUtdEzMswR06Y8VWvNciTB/Zqxcz9ta8rD0DKePOfSuw==}
+
+ '@unocss/transformer-variant-group@0.58.9':
+ resolution: {integrity: sha512-3A6voHSnFcyw6xpcZT6oxE+KN4SHRnG4z862tdtWvRGcN+jGyNr20ylEZtnbk4xj0VNMeGHHQRZ0WLvmrAwvOQ==}
+
+ '@unocss/vite@0.58.9':
+ resolution: {integrity: sha512-mmppBuulAHCal+sC0Qz36Y99t0HicAmznpj70Kzwl7g/yvXwm58/DW2OnpCWw+uA8/JBft/+z3zE+XvrI+T1HA==}
+ peerDependencies:
+ vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
+
+ '@uppy/companion-client@2.2.2':
+ resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
+
+ '@uppy/core@2.3.4':
+ resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==}
+
+ '@uppy/store-default@2.1.1':
+ resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==}
+
+ '@uppy/utils@4.1.3':
+ resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==}
+
+ '@uppy/xhr-upload@2.1.3':
+ resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==}
+ peerDependencies:
+ '@uppy/core': ^2.3.3
+
+ '@videojs-player/vue@1.0.0':
+ resolution: {integrity: sha512-WonTezRfKu3fYdQLt/ta+nuKH6gMZUv8l40Jke/j4Lae7IqeO/+lLAmBnh3ni88bwR+vkFXIlZ2Ci7VKInIYJg==}
+ peerDependencies:
+ '@types/video.js': 7.x
+ video.js: 7.x
+ vue: 3.x
+
+ '@videojs/http-streaming@2.16.3':
+ resolution: {integrity: sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==}
+ engines: {node: '>=8', npm: '>=5'}
+ peerDependencies:
+ video.js: ^6 || ^7
+
+ '@videojs/vhs-utils@3.0.5':
+ resolution: {integrity: sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==}
+ engines: {node: '>=8', npm: '>=5'}
+
+ '@videojs/xhr@2.6.0':
+ resolution: {integrity: sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==}
+
+ '@vitejs/plugin-legacy@5.4.3':
+ resolution: {integrity: sha512-wsyXK9mascyplcqvww1gA1xYiy29iRHfyciw+a0t7qRNdzX6PdfSWmOoCi74epr87DujM+5J+rnnSv+4PazqVg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ peerDependencies:
+ terser: ^5.4.0
+ vite: ^5.0.0
+
+ '@vitejs/plugin-vue-jsx@3.1.0':
+ resolution: {integrity: sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ vite: ^4.0.0 || ^5.0.0
+ vue: ^3.0.0
+
+ '@vitejs/plugin-vue@5.2.1':
+ resolution: {integrity: sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ peerDependencies:
+ vite: ^5.0.0 || ^6.0.0
+ vue: ^3.2.25
+
+ '@volar/language-core@1.11.1':
+ resolution: {integrity: sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==}
+
+ '@volar/source-map@1.11.1':
+ resolution: {integrity: sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==}
+
+ '@volar/typescript@1.11.1':
+ resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==}
+
+ '@vue/babel-helper-vue-transform-on@1.2.5':
+ resolution: {integrity: sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==}
+
+ '@vue/babel-plugin-jsx@1.2.5':
+ resolution: {integrity: sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+
+ '@vue/babel-plugin-resolve-type@1.2.5':
+ resolution: {integrity: sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==}
+ peerDependencies:
+ '@babel/core': ^7.0.0-0
+
+ '@vue/compiler-core@3.5.12':
+ resolution: {integrity: sha512-ISyBTRMmMYagUxhcpyEH0hpXRd/KqDU4ymofPgl2XAkY9ZhQ+h0ovEZJIiPop13UmR/54oA2cgMDjgroRelaEw==}
+
+ '@vue/compiler-core@3.5.13':
+ resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
+
+ '@vue/compiler-dom@3.5.12':
+ resolution: {integrity: sha512-9G6PbJ03uwxLHKQ3P42cMTi85lDRvGLB2rSGOiQqtXELat6uI4n8cNz9yjfVHRPIu+MsK6TE418Giruvgptckg==}
+
+ '@vue/compiler-dom@3.5.13':
+ resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
+
+ '@vue/compiler-sfc@3.5.12':
+ resolution: {integrity: sha512-2k973OGo2JuAa5+ZlekuQJtitI5CgLMOwgl94BzMCsKZCX/xiqzJYzapl4opFogKHqwJk34vfsaKpfEhd1k5nw==}
+
+ '@vue/compiler-sfc@3.5.13':
+ resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
+
+ '@vue/compiler-ssr@3.5.12':
+ resolution: {integrity: sha512-eLwc7v6bfGBSM7wZOGPmRavSWzNFF6+PdRhE+VFJhNCgHiF8AM7ccoqcv5kBXA2eWUfigD7byekvf/JsOfKvPA==}
+
+ '@vue/compiler-ssr@3.5.13':
+ resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
+
+ '@vue/devtools-api@6.6.4':
+ resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
+
+ '@vue/language-core@1.8.27':
+ resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ '@vue/reactivity@3.5.12':
+ resolution: {integrity: sha512-UzaN3Da7xnJXdz4Okb/BGbAaomRHc3RdoWqTzlvd9+WBR5m3J39J1fGcHes7U3za0ruYn/iYy/a1euhMEHvTAg==}
+
+ '@vue/runtime-core@3.5.12':
+ resolution: {integrity: sha512-hrMUYV6tpocr3TL3Ad8DqxOdpDe4zuQY4HPY3X/VRh+L2myQO8MFXPAMarIOSGNu0bFAjh1yBkMPXZBqCk62Uw==}
+
+ '@vue/runtime-dom@3.5.12':
+ resolution: {integrity: sha512-q8VFxR9A2MRfBr6/55Q3umyoN7ya836FzRXajPB6/Vvuv0zOPL+qltd9rIMzG/DbRLAIlREmnLsplEF/kotXKA==}
+
+ '@vue/server-renderer@3.5.12':
+ resolution: {integrity: sha512-I3QoeDDeEPZm8yR28JtY+rk880Oqmj43hreIBVTicisFTx/Dl7JpG72g/X7YF8hnQD3IFhkky5i2bPonwrTVPg==}
+ peerDependencies:
+ vue: 3.5.12
+
+ '@vue/shared@3.5.12':
+ resolution: {integrity: sha512-L2RPSAwUFbgZH20etwrXyVyCBu9OxRSi8T/38QsvnkJyvq2LufW2lDCOzm7t/U9C1mkhJGWYfCuFBCmIuNivrg==}
+
+ '@vue/shared@3.5.13':
+ resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
+
+ '@vueuse/core@10.11.1':
+ resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
+
+ '@vueuse/core@9.13.0':
+ resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
+
+ '@vueuse/metadata@10.11.1':
+ resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
+
+ '@vueuse/metadata@9.13.0':
+ resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
+
+ '@vueuse/shared@10.11.1':
+ resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
+
+ '@vueuse/shared@9.13.0':
+ resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
+
+ '@wangeditor-next/basic-modules@1.5.46':
+ resolution: {integrity: sha512-q8/al99P7+koPTJ+X79WjrEP5pDXs8+vFYo0pIbs59Oi1cM1b9CCB0384Mdcg0DQU7eZrN7dl2bHKO5Z5r7E8g==}
+ peerDependencies:
+ '@wangeditor-next/core': 1.7.45
+ dom7: ^3.0.0 || ^4.0.0
+ lodash.throttle: ^4.1.1
+ nanoid: ^5.0.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/code-highlight@1.3.43':
+ resolution: {integrity: sha512-22eHjYDmtTxZqZOma2ls9zWA6gsgSkWq3XtmLylA15kegVBKAy7YxYbRrdS7D4Y/igqOerSbc5oMsOdeYjRfnQ==}
+ peerDependencies:
+ '@wangeditor-next/core': 1.7.45
+ dom7: ^3.0.0 || ^4.0.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/core@1.7.45':
+ resolution: {integrity: sha512-5Pt8JCmdzJWk4q18zUZse+zM+mBW6jYt3npXVkLswYysx01krC3bBQq1J9JeZe4Ci+rQAs0tQj3t1imjpsmRgg==}
+ peerDependencies:
+ '@uppy/core': ^2.1.1
+ '@uppy/xhr-upload': ^2.0.3
+ dom7: ^3.0.0 || ^4.0.0
+ is-hotkey: ^0.2.0
+ lodash.camelcase: ^4.3.0
+ lodash.clonedeep: ^4.5.0
+ lodash.debounce: ^4.0.8
+ lodash.foreach: ^4.5.0
+ lodash.throttle: ^4.1.1
+ lodash.toarray: ^4.4.0
+ nanoid: ^5.0.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/editor-for-vue@5.1.14':
+ resolution: {integrity: sha512-Xkrdo590AhLHvzyR+U246t6T89nIWHz1weAgMuo8jEA2HS5RiUnsA4U6+iUGaQ2E5c8mYQaeNqzHQXUp9Okbiw==}
+ peerDependencies:
+ '@wangeditor-next/editor': '>=5.1.0'
+ vue: ^3.0.5
+
+ '@wangeditor-next/editor@5.6.46':
+ resolution: {integrity: sha512-Hio4MFSiICacxqIs7oRcfDQC2udqB502mL5gFfcxkfv8EUVESkKPMu9pjmOJcKNTaINp18svPnUEKDB5+v988A==}
+
+ '@wangeditor-next/list-module@1.1.51':
+ resolution: {integrity: sha512-gsHmXAO0rXCd2vXhxJHbRCClEjzokzpS2ozdt1tZHZv/fZUIdTnwwCCT2OThF8SxnlBS5bmZxVa9zYnDxXGjaQ==}
+ peerDependencies:
+ '@wangeditor-next/core': 1.7.45
+ dom7: ^3.0.0 || ^4.0.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/plugin-mention@1.0.16':
+ resolution: {integrity: sha512-gj6uxjqxJ2lgCzhUpE4TPwK51DRRmVTaLOMEImkyVRYqNlEkjuFe50ujY0CQUtIoRepaNEewOX/9Wda7QhE5mw==}
+ peerDependencies:
+ '@wangeditor-next/editor': 5.6.46
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/table-module@1.6.58':
+ resolution: {integrity: sha512-K06b0MVOQJW+trIWWlMB53Q+W9BojMyHOtaoCBQWX1XpvBGHJZRBdTVP0VMIBx398agPe+td0AZgcyWi5qsy0g==}
+ peerDependencies:
+ '@wangeditor-next/core': 1.7.45
+ dom7: ^3.0.0 || ^4.0.0
+ lodash.debounce: ^4.0.8
+ lodash.throttle: ^4.1.1
+ nanoid: ^5.0.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/upload-image-module@1.1.49':
+ resolution: {integrity: sha512-nT7w5kSYdlZj+ImvjDObGhoW9fGU2OK9Tw3+gGdOx1MidWzoAuOqZK++2GWI5zhQRIhTmfDRrFTmLNDC8qb7YA==}
+ peerDependencies:
+ '@uppy/core': ^2.0.3
+ '@uppy/xhr-upload': ^2.0.3
+ '@wangeditor-next/basic-modules': 1.5.46
+ '@wangeditor-next/core': 1.7.45
+ dom7: ^3.0.0 || ^4.0.0
+ lodash.foreach: ^4.5.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@wangeditor-next/video-module@1.3.51':
+ resolution: {integrity: sha512-67ecZCGIY+MUsqFtmwR9QKWlzGeIXVyXHmzPuevYwEqRwg50oR2xCSuoQLhfs5CKjXDZKsZhOnD/CGgt82TU+A==}
+ peerDependencies:
+ '@uppy/core': ^2.1.4
+ '@uppy/xhr-upload': ^2.0.7
+ '@wangeditor-next/core': 1.7.45
+ dom7: ^3.0.0 || ^4.0.0
+ nanoid: ^5.0.0
+ slate: ^0.82.0
+ snabbdom: ^3.6.0
+
+ '@xmldom/xmldom@0.8.10':
+ resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==}
+ engines: {node: '>=10.0.0'}
+
+ '@zxcvbn-ts/core@3.0.4':
+ resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==}
+
+ JSONStream@1.3.5:
+ resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
+ hasBin: true
+
+ ace-builds@1.39.1:
+ resolution: {integrity: sha512-HcJbBzx8qY66t9gZo/sQu7pi0wO/CFLdYn1LxQO1WQTfIkMfyc7LRnBpsp/oNCSSU/LL83jXHN1fqyOTuIhUjg==}
+
+ acorn-jsx@5.3.2:
+ resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+ peerDependencies:
+ acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+ acorn@8.14.0:
+ resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
+ engines: {node: '>=0.4.0'}
+ hasBin: true
+
+ aes-decrypter@3.1.3:
+ resolution: {integrity: sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==}
+
+ ajv@6.12.6:
+ resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+ ajv@8.17.1:
+ resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
+
+ animate.css@4.1.1:
+ resolution: {integrity: sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==}
+
+ ansi-escapes@7.0.0:
+ resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==}
+ engines: {node: '>=18'}
+
+ ansi-regex@2.1.1:
+ resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
+ engines: {node: '>=0.10.0'}
+
+ ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+
+ ansi-regex@6.1.0:
+ resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
+ engines: {node: '>=12'}
+
+ ansi-styles@2.2.1:
+ resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
+ engines: {node: '>=0.10.0'}
+
+ ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+
+ ansi-styles@5.2.0:
+ resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
+ engines: {node: '>=10'}
+
+ ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ array-ify@1.0.0:
+ resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
+
+ array-move@4.0.0:
+ resolution: {integrity: sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ array-union@2.1.0:
+ resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
+ engines: {node: '>=8'}
+
+ astral-regex@2.0.0:
+ resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
+ engines: {node: '>=8'}
+
+ async-validator@4.2.5:
+ resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
+
+ async@3.2.6:
+ resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
+
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ autolinker@3.16.2:
+ resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==}
+
+ autoprefixer@10.4.20:
+ resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
+ engines: {node: ^10 || ^12 || >=14}
+ hasBin: true
+ peerDependencies:
+ postcss: ^8.1.0
+
+ axios@0.26.1:
+ resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
+
+ axios@1.9.0:
+ resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
+
+ babel-plugin-polyfill-corejs2@0.4.12:
+ resolution: {integrity: sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-corejs3@0.10.6:
+ resolution: {integrity: sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ babel-plugin-polyfill-regenerator@0.6.3:
+ resolution: {integrity: sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==}
+ peerDependencies:
+ '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0
+
+ balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+ balanced-match@2.0.0:
+ resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
+
+ benz-amr-recorder@1.1.5:
+ resolution: {integrity: sha512-NepctcNTsZHK8NxBb5uKO5p8S+xkbm+vD6GLSkCYdJeEsriexvgumLHpDkanX4QJBcLRMVtg16buWMs+gUPB3g==}
+
+ benz-recorderjs@1.0.5:
+ resolution: {integrity: sha512-EwedOQo9KLti7HxDi/eZY51PSRbAXnOdEZmLvJ6ro3QQSoF9Y3AXBt57MIllGvVz5vtFYMeikG+GD7qTm3+p9w==}
+
+ binary-extensions@2.3.0:
+ resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
+ engines: {node: '>=8'}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ bpmn-js-properties-panel@5.23.0:
+ resolution: {integrity: sha512-4B27LM8oV14A2QWRvazV17h4NxbkNERcqU+AGJmxKImMlLhu9893MWR+pCdTQCTphBdBkuD8ksWm+1wVCedJ7g==}
+ peerDependencies:
+ '@bpmn-io/properties-panel': '>= 3.7'
+ bpmn-js: '>= 11.5'
+ camunda-bpmn-js-behaviors: '>= 0.4'
+ diagram-js: '>= 11.9'
+
+ bpmn-js-token-simulation@0.36.2:
+ resolution: {integrity: sha512-sN7US4gIA5tGs74gYLnZ2Eay+gPqkKPjEttp/VRTeydSg0RGPuGiGwTo1TaLf8cV8FXFCDD2actkQWn/aeg79Q==}
+ engines: {node: '>= 16'}
+
+ bpmn-js@17.11.1:
+ resolution: {integrity: sha512-ywCeTg5kvN8lYkU+fHE+YXTGlfKc55lRBn7zW3k1//toeMNPy/PS/uQiujRWdFhMrH5dbtDvlwWukNw2pjWw8Q==}
+
+ bpmn-moddle@8.1.0:
+ resolution: {integrity: sha512-yI5OAFfYVJwViKTsTsonVfCBPtB3MlefADUORwNIxxBOMp21vnoxuxsdgUWlPH/dvAEZh/+mr8UtqOBNu8NC5Q==}
+
+ brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+
+ brace-expansion@2.0.1:
+ resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
+
+ braces@3.0.3:
+ resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+ engines: {node: '>=8'}
+
+ browserslist-to-esbuild@2.1.1:
+ resolution: {integrity: sha512-KN+mty6C3e9AN8Z5dI1xeN15ExcRNeISoC3g7V0Kax/MMF9MSoYA2G7lkTTcVUFntiEjkpI0HNgqJC1NjdyNUw==}
+ engines: {node: '>=18'}
+ hasBin: true
+ peerDependencies:
+ browserslist: '*'
+
+ browserslist@4.24.2:
+ resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==}
+ engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+ hasBin: true
+
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+ cac@6.7.14:
+ resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
+ engines: {node: '>=8'}
+
+ call-bind@1.0.7:
+ resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
+ engines: {node: '>= 0.4'}
+
+ callsites@3.1.0:
+ resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+ engines: {node: '>=6'}
+
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
+ camunda-bpmn-js-behaviors@1.7.2:
+ resolution: {integrity: sha512-xjLJHc18T40tcYu4JCeYDo1wR5i9+ZqcVnXVP6c4ooAe2gKISbBvFc07gqGpqiwm7TpEBvUfDj3PrRr+ofaf4w==}
+ peerDependencies:
+ bpmn-js: '>= 9'
+ camunda-bpmn-moddle: '>= 7'
+ zeebe-bpmn-moddle: '>= 0.18'
+
+ camunda-bpmn-moddle@7.0.1:
+ resolution: {integrity: sha512-Br8Diu6roMpziHdpl66Dhnm0DTnCFMrSD9zwLV08LpD52QA0UsXxU87XfHf08HjuB7ly0Hd1bvajZRpf9hbmYQ==}
+
+ caniuse-lite@1.0.30001684:
+ resolution: {integrity: sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==}
+
+ chalk@1.1.3:
+ resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
+ engines: {node: '>=0.10.0'}
+
+ chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+
+ chalk@5.3.0:
+ resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+
+ cheerio-select@2.1.0:
+ resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
+
+ cheerio@1.0.0-rc.12:
+ resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
+ engines: {node: '>= 6'}
+
+ chokidar@3.6.0:
+ resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
+ engines: {node: '>= 8.10.0'}
+
+ chokidar@4.0.1:
+ resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==}
+ engines: {node: '>= 14.16.0'}
+
+ classnames@2.5.1:
+ resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
+
+ cli-cursor@5.0.0:
+ resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
+ engines: {node: '>=18'}
+
+ cli-truncate@4.0.0:
+ resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
+ engines: {node: '>=18'}
+
+ cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+
+ cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ codemirror@6.65.7:
+ resolution: {integrity: sha512-HcfnUFJwI2FvH73YWVbbMh7ObWxZiHIycEhv9ZEXy6e8ZKDjtZKbbYFUtsLN46HFXPvU5V2Uvc2d55Z//oFW5A==}
+ deprecated: This is an accidentally mis-tagged instance of 5.65.7
+
+ color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+
+ color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+ colord@2.9.3:
+ resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
+
+ colorette@2.0.20:
+ resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
+
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ commander@12.1.0:
+ resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
+ engines: {node: '>=18'}
+
+ commander@2.20.3:
+ resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
+ commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
+ common-tags@1.8.2:
+ resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==}
+ engines: {node: '>=4.0.0'}
+
+ compare-func@2.0.0:
+ resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==}
+
+ component-event@0.2.1:
+ resolution: {integrity: sha512-wGA++isMqiDq1jPYeyv2as/Bt/u+3iLW0rEa+8NQ82jAv3TgqMiCM+B2SaBdn2DfLilLjjq736YcezihRYhfxw==}
+
+ compute-scroll-into-view@3.1.1:
+ resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
+
+ computeds@0.0.1:
+ resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==}
+
+ concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+ confbox@0.1.8:
+ resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
+
+ consola@3.2.3:
+ resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==}
+ engines: {node: ^14.18.0 || >=16.10.0}
+
+ conventional-changelog-angular@7.0.0:
+ resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==}
+ engines: {node: '>=16'}
+
+ conventional-changelog-conventionalcommits@7.0.2:
+ resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==}
+ engines: {node: '>=16'}
+
+ conventional-commits-parser@5.0.0:
+ resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ convert-source-map@2.0.0:
+ resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
+
+ core-js-compat@3.39.0:
+ resolution: {integrity: sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==}
+
+ core-js-pure@3.39.0:
+ resolution: {integrity: sha512-7fEcWwKI4rJinnK+wLTezeg2smbFFdSBP6E2kQZNbnzM2s1rpKQ6aaRteZSSg7FLU3P0HGGVo/gbpfanU36urg==}
+
+ core-js@3.39.0:
+ resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
+
+ cosmiconfig-typescript-loader@5.1.0:
+ resolution: {integrity: sha512-7PtBB+6FdsOvZyJtlF3hEPpACq7RQX6BVGsgC7/lfVXnKMvNCu/XY3ykreqG5w/rBNdu2z8LCIKoF3kpHHdHlA==}
+ engines: {node: '>=v16'}
+ peerDependencies:
+ '@types/node': '*'
+ cosmiconfig: '>=8.2'
+ typescript: '>=4'
+
+ cosmiconfig@9.0.0:
+ resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ typescript: '>=4.9.5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ crelt@1.0.6:
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
+ cropperjs@1.6.2:
+ resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
+
+ cross-fetch@3.1.8:
+ resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
+
+ cross-spawn@7.0.6:
+ resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+ engines: {node: '>= 8'}
+
+ crypto-js@4.2.0:
+ resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
+
+ css-functions-list@3.2.3:
+ resolution: {integrity: sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==}
+ engines: {node: '>=12 || >=16'}
+
+ css-select@5.1.0:
+ resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
+
+ css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ css-tree@2.3.1:
+ resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css-tree@3.0.1:
+ resolution: {integrity: sha512-8Fxxv+tGhORlshCdCwnNJytvlvq46sOLSYEx2ZIGurahWvMucSRnyjPA3AmrMq4VPRYbHVpWj5VkiVasrM2H4Q==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css-what@6.1.0:
+ resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
+ engines: {node: '>= 6'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ csstype@3.1.3:
+ resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-axis@3.0.0:
+ resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
+ engines: {node: '>=12'}
+
+ d3-brush@3.0.0:
+ resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
+ engines: {node: '>=12'}
+
+ d3-chord@3.0.1:
+ resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-contour@4.0.2:
+ resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==}
+ engines: {node: '>=12'}
+
+ d3-delaunay@6.0.4:
+ resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
+ engines: {node: '>=12'}
+
+ d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+
+ d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+
+ d3-dsv@3.0.1:
+ resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-fetch@3.0.1:
+ resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
+ engines: {node: '>=12'}
+
+ d3-flextree@2.1.2:
+ resolution: {integrity: sha512-gJiHrx5uTTHq44bjyIb3xpbmmdZcWLYPKeO9EPVOq8EylMFOiH2+9sWqKAiQ4DcFuOZTAxPOQyv0Rnmji/g15A==}
+
+ d3-force@3.0.0:
+ resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.0:
+ resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+ engines: {node: '>=12'}
+
+ d3-geo@3.1.1:
+ resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==}
+ engines: {node: '>=12'}
+
+ d3-hierarchy@1.1.9:
+ resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==}
+
+ d3-hierarchy@3.1.2:
+ resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-polygon@3.0.1:
+ resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==}
+ engines: {node: '>=12'}
+
+ d3-quadtree@3.0.1:
+ resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==}
+ engines: {node: '>=12'}
+
+ d3-random@3.0.1:
+ resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==}
+ engines: {node: '>=12'}
+
+ d3-scale-chromatic@3.1.0:
+ resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
+ d3-transition@3.0.1:
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+
+ d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+
+ d3@7.9.0:
+ resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
+ engines: {node: '>=12'}
+
+ d@1.0.2:
+ resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
+ engines: {node: '>=0.12'}
+
+ dargs@8.1.0:
+ resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
+ engines: {node: '>=12'}
+
+ dayjs@1.11.13:
+ resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
+
+ de-indent@1.0.2:
+ resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
+
+ debug@4.3.7:
+ resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
+ deep-is@0.1.4:
+ resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+ default-passive-events@2.0.0:
+ resolution: {integrity: sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==}
+
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
+ defu@6.1.4:
+ resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
+
+ delaunator@5.0.1:
+ resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==}
+
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
+ destr@2.0.3:
+ resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==}
+
+ detect-libc@1.0.3:
+ resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+
+ diagram-js-direct-editing@3.2.0:
+ resolution: {integrity: sha512-+pyxeQGBSdLiZX0/tmmsm2qZSvm9YtVzod5W3RMHSTR7VrkUMD6E7EX/W9JQv3ebxO7oIdqFmytmNDDpSHnYEw==}
+ peerDependencies:
+ diagram-js: '*'
+
+ diagram-js@12.8.1:
+ resolution: {integrity: sha512-LF9BiwjbOPpZd0ez5VSlYRbdbEA59YQX43bWvNDp1rLMv0xwZ5yIg4oaYDK82nIQ0kH1tjvoQRpNevMTCgQVyw==}
+
+ diagram-js@14.11.3:
+ resolution: {integrity: sha512-Seq9BHAXfzKS60L4v4Gvgvv72wOtvrfJQAyyPm9pntSZDMzjoodPSXnEUPud1G2zVCMGEUUW++s0reEdaWgkXA==}
+
+ didi@10.2.2:
+ resolution: {integrity: sha512-l8NYkYFXV1izHI65EyT8EXOjUZtKmQkHLTT89cSP7HU5J/G7AOj0dXKtLc04EXYlga99PBY18IPjOeZ+c3DI4w==}
+ engines: {node: '>= 16'}
+
+ didi@9.0.2:
+ resolution: {integrity: sha512-q2+aj+lnJcUweV7A9pdUrwFr4LHVmRPwTmQLtHPFz4aT7IBoryN6Iy+jmFku+oIzr5ebBkvtBCOb87+dJhb7bg==}
+
+ dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+
+ dir-glob@3.0.1:
+ resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
+ engines: {node: '>=8'}
+
+ dlv@1.1.3:
+ resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
+
+ doctrine@3.0.0:
+ resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
+ engines: {node: '>=6.0.0'}
+
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ dom-walk@0.1.2:
+ resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
+
+ dom7@4.0.6:
+ resolution: {integrity: sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domify@1.4.2:
+ resolution: {integrity: sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==}
+
+ domify@2.0.0:
+ resolution: {integrity: sha512-rmvrrmWQPD/X1A/nPBfIVg4r05792QdG9Z4Prk6oQG0F9zBMDkr0GKAdds1wjb2dq1rTz/ywc4ZxpZbgz0tttg==}
+ engines: {node: '>=18'}
+
+ dompurify@3.2.1:
+ resolution: {integrity: sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==}
+
+ domutils@3.1.0:
+ resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+
+ dot-prop@5.3.0:
+ resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==}
+ engines: {node: '>=8'}
+
+ driver.js@1.3.1:
+ resolution: {integrity: sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==}
+
+ duplexer@0.1.2:
+ resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
+
+ eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+
+ echarts-wordcloud@2.1.0:
+ resolution: {integrity: sha512-Kt1JmbcROgb+3IMI48KZECK2AP5lG6bSsOEs+AsuwaWJxQom31RTNd6NFYI01E/YaI1PFZeueaupjlmzSQasjQ==}
+ peerDependencies:
+ echarts: ^5.0.1
+
+ echarts@5.5.1:
+ resolution: {integrity: sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==}
+
+ ejs@3.1.10:
+ resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==}
+ engines: {node: '>=0.10.0'}
+ hasBin: true
+
+ electron-to-chromium@1.5.67:
+ resolution: {integrity: sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==}
+
+ element-plus@2.11.1:
+ resolution: {integrity: sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ emoji-regex@10.4.0:
+ resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
+
+ emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+
+ emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ environment@1.1.0:
+ resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
+ engines: {node: '>=18'}
+
+ error-ex@1.3.2:
+ resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
+
+ es-define-property@1.0.0:
+ resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
+ es-module-lexer@1.5.4:
+ resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==}
+
+ es5-ext@0.10.64:
+ resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
+ engines: {node: '>=0.10'}
+
+ es6-iterator@2.0.3:
+ resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
+
+ es6-symbol@3.1.4:
+ resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
+ engines: {node: '>=0.12'}
+
+ esbuild@0.19.12:
+ resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==}
+ engines: {node: '>=12'}
+ hasBin: true
+
+ escalade@3.2.0:
+ resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+ engines: {node: '>=6'}
+
+ escape-html@1.0.3:
+ resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
+
+ escape-string-regexp@1.0.5:
+ resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+ engines: {node: '>=0.8.0'}
+
+ escape-string-regexp@4.0.0:
+ resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+ engines: {node: '>=10'}
+
+ escape-string-regexp@5.0.0:
+ resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
+ engines: {node: '>=12'}
+
+ escodegen@2.1.0:
+ resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==}
+ engines: {node: '>=6.0'}
+ hasBin: true
+
+ eslint-config-prettier@9.1.0:
+ resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
+ hasBin: true
+ peerDependencies:
+ eslint: '>=7.0.0'
+
+ eslint-define-config@2.1.0:
+ resolution: {integrity: sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==}
+ engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>=8.6.0'}
+
+ eslint-plugin-prettier@5.2.1:
+ resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+ peerDependencies:
+ '@types/eslint': '>=8.0.0'
+ eslint: '>=8.0.0'
+ eslint-config-prettier: '*'
+ prettier: '>=3.0.0'
+ peerDependenciesMeta:
+ '@types/eslint':
+ optional: true
+ eslint-config-prettier:
+ optional: true
+
+ eslint-plugin-vue@9.31.0:
+ resolution: {integrity: sha512-aYMUCgivhz1o4tLkRHj5oq9YgYPM4/EJc0M7TAKRLCUA5OYxRLAhYEVD2nLtTwLyixEFI+/QXSvKU9ESZFgqjQ==}
+ engines: {node: ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+
+ eslint-scope@7.2.2:
+ resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@3.4.3:
+ resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ eslint-visitor-keys@4.2.0:
+ resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+ eslint@8.57.1:
+ resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
+ hasBin: true
+
+ esniff@2.0.1:
+ resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
+ engines: {node: '>=0.10'}
+
+ espree@9.6.1:
+ resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ esquery@1.6.0:
+ resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+ engines: {node: '>=0.10'}
+
+ esrecurse@4.3.0:
+ resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+ engines: {node: '>=4.0'}
+
+ estraverse@5.3.0:
+ resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+ engines: {node: '>=4.0'}
+
+ estree-walker@2.0.2:
+ resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
+
+ estree-walker@3.0.3:
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
+
+ esutils@2.0.3:
+ resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+ engines: {node: '>=0.10.0'}
+
+ event-emitter@0.3.5:
+ resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
+
+ eventemitter3@5.0.1:
+ resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
+
+ execa@8.0.1:
+ resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
+ engines: {node: '>=16.17'}
+
+ ext@1.7.0:
+ resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
+
+ fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+ fast-diff@1.3.0:
+ resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
+
+ fast-glob@3.3.2:
+ resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
+ engines: {node: '>=8.6.0'}
+
+ fast-glob@3.3.3:
+ resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+ engines: {node: '>=8.6.0'}
+
+ fast-json-stable-stringify@2.1.0:
+ resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+ fast-levenshtein@2.0.6:
+ resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+ fast-uri@3.0.3:
+ resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==}
+
+ fast-xml-parser@4.5.0:
+ resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==}
+ hasBin: true
+
+ fastest-levenshtein@1.0.16:
+ resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==}
+ engines: {node: '>= 4.9.1'}
+
+ fastq@1.17.1:
+ resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+
+ fdir@6.4.2:
+ resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ feelers@1.4.0:
+ resolution: {integrity: sha512-CGa/7ILuqoqTaeYeoKsg/4tzu2es9sEEJTmSjdu0lousZBw4V9gcYhHYFNmbrSrKmbAVfOzj6/DsymGJWFIOeg==}
+
+ feelin@3.2.0:
+ resolution: {integrity: sha512-GFDbHsTYk7YXO1tyw1dOjb7IODeAZvNIosdGZThUwPx5XcD/XhO0hnPZXsIbAzSsIdrgGlTEEdby9fZ2gixysA==}
+
+ file-entry-cache@6.0.1:
+ resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+
+ file-entry-cache@9.1.0:
+ resolution: {integrity: sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==}
+ engines: {node: '>=18'}
+
+ filelist@1.0.4:
+ resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==}
+
+ fill-range@7.1.1:
+ resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+ engines: {node: '>=8'}
+
+ find-up@4.1.0:
+ resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
+ engines: {node: '>=8'}
+
+ find-up@5.0.0:
+ resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+ engines: {node: '>=10'}
+
+ find-up@7.0.0:
+ resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==}
+ engines: {node: '>=18'}
+
+ flat-cache@3.2.0:
+ resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
+ engines: {node: ^10.12.0 || >=12.0.0}
+
+ flat-cache@5.0.0:
+ resolution: {integrity: sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==}
+ engines: {node: '>=18'}
+
+ flatted@3.3.2:
+ resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
+
+ focus-trap@7.6.2:
+ resolution: {integrity: sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w==}
+
+ follow-redirects@1.15.9:
+ resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+ engines: {node: '>=4.0'}
+ peerDependencies:
+ debug: '*'
+ peerDependenciesMeta:
+ debug:
+ optional: true
+
+ foreground-child@3.3.0:
+ resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
+ engines: {node: '>=14'}
+
+ form-data@4.0.1:
+ resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
+ engines: {node: '>= 6'}
+
+ fraction.js@4.3.7:
+ resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+
+ fs-extra@10.1.0:
+ resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
+ engines: {node: '>=12'}
+
+ fs-extra@11.3.0:
+ resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==}
+ engines: {node: '>=14.14'}
+
+ fs.realpath@1.0.0:
+ resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ function-bind@1.1.2:
+ resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+ gensync@1.0.0-beta.2:
+ resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
+ engines: {node: '>=6.9.0'}
+
+ get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+
+ get-east-asian-width@1.3.0:
+ resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
+ engines: {node: '>=18'}
+
+ get-intrinsic@1.2.4:
+ resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
+ engines: {node: '>= 0.4'}
+
+ get-stream@8.0.1:
+ resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
+ engines: {node: '>=16'}
+
+ git-raw-commits@4.0.0:
+ resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ glob-parent@5.1.2:
+ resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+ engines: {node: '>= 6'}
+
+ glob-parent@6.0.2:
+ resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+ engines: {node: '>=10.13.0'}
+
+ glob@10.4.5:
+ resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+ hasBin: true
+
+ glob@7.2.3:
+ resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
+ deprecated: Glob versions prior to v9 are no longer supported
+
+ global-directory@4.0.1:
+ resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
+ engines: {node: '>=18'}
+
+ global-modules@2.0.0:
+ resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==}
+ engines: {node: '>=6'}
+
+ global-prefix@3.0.0:
+ resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==}
+ engines: {node: '>=6'}
+
+ global@4.4.0:
+ resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==}
+
+ globals@11.12.0:
+ resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
+ engines: {node: '>=4'}
+
+ globals@13.24.0:
+ resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
+ engines: {node: '>=8'}
+
+ globby@11.1.0:
+ resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
+ engines: {node: '>=10'}
+
+ globjoin@0.1.4:
+ resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
+
+ gopd@1.0.1:
+ resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
+
+ graceful-fs@4.2.11:
+ resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+ graphemer@1.4.0:
+ resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+ gzip-size@6.0.0:
+ resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==}
+ engines: {node: '>=10'}
+
+ hammerjs@2.0.8:
+ resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==}
+ engines: {node: '>=0.8.0'}
+
+ has-ansi@2.0.0:
+ resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
+ engines: {node: '>=0.10.0'}
+
+ has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+ has-proto@1.0.3:
+ resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
+ engines: {node: '>= 0.4'}
+
+ has-symbols@1.0.3:
+ resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
+ engines: {node: '>= 0.4'}
+
+ hasown@2.0.2:
+ resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+ engines: {node: '>= 0.4'}
+
+ he@1.2.0:
+ resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
+ hasBin: true
+
+ highlight.js@11.10.0:
+ resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==}
+ engines: {node: '>=12.0.0'}
+
+ htm@3.1.1:
+ resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
+
+ html-tags@3.3.1:
+ resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
+ engines: {node: '>=8'}
+
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
+ htmlparser2@8.0.2:
+ resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+
+ human-signals@5.0.0:
+ resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==}
+ engines: {node: '>=16.17.0'}
+
+ i18next@23.16.8:
+ resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==}
+
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
+ ids@1.0.5:
+ resolution: {integrity: sha512-XQ0yom/4KWTL29sLG+tyuycy7UmeaM/79GRtSJq6IG9cJGIPeBz5kwDCguie3TwxaMNIc3WtPi0cTa1XYHicpw==}
+
+ ignore@5.3.2:
+ resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+ engines: {node: '>= 4'}
+
+ ignore@6.0.2:
+ resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==}
+ engines: {node: '>= 4'}
+
+ immer@9.0.21:
+ resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
+
+ immutable@5.0.3:
+ resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==}
+
+ import-fresh@3.3.0:
+ resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
+ engines: {node: '>=6'}
+
+ import-meta-resolve@4.1.0:
+ resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==}
+
+ imurmurhash@0.1.4:
+ resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+ engines: {node: '>=0.8.19'}
+
+ indent-string@4.0.0:
+ resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==}
+ engines: {node: '>=8'}
+
+ individual@2.0.0:
+ resolution: {integrity: sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g==}
+
+ inflight@1.0.6:
+ resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
+ deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+ inherits-browser@0.1.0:
+ resolution: {integrity: sha512-CJHHvW3jQ6q7lzsXPpapLdMx5hDpSF3FSh45pwsj6bKxJJ8Nl8v43i5yXnr3BdfOimGHKyniewQtnAIp3vyJJw==}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+
+ ini@4.1.1:
+ resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
+ is-arrayish@0.2.1:
+ resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
+
+ is-binary-path@2.1.0:
+ resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
+ engines: {node: '>=8'}
+
+ is-core-module@2.15.1:
+ resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
+ engines: {node: '>= 0.4'}
+
+ is-extglob@2.1.1:
+ resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+ engines: {node: '>=0.10.0'}
+
+ is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+
+ is-fullwidth-code-point@4.0.0:
+ resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
+ engines: {node: '>=12'}
+
+ is-fullwidth-code-point@5.0.0:
+ resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
+ engines: {node: '>=18'}
+
+ is-function@1.0.2:
+ resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==}
+
+ is-glob@4.0.3:
+ resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+ engines: {node: '>=0.10.0'}
+
+ is-hotkey@0.2.0:
+ resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
+
+ is-number@7.0.0:
+ resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+ engines: {node: '>=0.12.0'}
+
+ is-obj@2.0.0:
+ resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==}
+ engines: {node: '>=8'}
+
+ is-path-inside@3.0.3:
+ resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
+ engines: {node: '>=8'}
+
+ is-plain-object@5.0.0:
+ resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
+ engines: {node: '>=0.10.0'}
+
+ is-stream@3.0.0:
+ resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ is-text-path@2.0.0:
+ resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==}
+ engines: {node: '>=8'}
+
+ is-url@1.2.4:
+ resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
+
+ isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+ jackspeak@3.4.3:
+ resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
+
+ jake@10.9.2:
+ resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ javascript-natural-sort@0.7.1:
+ resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
+
+ jiti@1.21.6:
+ resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
+ hasBin: true
+
+ jiti@2.4.2:
+ resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
+ hasBin: true
+
+ jmespath@0.16.0:
+ resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==}
+ engines: {node: '>= 0.6.0'}
+
+ js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+ js-tokens@9.0.1:
+ resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+
+ js-yaml@4.1.0:
+ resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+ hasBin: true
+
+ jsencrypt@3.3.2:
+ resolution: {integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==}
+
+ jsesc@3.0.2:
+ resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ json-buffer@3.0.1:
+ resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+ json-parse-even-better-errors@2.3.1:
+ resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
+
+ json-schema-traverse@0.4.1:
+ resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+ json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+
+ json-source-map@0.6.1:
+ resolution: {integrity: sha512-1QoztHPsMQqhDq0hlXY5ZqcEdUzxQEIxgFkKl4WUp2pgShObl+9ovi4kRh2TfvAfxAoHOJ9vIMEqk3k4iex7tg==}
+
+ json-stable-stringify-without-jsonify@1.0.1:
+ resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+ json5@2.2.3:
+ resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
+ engines: {node: '>=6'}
+ hasBin: true
+
+ jsonc-eslint-parser@2.4.0:
+ resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+ jsoneditor@10.4.1:
+ resolution: {integrity: sha512-89ao8IOKq6yTY+LSNw7FHoqcNrkATZN9W1u476P9ofGLSN/V0l2Je0MWG8HrYKMYqriJEpXmlsGT1CZbr99GWg==}
+
+ jsonfile@6.1.0:
+ resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
+
+ jsonparse@1.3.1:
+ resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
+ engines: {'0': node >= 0.2.0}
+
+ jsonrepair@3.13.0:
+ resolution: {integrity: sha512-5YRzlAQ7tuzV1nAJu3LvDlrKtBFIALHN2+a+I1MGJCt3ldRDBF/bZuvIPzae8Epot6KBXd0awRZZcuoeAsZ/mw==}
+ hasBin: true
+
+ katex@0.16.11:
+ resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
+ hasBin: true
+
+ keycode@2.2.1:
+ resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==}
+
+ keyv@4.5.4:
+ resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+ kind-of@6.0.3:
+ resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
+ engines: {node: '>=0.10.0'}
+
+ known-css-properties@0.35.0:
+ resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==}
+
+ kolorist@1.8.0:
+ resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
+
+ lang-feel@2.2.0:
+ resolution: {integrity: sha512-Ebo5nftYsMfJzB3Ny8Oy4oaDXZXb5x61qtVVmKv6aImvAZUbT76mD60ZbEilizjZQzsR2CcU1iMK5sacIa1NVA==}
+
+ levn@0.4.1:
+ resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+ engines: {node: '>= 0.8.0'}
+
+ lezer-feel@1.4.0:
+ resolution: {integrity: sha512-kNxG7O38gwpuYy+C3JCRxQNTCE2qu9uTuH5dE3EGVnRhIQMe6rPDz0S8t3urLEOsMud6HI795m6zX2ujfUaqTw==}
+
+ lilconfig@3.1.2:
+ resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==}
+ engines: {node: '>=14'}
+
+ lines-and-columns@1.2.4:
+ resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+
+ linkify-it@5.0.0:
+ resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
+ lint-staged@15.2.10:
+ resolution: {integrity: sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==}
+ engines: {node: '>=18.12.0'}
+ hasBin: true
+
+ listr2@8.2.5:
+ resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==}
+ engines: {node: '>=18.0.0'}
+
+ local-pkg@0.4.3:
+ resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==}
+ engines: {node: '>=14'}
+
+ local-pkg@0.5.1:
+ resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
+ engines: {node: '>=14'}
+
+ locate-path@5.0.0:
+ resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
+ engines: {node: '>=8'}
+
+ locate-path@6.0.0:
+ resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+ engines: {node: '>=10'}
+
+ locate-path@7.2.0:
+ resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ lodash-es@4.17.21:
+ resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
+
+ lodash-unified@1.0.3:
+ resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==}
+ peerDependencies:
+ '@types/lodash-es': '*'
+ lodash: '*'
+ lodash-es: '*'
+
+ lodash.camelcase@4.3.0:
+ resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
+
+ lodash.clonedeep@4.5.0:
+ resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
+
+ lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+
+ lodash.foreach@4.5.0:
+ resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
+
+ lodash.isplainobject@4.0.6:
+ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
+
+ lodash.kebabcase@4.1.1:
+ resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==}
+
+ lodash.merge@4.6.2:
+ resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+ lodash.mergewith@4.6.2:
+ resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==}
+
+ lodash.snakecase@4.1.1:
+ resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
+
+ lodash.startcase@4.4.0:
+ resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
+
+ lodash.throttle@4.1.1:
+ resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
+
+ lodash.toarray@4.4.0:
+ resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
+
+ lodash.truncate@4.4.2:
+ resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
+
+ lodash.uniq@4.5.0:
+ resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==}
+
+ lodash.upperfirst@4.3.1:
+ resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==}
+
+ lodash@4.17.21:
+ resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+ log-update@6.1.0:
+ resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
+ engines: {node: '>=18'}
+
+ loglevel-colored-level-prefix@1.0.0:
+ resolution: {integrity: sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA==}
+
+ loglevel@1.9.2:
+ resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==}
+ engines: {node: '>= 0.6.0'}
+
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
+ lru-cache@5.1.1:
+ resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+ luxon@3.5.0:
+ resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
+ engines: {node: '>=12'}
+
+ m3u8-parser@4.8.0:
+ resolution: {integrity: sha512-UqA2a/Pw3liR6Df3gwxrqghCP17OpPlQj6RBPLYygf/ZSQ4MoSgvdvhvt35qV+3NaaA0FSZx93Ix+2brT1U7cA==}
+
+ magic-string@0.30.14:
+ resolution: {integrity: sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==}
+
+ magic-string@0.30.17:
+ resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+
+ markdown-it@14.1.0:
+ resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+ hasBin: true
+
+ markmap-common@0.16.0:
+ resolution: {integrity: sha512-q3nlNDMKuWXTm3VwZFY9V5zteL/+iBLZanUK5vS+e26bUbzTSG5VtAzsyJbmgJm1WhwmIIAxbXEnp6JdvtTduA==}
+
+ markmap-html-parser@0.16.1:
+ resolution: {integrity: sha512-/Mgm4g1qMQ8uEOz8h8K+jPspdgjfw29NqmfTLZSt8yG+vW7fWWduPjGRFc5axAZxCzP7PTzZLEuOxAqOwEg8Bg==}
+ peerDependencies:
+ markmap-common: '*'
+
+ markmap-lib@0.16.1:
+ resolution: {integrity: sha512-jD8VsB67m677IRehGSwwVJDlC6PS+xzDKsJOwdvjZ+ndfXrHa1lyqfvR6mIwvGGUIciF86YEITSKL9hQTHE4Rw==}
+ peerDependencies:
+ markmap-common: '*'
+
+ markmap-toolbar@0.17.2:
+ resolution: {integrity: sha512-WQ05P2xvQmZT0ybRUE0uRzrs30aXlJ6/yEUsA6A9nYEwm8T9jSwBxIM/5zYlkH/XzUcsRRxtCa4k1IWR74gkpQ==}
+ peerDependencies:
+ markmap-common: '*'
+
+ markmap-view@0.16.0:
+ resolution: {integrity: sha512-JOiSEThs8B4bAP9E6rcCWOz2SsMwCBFaR76wLARRVb04C/qLiLmvrm675kNPq4lRBAwtugHCYvjG0otpSlB4Cw==}
+ peerDependencies:
+ markmap-common: '*'
+
+ mathml-tag-names@2.1.3:
+ resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
+
+ mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+
+ mdn-data@2.0.30:
+ resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
+
+ mdn-data@2.12.1:
+ resolution: {integrity: sha512-rsfnCbOHjqrhWxwt5/wtSLzpoKTzW7OXdT5lLOIH1OTYhWu9rRJveGq0sKvDZODABH7RX+uoR+DYcpFnq4Tf6Q==}
+
+ mdurl@2.0.0:
+ resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
+ memoize-one@6.0.0:
+ resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
+ meow@12.1.1:
+ resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
+ engines: {node: '>=16.10'}
+
+ meow@13.2.0:
+ resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
+ engines: {node: '>=18'}
+
+ merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+
+ merge2@1.4.1:
+ resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+ engines: {node: '>= 8'}
+
+ micromatch@4.0.8:
+ resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+ engines: {node: '>=8.6'}
+
+ mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+
+ mime-match@1.0.2:
+ resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
+
+ mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+
+ mimic-fn@4.0.0:
+ resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
+ engines: {node: '>=12'}
+
+ mimic-function@5.0.1:
+ resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
+ engines: {node: '>=18'}
+
+ min-dash@4.2.2:
+ resolution: {integrity: sha512-qbhSYUxk6mBaF096B3JOQSumXbKWHenmT97cSpdNzgkWwGjhjhE/KZODCoDNhI2I4C9Cb6R/Q13S4BYkUSXoXQ==}
+
+ min-document@2.19.0:
+ resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
+
+ min-dom@4.2.1:
+ resolution: {integrity: sha512-TMoL8SEEIhUWYgkj7XMSgxmwSyGI+4fP2KFFGnN3FbHfbGHVdsLYSz8LoIsgPhz4dWRmLvxWWSMgzZMJW5sZuA==}
+
+ min-dom@5.1.1:
+ resolution: {integrity: sha512-GaKUlguMAofd3OJsB0OkP17i5kucKqErgVCJxPawO9l5NwIPnr28SAr99zzlzMCWWljISBYrnZVWdE2Q92YGFQ==}
+
+ minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+ minimatch@5.1.6:
+ resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
+ engines: {node: '>=10'}
+
+ minimatch@9.0.3:
+ resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minimatch@9.0.5:
+ resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+ minipass@7.1.2:
+ resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+ engines: {node: '>=16 || 14 >=14.17'}
+
+ mitt@3.0.1:
+ resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+
+ mlly@1.7.3:
+ resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==}
+
+ moddle-xml@10.1.0:
+ resolution: {integrity: sha512-erWckwLt+dYskewKXJso9u+aAZ5172lOiYxSOqKCPTy7L/xmqH1PoeoA7eVC7oJTt3PqF5TkZzUmbjGH6soQBg==}
+
+ moddle@6.2.3:
+ resolution: {integrity: sha512-bLVN+ZHL3aKnhxc19XtjUfvdJsS3EsiEJC7bT6YPD11qYmTzvsxrGgyYz1Ouof7TZuGw0lDJ1OLmEnxcpQWk3Q==}
+
+ mpd-parser@0.22.1:
+ resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==}
+ hasBin: true
+
+ mrmime@2.0.0:
+ resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ muggle-string@0.3.1:
+ resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==}
+
+ mux.js@6.0.1:
+ resolution: {integrity: sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==}
+ engines: {node: '>=8', npm: '>=5'}
+ hasBin: true
+
+ namespace-emitter@2.0.1:
+ resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
+
+ nanoid@3.3.8:
+ resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ nanoid@5.1.6:
+ resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
+ engines: {node: ^18 || >=20}
+ hasBin: true
+
+ natural-compare@1.4.0:
+ resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+ next-tick@1.1.0:
+ resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
+
+ node-addon-api@7.1.1:
+ resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
+
+ node-fetch-native@1.6.4:
+ resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
+
+ node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
+ node-html-parser@7.0.1:
+ resolution: {integrity: sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==}
+
+ node-releases@2.0.18:
+ resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ normalize-range@0.1.2:
+ resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
+ engines: {node: '>=0.10.0'}
+
+ normalize-wheel-es@1.2.0:
+ resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
+
+ npm-run-path@5.3.0:
+ resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ npm2url@0.2.4:
+ resolution: {integrity: sha512-arzGp/hQz0Ey+ZGhF64XVH7Xqwd+1Q/po5uGiBbzph8ebX6T0uvt3N7c1nBHQNsQVykQgHhqoRTX7JFcHecGuw==}
+
+ nprogress@0.2.0:
+ resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ object-inspect@1.13.3:
+ resolution: {integrity: sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==}
+ engines: {node: '>= 0.4'}
+
+ object-refs@0.3.0:
+ resolution: {integrity: sha512-eP0ywuoWOaDoiake/6kTJlPJhs+k0qNm4nYRzXLNHj6vh+5M3i9R1epJTdxIPGlhWc4fNRQ7a6XJNCX+/L4FOQ==}
+
+ object-refs@0.4.0:
+ resolution: {integrity: sha512-6kJqKWryKZmtte6QYvouas0/EIJKPI1/MMIuRsiBlNuhIMfqYTggzX2F1AJ2+cDs288xyi9GL7FyasHINR98BQ==}
+
+ ofetch@1.4.1:
+ resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==}
+
+ once@1.4.0:
+ resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
+
+ onetime@6.0.0:
+ resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
+ engines: {node: '>=12'}
+
+ onetime@7.0.0:
+ resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
+ engines: {node: '>=18'}
+
+ optionator@0.9.4:
+ resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+ engines: {node: '>= 0.8.0'}
+
+ p-limit@2.3.0:
+ resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
+ engines: {node: '>=6'}
+
+ p-limit@3.1.0:
+ resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+ engines: {node: '>=10'}
+
+ p-limit@4.0.0:
+ resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ p-locate@4.1.0:
+ resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
+ engines: {node: '>=8'}
+
+ p-locate@5.0.0:
+ resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+ engines: {node: '>=10'}
+
+ p-locate@6.0.0:
+ resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ p-try@2.2.0:
+ resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
+ engines: {node: '>=6'}
+
+ package-json-from-dist@1.0.1:
+ resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
+
+ package-manager-detector@0.2.5:
+ resolution: {integrity: sha512-3dS7y28uua+UDbRCLBqltMBrbI+A5U2mI9YuxHRxIWYmLj3DwntEBmERYzIAQ4DMeuCUOBSak7dBHHoXKpOTYQ==}
+
+ parent-module@1.0.1:
+ resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+ engines: {node: '>=6'}
+
+ parse-json@5.2.0:
+ resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
+ engines: {node: '>=8'}
+
+ parse5-htmlparser2-tree-adapter@7.1.0:
+ resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==}
+
+ parse5@7.2.1:
+ resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==}
+
+ path-browserify@1.0.1:
+ resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
+
+ path-exists@4.0.0:
+ resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+ engines: {node: '>=8'}
+
+ path-exists@5.0.0:
+ resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+ path-intersection@2.2.1:
+ resolution: {integrity: sha512-9u8xvMcSfuOiStv9bPdnRJQhGQXLKurew94n4GPQCdH1nj9QKC9ObbNoIpiRq8skiOBxKkt277PgOoFgAt3/rA==}
+
+ path-intersection@3.1.0:
+ resolution: {integrity: sha512-3xS3lvv/vuwm5aH2BVvNRvnvwR2Drde7jQClKpCXTYXIMMjcw/EnMhzCgeHwqbCpzi760PEfAkU53vSIlrNr9A==}
+ engines: {node: '>= 14.20'}
+
+ path-is-absolute@1.0.1:
+ resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
+ engines: {node: '>=0.10.0'}
+
+ path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+
+ path-key@4.0.0:
+ resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==}
+ engines: {node: '>=12'}
+
+ path-parse@1.0.7:
+ resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+ path-scurry@1.11.1:
+ resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
+ engines: {node: '>=16 || 14 >=14.18'}
+
+ path-type@4.0.0:
+ resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
+ engines: {node: '>=8'}
+
+ pathe@1.1.2:
+ resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
+
+ pathe@2.0.3:
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
+
+ perfect-debounce@1.0.0:
+ resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.1:
+ resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.2:
+ resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
+ engines: {node: '>=12'}
+
+ picomodal@3.0.0:
+ resolution: {integrity: sha512-FoR3TDfuLlqUvcEeK5ifpKSVVns6B4BQvc8SDF6THVMuadya6LLtji0QgUDSStw0ZR2J7I6UGi5V2V23rnPWTw==}
+
+ pidtree@0.6.0:
+ resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
+ engines: {node: '>=0.10'}
+ hasBin: true
+
+ pinia-plugin-persistedstate@3.2.3:
+ resolution: {integrity: sha512-Cm819WBj/s5K5DGw55EwbXDtx+EZzM0YR5AZbq9XE3u0xvXwvX2JnWoFpWIcdzISBHqy9H1UiSIUmXyXqWsQRQ==}
+ peerDependencies:
+ pinia: ^2.0.0
+
+ pinia@2.2.8:
+ resolution: {integrity: sha512-NRTYy2g+kju5tBRe0oNlriZIbMNvma8ZJrpHsp3qudyiMEA8jMmPPKQ2QMHg0Oc4BkUyQYWagACabrwriCK9HQ==}
+ peerDependencies:
+ '@vue/composition-api': ^1.4.0
+ typescript: '>=4.4.4'
+ vue: ^2.6.14 || ^3.5.11
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+ typescript:
+ optional: true
+
+ pkcs7@1.0.4:
+ resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==}
+ hasBin: true
+
+ pkg-types@1.2.1:
+ resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==}
+
+ pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+
+ postcss-html@1.7.0:
+ resolution: {integrity: sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==}
+ engines: {node: ^12 || >=14}
+
+ postcss-resolve-nested-selector@0.1.6:
+ resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==}
+
+ postcss-safe-parser@6.0.0:
+ resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.3.3
+
+ postcss-safe-parser@7.0.1:
+ resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==}
+ engines: {node: '>=18.0'}
+ peerDependencies:
+ postcss: ^8.4.31
+
+ postcss-scss@4.0.9:
+ resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.4.29
+
+ postcss-selector-parser@6.1.2:
+ resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==}
+ engines: {node: '>=4'}
+
+ postcss-selector-parser@7.0.0:
+ resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==}
+ engines: {node: '>=4'}
+
+ postcss-sorting@8.0.2:
+ resolution: {integrity: sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==}
+ peerDependencies:
+ postcss: ^8.4.20
+
+ postcss-value-parser@4.2.0:
+ resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
+
+ postcss@8.4.49:
+ resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ preact@10.25.0:
+ resolution: {integrity: sha512-6bYnzlLxXV3OSpUxLdaxBmE7PMOu0aR3pG6lryK/0jmvcDFPlcXGQAt5DpK3RITWiDrfYZRI0druyaK/S9kYLg==}
+
+ prelude-ls@1.2.1:
+ resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+ engines: {node: '>= 0.8.0'}
+
+ prettier-eslint@16.3.0:
+ resolution: {integrity: sha512-Lh102TIFCr11PJKUMQ2kwNmxGhTsv/KzUg9QYF2Gkw259g/kPgndZDWavk7/ycbRvj2oz4BPZ1gCU8bhfZH/Xg==}
+ engines: {node: '>=16.10.0'}
+ peerDependencies:
+ prettier-plugin-svelte: ^3.0.0
+ svelte-eslint-parser: '*'
+ peerDependenciesMeta:
+ prettier-plugin-svelte:
+ optional: true
+ svelte-eslint-parser:
+ optional: true
+
+ prettier-linter-helpers@1.0.0:
+ resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
+ engines: {node: '>=6.0.0'}
+
+ prettier@3.4.1:
+ resolution: {integrity: sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==}
+ engines: {node: '>=14'}
+ hasBin: true
+
+ pretty-format@29.7.0:
+ resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+ engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+
+ prismjs@1.29.0:
+ resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
+ engines: {node: '>=6'}
+
+ process@0.11.10:
+ resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
+ engines: {node: '>= 0.6.0'}
+
+ progress@2.0.3:
+ resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
+ engines: {node: '>=0.4.0'}
+
+ proxy-from-env@1.1.0:
+ resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+ punycode.js@2.3.1:
+ resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+ engines: {node: '>=6'}
+
+ punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+
+ punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+
+ qrcode@1.5.4:
+ resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
+ qs@6.13.1:
+ resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==}
+ engines: {node: '>=0.6'}
+
+ quansync@0.2.8:
+ resolution: {integrity: sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA==}
+
+ queue-microtask@1.2.3:
+ resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+ randomcolor@0.6.2:
+ resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==}
+
+ rd@2.0.1:
+ resolution: {integrity: sha512-/XdKU4UazUZTXFmI0dpABt8jSXPWcEyaGdk340KdHnsEOdkTctlX23aAK7ChQDn39YGNlAJr1M5uvaKt4QnpNw==}
+
+ react-is@18.3.1:
+ resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
+ readdirp@3.6.0:
+ resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
+ engines: {node: '>=8.10.0'}
+
+ readdirp@4.0.2:
+ resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==}
+ engines: {node: '>= 14.16.0'}
+
+ regenerate-unicode-properties@10.2.0:
+ resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==}
+ engines: {node: '>=4'}
+
+ regenerate@1.4.2:
+ resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
+
+ regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
+ regenerator-transform@0.15.2:
+ resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
+
+ regexpu-core@6.2.0:
+ resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==}
+ engines: {node: '>=4'}
+
+ regjsgen@0.8.0:
+ resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==}
+
+ regjsparser@0.12.0:
+ resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==}
+ hasBin: true
+
+ remarkable-katex@1.2.1:
+ resolution: {integrity: sha512-Y1VquJBZnaVsfsVcKW2hmjT+pDL7mp8l5WAVlvuvViltrdok2m1AIKmJv8SsH+mBY84PoMw67t3kTWw1dIm8+g==}
+
+ remarkable@2.0.1:
+ resolution: {integrity: sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==}
+ engines: {node: '>= 6.0.0'}
+ hasBin: true
+
+ require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+
+ require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+
+ require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+
+ require-relative@0.8.7:
+ resolution: {integrity: sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg==}
+
+ resolve-from@4.0.0:
+ resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+ engines: {node: '>=4'}
+
+ resolve-from@5.0.0:
+ resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
+ engines: {node: '>=8'}
+
+ resolve@1.22.8:
+ resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
+ hasBin: true
+
+ restore-cursor@5.1.0:
+ resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
+ engines: {node: '>=18'}
+
+ reusify@1.0.4:
+ resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
+ engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+ rfdc@1.4.1:
+ resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
+
+ rimraf@3.0.2:
+ resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
+ deprecated: Rimraf versions prior to v4 are no longer supported
+ hasBin: true
+
+ rimraf@5.0.10:
+ resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
+ hasBin: true
+
+ robust-predicates@3.0.2:
+ resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
+
+ rollup-plugin-purge-icons@0.10.0:
+ resolution: {integrity: sha512-GD2ftg4L9G/sagIhtCmBn5vdyzePOisniythubpbywP0Q3ix9rZuDeFvgXTPemOsc22pvH7t22ryYQIl0rwGog==}
+ engines: {node: '>= 12'}
+
+ rollup@2.79.2:
+ resolution: {integrity: sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+
+ rollup@4.27.4:
+ resolution: {integrity: sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+
+ run-parallel@1.2.0:
+ resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+ rust-result@1.0.0:
+ resolution: {integrity: sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==}
+
+ rw@1.3.3:
+ resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+
+ safe-json-parse@4.0.0:
+ resolution: {integrity: sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==}
+
+ safer-buffer@2.1.2:
+ resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+
+ sass@1.81.0:
+ resolution: {integrity: sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ sax@1.4.1:
+ resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
+
+ saxen@8.1.2:
+ resolution: {integrity: sha512-xUOiiFbc3Ow7p8KMxwsGICPx46ZQvy3+qfNVhrkwfz3Vvq45eGt98Ft5IQaA1R/7Tb5B5MKh9fUR9x3c3nDTxw==}
+
+ scroll-into-view-if-needed@3.1.0:
+ resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
+
+ scule@1.3.0:
+ resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
+
+ semver@6.3.1:
+ resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+ hasBin: true
+
+ semver@7.6.3:
+ resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
+ shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+
+ shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+
+ side-channel@1.0.6:
+ resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
+ engines: {node: '>= 0.4'}
+
+ signal-exit@4.1.0:
+ resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
+ engines: {node: '>=14'}
+
+ signature_pad@3.0.0-beta.4:
+ resolution: {integrity: sha512-cOf2NhVuTiuNqe2X/ycEmizvCDXk0DoemhsEpnkcGnA4kS5iJYTCqZ9As7tFBbsch45Q1EdX61833+6sjJ8rrw==}
+
+ sirv@2.0.4:
+ resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==}
+ engines: {node: '>= 10'}
+
+ slash@3.0.0:
+ resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
+ engines: {node: '>=8'}
+
+ slate-history@0.109.0:
+ resolution: {integrity: sha512-DHavPwrTTAEAV66eAocB3iQHEj65N6IVtbRK98ZuqGT0S44T3zXlhzY+5SZ7EPxRcoOYVt1dioRxXYM/+PmCiQ==}
+ peerDependencies:
+ slate: '>=0.65.3'
+
+ slate@0.82.1:
+ resolution: {integrity: sha512-3mdRdq7U3jSEoyFrGvbeb28hgrvrr4NdFCtJX+IjaNvSFozY0VZd/CGHF0zf/JDx7aEov864xd5uj0HQxxEWTQ==}
+
+ slice-ansi@4.0.0:
+ resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
+ engines: {node: '>=10'}
+
+ slice-ansi@5.0.0:
+ resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
+ engines: {node: '>=12'}
+
+ slice-ansi@7.1.0:
+ resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
+ engines: {node: '>=18'}
+
+ snabbdom@3.6.2:
+ resolution: {integrity: sha512-ig5qOnCDbugFntKi6c7Xlib8bA6xiJVk8O+WdFrV3wxbMqeHO0hXFQC4nAhPVWfZfi8255lcZkNhtIBINCc4+Q==}
+ engines: {node: '>=12.17.0'}
+
+ sortablejs@1.14.0:
+ resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
+
+ sortablejs@1.15.6:
+ resolution: {integrity: sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ source-map-support@0.5.21:
+ resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+ source-map@0.6.1:
+ resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+ engines: {node: '>=0.10.0'}
+
+ split2@4.2.0:
+ resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
+ engines: {node: '>= 10.x'}
+
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
+ ssr-window@4.0.2:
+ resolution: {integrity: sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==}
+
+ steady-xml@0.1.0:
+ resolution: {integrity: sha512-5sk17qO2wWRtonTNoBhoKAB35OSsGJOa3+NEa6D+1GS+de+ujDWxnflMkXBrviOfkNrPTUqduAdXhrMJs89nAw==}
+ engines: {node: '>=12.0.0'}
+
+ string-argv@0.3.2:
+ resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
+ engines: {node: '>=0.6.19'}
+
+ string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+
+ string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+
+ string-width@7.2.0:
+ resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
+ engines: {node: '>=18'}
+
+ strip-ansi@3.0.1:
+ resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
+ engines: {node: '>=0.10.0'}
+
+ strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+
+ strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+
+ strip-final-newline@3.0.0:
+ resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==}
+ engines: {node: '>=12'}
+
+ strip-json-comments@3.1.1:
+ resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+ engines: {node: '>=8'}
+
+ strip-literal@2.1.1:
+ resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
+
+ strnum@1.0.5:
+ resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==}
+
+ style-mod@4.1.2:
+ resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
+
+ stylelint-config-html@1.1.0:
+ resolution: {integrity: sha512-IZv4IVESjKLumUGi+HWeb7skgO6/g4VMuAYrJdlqQFndgbj6WJAXPhaysvBiXefX79upBdQVumgYcdd17gCpjQ==}
+ engines: {node: ^12 || >=14}
+ peerDependencies:
+ postcss-html: ^1.0.0
+ stylelint: '>=14.0.0'
+
+ stylelint-config-recommended@14.0.1:
+ resolution: {integrity: sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ stylelint: ^16.1.0
+
+ stylelint-config-standard@36.0.1:
+ resolution: {integrity: sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==}
+ engines: {node: '>=18.12.0'}
+ peerDependencies:
+ stylelint: ^16.1.0
+
+ stylelint-order@6.0.4:
+ resolution: {integrity: sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==}
+ peerDependencies:
+ stylelint: ^14.0.0 || ^15.0.0 || ^16.0.1
+
+ stylelint@16.11.0:
+ resolution: {integrity: sha512-zrl4IrKmjJQ+h9FoMp69UMCq5SxeHk0URhxUBj4d3ISzo/DplOFBJZc7t7Dr6otB+1bfbbKNLOmCDpzKSlW+Nw==}
+ engines: {node: '>=18.12.0'}
+ hasBin: true
+
+ supports-color@2.0.0:
+ resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
+ engines: {node: '>=0.8.0'}
+
+ supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+
+ supports-hyperlinks@3.1.0:
+ resolution: {integrity: sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==}
+ engines: {node: '>=14.18'}
+
+ supports-preserve-symlinks-flag@1.0.0:
+ resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+ engines: {node: '>= 0.4'}
+
+ svg-tags@1.0.0:
+ resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==}
+
+ svgo@3.3.2:
+ resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
+ synckit@0.8.8:
+ resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+
+ synckit@0.9.2:
+ resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==}
+ engines: {node: ^14.18.0 || >=16.0.0}
+
+ systemjs@6.15.1:
+ resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
+
+ tabbable@6.2.0:
+ resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
+
+ table@6.8.2:
+ resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==}
+ engines: {node: '>=10.0.0'}
+
+ terser@5.36.0:
+ resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ text-extensions@2.4.0:
+ resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
+ engines: {node: '>=8'}
+
+ text-table@0.2.0:
+ resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
+
+ through@2.3.8:
+ resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+
+ tiny-svg@3.1.3:
+ resolution: {integrity: sha512-9mwnPqXInRsBmH/DO6NMxBE++9LsqpVXQSSTZGc5bomoKKvL5OX/Hlotw7XVXP6XLRcHWIzZpxfovGqWKgCypQ==}
+
+ tiny-warning@1.0.3:
+ resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
+
+ tinyexec@0.3.1:
+ resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==}
+
+ tinyglobby@0.2.10:
+ resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==}
+ engines: {node: '>=12.0.0'}
+
+ to-regex-range@5.0.1:
+ resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+ engines: {node: '>=8.0'}
+
+ totalist@3.0.1:
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
+ engines: {node: '>=6'}
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ ts-api-utils@1.4.3:
+ resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
+ engines: {node: '>=16'}
+ peerDependencies:
+ typescript: '>=4.2.0'
+
+ ts-api-utils@2.0.1:
+ resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==}
+ engines: {node: '>=18.12'}
+ peerDependencies:
+ typescript: '>=4.8.4'
+
+ tslib@2.3.0:
+ resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ type-check@0.4.0:
+ resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+ engines: {node: '>= 0.8.0'}
+
+ type-fest@0.20.2:
+ resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
+ engines: {node: '>=10'}
+
+ type@2.7.3:
+ resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
+
+ typescript@5.3.3:
+ resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ uc.micro@2.1.0:
+ resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
+ ufo@1.5.4:
+ resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
+
+ unconfig@0.3.13:
+ resolution: {integrity: sha512-N9Ph5NC4+sqtcOjPfHrRcHekBCadCXWTBzp2VYYbySOHW0PfD9XLCeXshTXjkPYwLrBr9AtSeU0CZmkYECJhng==}
+
+ unconfig@7.3.1:
+ resolution: {integrity: sha512-LH5WL+un92tGAzWS87k7LkAfwpMdm7V0IXG2FxEjZz/QxiIW5J5LkcrKQThj0aRz6+h/lFmKI9EUXmK/T0bcrw==}
+
+ undici-types@6.19.8:
+ resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
+
+ unicode-canonical-property-names-ecmascript@2.0.1:
+ resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-ecmascript@2.0.0:
+ resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
+ engines: {node: '>=4'}
+
+ unicode-match-property-value-ecmascript@2.2.0:
+ resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==}
+ engines: {node: '>=4'}
+
+ unicode-property-aliases-ecmascript@2.1.0:
+ resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==}
+ engines: {node: '>=4'}
+
+ unicorn-magic@0.1.0:
+ resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==}
+ engines: {node: '>=18'}
+
+ unimport@3.14.2:
+ resolution: {integrity: sha512-FSxhbAylGGanyuTb3K0Ka3T9mnsD0+cRKbwOS11Li4Lh2whWS091e32JH4bIHrTckxlW9GnExAglADlxXjjzFw==}
+
+ universalify@2.0.1:
+ resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
+ engines: {node: '>= 10.0.0'}
+
+ unocss@0.58.9:
+ resolution: {integrity: sha512-aqANXXP0RrtN4kSaTLn/7I6wh8o45LUdVgPzGu7Fan2DfH2+wpIs6frlnlHlOymnb+52dp6kXluQinddaUKW1A==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@unocss/webpack': 0.58.9
+ vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
+ peerDependenciesMeta:
+ '@unocss/webpack':
+ optional: true
+ vite:
+ optional: true
+
+ unplugin-auto-import@0.16.7:
+ resolution: {integrity: sha512-w7XmnRlchq6YUFJVFGSvG1T/6j8GrdYN6Em9Wf0Ye+HXgD/22kont+WnuCAA0UaUoxtuvRR1u/mXKy63g/hfqQ==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@nuxt/kit': ^3.2.2
+ '@vueuse/core': '*'
+ peerDependenciesMeta:
+ '@nuxt/kit':
+ optional: true
+ '@vueuse/core':
+ optional: true
+
+ unplugin-element-plus@0.8.0:
+ resolution: {integrity: sha512-jByUGY3FG2B8RJKFryqxx4eNtSTj+Hjlo8edcOdJymewndDQjThZ1pRUQHRjQsbKhTV2jEctJV7t7RJ405UL4g==}
+ engines: {node: '>=14.19.0'}
+
+ unplugin-vue-components@0.25.2:
+ resolution: {integrity: sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ '@babel/parser': ^7.15.8
+ '@nuxt/kit': ^3.2.2
+ vue: 2 || 3
+ peerDependenciesMeta:
+ '@babel/parser':
+ optional: true
+ '@nuxt/kit':
+ optional: true
+
+ unplugin@1.16.0:
+ resolution: {integrity: sha512-5liCNPuJW8dqh3+DM6uNM2EI3MLLpCKp/KY+9pB5M2S2SR2qvvDHhKgBOaTWEbZTAws3CXfB0rKTIolWKL05VQ==}
+ engines: {node: '>=14.0.0'}
+
+ update-browserslist-db@1.1.1:
+ resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==}
+ hasBin: true
+ peerDependencies:
+ browserslist: '>= 4.21.0'
+
+ uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+ url-toolkit@2.2.5:
+ resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==}
+
+ url@0.11.4:
+ resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
+ engines: {node: '>= 0.4'}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ uuid@10.0.0:
+ resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+ hasBin: true
+
+ vanilla-picker@2.12.3:
+ resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==}
+
+ video.js@7.21.6:
+ resolution: {integrity: sha512-m41TbODrUCToVfK1aljVd296CwDQnCRewpIm5tTXMuV87YYSGw1H+VDOaV45HlpcWSsTWWLF++InDgGJfthfUw==}
+
+ videojs-font@3.2.0:
+ resolution: {integrity: sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==}
+
+ videojs-vtt.js@0.15.5:
+ resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==}
+
+ vite-plugin-compression@0.5.1:
+ resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==}
+ peerDependencies:
+ vite: '>=2.0.0'
+
+ vite-plugin-ejs@1.7.0:
+ resolution: {integrity: sha512-JNP3zQDC4mSbfoJ3G73s5mmZITD8NGjUmLkq4swxyahy/W0xuokK9U9IJGXw7KCggq6UucT6hJ0p+tQrNtqTZw==}
+ peerDependencies:
+ vite: '>=5.0.0'
+
+ vite-plugin-eslint@1.8.1:
+ resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
+ peerDependencies:
+ eslint: '>=7'
+ vite: '>=2'
+
+ vite-plugin-progress@0.0.7:
+ resolution: {integrity: sha512-zyvKdcc/X+6hnw3J1HVV1TKrlFKC4Rh8GnDnWG/2qhRXjqytTcM++xZ+SAPnoDsSyWl8O93ymK0wZRgHAoglEQ==}
+ engines: {node: '>=14', pnpm: '>=7.0.0'}
+ peerDependencies:
+ vite: '>2.0.0-0'
+
+ vite-plugin-purge-icons@0.10.0:
+ resolution: {integrity: sha512-4fMJKQuBu9lAPJWjqGEytRaxty1pP9bWgQLA68dwbbaCXu6NBrOUb/3kMaUc7TP09kerEk+qTriCk05OZXpjwA==}
+ engines: {node: '>= 12'}
+ peerDependencies:
+ vite: '>=2'
+
+ vite-plugin-svg-icons-ng@1.3.1:
+ resolution: {integrity: sha512-86oYE/MACMyqebcbuKpUTUJsptHoAfgnPS8680jQ0VigDEM2oxb59Nj2G+1FqA8CzyLCFftAKtwMAz8UGqiRfg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ peerDependencies:
+ vite: '>=5.0.0'
+
+ vite-plugin-top-level-await@1.4.4:
+ resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==}
+ peerDependencies:
+ vite: '>=2.8'
+
+ vite@5.1.4:
+ resolution: {integrity: sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+
+ vue-demi@0.14.10:
+ resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
+ engines: {node: '>=12'}
+ hasBin: true
+ peerDependencies:
+ '@vue/composition-api': ^1.0.0-rc.1
+ vue: ^3.0.0-0 || ^2.6.0
+ peerDependenciesMeta:
+ '@vue/composition-api':
+ optional: true
+
+ vue-dompurify-html@4.1.4:
+ resolution: {integrity: sha512-K0XDSZA4dmMMvAgW8yaCx1kAYQldmgXeHJaLPS0mlSKOu8B+onE06X4KfB5LGyX4jR3rlVosyWJczRBzR0sZ/g==}
+ peerDependencies:
+ vue: ^2.7.0 || ^3.0.0
+
+ vue-eslint-parser@9.4.3:
+ resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==}
+ engines: {node: ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: '>=6.0.0'
+
+ vue-i18n@9.10.2:
+ resolution: {integrity: sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ vue: ^3.0.0
+
+ vue-router@4.4.5:
+ resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ vue-template-compiler@2.7.16:
+ resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==}
+
+ vue-tsc@1.8.27:
+ resolution: {integrity: sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==}
+ hasBin: true
+ peerDependencies:
+ typescript: '*'
+
+ vue-types@5.1.3:
+ resolution: {integrity: sha512-3Wy6QcZl0VusCCHX3vYrWSILFlrOB2EQDoySnuYmASM5cUp1FivJGfkS5lp1CutDgyRb41g32r/1QCmiBj5i1Q==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ vue: ^2.0.0 || ^3.0.0
+ peerDependenciesMeta:
+ vue:
+ optional: true
+
+ vue3-print-nb@0.1.4:
+ resolution: {integrity: sha512-LExI7viEzplR6ZKQ2b+V4U0cwGYbVD4fut/XHvk3UPGlT5CcvIGs6VlwGp107aKgk6P8Pgx4rco3Rehv2lti3A==}
+
+ vue3-signature@0.2.4:
+ resolution: {integrity: sha512-XFwwFVK9OG3F085pKIq2SlNVqx32WdFH+TXbGEWc5FfEKpx8oMmZuAwZZ50K/pH2FgmJSE8IRwU9DDhrLpd6iA==}
+ peerDependencies:
+ vue: ^3.2.0
+
+ vue@3.5.12:
+ resolution: {integrity: sha512-CLVZtXtn2ItBIi/zHZ0Sg1Xkb7+PU32bJJ8Bmy7ts3jxXTcbfsEfBivFYYWz1Hur+lalqGAh65Coin0r+HRUfg==}
+ peerDependencies:
+ typescript: '*'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ vuedraggable@4.1.0:
+ resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
+ peerDependencies:
+ vue: ^3.0.1
+
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
+ wangeditor@4.7.15:
+ resolution: {integrity: sha512-aPTdREd8BxXVyJ5MI+LU83FQ7u1EPd341iXIorRNYSOvoimNoZ4nPg+yn3FGbB93/owEa6buLw8wdhYnMCJQLg==}
+
+ web-storage-cache@1.1.1:
+ resolution: {integrity: sha512-D0MieGooOs8RpsrK+vnejXnvh4OOv/+lTFB35JRkJJQt+uOjPE08XpaE0QBLMTRu47B1KGT/Nq3Gbag3Orinzw==}
+
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ webpack-virtual-modules@0.6.2:
+ resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
+ which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
+ which@1.3.1:
+ resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+ hasBin: true
+
+ which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+
+ wildcard@1.1.2:
+ resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
+
+ word-wrap@1.2.5:
+ resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+ engines: {node: '>=0.10.0'}
+
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
+ wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+
+ wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+
+ wrap-ansi@9.0.0:
+ resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
+ engines: {node: '>=18'}
+
+ wrappy@1.0.2:
+ resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+
+ write-file-atomic@5.0.1:
+ resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
+ engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+ xml-js@1.6.11:
+ resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
+ hasBin: true
+
+ xml-name-validator@4.0.0:
+ resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
+ engines: {node: '>=12'}
+
+ y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
+ y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+
+ yallist@3.1.1:
+ resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+ yaml-eslint-parser@1.2.3:
+ resolution: {integrity: sha512-4wZWvE398hCP7O8n3nXKu/vdq1HcH01ixYlCREaJL5NUMwQ0g3MaGFUBNSlmBtKmhbtVG/Cm6lyYmSVTEVil8A==}
+ engines: {node: ^14.17.0 || >=16.0.0}
+
+ yaml@2.5.1:
+ resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==}
+ engines: {node: '>= 14'}
+ hasBin: true
+
+ yaml@2.6.1:
+ resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==}
+ engines: {node: '>= 14'}
+ hasBin: true
+
+ yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
+ yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+
+ yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+
+ yargs@17.7.2:
+ resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
+ engines: {node: '>=12'}
+
+ yocto-queue@0.1.0:
+ resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+ engines: {node: '>=10'}
+
+ yocto-queue@1.1.1:
+ resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
+ engines: {node: '>=12.20'}
+
+ zeebe-bpmn-moddle@1.7.0:
+ resolution: {integrity: sha512-eZ6OXSt0c4n9V/oN/46gTlwDIS3GhWQLt9jbM5uS/YryB4yN8wdrrKrtw+TpyNy0SSKWXNDHyC83nCA2blPO3Q==}
+
+ zrender@5.6.0:
+ resolution: {integrity: sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==}
+
+snapshots:
+
+ '@ampproject/remapping@2.3.0':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@antfu/install-pkg@0.4.1':
+ dependencies:
+ package-manager-detector: 0.2.5
+ tinyexec: 0.3.1
+
+ '@antfu/utils@0.7.10': {}
+
+ '@babel/code-frame@7.26.2':
+ dependencies:
+ '@babel/helper-validator-identifier': 7.25.9
+ js-tokens: 4.0.0
+ picocolors: 1.1.1
+
+ '@babel/compat-data@7.26.2': {}
+
+ '@babel/core@7.26.0':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@babel/code-frame': 7.26.2
+ '@babel/generator': 7.26.2
+ '@babel/helper-compilation-targets': 7.25.9
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
+ '@babel/helpers': 7.26.0
+ '@babel/parser': 7.26.2
+ '@babel/template': 7.25.9
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ convert-source-map: 2.0.0
+ debug: 4.3.7
+ gensync: 1.0.0-beta.2
+ json5: 2.2.3
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/generator@7.26.2':
+ dependencies:
+ '@babel/parser': 7.26.2
+ '@babel/types': 7.26.0
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+ jsesc: 3.0.2
+
+ '@babel/helper-annotate-as-pure@7.25.9':
+ dependencies:
+ '@babel/types': 7.26.0
+
+ '@babel/helper-builder-binary-assignment-operator-visitor@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-compilation-targets@7.25.9':
+ dependencies:
+ '@babel/compat-data': 7.26.2
+ '@babel/helper-validator-option': 7.25.9
+ browserslist: 4.24.2
+ lru-cache: 5.1.1
+ semver: 6.3.1
+
+ '@babel/helper-create-class-features-plugin@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-annotate-as-pure': 7.25.9
+ '@babel/helper-member-expression-to-functions': 7.25.9
+ '@babel/helper-optimise-call-expression': 7.25.9
+ '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-skip-transparent-expression-wrappers': 7.25.9
+ '@babel/traverse': 7.25.9
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-create-regexp-features-plugin@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-annotate-as-pure': 7.25.9
+ regexpu-core: 6.2.0
+ semver: 6.3.1
+
+ '@babel/helper-define-polyfill-provider@0.6.3(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-compilation-targets': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ debug: 4.3.7
+ lodash.debounce: 4.0.8
+ resolve: 1.22.8
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-member-expression-to-functions@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-imports@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-optimise-call-expression@7.25.9':
+ dependencies:
+ '@babel/types': 7.26.0
+
+ '@babel/helper-plugin-utils@7.25.9': {}
+
+ '@babel/helper-remap-async-to-generator@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-annotate-as-pure': 7.25.9
+ '@babel/helper-wrap-function': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-replace-supers@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-member-expression-to-functions': 7.25.9
+ '@babel/helper-optimise-call-expression': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-simple-access@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-skip-transparent-expression-wrappers@7.25.9':
+ dependencies:
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helper-string-parser@7.25.9': {}
+
+ '@babel/helper-validator-identifier@7.25.9': {}
+
+ '@babel/helper-validator-option@7.25.9': {}
+
+ '@babel/helper-wrap-function@7.25.9':
+ dependencies:
+ '@babel/template': 7.25.9
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/helpers@7.26.0':
+ dependencies:
+ '@babel/template': 7.25.9
+ '@babel/types': 7.26.0
+
+ '@babel/parser@7.26.2':
+ dependencies:
+ '@babel/types': 7.26.0
+
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-skip-transparent-expression-wrappers': 7.25.9
+ '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+
+ '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-async-generator-functions@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-async-to-generator@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-remap-async-to-generator': 7.25.9(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-class-properties@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-class-static-block@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-annotate-as-pure': 7.25.9
+ '@babel/helper-compilation-targets': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
+ '@babel/traverse': 7.25.9
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/template': 7.25.9
+
+ '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-dotall-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-duplicate-keys@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-dynamic-import@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-exponentiation-operator@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-builder-binary-assignment-operator-visitor': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-export-namespace-from@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-skip-transparent-expression-wrappers': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-compilation-targets': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-json-strings@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-logical-assignment-operators@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-modules-amd@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-commonjs@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-simple-access': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-systemjs@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+ '@babel/traverse': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-modules-umd@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-named-capturing-groups-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-new-target@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-nullish-coalescing-operator@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-numeric-separator@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-object-rest-spread@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-compilation-targets': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0)
+
+ '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-replace-supers': 7.25.9(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-optional-catch-binding@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-optional-chaining@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-skip-transparent-expression-wrappers': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-private-methods@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-private-property-in-object@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-annotate-as-pure': 7.25.9
+ '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-regenerator@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ regenerator-transform: 0.15.2
+
+ '@babel/plugin-transform-regexp-modifiers@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-reserved-words@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-skip-transparent-expression-wrappers': 7.25.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-sticky-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-typeof-symbol@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-typescript@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-annotate-as-pure': 7.25.9
+ '@babel/helper-create-class-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-skip-transparent-expression-wrappers': 7.25.9
+ '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/plugin-transform-unicode-escapes@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-unicode-property-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-unicode-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/plugin-transform-unicode-sets-regex@7.25.9(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-create-regexp-features-plugin': 7.25.9(@babel/core@7.26.0)
+ '@babel/helper-plugin-utils': 7.25.9
+
+ '@babel/preset-env@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/compat-data': 7.26.2
+ '@babel/core': 7.26.0
+ '@babel/helper-compilation-targets': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-validator-option': 7.25.9
+ '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)
+ '@babel/plugin-syntax-import-assertions': 7.26.0(@babel/core@7.26.0)
+ '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0)
+ '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.26.0)
+ '@babel/plugin-transform-arrow-functions': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-async-generator-functions': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-async-to-generator': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-block-scoped-functions': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-block-scoping': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-class-properties': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-class-static-block': 7.26.0(@babel/core@7.26.0)
+ '@babel/plugin-transform-classes': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-computed-properties': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-destructuring': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-dotall-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-duplicate-keys': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-dynamic-import': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-exponentiation-operator': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-export-namespace-from': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-for-of': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-function-name': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-json-strings': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-literals': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-logical-assignment-operators': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-member-expression-literals': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-modules-amd': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-modules-systemjs': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-modules-umd': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-named-capturing-groups-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-new-target': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-nullish-coalescing-operator': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-numeric-separator': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-object-rest-spread': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-object-super': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-optional-catch-binding': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-optional-chaining': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-private-methods': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-private-property-in-object': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-property-literals': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-regenerator': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-regexp-modifiers': 7.26.0(@babel/core@7.26.0)
+ '@babel/plugin-transform-reserved-words': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-shorthand-properties': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-spread': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-sticky-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-template-literals': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-typeof-symbol': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-unicode-escapes': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-unicode-property-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-unicode-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-unicode-sets-regex': 7.25.9(@babel/core@7.26.0)
+ '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.26.0)
+ babel-plugin-polyfill-corejs2: 0.4.12(@babel/core@7.26.0)
+ babel-plugin-polyfill-corejs3: 0.10.6(@babel/core@7.26.0)
+ babel-plugin-polyfill-regenerator: 0.6.3(@babel/core@7.26.0)
+ core-js-compat: 3.39.0
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/types': 7.26.0
+ esutils: 2.0.3
+
+ '@babel/preset-typescript@7.26.0(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/helper-validator-option': 7.25.9
+ '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-modules-commonjs': 7.25.9(@babel/core@7.26.0)
+ '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/runtime-corejs3@7.26.0':
+ dependencies:
+ core-js-pure: 3.39.0
+ regenerator-runtime: 0.14.1
+
+ '@babel/runtime@7.26.0':
+ dependencies:
+ regenerator-runtime: 0.14.1
+
+ '@babel/template@7.25.9':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/parser': 7.26.2
+ '@babel/types': 7.26.0
+
+ '@babel/traverse@7.25.9':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/generator': 7.26.2
+ '@babel/parser': 7.26.2
+ '@babel/template': 7.25.9
+ '@babel/types': 7.26.0
+ debug: 4.3.7
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@babel/types@7.26.0':
+ dependencies:
+ '@babel/helper-string-parser': 7.25.9
+ '@babel/helper-validator-identifier': 7.25.9
+
+ '@bpmn-io/cm-theme@0.1.0-alpha.2':
+ dependencies:
+ '@codemirror/language': 6.10.6
+ '@codemirror/view': 6.35.0
+ '@lezer/highlight': 1.2.1
+
+ '@bpmn-io/diagram-js-ui@0.2.3':
+ dependencies:
+ htm: 3.1.1
+ preact: 10.25.0
+
+ '@bpmn-io/extract-process-variables@0.8.0':
+ dependencies:
+ min-dash: 4.2.2
+
+ '@bpmn-io/feel-editor@1.9.1(@lezer/common@1.2.3)':
+ dependencies:
+ '@bpmn-io/feel-lint': 1.3.1
+ '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)
+ '@codemirror/commands': 6.7.1
+ '@codemirror/language': 6.10.6
+ '@codemirror/lint': 6.8.4
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ '@lezer/highlight': 1.2.1
+ lang-feel: 2.2.0
+ min-dom: 4.2.1
+ transitivePeerDependencies:
+ - '@lezer/common'
+
+ '@bpmn-io/feel-lint@1.3.1':
+ dependencies:
+ '@codemirror/language': 6.10.6
+ lezer-feel: 1.4.0
+
+ '@bpmn-io/properties-panel@3.25.0(@lezer/common@1.2.3)':
+ dependencies:
+ '@bpmn-io/feel-editor': 1.9.1(@lezer/common@1.2.3)
+ '@codemirror/view': 6.35.0
+ classnames: 2.5.1
+ feelers: 1.4.0
+ focus-trap: 7.6.2
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+ transitivePeerDependencies:
+ - '@lezer/common'
+
+ '@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)':
+ dependencies:
+ '@codemirror/language': 6.10.6
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ '@lezer/common': 1.2.3
+
+ '@codemirror/commands@6.7.1':
+ dependencies:
+ '@codemirror/language': 6.10.6
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ '@lezer/common': 1.2.3
+
+ '@codemirror/language@6.10.6':
+ dependencies:
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+ style-mod: 4.1.2
+
+ '@codemirror/lint@6.8.4':
+ dependencies:
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ crelt: 1.0.6
+
+ '@codemirror/state@6.4.1': {}
+
+ '@codemirror/view@6.35.0':
+ dependencies:
+ '@codemirror/state': 6.4.1
+ style-mod: 4.1.2
+ w3c-keyname: 2.2.8
+
+ '@commitlint/cli@19.6.0(@types/node@20.17.9)(typescript@5.3.3)':
+ dependencies:
+ '@commitlint/format': 19.5.0
+ '@commitlint/lint': 19.6.0
+ '@commitlint/load': 19.5.0(@types/node@20.17.9)(typescript@5.3.3)
+ '@commitlint/read': 19.5.0
+ '@commitlint/types': 19.5.0
+ tinyexec: 0.3.1
+ yargs: 17.7.2
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
+ '@commitlint/config-conventional@19.6.0':
+ dependencies:
+ '@commitlint/types': 19.5.0
+ conventional-changelog-conventionalcommits: 7.0.2
+
+ '@commitlint/config-validator@19.5.0':
+ dependencies:
+ '@commitlint/types': 19.5.0
+ ajv: 8.17.1
+
+ '@commitlint/ensure@19.5.0':
+ dependencies:
+ '@commitlint/types': 19.5.0
+ lodash.camelcase: 4.3.0
+ lodash.kebabcase: 4.1.1
+ lodash.snakecase: 4.1.1
+ lodash.startcase: 4.4.0
+ lodash.upperfirst: 4.3.1
+
+ '@commitlint/execute-rule@19.5.0': {}
+
+ '@commitlint/format@19.5.0':
+ dependencies:
+ '@commitlint/types': 19.5.0
+ chalk: 5.3.0
+
+ '@commitlint/is-ignored@19.6.0':
+ dependencies:
+ '@commitlint/types': 19.5.0
+ semver: 7.6.3
+
+ '@commitlint/lint@19.6.0':
+ dependencies:
+ '@commitlint/is-ignored': 19.6.0
+ '@commitlint/parse': 19.5.0
+ '@commitlint/rules': 19.6.0
+ '@commitlint/types': 19.5.0
+
+ '@commitlint/load@19.5.0(@types/node@20.17.9)(typescript@5.3.3)':
+ dependencies:
+ '@commitlint/config-validator': 19.5.0
+ '@commitlint/execute-rule': 19.5.0
+ '@commitlint/resolve-extends': 19.5.0
+ '@commitlint/types': 19.5.0
+ chalk: 5.3.0
+ cosmiconfig: 9.0.0(typescript@5.3.3)
+ cosmiconfig-typescript-loader: 5.1.0(@types/node@20.17.9)(cosmiconfig@9.0.0(typescript@5.3.3))(typescript@5.3.3)
+ lodash.isplainobject: 4.0.6
+ lodash.merge: 4.6.2
+ lodash.uniq: 4.5.0
+ transitivePeerDependencies:
+ - '@types/node'
+ - typescript
+
+ '@commitlint/message@19.5.0': {}
+
+ '@commitlint/parse@19.5.0':
+ dependencies:
+ '@commitlint/types': 19.5.0
+ conventional-changelog-angular: 7.0.0
+ conventional-commits-parser: 5.0.0
+
+ '@commitlint/read@19.5.0':
+ dependencies:
+ '@commitlint/top-level': 19.5.0
+ '@commitlint/types': 19.5.0
+ git-raw-commits: 4.0.0
+ minimist: 1.2.8
+ tinyexec: 0.3.1
+
+ '@commitlint/resolve-extends@19.5.0':
+ dependencies:
+ '@commitlint/config-validator': 19.5.0
+ '@commitlint/types': 19.5.0
+ global-directory: 4.0.1
+ import-meta-resolve: 4.1.0
+ lodash.mergewith: 4.6.2
+ resolve-from: 5.0.0
+
+ '@commitlint/rules@19.6.0':
+ dependencies:
+ '@commitlint/ensure': 19.5.0
+ '@commitlint/message': 19.5.0
+ '@commitlint/to-lines': 19.5.0
+ '@commitlint/types': 19.5.0
+
+ '@commitlint/to-lines@19.5.0': {}
+
+ '@commitlint/top-level@19.5.0':
+ dependencies:
+ find-up: 7.0.0
+
+ '@commitlint/types@19.5.0':
+ dependencies:
+ '@types/conventional-commits-parser': 5.0.1
+ chalk: 5.3.0
+
+ '@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.3
+
+ '@csstools/css-tokenizer@3.0.3': {}
+
+ '@csstools/media-query-list-parser@4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
+ '@csstools/css-tokenizer': 3.0.3
+
+ '@csstools/selector-specificity@5.0.0(postcss-selector-parser@7.0.0)':
+ dependencies:
+ postcss-selector-parser: 7.0.0
+
+ '@ctrl/tinycolor@3.6.1': {}
+
+ '@dual-bundle/import-meta-resolve@4.1.0': {}
+
+ '@element-plus/icons-vue@2.3.2(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@esbuild/aix-ppc64@0.19.12':
+ optional: true
+
+ '@esbuild/android-arm64@0.19.12':
+ optional: true
+
+ '@esbuild/android-arm@0.19.12':
+ optional: true
+
+ '@esbuild/android-x64@0.19.12':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.19.12':
+ optional: true
+
+ '@esbuild/darwin-x64@0.19.12':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.19.12':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.19.12':
+ optional: true
+
+ '@esbuild/linux-arm64@0.19.12':
+ optional: true
+
+ '@esbuild/linux-arm@0.19.12':
+ optional: true
+
+ '@esbuild/linux-ia32@0.19.12':
+ optional: true
+
+ '@esbuild/linux-loong64@0.19.12':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.19.12':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.19.12':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.19.12':
+ optional: true
+
+ '@esbuild/linux-s390x@0.19.12':
+ optional: true
+
+ '@esbuild/linux-x64@0.19.12':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.19.12':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.19.12':
+ optional: true
+
+ '@esbuild/sunos-x64@0.19.12':
+ optional: true
+
+ '@esbuild/win32-arm64@0.19.12':
+ optional: true
+
+ '@esbuild/win32-ia32@0.19.12':
+ optional: true
+
+ '@esbuild/win32-x64@0.19.12':
+ optional: true
+
+ '@eslint-community/eslint-utils@4.4.1(eslint@8.57.1)':
+ dependencies:
+ eslint: 8.57.1
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/regexpp@4.12.1': {}
+
+ '@eslint/eslintrc@2.1.4':
+ dependencies:
+ ajv: 6.12.6
+ debug: 4.3.7
+ espree: 9.6.1
+ globals: 13.24.0
+ ignore: 5.3.2
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
+ minimatch: 3.1.2
+ strip-json-comments: 3.1.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@eslint/js@8.57.1': {}
+
+ '@floating-ui/core@1.6.8':
+ dependencies:
+ '@floating-ui/utils': 0.2.8
+
+ '@floating-ui/dom@1.6.12':
+ dependencies:
+ '@floating-ui/core': 1.6.8
+ '@floating-ui/utils': 0.2.8
+
+ '@floating-ui/utils@0.2.8': {}
+
+ '@form-create/component-elm-checkbox@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-elm-frame@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-elm-group@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-elm-radio@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-elm-select@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-elm-tree@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-elm-upload@3.2.14':
+ dependencies:
+ '@form-create/utils': 3.2.14
+
+ '@form-create/component-subform@3.1.34': {}
+
+ '@form-create/component-wangeditor@3.2.14':
+ dependencies:
+ wangeditor: 4.7.15
+
+ '@form-create/core@3.2.14(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@form-create/utils': 3.2.14
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@form-create/designer@3.2.8(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@form-create/component-wangeditor': 3.2.14
+ '@form-create/element-ui': 3.2.14(vue@3.5.12(typescript@5.3.3))
+ '@form-create/utils': 3.2.14
+ codemirror: 6.65.7
+ element-plus: 2.11.1(vue@3.5.12(typescript@5.3.3))
+ vue: 3.5.12(typescript@5.3.3)
+ vuedraggable: 4.1.0(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+
+ '@form-create/element-ui@3.2.14(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@form-create/component-elm-checkbox': 3.2.14
+ '@form-create/component-elm-frame': 3.2.14
+ '@form-create/component-elm-group': 3.2.14
+ '@form-create/component-elm-radio': 3.2.14
+ '@form-create/component-elm-select': 3.2.14
+ '@form-create/component-elm-tree': 3.2.14
+ '@form-create/component-elm-upload': 3.2.14
+ '@form-create/component-subform': 3.1.34
+ '@form-create/core': 3.2.14(vue@3.5.12(typescript@5.3.3))
+ '@form-create/utils': 3.2.14
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@form-create/utils@3.2.14': {}
+
+ '@gera2ld/jsx-dom@2.2.2':
+ dependencies:
+ '@babel/runtime': 7.26.0
+
+ '@humanwhocodes/config-array@0.13.0':
+ dependencies:
+ '@humanwhocodes/object-schema': 2.0.3
+ debug: 4.3.7
+ minimatch: 3.1.2
+ transitivePeerDependencies:
+ - supports-color
+
+ '@humanwhocodes/module-importer@1.0.1': {}
+
+ '@humanwhocodes/object-schema@2.0.3': {}
+
+ '@iconify/iconify@2.1.2':
+ dependencies:
+ cross-fetch: 3.1.8
+ transitivePeerDependencies:
+ - encoding
+
+ '@iconify/iconify@3.1.1':
+ dependencies:
+ '@iconify/types': 2.0.0
+
+ '@iconify/json@2.2.277':
+ dependencies:
+ '@iconify/types': 2.0.0
+ pathe: 1.1.2
+
+ '@iconify/types@2.0.0': {}
+
+ '@iconify/utils@2.1.33':
+ dependencies:
+ '@antfu/install-pkg': 0.4.1
+ '@antfu/utils': 0.7.10
+ '@iconify/types': 2.0.0
+ debug: 4.3.7
+ kolorist: 1.8.0
+ local-pkg: 0.5.1
+ mlly: 1.7.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@intlify/bundle-utils@7.5.1(vue-i18n@9.10.2(vue@3.5.12(typescript@5.3.3)))':
+ dependencies:
+ '@intlify/message-compiler': 9.14.2
+ '@intlify/shared': 9.14.2
+ acorn: 8.14.0
+ escodegen: 2.1.0
+ estree-walker: 2.0.2
+ jsonc-eslint-parser: 2.4.0
+ magic-string: 0.30.14
+ mlly: 1.7.3
+ source-map-js: 1.2.1
+ yaml-eslint-parser: 1.2.3
+ optionalDependencies:
+ vue-i18n: 9.10.2(vue@3.5.12(typescript@5.3.3))
+
+ '@intlify/core-base@9.10.2':
+ dependencies:
+ '@intlify/message-compiler': 9.10.2
+ '@intlify/shared': 9.10.2
+
+ '@intlify/message-compiler@9.10.2':
+ dependencies:
+ '@intlify/shared': 9.10.2
+ source-map-js: 1.2.1
+
+ '@intlify/message-compiler@9.14.2':
+ dependencies:
+ '@intlify/shared': 9.14.2
+ source-map-js: 1.2.1
+
+ '@intlify/shared@9.10.2': {}
+
+ '@intlify/shared@9.14.2': {}
+
+ '@intlify/unplugin-vue-i18n@2.0.0(rollup@4.27.4)(vue-i18n@9.10.2(vue@3.5.12(typescript@5.3.3)))':
+ dependencies:
+ '@intlify/bundle-utils': 7.5.1(vue-i18n@9.10.2(vue@3.5.12(typescript@5.3.3)))
+ '@intlify/shared': 9.14.2
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ '@vue/compiler-sfc': 3.5.13
+ debug: 4.3.7
+ fast-glob: 3.3.2
+ js-yaml: 4.1.0
+ json5: 2.2.3
+ pathe: 1.1.2
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+ unplugin: 1.16.0
+ optionalDependencies:
+ vue-i18n: 9.10.2(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - rollup
+ - supports-color
+
+ '@isaacs/cliui@8.0.2':
+ dependencies:
+ string-width: 5.1.2
+ string-width-cjs: string-width@4.2.3
+ strip-ansi: 7.1.0
+ strip-ansi-cjs: strip-ansi@6.0.1
+ wrap-ansi: 8.1.0
+ wrap-ansi-cjs: wrap-ansi@7.0.0
+
+ '@jest/schemas@29.6.3':
+ dependencies:
+ '@sinclair/typebox': 0.27.8
+
+ '@jridgewell/gen-mapping@0.3.5':
+ dependencies:
+ '@jridgewell/set-array': 1.2.1
+ '@jridgewell/sourcemap-codec': 1.5.0
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@jridgewell/resolve-uri@3.1.2': {}
+
+ '@jridgewell/set-array@1.2.1': {}
+
+ '@jridgewell/source-map@0.3.6':
+ dependencies:
+ '@jridgewell/gen-mapping': 0.3.5
+ '@jridgewell/trace-mapping': 0.3.25
+
+ '@jridgewell/sourcemap-codec@1.5.0': {}
+
+ '@jridgewell/trace-mapping@0.3.25':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ '@lezer/common@1.2.3': {}
+
+ '@lezer/highlight@1.2.1':
+ dependencies:
+ '@lezer/common': 1.2.3
+
+ '@lezer/lr@1.4.2':
+ dependencies:
+ '@lezer/common': 1.2.3
+
+ '@lezer/markdown@1.3.2':
+ dependencies:
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+
+ '@microsoft/fetch-event-source@2.0.1': {}
+
+ '@nodelib/fs.scandir@2.1.5':
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ run-parallel: 1.2.0
+
+ '@nodelib/fs.stat@2.0.5': {}
+
+ '@nodelib/fs.walk@1.2.8':
+ dependencies:
+ '@nodelib/fs.scandir': 2.1.5
+ fastq: 1.17.1
+
+ '@parcel/watcher-android-arm64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-darwin-arm64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-darwin-x64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-freebsd-x64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm-glibc@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm-musl@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-glibc@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-arm64-musl@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-x64-glibc@2.5.0':
+ optional: true
+
+ '@parcel/watcher-linux-x64-musl@2.5.0':
+ optional: true
+
+ '@parcel/watcher-win32-arm64@2.5.0':
+ optional: true
+
+ '@parcel/watcher-win32-ia32@2.5.0':
+ optional: true
+
+ '@parcel/watcher-win32-x64@2.5.0':
+ optional: true
+
+ '@parcel/watcher@2.5.0':
+ dependencies:
+ detect-libc: 1.0.3
+ is-glob: 4.0.3
+ micromatch: 4.0.8
+ node-addon-api: 7.1.1
+ optionalDependencies:
+ '@parcel/watcher-android-arm64': 2.5.0
+ '@parcel/watcher-darwin-arm64': 2.5.0
+ '@parcel/watcher-darwin-x64': 2.5.0
+ '@parcel/watcher-freebsd-x64': 2.5.0
+ '@parcel/watcher-linux-arm-glibc': 2.5.0
+ '@parcel/watcher-linux-arm-musl': 2.5.0
+ '@parcel/watcher-linux-arm64-glibc': 2.5.0
+ '@parcel/watcher-linux-arm64-musl': 2.5.0
+ '@parcel/watcher-linux-x64-glibc': 2.5.0
+ '@parcel/watcher-linux-x64-musl': 2.5.0
+ '@parcel/watcher-win32-arm64': 2.5.0
+ '@parcel/watcher-win32-ia32': 2.5.0
+ '@parcel/watcher-win32-x64': 2.5.0
+ optional: true
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
+ '@pkgr/core@0.1.1': {}
+
+ '@polka/url@1.0.0-next.28': {}
+
+ '@purge-icons/core@0.10.0':
+ dependencies:
+ '@iconify/iconify': 2.1.2
+ axios: 0.26.1(debug@4.3.7)
+ debug: 4.3.7
+ fast-glob: 3.3.2
+ fs-extra: 10.1.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ '@purge-icons/generated@0.10.0':
+ dependencies:
+ '@iconify/iconify': 3.1.1
+
+ '@purge-icons/generated@0.9.0':
+ dependencies:
+ '@iconify/iconify': 3.1.1
+
+ '@quansync/fs@0.1.1':
+ dependencies:
+ quansync: 0.2.8
+
+ '@rollup/plugin-virtual@3.0.2(rollup@4.27.4)':
+ optionalDependencies:
+ rollup: 4.27.4
+
+ '@rollup/pluginutils@4.2.1':
+ dependencies:
+ estree-walker: 2.0.2
+ picomatch: 2.3.1
+
+ '@rollup/pluginutils@5.1.3(rollup@4.27.4)':
+ dependencies:
+ '@types/estree': 1.0.6
+ estree-walker: 2.0.2
+ picomatch: 4.0.2
+ optionalDependencies:
+ rollup: 4.27.4
+
+ '@rollup/rollup-android-arm-eabi@4.27.4':
+ optional: true
+
+ '@rollup/rollup-android-arm64@4.27.4':
+ optional: true
+
+ '@rollup/rollup-darwin-arm64@4.27.4':
+ optional: true
+
+ '@rollup/rollup-darwin-x64@4.27.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-arm64@4.27.4':
+ optional: true
+
+ '@rollup/rollup-freebsd-x64@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-gnueabihf@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm-musleabihf@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-gnu@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-arm64-musl@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-powerpc64le-gnu@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-riscv64-gnu@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-s390x-gnu@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-gnu@4.27.4':
+ optional: true
+
+ '@rollup/rollup-linux-x64-musl@4.27.4':
+ optional: true
+
+ '@rollup/rollup-win32-arm64-msvc@4.27.4':
+ optional: true
+
+ '@rollup/rollup-win32-ia32-msvc@4.27.4':
+ optional: true
+
+ '@rollup/rollup-win32-x64-msvc@4.27.4':
+ optional: true
+
+ '@sinclair/typebox@0.27.8': {}
+
+ '@sphinxxxx/color-conversion@2.2.2': {}
+
+ '@swc/core-darwin-arm64@1.9.3':
+ optional: true
+
+ '@swc/core-darwin-x64@1.9.3':
+ optional: true
+
+ '@swc/core-linux-arm-gnueabihf@1.9.3':
+ optional: true
+
+ '@swc/core-linux-arm64-gnu@1.9.3':
+ optional: true
+
+ '@swc/core-linux-arm64-musl@1.9.3':
+ optional: true
+
+ '@swc/core-linux-x64-gnu@1.9.3':
+ optional: true
+
+ '@swc/core-linux-x64-musl@1.9.3':
+ optional: true
+
+ '@swc/core-win32-arm64-msvc@1.9.3':
+ optional: true
+
+ '@swc/core-win32-ia32-msvc@1.9.3':
+ optional: true
+
+ '@swc/core-win32-x64-msvc@1.9.3':
+ optional: true
+
+ '@swc/core@1.9.3':
+ dependencies:
+ '@swc/counter': 0.1.3
+ '@swc/types': 0.1.17
+ optionalDependencies:
+ '@swc/core-darwin-arm64': 1.9.3
+ '@swc/core-darwin-x64': 1.9.3
+ '@swc/core-linux-arm-gnueabihf': 1.9.3
+ '@swc/core-linux-arm64-gnu': 1.9.3
+ '@swc/core-linux-arm64-musl': 1.9.3
+ '@swc/core-linux-x64-gnu': 1.9.3
+ '@swc/core-linux-x64-musl': 1.9.3
+ '@swc/core-win32-arm64-msvc': 1.9.3
+ '@swc/core-win32-ia32-msvc': 1.9.3
+ '@swc/core-win32-x64-msvc': 1.9.3
+
+ '@swc/counter@0.1.3': {}
+
+ '@swc/types@0.1.17':
+ dependencies:
+ '@swc/counter': 0.1.3
+
+ '@sxzz/popperjs-es@2.11.7': {}
+
+ '@transloadit/prettier-bytes@0.0.7': {}
+
+ '@trysound/sax@0.2.0': {}
+
+ '@types/ace@0.0.52': {}
+
+ '@types/conventional-commits-parser@5.0.1':
+ dependencies:
+ '@types/node': 20.17.9
+
+ '@types/d3-array@3.2.1': {}
+
+ '@types/d3-axis@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-brush@3.0.6':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-chord@3.0.6': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-contour@3.0.6':
+ dependencies:
+ '@types/d3-array': 3.2.1
+ '@types/geojson': 7946.0.14
+
+ '@types/d3-delaunay@6.0.4': {}
+
+ '@types/d3-dispatch@3.0.6': {}
+
+ '@types/d3-drag@3.0.7':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-dsv@3.0.7': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-fetch@3.0.7':
+ dependencies:
+ '@types/d3-dsv': 3.0.7
+
+ '@types/d3-force@3.0.10': {}
+
+ '@types/d3-format@3.0.4': {}
+
+ '@types/d3-geo@3.1.0':
+ dependencies:
+ '@types/geojson': 7946.0.14
+
+ '@types/d3-hierarchy@3.1.7': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.0': {}
+
+ '@types/d3-polygon@3.0.2': {}
+
+ '@types/d3-quadtree@3.0.6': {}
+
+ '@types/d3-random@3.0.3': {}
+
+ '@types/d3-scale-chromatic@3.1.0': {}
+
+ '@types/d3-scale@4.0.8':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-selection@3.0.11': {}
+
+ '@types/d3-shape@3.1.6':
+ dependencies:
+ '@types/d3-path': 3.1.0
+
+ '@types/d3-time-format@4.0.3': {}
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
+ '@types/d3-transition@3.0.9':
+ dependencies:
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3-zoom@3.0.8':
+ dependencies:
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-selection': 3.0.11
+
+ '@types/d3@7.4.3':
+ dependencies:
+ '@types/d3-array': 3.2.1
+ '@types/d3-axis': 3.0.6
+ '@types/d3-brush': 3.0.6
+ '@types/d3-chord': 3.0.6
+ '@types/d3-color': 3.1.3
+ '@types/d3-contour': 3.0.6
+ '@types/d3-delaunay': 6.0.4
+ '@types/d3-dispatch': 3.0.6
+ '@types/d3-drag': 3.0.7
+ '@types/d3-dsv': 3.0.7
+ '@types/d3-ease': 3.0.2
+ '@types/d3-fetch': 3.0.7
+ '@types/d3-force': 3.0.10
+ '@types/d3-format': 3.0.4
+ '@types/d3-geo': 3.1.0
+ '@types/d3-hierarchy': 3.1.7
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-path': 3.1.0
+ '@types/d3-polygon': 3.0.2
+ '@types/d3-quadtree': 3.0.6
+ '@types/d3-random': 3.0.3
+ '@types/d3-scale': 4.0.8
+ '@types/d3-scale-chromatic': 3.1.0
+ '@types/d3-selection': 3.0.11
+ '@types/d3-shape': 3.1.6
+ '@types/d3-time': 3.0.4
+ '@types/d3-time-format': 4.0.3
+ '@types/d3-timer': 3.0.2
+ '@types/d3-transition': 3.0.9
+ '@types/d3-zoom': 3.0.8
+
+ '@types/eslint@8.56.12':
+ dependencies:
+ '@types/estree': 1.0.6
+ '@types/json-schema': 7.0.15
+
+ '@types/estree@1.0.6': {}
+
+ '@types/event-emitter@0.3.5': {}
+
+ '@types/geojson@7946.0.14': {}
+
+ '@types/json-schema@7.0.15': {}
+
+ '@types/jsoneditor@9.9.6':
+ dependencies:
+ '@types/ace': 0.0.52
+ ajv: 6.12.6
+
+ '@types/lodash-es@4.17.12':
+ dependencies:
+ '@types/lodash': 4.17.13
+
+ '@types/lodash@4.17.13': {}
+
+ '@types/node@10.17.60': {}
+
+ '@types/node@20.17.9':
+ dependencies:
+ undici-types: 6.19.8
+
+ '@types/nprogress@0.2.3': {}
+
+ '@types/qrcode@1.5.5':
+ dependencies:
+ '@types/node': 20.17.9
+
+ '@types/qs@6.9.17': {}
+
+ '@types/semver@7.5.8': {}
+
+ '@types/trusted-types@2.0.7':
+ optional: true
+
+ '@types/video.js@7.3.58': {}
+
+ '@types/web-bluetooth@0.0.16': {}
+
+ '@types/web-bluetooth@0.0.20': {}
+
+ '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@eslint-community/regexpp': 4.12.1
+ '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.3.3)
+ '@typescript-eslint/scope-manager': 7.18.0
+ '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.3.3)
+ '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.3.3)
+ '@typescript-eslint/visitor-keys': 7.18.0
+ eslint: 8.57.1
+ graphemer: 1.4.0
+ ignore: 5.3.2
+ natural-compare: 1.4.0
+ ts-api-utils: 1.4.3(typescript@5.3.3)
+ optionalDependencies:
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.3.7
+ eslint: 8.57.1
+ optionalDependencies:
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/scope-manager': 7.18.0
+ '@typescript-eslint/types': 7.18.0
+ '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3)
+ '@typescript-eslint/visitor-keys': 7.18.0
+ debug: 4.3.7
+ eslint: 8.57.1
+ optionalDependencies:
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/scope-manager@6.21.0':
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/visitor-keys': 6.21.0
+
+ '@typescript-eslint/scope-manager@7.18.0':
+ dependencies:
+ '@typescript-eslint/types': 7.18.0
+ '@typescript-eslint/visitor-keys': 7.18.0
+
+ '@typescript-eslint/scope-manager@8.26.1':
+ dependencies:
+ '@typescript-eslint/types': 8.26.1
+ '@typescript-eslint/visitor-keys': 8.26.1
+
+ '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3)
+ '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.3.3)
+ debug: 4.3.7
+ eslint: 8.57.1
+ ts-api-utils: 1.4.3(typescript@5.3.3)
+ optionalDependencies:
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/types@6.21.0': {}
+
+ '@typescript-eslint/types@7.18.0': {}
+
+ '@typescript-eslint/types@8.26.1': {}
+
+ '@typescript-eslint/typescript-estree@6.21.0(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/visitor-keys': 6.21.0
+ debug: 4.3.7
+ globby: 11.1.0
+ is-glob: 4.0.3
+ minimatch: 9.0.3
+ semver: 7.6.3
+ ts-api-utils: 1.4.3(typescript@5.3.3)
+ optionalDependencies:
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/typescript-estree@7.18.0(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/types': 7.18.0
+ '@typescript-eslint/visitor-keys': 7.18.0
+ debug: 4.3.7
+ globby: 11.1.0
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.6.3
+ ts-api-utils: 1.4.3(typescript@5.3.3)
+ optionalDependencies:
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/typescript-estree@8.26.1(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/types': 8.26.1
+ '@typescript-eslint/visitor-keys': 8.26.1
+ debug: 4.3.7
+ fast-glob: 3.3.2
+ is-glob: 4.0.3
+ minimatch: 9.0.5
+ semver: 7.6.3
+ ts-api-utils: 2.0.1(typescript@5.3.3)
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
+ '@types/json-schema': 7.0.15
+ '@types/semver': 7.5.8
+ '@typescript-eslint/scope-manager': 6.21.0
+ '@typescript-eslint/types': 6.21.0
+ '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3)
+ eslint: 8.57.1
+ semver: 7.6.3
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
+ '@typescript-eslint/scope-manager': 7.18.0
+ '@typescript-eslint/types': 7.18.0
+ '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3)
+ eslint: 8.57.1
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@typescript-eslint/utils@8.26.1(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
+ '@typescript-eslint/scope-manager': 8.26.1
+ '@typescript-eslint/types': 8.26.1
+ '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.3.3)
+ eslint: 8.57.1
+ typescript: 5.3.3
+ transitivePeerDependencies:
+ - supports-color
+
+ '@typescript-eslint/visitor-keys@6.21.0':
+ dependencies:
+ '@typescript-eslint/types': 6.21.0
+ eslint-visitor-keys: 3.4.3
+
+ '@typescript-eslint/visitor-keys@7.18.0':
+ dependencies:
+ '@typescript-eslint/types': 7.18.0
+ eslint-visitor-keys: 3.4.3
+
+ '@typescript-eslint/visitor-keys@8.26.1':
+ dependencies:
+ '@typescript-eslint/types': 8.26.1
+ eslint-visitor-keys: 4.2.0
+
+ '@ungap/structured-clone@1.2.0': {}
+
+ '@unocss/astro@0.58.9(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/reset': 0.58.9
+ '@unocss/vite': 0.58.9(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ optionalDependencies:
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - rollup
+
+ '@unocss/cli@0.58.9(rollup@4.27.4)':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ '@unocss/config': 0.58.9
+ '@unocss/core': 0.58.9
+ '@unocss/preset-uno': 0.58.9
+ cac: 6.7.14
+ chokidar: 3.6.0
+ colorette: 2.0.20
+ consola: 3.2.3
+ fast-glob: 3.3.2
+ magic-string: 0.30.14
+ pathe: 1.1.2
+ perfect-debounce: 1.0.0
+ transitivePeerDependencies:
+ - rollup
+
+ '@unocss/config@0.57.7':
+ dependencies:
+ '@unocss/core': 0.57.7
+ unconfig: 0.3.13
+
+ '@unocss/config@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ unconfig: 0.3.13
+
+ '@unocss/config@66.1.0-beta.5':
+ dependencies:
+ '@unocss/core': 66.1.0-beta.5
+ unconfig: 7.3.1
+
+ '@unocss/core@0.57.7': {}
+
+ '@unocss/core@0.58.9': {}
+
+ '@unocss/core@66.1.0-beta.5': {}
+
+ '@unocss/eslint-config@0.57.7(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@unocss/eslint-plugin': 0.57.7(eslint@8.57.1)(typescript@5.3.3)
+ transitivePeerDependencies:
+ - eslint
+ - supports-color
+ - typescript
+
+ '@unocss/eslint-plugin@0.57.7(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.3.3)
+ '@unocss/config': 0.57.7
+ '@unocss/core': 0.57.7
+ magic-string: 0.30.14
+ synckit: 0.8.8
+ transitivePeerDependencies:
+ - eslint
+ - supports-color
+ - typescript
+
+ '@unocss/eslint-plugin@66.1.0-beta.5(eslint@8.57.1)(typescript@5.3.3)':
+ dependencies:
+ '@typescript-eslint/utils': 8.26.1(eslint@8.57.1)(typescript@5.3.3)
+ '@unocss/config': 66.1.0-beta.5
+ '@unocss/core': 66.1.0-beta.5
+ '@unocss/rule-utils': 66.1.0-beta.5
+ magic-string: 0.30.17
+ synckit: 0.9.2
+ transitivePeerDependencies:
+ - eslint
+ - supports-color
+ - typescript
+
+ '@unocss/extractor-arbitrary-variants@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+
+ '@unocss/inspector@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/rule-utils': 0.58.9
+ gzip-size: 6.0.0
+ sirv: 2.0.4
+
+ '@unocss/postcss@0.58.9(postcss@8.4.49)':
+ dependencies:
+ '@unocss/config': 0.58.9
+ '@unocss/core': 0.58.9
+ '@unocss/rule-utils': 0.58.9
+ css-tree: 2.3.1
+ fast-glob: 3.3.2
+ magic-string: 0.30.14
+ postcss: 8.4.49
+
+ '@unocss/preset-attributify@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+
+ '@unocss/preset-icons@0.58.9':
+ dependencies:
+ '@iconify/utils': 2.1.33
+ '@unocss/core': 0.58.9
+ ofetch: 1.4.1
+ transitivePeerDependencies:
+ - supports-color
+
+ '@unocss/preset-mini@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/extractor-arbitrary-variants': 0.58.9
+ '@unocss/rule-utils': 0.58.9
+
+ '@unocss/preset-tagify@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+
+ '@unocss/preset-typography@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/preset-mini': 0.58.9
+
+ '@unocss/preset-uno@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/preset-mini': 0.58.9
+ '@unocss/preset-wind': 0.58.9
+ '@unocss/rule-utils': 0.58.9
+
+ '@unocss/preset-web-fonts@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ ofetch: 1.4.1
+
+ '@unocss/preset-wind@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/preset-mini': 0.58.9
+ '@unocss/rule-utils': 0.58.9
+
+ '@unocss/reset@0.58.9': {}
+
+ '@unocss/rule-utils@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ magic-string: 0.30.14
+
+ '@unocss/rule-utils@66.1.0-beta.5':
+ dependencies:
+ '@unocss/core': 66.1.0-beta.5
+ magic-string: 0.30.17
+
+ '@unocss/scope@0.58.9': {}
+
+ '@unocss/transformer-attributify-jsx-babel@0.58.9':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0)
+ '@babel/preset-typescript': 7.26.0(@babel/core@7.26.0)
+ '@unocss/core': 0.58.9
+ transitivePeerDependencies:
+ - supports-color
+
+ '@unocss/transformer-attributify-jsx@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+
+ '@unocss/transformer-compile-class@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+
+ '@unocss/transformer-directives@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+ '@unocss/rule-utils': 0.58.9
+ css-tree: 2.3.1
+
+ '@unocss/transformer-variant-group@0.58.9':
+ dependencies:
+ '@unocss/core': 0.58.9
+
+ '@unocss/vite@0.58.9(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))':
+ dependencies:
+ '@ampproject/remapping': 2.3.0
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ '@unocss/config': 0.58.9
+ '@unocss/core': 0.58.9
+ '@unocss/inspector': 0.58.9
+ '@unocss/scope': 0.58.9
+ '@unocss/transformer-directives': 0.58.9
+ chokidar: 3.6.0
+ fast-glob: 3.3.2
+ magic-string: 0.30.14
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - rollup
+
+ '@uppy/companion-client@2.2.2':
+ dependencies:
+ '@uppy/utils': 4.1.3
+ namespace-emitter: 2.0.1
+
+ '@uppy/core@2.3.4':
+ dependencies:
+ '@transloadit/prettier-bytes': 0.0.7
+ '@uppy/store-default': 2.1.1
+ '@uppy/utils': 4.1.3
+ lodash.throttle: 4.1.1
+ mime-match: 1.0.2
+ namespace-emitter: 2.0.1
+ nanoid: 3.3.8
+ preact: 10.25.0
+
+ '@uppy/store-default@2.1.1': {}
+
+ '@uppy/utils@4.1.3':
+ dependencies:
+ lodash.throttle: 4.1.1
+
+ '@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)':
+ dependencies:
+ '@uppy/companion-client': 2.2.2
+ '@uppy/core': 2.3.4
+ '@uppy/utils': 4.1.3
+ nanoid: 3.3.8
+
+ '@videojs-player/vue@1.0.0(@types/video.js@7.3.58)(video.js@7.21.6)(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@types/video.js': 7.3.58
+ video.js: 7.21.6
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@videojs/http-streaming@2.16.3(video.js@7.21.6)':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@videojs/vhs-utils': 3.0.5
+ aes-decrypter: 3.1.3
+ global: 4.4.0
+ m3u8-parser: 4.8.0
+ mpd-parser: 0.22.1
+ mux.js: 6.0.1
+ video.js: 7.21.6
+
+ '@videojs/vhs-utils@3.0.5':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ global: 4.4.0
+ url-toolkit: 2.2.5
+
+ '@videojs/xhr@2.6.0':
+ dependencies:
+ '@babel/runtime': 7.26.0
+ global: 4.4.0
+ is-function: 1.0.2
+
+ '@vitejs/plugin-legacy@5.4.3(terser@5.36.0)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/preset-env': 7.26.0(@babel/core@7.26.0)
+ browserslist: 4.24.2
+ browserslist-to-esbuild: 2.1.1(browserslist@4.24.2)
+ core-js: 3.39.0
+ magic-string: 0.30.14
+ regenerator-runtime: 0.14.1
+ systemjs: 6.15.1
+ terser: 5.36.0
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitejs/plugin-vue-jsx@3.1.0(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/plugin-transform-typescript': 7.25.9(@babel/core@7.26.0)
+ '@vue/babel-plugin-jsx': 1.2.5(@babel/core@7.26.0)
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ vue: 3.5.12(typescript@5.3.3)
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vitejs/plugin-vue@5.2.1(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@volar/language-core@1.11.1':
+ dependencies:
+ '@volar/source-map': 1.11.1
+
+ '@volar/source-map@1.11.1':
+ dependencies:
+ muggle-string: 0.3.1
+
+ '@volar/typescript@1.11.1':
+ dependencies:
+ '@volar/language-core': 1.11.1
+ path-browserify: 1.0.1
+
+ '@vue/babel-helper-vue-transform-on@1.2.5': {}
+
+ '@vue/babel-plugin-jsx@1.2.5(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0)
+ '@babel/template': 7.25.9
+ '@babel/traverse': 7.25.9
+ '@babel/types': 7.26.0
+ '@vue/babel-helper-vue-transform-on': 1.2.5
+ '@vue/babel-plugin-resolve-type': 1.2.5(@babel/core@7.26.0)
+ html-tags: 3.3.1
+ svg-tags: 1.0.0
+ optionalDependencies:
+ '@babel/core': 7.26.0
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vue/babel-plugin-resolve-type@1.2.5(@babel/core@7.26.0)':
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ '@babel/core': 7.26.0
+ '@babel/helper-module-imports': 7.25.9
+ '@babel/helper-plugin-utils': 7.25.9
+ '@babel/parser': 7.26.2
+ '@vue/compiler-sfc': 3.5.13
+ transitivePeerDependencies:
+ - supports-color
+
+ '@vue/compiler-core@3.5.12':
+ dependencies:
+ '@babel/parser': 7.26.2
+ '@vue/shared': 3.5.12
+ entities: 4.5.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-core@3.5.13':
+ dependencies:
+ '@babel/parser': 7.26.2
+ '@vue/shared': 3.5.13
+ entities: 4.5.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
+
+ '@vue/compiler-dom@3.5.12':
+ dependencies:
+ '@vue/compiler-core': 3.5.12
+ '@vue/shared': 3.5.12
+
+ '@vue/compiler-dom@3.5.13':
+ dependencies:
+ '@vue/compiler-core': 3.5.13
+ '@vue/shared': 3.5.13
+
+ '@vue/compiler-sfc@3.5.12':
+ dependencies:
+ '@babel/parser': 7.26.2
+ '@vue/compiler-core': 3.5.12
+ '@vue/compiler-dom': 3.5.12
+ '@vue/compiler-ssr': 3.5.12
+ '@vue/shared': 3.5.12
+ estree-walker: 2.0.2
+ magic-string: 0.30.14
+ postcss: 8.4.49
+ source-map-js: 1.2.1
+
+ '@vue/compiler-sfc@3.5.13':
+ dependencies:
+ '@babel/parser': 7.26.2
+ '@vue/compiler-core': 3.5.13
+ '@vue/compiler-dom': 3.5.13
+ '@vue/compiler-ssr': 3.5.13
+ '@vue/shared': 3.5.13
+ estree-walker: 2.0.2
+ magic-string: 0.30.14
+ postcss: 8.4.49
+ source-map-js: 1.2.1
+
+ '@vue/compiler-ssr@3.5.12':
+ dependencies:
+ '@vue/compiler-dom': 3.5.12
+ '@vue/shared': 3.5.12
+
+ '@vue/compiler-ssr@3.5.13':
+ dependencies:
+ '@vue/compiler-dom': 3.5.13
+ '@vue/shared': 3.5.13
+
+ '@vue/devtools-api@6.6.4': {}
+
+ '@vue/language-core@1.8.27(typescript@5.3.3)':
+ dependencies:
+ '@volar/language-core': 1.11.1
+ '@volar/source-map': 1.11.1
+ '@vue/compiler-dom': 3.5.13
+ '@vue/shared': 3.5.13
+ computeds: 0.0.1
+ minimatch: 9.0.5
+ muggle-string: 0.3.1
+ path-browserify: 1.0.1
+ vue-template-compiler: 2.7.16
+ optionalDependencies:
+ typescript: 5.3.3
+
+ '@vue/reactivity@3.5.12':
+ dependencies:
+ '@vue/shared': 3.5.12
+
+ '@vue/runtime-core@3.5.12':
+ dependencies:
+ '@vue/reactivity': 3.5.12
+ '@vue/shared': 3.5.12
+
+ '@vue/runtime-dom@3.5.12':
+ dependencies:
+ '@vue/reactivity': 3.5.12
+ '@vue/runtime-core': 3.5.12
+ '@vue/shared': 3.5.12
+ csstype: 3.1.3
+
+ '@vue/server-renderer@3.5.12(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@vue/compiler-ssr': 3.5.12
+ '@vue/shared': 3.5.12
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@vue/shared@3.5.12': {}
+
+ '@vue/shared@3.5.13': {}
+
+ '@vueuse/core@10.11.1(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@types/web-bluetooth': 0.0.20
+ '@vueuse/metadata': 10.11.1
+ '@vueuse/shared': 10.11.1(vue@3.5.12(typescript@5.3.3))
+ vue-demi: 0.14.10(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+
+ '@vueuse/core@9.13.0(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@types/web-bluetooth': 0.0.16
+ '@vueuse/metadata': 9.13.0
+ '@vueuse/shared': 9.13.0(vue@3.5.12(typescript@5.3.3))
+ vue-demi: 0.14.10(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+
+ '@vueuse/metadata@10.11.1': {}
+
+ '@vueuse/metadata@9.13.0': {}
+
+ '@vueuse/shared@10.11.1(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ vue-demi: 0.14.10(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+
+ '@vueuse/shared@9.13.0(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ vue-demi: 0.14.10(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+ - vue
+
+ '@wangeditor-next/basic-modules@1.5.46(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ is-url: 1.2.4
+ lodash.throttle: 4.1.1
+ nanoid: 5.1.6
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/code-highlight@1.3.43(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ prismjs: 1.29.0
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@types/event-emitter': 0.3.5
+ '@uppy/core': 2.3.4
+ '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
+ dom7: 4.0.6
+ event-emitter: 0.3.5
+ html-void-elements: 3.0.0
+ i18next: 23.16.8
+ is-hotkey: 0.2.0
+ lodash.camelcase: 4.3.0
+ lodash.clonedeep: 4.5.0
+ lodash.debounce: 4.0.8
+ lodash.foreach: 4.5.0
+ lodash.throttle: 4.1.1
+ lodash.toarray: 4.4.0
+ nanoid: 5.1.6
+ scroll-into-view-if-needed: 3.1.0
+ slate: 0.82.1
+ slate-history: 0.109.0(slate@0.82.1)
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/editor-for-vue@5.1.14(@wangeditor-next/editor@5.6.46)(vue@3.5.12(typescript@5.3.3))':
+ dependencies:
+ '@wangeditor-next/editor': 5.6.46
+ vue: 3.5.12(typescript@5.3.3)
+
+ '@wangeditor-next/editor@5.6.46':
+ dependencies:
+ '@uppy/core': 2.3.4
+ '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
+ '@wangeditor-next/basic-modules': 1.5.46(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/code-highlight': 1.3.43(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/list-module': 1.1.51(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/table-module': 1.6.58(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/upload-image-module': 1.1.49(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@1.5.46(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/video-module': 1.3.51(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ is-hotkey: 0.2.0
+ lodash.camelcase: 4.3.0
+ lodash.clonedeep: 4.5.0
+ lodash.debounce: 4.0.8
+ lodash.foreach: 4.5.0
+ lodash.throttle: 4.1.1
+ lodash.toarray: 4.4.0
+ nanoid: 5.1.6
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/list-module@1.1.51(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/plugin-mention@1.0.16(@wangeditor-next/editor@5.6.46)(snabbdom@3.6.2)':
+ dependencies:
+ '@wangeditor-next/editor': 5.6.46
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/table-module@1.6.58(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.debounce@4.0.8)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ lodash.debounce: 4.0.8
+ lodash.throttle: 4.1.1
+ nanoid: 5.1.6
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/upload-image-module@1.1.49(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/basic-modules@1.5.46(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.foreach@4.5.0)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@uppy/core': 2.3.4
+ '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
+ '@wangeditor-next/basic-modules': 1.5.46(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(lodash.throttle@4.1.1)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ lodash.foreach: 4.5.0
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@wangeditor-next/video-module@1.3.51(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor-next/core@1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2))(dom7@4.0.6)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)':
+ dependencies:
+ '@uppy/core': 2.3.4
+ '@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
+ '@wangeditor-next/core': 1.7.45(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@4.0.6)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@5.1.6)(slate@0.82.1)(snabbdom@3.6.2)
+ dom7: 4.0.6
+ nanoid: 5.1.6
+ slate: 0.82.1
+ snabbdom: 3.6.2
+
+ '@xmldom/xmldom@0.8.10': {}
+
+ '@zxcvbn-ts/core@3.0.4':
+ dependencies:
+ fastest-levenshtein: 1.0.16
+
+ JSONStream@1.3.5:
+ dependencies:
+ jsonparse: 1.3.1
+ through: 2.3.8
+
+ ace-builds@1.39.1: {}
+
+ acorn-jsx@5.3.2(acorn@8.14.0):
+ dependencies:
+ acorn: 8.14.0
+
+ acorn@8.14.0: {}
+
+ aes-decrypter@3.1.3:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@videojs/vhs-utils': 3.0.5
+ global: 4.4.0
+ pkcs7: 1.0.4
+
+ ajv@6.12.6:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-json-stable-stringify: 2.1.0
+ json-schema-traverse: 0.4.1
+ uri-js: 4.4.1
+
+ ajv@8.17.1:
+ dependencies:
+ fast-deep-equal: 3.1.3
+ fast-uri: 3.0.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+
+ animate.css@4.1.1: {}
+
+ ansi-escapes@7.0.0:
+ dependencies:
+ environment: 1.1.0
+
+ ansi-regex@2.1.1: {}
+
+ ansi-regex@5.0.1: {}
+
+ ansi-regex@6.1.0: {}
+
+ ansi-styles@2.2.1: {}
+
+ ansi-styles@4.3.0:
+ dependencies:
+ color-convert: 2.0.1
+
+ ansi-styles@5.2.0: {}
+
+ ansi-styles@6.2.1: {}
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.1
+
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
+ argparse@2.0.1: {}
+
+ array-ify@1.0.0: {}
+
+ array-move@4.0.0: {}
+
+ array-union@2.1.0: {}
+
+ astral-regex@2.0.0: {}
+
+ async-validator@4.2.5: {}
+
+ async@3.2.6: {}
+
+ asynckit@0.4.0: {}
+
+ autolinker@3.16.2:
+ dependencies:
+ tslib: 2.8.1
+
+ autoprefixer@10.4.20(postcss@8.4.49):
+ dependencies:
+ browserslist: 4.24.2
+ caniuse-lite: 1.0.30001684
+ fraction.js: 4.3.7
+ normalize-range: 0.1.2
+ picocolors: 1.1.1
+ postcss: 8.4.49
+ postcss-value-parser: 4.2.0
+
+ axios@0.26.1(debug@4.3.7):
+ dependencies:
+ follow-redirects: 1.15.9(debug@4.3.7)
+ transitivePeerDependencies:
+ - debug
+
+ axios@1.9.0:
+ dependencies:
+ follow-redirects: 1.15.9(debug@4.3.7)
+ form-data: 4.0.1
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
+ babel-plugin-polyfill-corejs2@0.4.12(@babel/core@7.26.0):
+ dependencies:
+ '@babel/compat-data': 7.26.2
+ '@babel/core': 7.26.0
+ '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0)
+ semver: 6.3.1
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-corejs3@0.10.6(@babel/core@7.26.0):
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0)
+ core-js-compat: 3.39.0
+ transitivePeerDependencies:
+ - supports-color
+
+ babel-plugin-polyfill-regenerator@0.6.3(@babel/core@7.26.0):
+ dependencies:
+ '@babel/core': 7.26.0
+ '@babel/helper-define-polyfill-provider': 0.6.3(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ balanced-match@1.0.2: {}
+
+ balanced-match@2.0.0: {}
+
+ benz-amr-recorder@1.1.5:
+ dependencies:
+ benz-recorderjs: 1.0.5
+
+ benz-recorderjs@1.0.5: {}
+
+ binary-extensions@2.3.0: {}
+
+ boolbase@1.0.0: {}
+
+ bpmn-js-properties-panel@5.23.0(@bpmn-io/properties-panel@3.25.0(@lezer/common@1.2.3))(bpmn-js@17.11.1)(camunda-bpmn-js-behaviors@1.7.2(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.7.0))(diagram-js@12.8.1):
+ dependencies:
+ '@bpmn-io/extract-process-variables': 0.8.0
+ '@bpmn-io/properties-panel': 3.25.0(@lezer/common@1.2.3)
+ array-move: 4.0.0
+ bpmn-js: 17.11.1
+ camunda-bpmn-js-behaviors: 1.7.2(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.7.0)
+ diagram-js: 12.8.1
+ ids: 1.0.5
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+
+ bpmn-js-token-simulation@0.36.2:
+ dependencies:
+ inherits-browser: 0.1.0
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+ randomcolor: 0.6.2
+
+ bpmn-js@17.11.1:
+ dependencies:
+ bpmn-moddle: 8.1.0
+ diagram-js: 14.11.3
+ diagram-js-direct-editing: 3.2.0(diagram-js@14.11.3)
+ ids: 1.0.5
+ inherits-browser: 0.1.0
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+ tiny-svg: 3.1.3
+
+ bpmn-moddle@8.1.0:
+ dependencies:
+ min-dash: 4.2.2
+ moddle: 6.2.3
+ moddle-xml: 10.1.0
+
+ brace-expansion@1.1.11:
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+
+ brace-expansion@2.0.1:
+ dependencies:
+ balanced-match: 1.0.2
+
+ braces@3.0.3:
+ dependencies:
+ fill-range: 7.1.1
+
+ browserslist-to-esbuild@2.1.1(browserslist@4.24.2):
+ dependencies:
+ browserslist: 4.24.2
+ meow: 13.2.0
+
+ browserslist@4.24.2:
+ dependencies:
+ caniuse-lite: 1.0.30001684
+ electron-to-chromium: 1.5.67
+ node-releases: 2.0.18
+ update-browserslist-db: 1.1.1(browserslist@4.24.2)
+
+ buffer-from@1.1.2: {}
+
+ cac@6.7.14: {}
+
+ call-bind@1.0.7:
+ dependencies:
+ es-define-property: 1.0.0
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.4
+ set-function-length: 1.2.2
+
+ callsites@3.1.0: {}
+
+ camelcase@5.3.1: {}
+
+ camunda-bpmn-js-behaviors@1.7.2(bpmn-js@17.11.1)(camunda-bpmn-moddle@7.0.1)(zeebe-bpmn-moddle@1.7.0):
+ dependencies:
+ bpmn-js: 17.11.1
+ camunda-bpmn-moddle: 7.0.1
+ ids: 1.0.5
+ min-dash: 4.2.2
+ zeebe-bpmn-moddle: 1.7.0
+
+ camunda-bpmn-moddle@7.0.1: {}
+
+ caniuse-lite@1.0.30001684: {}
+
+ chalk@1.1.3:
+ dependencies:
+ ansi-styles: 2.2.1
+ escape-string-regexp: 1.0.5
+ has-ansi: 2.0.0
+ strip-ansi: 3.0.1
+ supports-color: 2.0.0
+
+ chalk@4.1.2:
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+
+ chalk@5.3.0: {}
+
+ cheerio-select@2.1.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-select: 5.1.0
+ css-what: 6.1.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.1.0
+
+ cheerio@1.0.0-rc.12:
+ dependencies:
+ cheerio-select: 2.1.0
+ dom-serializer: 2.0.0
+ domhandler: 5.0.3
+ domutils: 3.1.0
+ htmlparser2: 8.0.2
+ parse5: 7.2.1
+ parse5-htmlparser2-tree-adapter: 7.1.0
+
+ chokidar@3.6.0:
+ dependencies:
+ anymatch: 3.1.3
+ braces: 3.0.3
+ glob-parent: 5.1.2
+ is-binary-path: 2.1.0
+ is-glob: 4.0.3
+ normalize-path: 3.0.0
+ readdirp: 3.6.0
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ chokidar@4.0.1:
+ dependencies:
+ readdirp: 4.0.2
+
+ classnames@2.5.1: {}
+
+ cli-cursor@5.0.0:
+ dependencies:
+ restore-cursor: 5.1.0
+
+ cli-truncate@4.0.0:
+ dependencies:
+ slice-ansi: 5.0.0
+ string-width: 7.2.0
+
+ cliui@6.0.0:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+
+ cliui@8.0.1:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+
+ clsx@2.1.1: {}
+
+ codemirror@6.65.7: {}
+
+ color-convert@2.0.1:
+ dependencies:
+ color-name: 1.1.4
+
+ color-name@1.1.4: {}
+
+ colord@2.9.3: {}
+
+ colorette@2.0.20: {}
+
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ commander@12.1.0: {}
+
+ commander@2.20.3: {}
+
+ commander@7.2.0: {}
+
+ commander@8.3.0: {}
+
+ common-tags@1.8.2: {}
+
+ compare-func@2.0.0:
+ dependencies:
+ array-ify: 1.0.0
+ dot-prop: 5.3.0
+
+ component-event@0.2.1: {}
+
+ compute-scroll-into-view@3.1.1: {}
+
+ computeds@0.0.1: {}
+
+ concat-map@0.0.1: {}
+
+ confbox@0.1.8: {}
+
+ consola@3.2.3: {}
+
+ conventional-changelog-angular@7.0.0:
+ dependencies:
+ compare-func: 2.0.0
+
+ conventional-changelog-conventionalcommits@7.0.2:
+ dependencies:
+ compare-func: 2.0.0
+
+ conventional-commits-parser@5.0.0:
+ dependencies:
+ JSONStream: 1.3.5
+ is-text-path: 2.0.0
+ meow: 12.1.1
+ split2: 4.2.0
+
+ convert-source-map@2.0.0: {}
+
+ core-js-compat@3.39.0:
+ dependencies:
+ browserslist: 4.24.2
+
+ core-js-pure@3.39.0: {}
+
+ core-js@3.39.0: {}
+
+ cosmiconfig-typescript-loader@5.1.0(@types/node@20.17.9)(cosmiconfig@9.0.0(typescript@5.3.3))(typescript@5.3.3):
+ dependencies:
+ '@types/node': 20.17.9
+ cosmiconfig: 9.0.0(typescript@5.3.3)
+ jiti: 1.21.6
+ typescript: 5.3.3
+
+ cosmiconfig@9.0.0(typescript@5.3.3):
+ dependencies:
+ env-paths: 2.2.1
+ import-fresh: 3.3.0
+ js-yaml: 4.1.0
+ parse-json: 5.2.0
+ optionalDependencies:
+ typescript: 5.3.3
+
+ crelt@1.0.6: {}
+
+ cropperjs@1.6.2: {}
+
+ cross-fetch@3.1.8:
+ dependencies:
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+
+ cross-spawn@7.0.6:
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+
+ crypto-js@4.2.0: {}
+
+ css-functions-list@3.2.3: {}
+
+ css-select@5.1.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.1.0
+ domhandler: 5.0.3
+ domutils: 3.1.0
+ nth-check: 2.1.1
+
+ css-tree@2.2.1:
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.2.1
+
+ css-tree@2.3.1:
+ dependencies:
+ mdn-data: 2.0.30
+ source-map-js: 1.2.1
+
+ css-tree@3.0.1:
+ dependencies:
+ mdn-data: 2.12.1
+ source-map-js: 1.2.1
+
+ css-what@6.1.0: {}
+
+ cssesc@3.0.0: {}
+
+ csso@5.0.5:
+ dependencies:
+ css-tree: 2.2.1
+
+ csstype@3.1.3: {}
+
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-axis@3.0.0: {}
+
+ d3-brush@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3-chord@3.0.1:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-color@3.1.0: {}
+
+ d3-contour@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-delaunay@6.0.4:
+ dependencies:
+ delaunator: 5.0.1
+
+ d3-dispatch@3.0.1: {}
+
+ d3-drag@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+
+ d3-dsv@3.0.1:
+ dependencies:
+ commander: 7.2.0
+ iconv-lite: 0.6.3
+ rw: 1.3.3
+
+ d3-ease@3.0.1: {}
+
+ d3-fetch@3.0.1:
+ dependencies:
+ d3-dsv: 3.0.1
+
+ d3-flextree@2.1.2:
+ dependencies:
+ d3-hierarchy: 1.1.9
+
+ d3-force@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-timer: 3.0.1
+
+ d3-format@3.1.0: {}
+
+ d3-geo@3.1.1:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-hierarchy@1.1.9: {}
+
+ d3-hierarchy@3.1.2: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@3.1.0: {}
+
+ d3-polygon@3.0.1: {}
+
+ d3-quadtree@3.0.1: {}
+
+ d3-random@3.0.1: {}
+
+ d3-scale-chromatic@3.1.0:
+ dependencies:
+ d3-color: 3.1.0
+ d3-interpolate: 3.0.1
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.0
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-selection@3.0.0: {}
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
+ d3-transition@3.0.1(d3-selection@3.0.0):
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+
+ d3-zoom@3.0.0:
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+
+ d3@7.9.0:
+ dependencies:
+ d3-array: 3.2.4
+ d3-axis: 3.0.0
+ d3-brush: 3.0.0
+ d3-chord: 3.0.1
+ d3-color: 3.1.0
+ d3-contour: 4.0.2
+ d3-delaunay: 6.0.4
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-dsv: 3.0.1
+ d3-ease: 3.0.1
+ d3-fetch: 3.0.1
+ d3-force: 3.0.0
+ d3-format: 3.1.0
+ d3-geo: 3.1.1
+ d3-hierarchy: 3.1.2
+ d3-interpolate: 3.0.1
+ d3-path: 3.1.0
+ d3-polygon: 3.0.1
+ d3-quadtree: 3.0.1
+ d3-random: 3.0.1
+ d3-scale: 4.0.2
+ d3-scale-chromatic: 3.1.0
+ d3-selection: 3.0.0
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ d3-timer: 3.0.1
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ d3-zoom: 3.0.0
+
+ d@1.0.2:
+ dependencies:
+ es5-ext: 0.10.64
+ type: 2.7.3
+
+ dargs@8.1.0: {}
+
+ dayjs@1.11.13: {}
+
+ de-indent@1.0.2: {}
+
+ debug@4.3.7:
+ dependencies:
+ ms: 2.1.3
+
+ decamelize@1.2.0: {}
+
+ deep-is@0.1.4: {}
+
+ default-passive-events@2.0.0: {}
+
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.0
+ es-errors: 1.3.0
+ gopd: 1.0.1
+
+ defu@6.1.4: {}
+
+ delaunator@5.0.1:
+ dependencies:
+ robust-predicates: 3.0.2
+
+ delayed-stream@1.0.0: {}
+
+ destr@2.0.3: {}
+
+ detect-libc@1.0.3:
+ optional: true
+
+ diagram-js-direct-editing@3.2.0(diagram-js@14.11.3):
+ dependencies:
+ diagram-js: 14.11.3
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+
+ diagram-js@12.8.1:
+ dependencies:
+ '@bpmn-io/diagram-js-ui': 0.2.3
+ clsx: 2.1.1
+ didi: 9.0.2
+ hammerjs: 2.0.8
+ inherits-browser: 0.1.0
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+ object-refs: 0.3.0
+ path-intersection: 2.2.1
+ tiny-svg: 3.1.3
+
+ diagram-js@14.11.3:
+ dependencies:
+ '@bpmn-io/diagram-js-ui': 0.2.3
+ clsx: 2.1.1
+ didi: 10.2.2
+ inherits-browser: 0.1.0
+ min-dash: 4.2.2
+ min-dom: 4.2.1
+ object-refs: 0.4.0
+ path-intersection: 3.1.0
+ tiny-svg: 3.1.3
+
+ didi@10.2.2: {}
+
+ didi@9.0.2: {}
+
+ dijkstrajs@1.0.3: {}
+
+ dir-glob@3.0.1:
+ dependencies:
+ path-type: 4.0.0
+
+ dlv@1.1.3: {}
+
+ doctrine@3.0.0:
+ dependencies:
+ esutils: 2.0.3
+
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ dom-walk@0.1.2: {}
+
+ dom7@4.0.6:
+ dependencies:
+ ssr-window: 4.0.2
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domify@1.4.2: {}
+
+ domify@2.0.0: {}
+
+ dompurify@3.2.1:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
+ domutils@3.1.0:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ dot-prop@5.3.0:
+ dependencies:
+ is-obj: 2.0.0
+
+ driver.js@1.3.1: {}
+
+ duplexer@0.1.2: {}
+
+ eastasianwidth@0.2.0: {}
+
+ echarts-wordcloud@2.1.0(echarts@5.5.1):
+ dependencies:
+ echarts: 5.5.1
+
+ echarts@5.5.1:
+ dependencies:
+ tslib: 2.3.0
+ zrender: 5.6.0
+
+ ejs@3.1.10:
+ dependencies:
+ jake: 10.9.2
+
+ electron-to-chromium@1.5.67: {}
+
+ element-plus@2.11.1(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ '@ctrl/tinycolor': 3.6.1
+ '@element-plus/icons-vue': 2.3.2(vue@3.5.12(typescript@5.3.3))
+ '@floating-ui/dom': 1.6.12
+ '@popperjs/core': '@sxzz/popperjs-es@2.11.7'
+ '@types/lodash': 4.17.13
+ '@types/lodash-es': 4.17.12
+ '@vueuse/core': 9.13.0(vue@3.5.12(typescript@5.3.3))
+ async-validator: 4.2.5
+ dayjs: 1.11.13
+ escape-html: 1.0.3
+ lodash: 4.17.21
+ lodash-es: 4.17.21
+ lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21)
+ memoize-one: 6.0.0
+ normalize-wheel-es: 1.2.0
+ vue: 3.5.12(typescript@5.3.3)
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+
+ emoji-regex@10.4.0: {}
+
+ emoji-regex@8.0.0: {}
+
+ emoji-regex@9.2.2: {}
+
+ entities@4.5.0: {}
+
+ env-paths@2.2.1: {}
+
+ environment@1.1.0: {}
+
+ error-ex@1.3.2:
+ dependencies:
+ is-arrayish: 0.2.1
+
+ es-define-property@1.0.0:
+ dependencies:
+ get-intrinsic: 1.2.4
+
+ es-errors@1.3.0: {}
+
+ es-module-lexer@1.5.4: {}
+
+ es5-ext@0.10.64:
+ dependencies:
+ es6-iterator: 2.0.3
+ es6-symbol: 3.1.4
+ esniff: 2.0.1
+ next-tick: 1.1.0
+
+ es6-iterator@2.0.3:
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ es6-symbol: 3.1.4
+
+ es6-symbol@3.1.4:
+ dependencies:
+ d: 1.0.2
+ ext: 1.7.0
+
+ esbuild@0.19.12:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.19.12
+ '@esbuild/android-arm': 0.19.12
+ '@esbuild/android-arm64': 0.19.12
+ '@esbuild/android-x64': 0.19.12
+ '@esbuild/darwin-arm64': 0.19.12
+ '@esbuild/darwin-x64': 0.19.12
+ '@esbuild/freebsd-arm64': 0.19.12
+ '@esbuild/freebsd-x64': 0.19.12
+ '@esbuild/linux-arm': 0.19.12
+ '@esbuild/linux-arm64': 0.19.12
+ '@esbuild/linux-ia32': 0.19.12
+ '@esbuild/linux-loong64': 0.19.12
+ '@esbuild/linux-mips64el': 0.19.12
+ '@esbuild/linux-ppc64': 0.19.12
+ '@esbuild/linux-riscv64': 0.19.12
+ '@esbuild/linux-s390x': 0.19.12
+ '@esbuild/linux-x64': 0.19.12
+ '@esbuild/netbsd-x64': 0.19.12
+ '@esbuild/openbsd-x64': 0.19.12
+ '@esbuild/sunos-x64': 0.19.12
+ '@esbuild/win32-arm64': 0.19.12
+ '@esbuild/win32-ia32': 0.19.12
+ '@esbuild/win32-x64': 0.19.12
+
+ escalade@3.2.0: {}
+
+ escape-html@1.0.3: {}
+
+ escape-string-regexp@1.0.5: {}
+
+ escape-string-regexp@4.0.0: {}
+
+ escape-string-regexp@5.0.0: {}
+
+ escodegen@2.1.0:
+ dependencies:
+ esprima: 4.0.1
+ estraverse: 5.3.0
+ esutils: 2.0.3
+ optionalDependencies:
+ source-map: 0.6.1
+
+ eslint-config-prettier@9.1.0(eslint@8.57.1):
+ dependencies:
+ eslint: 8.57.1
+
+ eslint-define-config@2.1.0: {}
+
+ eslint-plugin-prettier@5.2.1(@types/eslint@8.56.12)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.1):
+ dependencies:
+ eslint: 8.57.1
+ prettier: 3.4.1
+ prettier-linter-helpers: 1.0.0
+ synckit: 0.9.2
+ optionalDependencies:
+ '@types/eslint': 8.56.12
+ eslint-config-prettier: 9.1.0(eslint@8.57.1)
+
+ eslint-plugin-vue@9.31.0(eslint@8.57.1):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
+ eslint: 8.57.1
+ globals: 13.24.0
+ natural-compare: 1.4.0
+ nth-check: 2.1.1
+ postcss-selector-parser: 6.1.2
+ semver: 7.6.3
+ vue-eslint-parser: 9.4.3(eslint@8.57.1)
+ xml-name-validator: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ eslint-scope@7.2.2:
+ dependencies:
+ esrecurse: 4.3.0
+ estraverse: 5.3.0
+
+ eslint-visitor-keys@3.4.3: {}
+
+ eslint-visitor-keys@4.2.0: {}
+
+ eslint@8.57.1:
+ dependencies:
+ '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
+ '@eslint-community/regexpp': 4.12.1
+ '@eslint/eslintrc': 2.1.4
+ '@eslint/js': 8.57.1
+ '@humanwhocodes/config-array': 0.13.0
+ '@humanwhocodes/module-importer': 1.0.1
+ '@nodelib/fs.walk': 1.2.8
+ '@ungap/structured-clone': 1.2.0
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.3.7
+ doctrine: 3.0.0
+ escape-string-regexp: 4.0.0
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 6.0.1
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ globals: 13.24.0
+ graphemer: 1.4.0
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ is-path-inside: 3.0.3
+ js-yaml: 4.1.0
+ json-stable-stringify-without-jsonify: 1.0.1
+ levn: 0.4.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ strip-ansi: 6.0.1
+ text-table: 0.2.0
+ transitivePeerDependencies:
+ - supports-color
+
+ esniff@2.0.1:
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+ event-emitter: 0.3.5
+ type: 2.7.3
+
+ espree@9.6.1:
+ dependencies:
+ acorn: 8.14.0
+ acorn-jsx: 5.3.2(acorn@8.14.0)
+ eslint-visitor-keys: 3.4.3
+
+ esprima@4.0.1: {}
+
+ esquery@1.6.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ esrecurse@4.3.0:
+ dependencies:
+ estraverse: 5.3.0
+
+ estraverse@5.3.0: {}
+
+ estree-walker@2.0.2: {}
+
+ estree-walker@3.0.3:
+ dependencies:
+ '@types/estree': 1.0.6
+
+ esutils@2.0.3: {}
+
+ event-emitter@0.3.5:
+ dependencies:
+ d: 1.0.2
+ es5-ext: 0.10.64
+
+ eventemitter3@5.0.1: {}
+
+ execa@8.0.1:
+ dependencies:
+ cross-spawn: 7.0.6
+ get-stream: 8.0.1
+ human-signals: 5.0.0
+ is-stream: 3.0.0
+ merge-stream: 2.0.0
+ npm-run-path: 5.3.0
+ onetime: 6.0.0
+ signal-exit: 4.1.0
+ strip-final-newline: 3.0.0
+
+ ext@1.7.0:
+ dependencies:
+ type: 2.7.3
+
+ fast-deep-equal@3.1.3: {}
+
+ fast-diff@1.3.0: {}
+
+ fast-glob@3.3.2:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-glob@3.3.3:
+ dependencies:
+ '@nodelib/fs.stat': 2.0.5
+ '@nodelib/fs.walk': 1.2.8
+ glob-parent: 5.1.2
+ merge2: 1.4.1
+ micromatch: 4.0.8
+
+ fast-json-stable-stringify@2.1.0: {}
+
+ fast-levenshtein@2.0.6: {}
+
+ fast-uri@3.0.3: {}
+
+ fast-xml-parser@4.5.0:
+ dependencies:
+ strnum: 1.0.5
+
+ fastest-levenshtein@1.0.16: {}
+
+ fastq@1.17.1:
+ dependencies:
+ reusify: 1.0.4
+
+ fdir@6.4.2(picomatch@4.0.2):
+ optionalDependencies:
+ picomatch: 4.0.2
+
+ feelers@1.4.0:
+ dependencies:
+ '@bpmn-io/cm-theme': 0.1.0-alpha.2
+ '@bpmn-io/feel-lint': 1.3.1
+ '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)
+ '@codemirror/commands': 6.7.1
+ '@codemirror/language': 6.10.6
+ '@codemirror/lint': 6.8.4
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+ '@lezer/markdown': 1.3.2
+ feelin: 3.2.0
+ lezer-feel: 1.4.0
+ min-dom: 5.1.1
+
+ feelin@3.2.0:
+ dependencies:
+ '@lezer/lr': 1.4.2
+ lezer-feel: 1.4.0
+ luxon: 3.5.0
+
+ file-entry-cache@6.0.1:
+ dependencies:
+ flat-cache: 3.2.0
+
+ file-entry-cache@9.1.0:
+ dependencies:
+ flat-cache: 5.0.0
+
+ filelist@1.0.4:
+ dependencies:
+ minimatch: 5.1.6
+
+ fill-range@7.1.1:
+ dependencies:
+ to-regex-range: 5.0.1
+
+ find-up@4.1.0:
+ dependencies:
+ locate-path: 5.0.0
+ path-exists: 4.0.0
+
+ find-up@5.0.0:
+ dependencies:
+ locate-path: 6.0.0
+ path-exists: 4.0.0
+
+ find-up@7.0.0:
+ dependencies:
+ locate-path: 7.2.0
+ path-exists: 5.0.0
+ unicorn-magic: 0.1.0
+
+ flat-cache@3.2.0:
+ dependencies:
+ flatted: 3.3.2
+ keyv: 4.5.4
+ rimraf: 3.0.2
+
+ flat-cache@5.0.0:
+ dependencies:
+ flatted: 3.3.2
+ keyv: 4.5.4
+
+ flatted@3.3.2: {}
+
+ focus-trap@7.6.2:
+ dependencies:
+ tabbable: 6.2.0
+
+ follow-redirects@1.15.9(debug@4.3.7):
+ optionalDependencies:
+ debug: 4.3.7
+
+ foreground-child@3.3.0:
+ dependencies:
+ cross-spawn: 7.0.6
+ signal-exit: 4.1.0
+
+ form-data@4.0.1:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ mime-types: 2.1.35
+
+ fraction.js@4.3.7: {}
+
+ fs-extra@10.1.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.1.0
+ universalify: 2.0.1
+
+ fs-extra@11.3.0:
+ dependencies:
+ graceful-fs: 4.2.11
+ jsonfile: 6.1.0
+ universalify: 2.0.1
+
+ fs.realpath@1.0.0: {}
+
+ fsevents@2.3.3:
+ optional: true
+
+ function-bind@1.1.2: {}
+
+ gensync@1.0.0-beta.2: {}
+
+ get-caller-file@2.0.5: {}
+
+ get-east-asian-width@1.3.0: {}
+
+ get-intrinsic@1.2.4:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ has-proto: 1.0.3
+ has-symbols: 1.0.3
+ hasown: 2.0.2
+
+ get-stream@8.0.1: {}
+
+ git-raw-commits@4.0.0:
+ dependencies:
+ dargs: 8.1.0
+ meow: 12.1.1
+ split2: 4.2.0
+
+ glob-parent@5.1.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob-parent@6.0.2:
+ dependencies:
+ is-glob: 4.0.3
+
+ glob@10.4.5:
+ dependencies:
+ foreground-child: 3.3.0
+ jackspeak: 3.4.3
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ package-json-from-dist: 1.0.1
+ path-scurry: 1.11.1
+
+ glob@7.2.3:
+ dependencies:
+ fs.realpath: 1.0.0
+ inflight: 1.0.6
+ inherits: 2.0.4
+ minimatch: 3.1.2
+ once: 1.4.0
+ path-is-absolute: 1.0.1
+
+ global-directory@4.0.1:
+ dependencies:
+ ini: 4.1.1
+
+ global-modules@2.0.0:
+ dependencies:
+ global-prefix: 3.0.0
+
+ global-prefix@3.0.0:
+ dependencies:
+ ini: 1.3.8
+ kind-of: 6.0.3
+ which: 1.3.1
+
+ global@4.4.0:
+ dependencies:
+ min-document: 2.19.0
+ process: 0.11.10
+
+ globals@11.12.0: {}
+
+ globals@13.24.0:
+ dependencies:
+ type-fest: 0.20.2
+
+ globby@11.1.0:
+ dependencies:
+ array-union: 2.1.0
+ dir-glob: 3.0.1
+ fast-glob: 3.3.2
+ ignore: 5.3.2
+ merge2: 1.4.1
+ slash: 3.0.0
+
+ globjoin@0.1.4: {}
+
+ gopd@1.0.1:
+ dependencies:
+ get-intrinsic: 1.2.4
+
+ graceful-fs@4.2.11: {}
+
+ graphemer@1.4.0: {}
+
+ gzip-size@6.0.0:
+ dependencies:
+ duplexer: 0.1.2
+
+ hammerjs@2.0.8: {}
+
+ has-ansi@2.0.0:
+ dependencies:
+ ansi-regex: 2.1.1
+
+ has-flag@4.0.0: {}
+
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.0
+
+ has-proto@1.0.3: {}
+
+ has-symbols@1.0.3: {}
+
+ hasown@2.0.2:
+ dependencies:
+ function-bind: 1.1.2
+
+ he@1.2.0: {}
+
+ highlight.js@11.10.0: {}
+
+ htm@3.1.1: {}
+
+ html-tags@3.3.1: {}
+
+ html-void-elements@3.0.0: {}
+
+ htmlparser2@8.0.2:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ domutils: 3.1.0
+ entities: 4.5.0
+
+ human-signals@5.0.0: {}
+
+ i18next@23.16.8:
+ dependencies:
+ '@babel/runtime': 7.26.0
+
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
+ ids@1.0.5: {}
+
+ ignore@5.3.2: {}
+
+ ignore@6.0.2: {}
+
+ immer@9.0.21: {}
+
+ immutable@5.0.3: {}
+
+ import-fresh@3.3.0:
+ dependencies:
+ parent-module: 1.0.1
+ resolve-from: 4.0.0
+
+ import-meta-resolve@4.1.0: {}
+
+ imurmurhash@0.1.4: {}
+
+ indent-string@4.0.0: {}
+
+ individual@2.0.0: {}
+
+ inflight@1.0.6:
+ dependencies:
+ once: 1.4.0
+ wrappy: 1.0.2
+
+ inherits-browser@0.1.0: {}
+
+ inherits@2.0.4: {}
+
+ ini@1.3.8: {}
+
+ ini@4.1.1: {}
+
+ internmap@2.0.3: {}
+
+ is-arrayish@0.2.1: {}
+
+ is-binary-path@2.1.0:
+ dependencies:
+ binary-extensions: 2.3.0
+
+ is-core-module@2.15.1:
+ dependencies:
+ hasown: 2.0.2
+
+ is-extglob@2.1.1: {}
+
+ is-fullwidth-code-point@3.0.0: {}
+
+ is-fullwidth-code-point@4.0.0: {}
+
+ is-fullwidth-code-point@5.0.0:
+ dependencies:
+ get-east-asian-width: 1.3.0
+
+ is-function@1.0.2: {}
+
+ is-glob@4.0.3:
+ dependencies:
+ is-extglob: 2.1.1
+
+ is-hotkey@0.2.0: {}
+
+ is-number@7.0.0: {}
+
+ is-obj@2.0.0: {}
+
+ is-path-inside@3.0.3: {}
+
+ is-plain-object@5.0.0: {}
+
+ is-stream@3.0.0: {}
+
+ is-text-path@2.0.0:
+ dependencies:
+ text-extensions: 2.4.0
+
+ is-url@1.2.4: {}
+
+ isexe@2.0.0: {}
+
+ jackspeak@3.4.3:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
+ jake@10.9.2:
+ dependencies:
+ async: 3.2.6
+ chalk: 4.1.2
+ filelist: 1.0.4
+ minimatch: 3.1.2
+
+ javascript-natural-sort@0.7.1: {}
+
+ jiti@1.21.6: {}
+
+ jiti@2.4.2: {}
+
+ jmespath@0.16.0: {}
+
+ js-tokens@4.0.0: {}
+
+ js-tokens@9.0.1: {}
+
+ js-yaml@4.1.0:
+ dependencies:
+ argparse: 2.0.1
+
+ jsencrypt@3.3.2: {}
+
+ jsesc@3.0.2: {}
+
+ json-buffer@3.0.1: {}
+
+ json-parse-even-better-errors@2.3.1: {}
+
+ json-schema-traverse@0.4.1: {}
+
+ json-schema-traverse@1.0.0: {}
+
+ json-source-map@0.6.1: {}
+
+ json-stable-stringify-without-jsonify@1.0.1: {}
+
+ json5@2.2.3: {}
+
+ jsonc-eslint-parser@2.4.0:
+ dependencies:
+ acorn: 8.14.0
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ semver: 7.6.3
+
+ jsoneditor@10.4.1:
+ dependencies:
+ ace-builds: 1.39.1
+ ajv: 6.12.6
+ javascript-natural-sort: 0.7.1
+ jmespath: 0.16.0
+ json-source-map: 0.6.1
+ jsonrepair: 3.13.0
+ picomodal: 3.0.0
+ vanilla-picker: 2.12.3
+
+ jsonfile@6.1.0:
+ dependencies:
+ universalify: 2.0.1
+ optionalDependencies:
+ graceful-fs: 4.2.11
+
+ jsonparse@1.3.1: {}
+
+ jsonrepair@3.13.0: {}
+
+ katex@0.16.11:
+ dependencies:
+ commander: 8.3.0
+
+ keycode@2.2.1: {}
+
+ keyv@4.5.4:
+ dependencies:
+ json-buffer: 3.0.1
+
+ kind-of@6.0.3: {}
+
+ known-css-properties@0.35.0: {}
+
+ kolorist@1.8.0: {}
+
+ lang-feel@2.2.0:
+ dependencies:
+ '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.4.1)(@codemirror/view@6.35.0)(@lezer/common@1.2.3)
+ '@codemirror/language': 6.10.6
+ '@codemirror/state': 6.4.1
+ '@codemirror/view': 6.35.0
+ '@lezer/common': 1.2.3
+ lezer-feel: 1.4.0
+
+ levn@0.4.1:
+ dependencies:
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+
+ lezer-feel@1.4.0:
+ dependencies:
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+ min-dash: 4.2.2
+
+ lilconfig@3.1.2: {}
+
+ lines-and-columns@1.2.4: {}
+
+ linkify-it@5.0.0:
+ dependencies:
+ uc.micro: 2.1.0
+
+ lint-staged@15.2.10:
+ dependencies:
+ chalk: 5.3.0
+ commander: 12.1.0
+ debug: 4.3.7
+ execa: 8.0.1
+ lilconfig: 3.1.2
+ listr2: 8.2.5
+ micromatch: 4.0.8
+ pidtree: 0.6.0
+ string-argv: 0.3.2
+ yaml: 2.5.1
+ transitivePeerDependencies:
+ - supports-color
+
+ listr2@8.2.5:
+ dependencies:
+ cli-truncate: 4.0.0
+ colorette: 2.0.20
+ eventemitter3: 5.0.1
+ log-update: 6.1.0
+ rfdc: 1.4.1
+ wrap-ansi: 9.0.0
+
+ local-pkg@0.4.3: {}
+
+ local-pkg@0.5.1:
+ dependencies:
+ mlly: 1.7.3
+ pkg-types: 1.2.1
+
+ locate-path@5.0.0:
+ dependencies:
+ p-locate: 4.1.0
+
+ locate-path@6.0.0:
+ dependencies:
+ p-locate: 5.0.0
+
+ locate-path@7.2.0:
+ dependencies:
+ p-locate: 6.0.0
+
+ lodash-es@4.17.21: {}
+
+ lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
+ dependencies:
+ '@types/lodash-es': 4.17.12
+ lodash: 4.17.21
+ lodash-es: 4.17.21
+
+ lodash.camelcase@4.3.0: {}
+
+ lodash.clonedeep@4.5.0: {}
+
+ lodash.debounce@4.0.8: {}
+
+ lodash.foreach@4.5.0: {}
+
+ lodash.isplainobject@4.0.6: {}
+
+ lodash.kebabcase@4.1.1: {}
+
+ lodash.merge@4.6.2: {}
+
+ lodash.mergewith@4.6.2: {}
+
+ lodash.snakecase@4.1.1: {}
+
+ lodash.startcase@4.4.0: {}
+
+ lodash.throttle@4.1.1: {}
+
+ lodash.toarray@4.4.0: {}
+
+ lodash.truncate@4.4.2: {}
+
+ lodash.uniq@4.5.0: {}
+
+ lodash.upperfirst@4.3.1: {}
+
+ lodash@4.17.21: {}
+
+ log-update@6.1.0:
+ dependencies:
+ ansi-escapes: 7.0.0
+ cli-cursor: 5.0.0
+ slice-ansi: 7.1.0
+ strip-ansi: 7.1.0
+ wrap-ansi: 9.0.0
+
+ loglevel-colored-level-prefix@1.0.0:
+ dependencies:
+ chalk: 1.1.3
+ loglevel: 1.9.2
+
+ loglevel@1.9.2: {}
+
+ lru-cache@10.4.3: {}
+
+ lru-cache@5.1.1:
+ dependencies:
+ yallist: 3.1.1
+
+ luxon@3.5.0: {}
+
+ m3u8-parser@4.8.0:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@videojs/vhs-utils': 3.0.5
+ global: 4.4.0
+
+ magic-string@0.30.14:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ magic-string@0.30.17:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.5.0
+
+ markdown-it@14.1.0:
+ dependencies:
+ argparse: 2.0.1
+ entities: 4.5.0
+ linkify-it: 5.0.0
+ mdurl: 2.0.0
+ punycode.js: 2.3.1
+ uc.micro: 2.1.0
+
+ markmap-common@0.16.0:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@gera2ld/jsx-dom': 2.2.2
+ npm2url: 0.2.4
+
+ markmap-html-parser@0.16.1(markmap-common@0.16.0):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ cheerio: 1.0.0-rc.12
+ markmap-common: 0.16.0
+
+ markmap-lib@0.16.1(markmap-common@0.16.0):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ highlight.js: 11.10.0
+ js-yaml: 4.1.0
+ katex: 0.16.11
+ markmap-common: 0.16.0
+ markmap-html-parser: 0.16.1(markmap-common@0.16.0)
+ markmap-view: 0.16.0(markmap-common@0.16.0)
+ prismjs: 1.29.0
+ remarkable: 2.0.1
+ remarkable-katex: 1.2.1
+
+ markmap-toolbar@0.17.2(markmap-common@0.16.0):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@gera2ld/jsx-dom': 2.2.2
+ markmap-common: 0.16.0
+
+ markmap-view@0.16.0(markmap-common@0.16.0):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@gera2ld/jsx-dom': 2.2.2
+ '@types/d3': 7.4.3
+ d3: 7.9.0
+ d3-flextree: 2.1.2
+ markmap-common: 0.16.0
+
+ mathml-tag-names@2.1.3: {}
+
+ mdn-data@2.0.28: {}
+
+ mdn-data@2.0.30: {}
+
+ mdn-data@2.12.1: {}
+
+ mdurl@2.0.0: {}
+
+ memoize-one@6.0.0: {}
+
+ meow@12.1.1: {}
+
+ meow@13.2.0: {}
+
+ merge-stream@2.0.0: {}
+
+ merge2@1.4.1: {}
+
+ micromatch@4.0.8:
+ dependencies:
+ braces: 3.0.3
+ picomatch: 2.3.1
+
+ mime-db@1.52.0: {}
+
+ mime-match@1.0.2:
+ dependencies:
+ wildcard: 1.1.2
+
+ mime-types@2.1.35:
+ dependencies:
+ mime-db: 1.52.0
+
+ mimic-fn@4.0.0: {}
+
+ mimic-function@5.0.1: {}
+
+ min-dash@4.2.2: {}
+
+ min-document@2.19.0:
+ dependencies:
+ dom-walk: 0.1.2
+
+ min-dom@4.2.1:
+ dependencies:
+ component-event: 0.2.1
+ domify: 1.4.2
+ min-dash: 4.2.2
+
+ min-dom@5.1.1:
+ dependencies:
+ domify: 2.0.0
+ min-dash: 4.2.2
+
+ minimatch@3.1.2:
+ dependencies:
+ brace-expansion: 1.1.11
+
+ minimatch@5.1.6:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ minimatch@9.0.3:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ minimatch@9.0.5:
+ dependencies:
+ brace-expansion: 2.0.1
+
+ minimist@1.2.8: {}
+
+ minipass@7.1.2: {}
+
+ mitt@3.0.1: {}
+
+ mlly@1.7.3:
+ dependencies:
+ acorn: 8.14.0
+ pathe: 1.1.2
+ pkg-types: 1.2.1
+ ufo: 1.5.4
+
+ moddle-xml@10.1.0:
+ dependencies:
+ min-dash: 4.2.2
+ moddle: 6.2.3
+ saxen: 8.1.2
+
+ moddle@6.2.3:
+ dependencies:
+ min-dash: 4.2.2
+
+ mpd-parser@0.22.1:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@videojs/vhs-utils': 3.0.5
+ '@xmldom/xmldom': 0.8.10
+ global: 4.4.0
+
+ mrmime@2.0.0: {}
+
+ ms@2.1.3: {}
+
+ muggle-string@0.3.1: {}
+
+ mux.js@6.0.1:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ global: 4.4.0
+
+ namespace-emitter@2.0.1: {}
+
+ nanoid@3.3.8: {}
+
+ nanoid@5.1.6: {}
+
+ natural-compare@1.4.0: {}
+
+ next-tick@1.1.0: {}
+
+ node-addon-api@7.1.1:
+ optional: true
+
+ node-fetch-native@1.6.4: {}
+
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ node-html-parser@7.0.1:
+ dependencies:
+ css-select: 5.1.0
+ he: 1.2.0
+
+ node-releases@2.0.18: {}
+
+ normalize-path@3.0.0: {}
+
+ normalize-range@0.1.2: {}
+
+ normalize-wheel-es@1.2.0: {}
+
+ npm-run-path@5.3.0:
+ dependencies:
+ path-key: 4.0.0
+
+ npm2url@0.2.4: {}
+
+ nprogress@0.2.0: {}
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ object-inspect@1.13.3: {}
+
+ object-refs@0.3.0: {}
+
+ object-refs@0.4.0: {}
+
+ ofetch@1.4.1:
+ dependencies:
+ destr: 2.0.3
+ node-fetch-native: 1.6.4
+ ufo: 1.5.4
+
+ once@1.4.0:
+ dependencies:
+ wrappy: 1.0.2
+
+ onetime@6.0.0:
+ dependencies:
+ mimic-fn: 4.0.0
+
+ onetime@7.0.0:
+ dependencies:
+ mimic-function: 5.0.1
+
+ optionator@0.9.4:
+ dependencies:
+ deep-is: 0.1.4
+ fast-levenshtein: 2.0.6
+ levn: 0.4.1
+ prelude-ls: 1.2.1
+ type-check: 0.4.0
+ word-wrap: 1.2.5
+
+ p-limit@2.3.0:
+ dependencies:
+ p-try: 2.2.0
+
+ p-limit@3.1.0:
+ dependencies:
+ yocto-queue: 0.1.0
+
+ p-limit@4.0.0:
+ dependencies:
+ yocto-queue: 1.1.1
+
+ p-locate@4.1.0:
+ dependencies:
+ p-limit: 2.3.0
+
+ p-locate@5.0.0:
+ dependencies:
+ p-limit: 3.1.0
+
+ p-locate@6.0.0:
+ dependencies:
+ p-limit: 4.0.0
+
+ p-try@2.2.0: {}
+
+ package-json-from-dist@1.0.1: {}
+
+ package-manager-detector@0.2.5: {}
+
+ parent-module@1.0.1:
+ dependencies:
+ callsites: 3.1.0
+
+ parse-json@5.2.0:
+ dependencies:
+ '@babel/code-frame': 7.26.2
+ error-ex: 1.3.2
+ json-parse-even-better-errors: 2.3.1
+ lines-and-columns: 1.2.4
+
+ parse5-htmlparser2-tree-adapter@7.1.0:
+ dependencies:
+ domhandler: 5.0.3
+ parse5: 7.2.1
+
+ parse5@7.2.1:
+ dependencies:
+ entities: 4.5.0
+
+ path-browserify@1.0.1: {}
+
+ path-exists@4.0.0: {}
+
+ path-exists@5.0.0: {}
+
+ path-intersection@2.2.1: {}
+
+ path-intersection@3.1.0: {}
+
+ path-is-absolute@1.0.1: {}
+
+ path-key@3.1.1: {}
+
+ path-key@4.0.0: {}
+
+ path-parse@1.0.7: {}
+
+ path-scurry@1.11.1:
+ dependencies:
+ lru-cache: 10.4.3
+ minipass: 7.1.2
+
+ path-type@4.0.0: {}
+
+ pathe@1.1.2: {}
+
+ pathe@2.0.3: {}
+
+ perfect-debounce@1.0.0: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.1: {}
+
+ picomatch@4.0.2: {}
+
+ picomodal@3.0.0: {}
+
+ pidtree@0.6.0: {}
+
+ pinia-plugin-persistedstate@3.2.3(pinia@2.2.8(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3))):
+ dependencies:
+ pinia: 2.2.8(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3))
+
+ pinia@2.2.8(typescript@5.3.3)(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.12(typescript@5.3.3)
+ vue-demi: 0.14.10(vue@3.5.12(typescript@5.3.3))
+ optionalDependencies:
+ typescript: 5.3.3
+
+ pkcs7@1.0.4:
+ dependencies:
+ '@babel/runtime': 7.26.0
+
+ pkg-types@1.2.1:
+ dependencies:
+ confbox: 0.1.8
+ mlly: 1.7.3
+ pathe: 1.1.2
+
+ pngjs@5.0.0: {}
+
+ postcss-html@1.7.0:
+ dependencies:
+ htmlparser2: 8.0.2
+ js-tokens: 9.0.1
+ postcss: 8.4.49
+ postcss-safe-parser: 6.0.0(postcss@8.4.49)
+
+ postcss-resolve-nested-selector@0.1.6: {}
+
+ postcss-safe-parser@6.0.0(postcss@8.4.49):
+ dependencies:
+ postcss: 8.4.49
+
+ postcss-safe-parser@7.0.1(postcss@8.4.49):
+ dependencies:
+ postcss: 8.4.49
+
+ postcss-scss@4.0.9(postcss@8.4.49):
+ dependencies:
+ postcss: 8.4.49
+
+ postcss-selector-parser@6.1.2:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-selector-parser@7.0.0:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss-sorting@8.0.2(postcss@8.4.49):
+ dependencies:
+ postcss: 8.4.49
+
+ postcss-value-parser@4.2.0: {}
+
+ postcss@8.4.49:
+ dependencies:
+ nanoid: 3.3.8
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ preact@10.25.0: {}
+
+ prelude-ls@1.2.1: {}
+
+ prettier-eslint@16.3.0:
+ dependencies:
+ '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.3.3)
+ common-tags: 1.8.2
+ dlv: 1.1.3
+ eslint: 8.57.1
+ indent-string: 4.0.0
+ lodash.merge: 4.6.2
+ loglevel-colored-level-prefix: 1.0.0
+ prettier: 3.4.1
+ pretty-format: 29.7.0
+ require-relative: 0.8.7
+ typescript: 5.3.3
+ vue-eslint-parser: 9.4.3(eslint@8.57.1)
+ transitivePeerDependencies:
+ - supports-color
+
+ prettier-linter-helpers@1.0.0:
+ dependencies:
+ fast-diff: 1.3.0
+
+ prettier@3.4.1: {}
+
+ pretty-format@29.7.0:
+ dependencies:
+ '@jest/schemas': 29.6.3
+ ansi-styles: 5.2.0
+ react-is: 18.3.1
+
+ prismjs@1.29.0: {}
+
+ process@0.11.10: {}
+
+ progress@2.0.3: {}
+
+ proxy-from-env@1.1.0: {}
+
+ punycode.js@2.3.1: {}
+
+ punycode@1.4.1: {}
+
+ punycode@2.3.1: {}
+
+ qrcode@1.5.4:
+ dependencies:
+ dijkstrajs: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
+
+ qs@6.13.1:
+ dependencies:
+ side-channel: 1.0.6
+
+ quansync@0.2.8: {}
+
+ queue-microtask@1.2.3: {}
+
+ randomcolor@0.6.2: {}
+
+ rd@2.0.1:
+ dependencies:
+ '@types/node': 10.17.60
+
+ react-is@18.3.1: {}
+
+ readdirp@3.6.0:
+ dependencies:
+ picomatch: 2.3.1
+
+ readdirp@4.0.2: {}
+
+ regenerate-unicode-properties@10.2.0:
+ dependencies:
+ regenerate: 1.4.2
+
+ regenerate@1.4.2: {}
+
+ regenerator-runtime@0.14.1: {}
+
+ regenerator-transform@0.15.2:
+ dependencies:
+ '@babel/runtime': 7.26.0
+
+ regexpu-core@6.2.0:
+ dependencies:
+ regenerate: 1.4.2
+ regenerate-unicode-properties: 10.2.0
+ regjsgen: 0.8.0
+ regjsparser: 0.12.0
+ unicode-match-property-ecmascript: 2.0.0
+ unicode-match-property-value-ecmascript: 2.2.0
+
+ regjsgen@0.8.0: {}
+
+ regjsparser@0.12.0:
+ dependencies:
+ jsesc: 3.0.2
+
+ remarkable-katex@1.2.1: {}
+
+ remarkable@2.0.1:
+ dependencies:
+ argparse: 1.0.10
+ autolinker: 3.16.2
+
+ require-directory@2.1.1: {}
+
+ require-from-string@2.0.2: {}
+
+ require-main-filename@2.0.0: {}
+
+ require-relative@0.8.7: {}
+
+ resolve-from@4.0.0: {}
+
+ resolve-from@5.0.0: {}
+
+ resolve@1.22.8:
+ dependencies:
+ is-core-module: 2.15.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
+ restore-cursor@5.1.0:
+ dependencies:
+ onetime: 7.0.0
+ signal-exit: 4.1.0
+
+ reusify@1.0.4: {}
+
+ rfdc@1.4.1: {}
+
+ rimraf@3.0.2:
+ dependencies:
+ glob: 7.2.3
+
+ rimraf@5.0.10:
+ dependencies:
+ glob: 10.4.5
+
+ robust-predicates@3.0.2: {}
+
+ rollup-plugin-purge-icons@0.10.0:
+ dependencies:
+ '@purge-icons/core': 0.10.0
+ '@purge-icons/generated': 0.10.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ rollup@2.79.2:
+ optionalDependencies:
+ fsevents: 2.3.3
+
+ rollup@4.27.4:
+ dependencies:
+ '@types/estree': 1.0.6
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.27.4
+ '@rollup/rollup-android-arm64': 4.27.4
+ '@rollup/rollup-darwin-arm64': 4.27.4
+ '@rollup/rollup-darwin-x64': 4.27.4
+ '@rollup/rollup-freebsd-arm64': 4.27.4
+ '@rollup/rollup-freebsd-x64': 4.27.4
+ '@rollup/rollup-linux-arm-gnueabihf': 4.27.4
+ '@rollup/rollup-linux-arm-musleabihf': 4.27.4
+ '@rollup/rollup-linux-arm64-gnu': 4.27.4
+ '@rollup/rollup-linux-arm64-musl': 4.27.4
+ '@rollup/rollup-linux-powerpc64le-gnu': 4.27.4
+ '@rollup/rollup-linux-riscv64-gnu': 4.27.4
+ '@rollup/rollup-linux-s390x-gnu': 4.27.4
+ '@rollup/rollup-linux-x64-gnu': 4.27.4
+ '@rollup/rollup-linux-x64-musl': 4.27.4
+ '@rollup/rollup-win32-arm64-msvc': 4.27.4
+ '@rollup/rollup-win32-ia32-msvc': 4.27.4
+ '@rollup/rollup-win32-x64-msvc': 4.27.4
+ fsevents: 2.3.3
+
+ run-parallel@1.2.0:
+ dependencies:
+ queue-microtask: 1.2.3
+
+ rust-result@1.0.0:
+ dependencies:
+ individual: 2.0.0
+
+ rw@1.3.3: {}
+
+ safe-json-parse@4.0.0:
+ dependencies:
+ rust-result: 1.0.0
+
+ safer-buffer@2.1.2: {}
+
+ sass@1.81.0:
+ dependencies:
+ chokidar: 4.0.1
+ immutable: 5.0.3
+ source-map-js: 1.2.1
+ optionalDependencies:
+ '@parcel/watcher': 2.5.0
+
+ sax@1.4.1: {}
+
+ saxen@8.1.2: {}
+
+ scroll-into-view-if-needed@3.1.0:
+ dependencies:
+ compute-scroll-into-view: 3.1.1
+
+ scule@1.3.0: {}
+
+ semver@6.3.1: {}
+
+ semver@7.6.3: {}
+
+ set-blocking@2.0.0: {}
+
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.4
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.2
+
+ shebang-command@2.0.0:
+ dependencies:
+ shebang-regex: 3.0.0
+
+ shebang-regex@3.0.0: {}
+
+ side-channel@1.0.6:
+ dependencies:
+ call-bind: 1.0.7
+ es-errors: 1.3.0
+ get-intrinsic: 1.2.4
+ object-inspect: 1.13.3
+
+ signal-exit@4.1.0: {}
+
+ signature_pad@3.0.0-beta.4: {}
+
+ sirv@2.0.4:
+ dependencies:
+ '@polka/url': 1.0.0-next.28
+ mrmime: 2.0.0
+ totalist: 3.0.1
+
+ slash@3.0.0: {}
+
+ slate-history@0.109.0(slate@0.82.1):
+ dependencies:
+ is-plain-object: 5.0.0
+ slate: 0.82.1
+
+ slate@0.82.1:
+ dependencies:
+ immer: 9.0.21
+ is-plain-object: 5.0.0
+ tiny-warning: 1.0.3
+
+ slice-ansi@4.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ astral-regex: 2.0.0
+ is-fullwidth-code-point: 3.0.0
+
+ slice-ansi@5.0.0:
+ dependencies:
+ ansi-styles: 6.2.1
+ is-fullwidth-code-point: 4.0.0
+
+ slice-ansi@7.1.0:
+ dependencies:
+ ansi-styles: 6.2.1
+ is-fullwidth-code-point: 5.0.0
+
+ snabbdom@3.6.2: {}
+
+ sortablejs@1.14.0: {}
+
+ sortablejs@1.15.6: {}
+
+ source-map-js@1.2.1: {}
+
+ source-map-support@0.5.21:
+ dependencies:
+ buffer-from: 1.1.2
+ source-map: 0.6.1
+
+ source-map@0.6.1: {}
+
+ split2@4.2.0: {}
+
+ sprintf-js@1.0.3: {}
+
+ ssr-window@4.0.2: {}
+
+ steady-xml@0.1.0: {}
+
+ string-argv@0.3.2: {}
+
+ string-width@4.2.3:
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+
+ string-width@5.1.2:
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+
+ string-width@7.2.0:
+ dependencies:
+ emoji-regex: 10.4.0
+ get-east-asian-width: 1.3.0
+ strip-ansi: 7.1.0
+
+ strip-ansi@3.0.1:
+ dependencies:
+ ansi-regex: 2.1.1
+
+ strip-ansi@6.0.1:
+ dependencies:
+ ansi-regex: 5.0.1
+
+ strip-ansi@7.1.0:
+ dependencies:
+ ansi-regex: 6.1.0
+
+ strip-final-newline@3.0.0: {}
+
+ strip-json-comments@3.1.1: {}
+
+ strip-literal@2.1.1:
+ dependencies:
+ js-tokens: 9.0.1
+
+ strnum@1.0.5: {}
+
+ style-mod@4.1.2: {}
+
+ stylelint-config-html@1.1.0(postcss-html@1.7.0)(stylelint@16.11.0(typescript@5.3.3)):
+ dependencies:
+ postcss-html: 1.7.0
+ stylelint: 16.11.0(typescript@5.3.3)
+
+ stylelint-config-recommended@14.0.1(stylelint@16.11.0(typescript@5.3.3)):
+ dependencies:
+ stylelint: 16.11.0(typescript@5.3.3)
+
+ stylelint-config-standard@36.0.1(stylelint@16.11.0(typescript@5.3.3)):
+ dependencies:
+ stylelint: 16.11.0(typescript@5.3.3)
+ stylelint-config-recommended: 14.0.1(stylelint@16.11.0(typescript@5.3.3))
+
+ stylelint-order@6.0.4(stylelint@16.11.0(typescript@5.3.3)):
+ dependencies:
+ postcss: 8.4.49
+ postcss-sorting: 8.0.2(postcss@8.4.49)
+ stylelint: 16.11.0(typescript@5.3.3)
+
+ stylelint@16.11.0(typescript@5.3.3):
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.4(@csstools/css-tokenizer@3.0.3)
+ '@csstools/css-tokenizer': 3.0.3
+ '@csstools/media-query-list-parser': 4.0.2(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3)
+ '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.0.0)
+ '@dual-bundle/import-meta-resolve': 4.1.0
+ balanced-match: 2.0.0
+ colord: 2.9.3
+ cosmiconfig: 9.0.0(typescript@5.3.3)
+ css-functions-list: 3.2.3
+ css-tree: 3.0.1
+ debug: 4.3.7
+ fast-glob: 3.3.2
+ fastest-levenshtein: 1.0.16
+ file-entry-cache: 9.1.0
+ global-modules: 2.0.0
+ globby: 11.1.0
+ globjoin: 0.1.4
+ html-tags: 3.3.1
+ ignore: 6.0.2
+ imurmurhash: 0.1.4
+ is-plain-object: 5.0.0
+ known-css-properties: 0.35.0
+ mathml-tag-names: 2.1.3
+ meow: 13.2.0
+ micromatch: 4.0.8
+ normalize-path: 3.0.0
+ picocolors: 1.1.1
+ postcss: 8.4.49
+ postcss-resolve-nested-selector: 0.1.6
+ postcss-safe-parser: 7.0.1(postcss@8.4.49)
+ postcss-selector-parser: 7.0.0
+ postcss-value-parser: 4.2.0
+ resolve-from: 5.0.0
+ string-width: 4.2.3
+ supports-hyperlinks: 3.1.0
+ svg-tags: 1.0.0
+ table: 6.8.2
+ write-file-atomic: 5.0.1
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ supports-color@2.0.0: {}
+
+ supports-color@7.2.0:
+ dependencies:
+ has-flag: 4.0.0
+
+ supports-hyperlinks@3.1.0:
+ dependencies:
+ has-flag: 4.0.0
+ supports-color: 7.2.0
+
+ supports-preserve-symlinks-flag@1.0.0: {}
+
+ svg-tags@1.0.0: {}
+
+ svgo@3.3.2:
+ dependencies:
+ '@trysound/sax': 0.2.0
+ commander: 7.2.0
+ css-select: 5.1.0
+ css-tree: 2.3.1
+ css-what: 6.1.0
+ csso: 5.0.5
+ picocolors: 1.1.1
+
+ synckit@0.8.8:
+ dependencies:
+ '@pkgr/core': 0.1.1
+ tslib: 2.8.1
+
+ synckit@0.9.2:
+ dependencies:
+ '@pkgr/core': 0.1.1
+ tslib: 2.8.1
+
+ systemjs@6.15.1: {}
+
+ tabbable@6.2.0: {}
+
+ table@6.8.2:
+ dependencies:
+ ajv: 8.17.1
+ lodash.truncate: 4.4.2
+ slice-ansi: 4.0.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ terser@5.36.0:
+ dependencies:
+ '@jridgewell/source-map': 0.3.6
+ acorn: 8.14.0
+ commander: 2.20.3
+ source-map-support: 0.5.21
+
+ text-extensions@2.4.0: {}
+
+ text-table@0.2.0: {}
+
+ through@2.3.8: {}
+
+ tiny-svg@3.1.3: {}
+
+ tiny-warning@1.0.3: {}
+
+ tinyexec@0.3.1: {}
+
+ tinyglobby@0.2.10:
+ dependencies:
+ fdir: 6.4.2(picomatch@4.0.2)
+ picomatch: 4.0.2
+
+ to-regex-range@5.0.1:
+ dependencies:
+ is-number: 7.0.0
+
+ totalist@3.0.1: {}
+
+ tr46@0.0.3: {}
+
+ ts-api-utils@1.4.3(typescript@5.3.3):
+ dependencies:
+ typescript: 5.3.3
+
+ ts-api-utils@2.0.1(typescript@5.3.3):
+ dependencies:
+ typescript: 5.3.3
+
+ tslib@2.3.0: {}
+
+ tslib@2.8.1: {}
+
+ type-check@0.4.0:
+ dependencies:
+ prelude-ls: 1.2.1
+
+ type-fest@0.20.2: {}
+
+ type@2.7.3: {}
+
+ typescript@5.3.3: {}
+
+ uc.micro@2.1.0: {}
+
+ ufo@1.5.4: {}
+
+ unconfig@0.3.13:
+ dependencies:
+ '@antfu/utils': 0.7.10
+ defu: 6.1.4
+ jiti: 1.21.6
+
+ unconfig@7.3.1:
+ dependencies:
+ '@quansync/fs': 0.1.1
+ defu: 6.1.4
+ jiti: 2.4.2
+ quansync: 0.2.8
+
+ undici-types@6.19.8: {}
+
+ unicode-canonical-property-names-ecmascript@2.0.1: {}
+
+ unicode-match-property-ecmascript@2.0.0:
+ dependencies:
+ unicode-canonical-property-names-ecmascript: 2.0.1
+ unicode-property-aliases-ecmascript: 2.1.0
+
+ unicode-match-property-value-ecmascript@2.2.0: {}
+
+ unicode-property-aliases-ecmascript@2.1.0: {}
+
+ unicorn-magic@0.1.0: {}
+
+ unimport@3.14.2(rollup@4.27.4):
+ dependencies:
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ acorn: 8.14.0
+ escape-string-regexp: 5.0.0
+ estree-walker: 3.0.3
+ local-pkg: 0.5.1
+ magic-string: 0.30.14
+ mlly: 1.7.3
+ pathe: 1.1.2
+ picomatch: 4.0.2
+ pkg-types: 1.2.1
+ scule: 1.3.0
+ strip-literal: 2.1.1
+ tinyglobby: 0.2.10
+ unplugin: 1.16.0
+ transitivePeerDependencies:
+ - rollup
+
+ universalify@2.0.1: {}
+
+ unocss@0.58.9(postcss@8.4.49)(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ '@unocss/astro': 0.58.9(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ '@unocss/cli': 0.58.9(rollup@4.27.4)
+ '@unocss/core': 0.58.9
+ '@unocss/extractor-arbitrary-variants': 0.58.9
+ '@unocss/postcss': 0.58.9(postcss@8.4.49)
+ '@unocss/preset-attributify': 0.58.9
+ '@unocss/preset-icons': 0.58.9
+ '@unocss/preset-mini': 0.58.9
+ '@unocss/preset-tagify': 0.58.9
+ '@unocss/preset-typography': 0.58.9
+ '@unocss/preset-uno': 0.58.9
+ '@unocss/preset-web-fonts': 0.58.9
+ '@unocss/preset-wind': 0.58.9
+ '@unocss/reset': 0.58.9
+ '@unocss/transformer-attributify-jsx': 0.58.9
+ '@unocss/transformer-attributify-jsx-babel': 0.58.9
+ '@unocss/transformer-compile-class': 0.58.9
+ '@unocss/transformer-directives': 0.58.9
+ '@unocss/transformer-variant-group': 0.58.9
+ '@unocss/vite': 0.58.9(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0))
+ optionalDependencies:
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - postcss
+ - rollup
+ - supports-color
+
+ unplugin-auto-import@0.16.7(@vueuse/core@10.11.1(vue@3.5.12(typescript@5.3.3)))(rollup@4.27.4):
+ dependencies:
+ '@antfu/utils': 0.7.10
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ fast-glob: 3.3.2
+ local-pkg: 0.5.1
+ magic-string: 0.30.14
+ minimatch: 9.0.5
+ unimport: 3.14.2(rollup@4.27.4)
+ unplugin: 1.16.0
+ optionalDependencies:
+ '@vueuse/core': 10.11.1(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - rollup
+
+ unplugin-element-plus@0.8.0(rollup@4.27.4):
+ dependencies:
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ es-module-lexer: 1.5.4
+ magic-string: 0.30.14
+ unplugin: 1.16.0
+ transitivePeerDependencies:
+ - rollup
+
+ unplugin-vue-components@0.25.2(@babel/parser@7.26.2)(rollup@4.27.4)(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ '@antfu/utils': 0.7.10
+ '@rollup/pluginutils': 5.1.3(rollup@4.27.4)
+ chokidar: 3.6.0
+ debug: 4.3.7
+ fast-glob: 3.3.2
+ local-pkg: 0.4.3
+ magic-string: 0.30.14
+ minimatch: 9.0.5
+ resolve: 1.22.8
+ unplugin: 1.16.0
+ vue: 3.5.12(typescript@5.3.3)
+ optionalDependencies:
+ '@babel/parser': 7.26.2
+ transitivePeerDependencies:
+ - rollup
+ - supports-color
+
+ unplugin@1.16.0:
+ dependencies:
+ acorn: 8.14.0
+ webpack-virtual-modules: 0.6.2
+
+ update-browserslist-db@1.1.1(browserslist@4.24.2):
+ dependencies:
+ browserslist: 4.24.2
+ escalade: 3.2.0
+ picocolors: 1.1.1
+
+ uri-js@4.4.1:
+ dependencies:
+ punycode: 2.3.1
+
+ url-toolkit@2.2.5: {}
+
+ url@0.11.4:
+ dependencies:
+ punycode: 1.4.1
+ qs: 6.13.1
+
+ util-deprecate@1.0.2: {}
+
+ uuid@10.0.0: {}
+
+ vanilla-picker@2.12.3:
+ dependencies:
+ '@sphinxxxx/color-conversion': 2.2.2
+
+ video.js@7.21.6:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@videojs/http-streaming': 2.16.3(video.js@7.21.6)
+ '@videojs/vhs-utils': 3.0.5
+ '@videojs/xhr': 2.6.0
+ aes-decrypter: 3.1.3
+ global: 4.4.0
+ keycode: 2.2.1
+ m3u8-parser: 4.8.0
+ mpd-parser: 0.22.1
+ mux.js: 6.0.1
+ safe-json-parse: 4.0.0
+ videojs-font: 3.2.0
+ videojs-vtt.js: 0.15.5
+
+ videojs-font@3.2.0: {}
+
+ videojs-vtt.js@0.15.5:
+ dependencies:
+ global: 4.4.0
+
+ vite-plugin-compression@0.5.1(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ chalk: 4.1.2
+ debug: 4.3.7
+ fs-extra: 10.1.0
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - supports-color
+
+ vite-plugin-ejs@1.7.0(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ ejs: 3.1.10
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+
+ vite-plugin-eslint@1.8.1(eslint@8.57.1)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ '@rollup/pluginutils': 4.2.1
+ '@types/eslint': 8.56.12
+ eslint: 8.57.1
+ rollup: 2.79.2
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+
+ vite-plugin-progress@0.0.7(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ picocolors: 1.1.1
+ progress: 2.0.3
+ rd: 2.0.1
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+
+ vite-plugin-purge-icons@0.10.0(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ '@purge-icons/core': 0.10.0
+ '@purge-icons/generated': 0.10.0
+ rollup-plugin-purge-icons: 0.10.0
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
+ vite-plugin-svg-icons-ng@1.3.1(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ fast-glob: 3.3.3
+ fs-extra: 11.3.0
+ node-html-parser: 7.0.1
+ pathe: 2.0.3
+ svgo: 3.3.2
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+
+ vite-plugin-top-level-await@1.4.4(rollup@4.27.4)(vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)):
+ dependencies:
+ '@rollup/plugin-virtual': 3.0.2(rollup@4.27.4)
+ '@swc/core': 1.9.3
+ uuid: 10.0.0
+ vite: 5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0)
+ transitivePeerDependencies:
+ - '@swc/helpers'
+ - rollup
+
+ vite@5.1.4(@types/node@20.17.9)(sass@1.81.0)(terser@5.36.0):
+ dependencies:
+ esbuild: 0.19.12
+ postcss: 8.4.49
+ rollup: 4.27.4
+ optionalDependencies:
+ '@types/node': 20.17.9
+ fsevents: 2.3.3
+ sass: 1.81.0
+ terser: 5.36.0
+
+ vue-demi@0.14.10(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ vue: 3.5.12(typescript@5.3.3)
+
+ vue-dompurify-html@4.1.4(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ dompurify: 3.2.1
+ vue: 3.5.12(typescript@5.3.3)
+ vue-demi: 0.14.10(vue@3.5.12(typescript@5.3.3))
+ transitivePeerDependencies:
+ - '@vue/composition-api'
+
+ vue-eslint-parser@9.4.3(eslint@8.57.1):
+ dependencies:
+ debug: 4.3.7
+ eslint: 8.57.1
+ eslint-scope: 7.2.2
+ eslint-visitor-keys: 3.4.3
+ espree: 9.6.1
+ esquery: 1.6.0
+ lodash: 4.17.21
+ semver: 7.6.3
+ transitivePeerDependencies:
+ - supports-color
+
+ vue-i18n@9.10.2(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ '@intlify/core-base': 9.10.2
+ '@intlify/shared': 9.10.2
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.12(typescript@5.3.3)
+
+ vue-router@4.4.5(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.12(typescript@5.3.3)
+
+ vue-template-compiler@2.7.16:
+ dependencies:
+ de-indent: 1.0.2
+ he: 1.2.0
+
+ vue-tsc@1.8.27(typescript@5.3.3):
+ dependencies:
+ '@volar/typescript': 1.11.1
+ '@vue/language-core': 1.8.27(typescript@5.3.3)
+ semver: 7.6.3
+ typescript: 5.3.3
+
+ vue-types@5.1.3(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ is-plain-object: 5.0.0
+ optionalDependencies:
+ vue: 3.5.12(typescript@5.3.3)
+
+ vue3-print-nb@0.1.4(typescript@5.3.3):
+ dependencies:
+ vue: 3.5.12(typescript@5.3.3)
+ transitivePeerDependencies:
+ - typescript
+
+ vue3-signature@0.2.4(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ default-passive-events: 2.0.0
+ signature_pad: 3.0.0-beta.4
+ vue: 3.5.12(typescript@5.3.3)
+
+ vue@3.5.12(typescript@5.3.3):
+ dependencies:
+ '@vue/compiler-dom': 3.5.12
+ '@vue/compiler-sfc': 3.5.12
+ '@vue/runtime-dom': 3.5.12
+ '@vue/server-renderer': 3.5.12(vue@3.5.12(typescript@5.3.3))
+ '@vue/shared': 3.5.12
+ optionalDependencies:
+ typescript: 5.3.3
+
+ vuedraggable@4.1.0(vue@3.5.12(typescript@5.3.3)):
+ dependencies:
+ sortablejs: 1.14.0
+ vue: 3.5.12(typescript@5.3.3)
+
+ w3c-keyname@2.2.8: {}
+
+ wangeditor@4.7.15:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ '@babel/runtime-corejs3': 7.26.0
+ tslib: 2.8.1
+
+ web-storage-cache@1.1.1: {}
+
+ webidl-conversions@3.0.1: {}
+
+ webpack-virtual-modules@0.6.2: {}
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
+ which-module@2.0.1: {}
+
+ which@1.3.1:
+ dependencies:
+ isexe: 2.0.0
+
+ which@2.0.2:
+ dependencies:
+ isexe: 2.0.0
+
+ wildcard@1.1.2: {}
+
+ word-wrap@1.2.5: {}
+
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@7.0.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
+ wrap-ansi@8.1.0:
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+
+ wrap-ansi@9.0.0:
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 7.2.0
+ strip-ansi: 7.1.0
+
+ wrappy@1.0.2: {}
+
+ write-file-atomic@5.0.1:
+ dependencies:
+ imurmurhash: 0.1.4
+ signal-exit: 4.1.0
+
+ xml-js@1.6.11:
+ dependencies:
+ sax: 1.4.1
+
+ xml-name-validator@4.0.0: {}
+
+ y18n@4.0.3: {}
+
+ y18n@5.0.8: {}
+
+ yallist@3.1.1: {}
+
+ yaml-eslint-parser@1.2.3:
+ dependencies:
+ eslint-visitor-keys: 3.4.3
+ lodash: 4.17.21
+ yaml: 2.6.1
+
+ yaml@2.5.1: {}
+
+ yaml@2.6.1: {}
+
+ yargs-parser@18.1.3:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+
+ yargs-parser@21.1.1: {}
+
+ yargs@15.4.1:
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+
+ yargs@17.7.2:
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.2.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+
+ yocto-queue@0.1.0: {}
+
+ yocto-queue@1.1.1: {}
+
+ zeebe-bpmn-moddle@1.7.0: {}
+
+ zrender@5.6.0:
+ dependencies:
+ tslib: 2.3.0
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..961986e
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ plugins: {
+ autoprefixer: {}
+ }
+}
diff --git a/prettier.config.js b/prettier.config.js
new file mode 100644
index 0000000..b014bbf
--- /dev/null
+++ b/prettier.config.js
@@ -0,0 +1,22 @@
+module.exports = {
+ printWidth: 100, // 姣忚浠g爜闀垮害锛堥粯璁�80锛�
+ tabWidth: 2, // 姣忎釜tab鐩稿綋浜庡灏戜釜绌烘牸锛堥粯璁�2锛塧b杩涜缂╄繘锛堥粯璁alse锛�
+ useTabs: false, // 鏄惁浣跨敤tab
+ semi: false, // 澹版槑缁撳熬浣跨敤鍒嗗彿(榛樿true)
+ vueIndentScriptAndStyle: false,
+ singleQuote: true, // 浣跨敤鍗曞紩鍙凤紙榛樿false锛�
+ quoteProps: 'as-needed',
+ bracketSpacing: true, // 瀵硅薄瀛楅潰閲忕殑澶ф嫭鍙烽棿浣跨敤绌烘牸锛堥粯璁rue锛�
+ trailingComma: 'none', // 澶氳浣跨敤鎷栧熬閫楀彿锛堥粯璁one锛�
+ jsxSingleQuote: false,
+ // 绠ご鍑芥暟鍙傛暟鎷彿 榛樿avoid 鍙�� avoid| always
+ // avoid 鑳界渷鐣ユ嫭鍙风殑鏃跺�欏氨鐪佺暐 渚嬪x => x
+ // always 鎬绘槸鏈夋嫭鍙�
+ arrowParens: 'always',
+ insertPragma: false,
+ requirePragma: false,
+ proseWrap: 'never',
+ htmlWhitespaceSensitivity: 'strict',
+ endOfLine: 'auto',
+ rangeStart: 0
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..5a7de08
--- /dev/null
+++ b/public/favicon.ico
Binary files differ
diff --git a/public/logo.gif b/public/logo.gif
new file mode 100644
index 0000000..fdbd32c
--- /dev/null
+++ b/public/logo.gif
Binary files differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..7407d97
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,57 @@
+<script lang="ts" setup>
+import { isDark } from '@/utils/is'
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import routerSearch from '@/components/RouterSearch/index.vue'
+
+defineOptions({ name: 'APP' })
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('app')
+const appStore = useAppStore()
+const currentSize = computed(() => appStore.getCurrentSize)
+const greyMode = computed(() => appStore.getGreyMode)
+const { wsCache } = useCache()
+
+// 鏍规嵁娴忚鍣ㄥ綋鍓嶄富棰樿缃郴缁熶富棰樿壊
+const setDefaultTheme = () => {
+ let isDarkTheme = wsCache.get(CACHE_KEY.IS_DARK)
+ if (isDarkTheme === null) {
+ isDarkTheme = isDark()
+ }
+ appStore.setIsDark(isDarkTheme)
+}
+setDefaultTheme()
+</script>
+<template>
+ <ConfigGlobal :size="currentSize">
+ <RouterView :class="greyMode ? `${prefixCls}-grey-mode` : ''" />
+ <routerSearch />
+ </ConfigGlobal>
+</template>
+<style lang="scss">
+$prefix-cls: #{$namespace}-app;
+
+.size {
+ width: 100%;
+ height: 100%;
+}
+
+html,
+body {
+ @extend .size;
+
+ padding: 0 !important;
+ margin: 0;
+ overflow: hidden;
+
+ #app {
+ @extend .size;
+ }
+}
+
+.#{$prefix-cls}-grey-mode {
+ filter: grayscale(100%);
+}
+</style>
diff --git a/src/api/ai/chat/conversation/index.ts b/src/api/ai/chat/conversation/index.ts
new file mode 100644
index 0000000..6ce4482
--- /dev/null
+++ b/src/api/ai/chat/conversation/index.ts
@@ -0,0 +1,65 @@
+import request from '@/config/axios'
+
+// AI 鑱婂ぉ瀵硅瘽 VO
+export interface ChatConversationVO {
+ id: number // ID 缂栧彿
+ userId: number // 鐢ㄦ埛缂栧彿
+ title: string // 瀵硅瘽鏍囬
+ pinned: boolean // 鏄惁缃《
+ roleId: number // 瑙掕壊缂栧彿
+ modelId: number // 妯″瀷缂栧彿
+ model: string // 妯″瀷鏍囧織
+ temperature: number // 娓╁害鍙傛暟
+ maxTokens: number // 鍗曟潯鍥炲鐨勬渶澶� Token 鏁伴噺
+ maxContexts: number // 涓婁笅鏂囩殑鏈�澶� Message 鏁伴噺
+ createTime?: Date // 鍒涘缓鏃堕棿
+ // 棰濆瀛楁
+ systemMessage?: string // 瑙掕壊璁惧畾
+ modelName?: string // 妯″瀷鍚嶅瓧
+ roleAvatar?: string // 瑙掕壊澶村儚
+ modelMaxTokens?: string // 妯″瀷鐨勫崟鏉″洖澶嶇殑鏈�澶� Token 鏁伴噺
+ modelMaxContexts?: string // 妯″瀷鐨勪笂涓嬫枃鐨勬渶澶� Message 鏁伴噺
+}
+
+// AI 鑱婂ぉ瀵硅瘽 API
+export const ChatConversationApi = {
+ // 鑾峰緱銆愭垜鐨勩�戣亰澶╁璇�
+ getChatConversationMy: async (id: number) => {
+ return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` })
+ },
+
+ // 鏂板銆愭垜鐨勩�戣亰澶╁璇�
+ createChatConversationMy: async (data?: ChatConversationVO) => {
+ return await request.post({ url: `/ai/chat/conversation/create-my`, data })
+ },
+
+ // 鏇存柊銆愭垜鐨勩�戣亰澶╁璇�
+ updateChatConversationMy: async (data: ChatConversationVO) => {
+ return await request.put({ url: `/ai/chat/conversation/update-my`, data })
+ },
+
+ // 鍒犻櫎銆愭垜鐨勩�戣亰澶╁璇�
+ deleteChatConversationMy: async (id: string) => {
+ return await request.delete({ url: `/ai/chat/conversation/delete-my?id=${id}` })
+ },
+
+ // 鍒犻櫎銆愭垜鐨勩�戞墍鏈夊璇濓紝缃《闄ゅ
+ deleteChatConversationMyByUnpinned: async () => {
+ return await request.delete({ url: `/ai/chat/conversation/delete-by-unpinned` })
+ },
+
+ // 鑾峰緱銆愭垜鐨勩�戣亰澶╁璇濆垪琛�
+ getChatConversationMyList: async () => {
+ return await request.get({ url: `/ai/chat/conversation/my-list` })
+ },
+
+ // 鑾峰緱瀵硅瘽鍒嗛〉
+ getChatConversationPage: async (params: any) => {
+ return await request.get({ url: `/ai/chat/conversation/page`, params })
+ },
+
+ // 绠$悊鍛樺垹闄ゆ秷鎭�
+ deleteChatConversationByAdmin: async (id: number) => {
+ return await request.delete({ url: `/ai/chat/conversation/delete-by-admin?id=${id}` })
+ }
+}
diff --git a/src/api/ai/chat/message/index.ts b/src/api/ai/chat/message/index.ts
new file mode 100644
index 0000000..19bc25b
--- /dev/null
+++ b/src/api/ai/chat/message/index.ts
@@ -0,0 +1,104 @@
+import request from '@/config/axios'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+import { getAccessToken } from '@/utils/auth'
+import { config } from '@/config/axios/config'
+
+// 鑱婂ぉVO
+export interface ChatMessageVO {
+ id: number // 缂栧彿
+ conversationId: number // 瀵硅瘽缂栧彿
+ type: string // 娑堟伅绫诲瀷
+ userId: string // 鐢ㄦ埛缂栧彿
+ roleId: string // 瑙掕壊缂栧彿
+ model: number // 妯″瀷鏍囧織
+ modelId: number // 妯″瀷缂栧彿
+ content: string // 鑱婂ぉ鍐呭
+ reasoningContent?: string // 鎺ㄧ悊鍐呭
+ attachmentUrls?: string[] // 闄勪欢 URL 鏁扮粍
+ tokens: number // 娑堣�� Token 鏁伴噺
+ segmentIds?: number[] // 娈佃惤缂栧彿
+ segments?: {
+ id: number // 娈佃惤缂栧彿
+ content: string // 娈佃惤鍐呭
+ documentId: number // 鏂囨。缂栧彿
+ documentName: string // 鏂囨。鍚嶇О
+ }[]
+ webSearchPages?: {
+ name: string // 鍚嶇О
+ icon: string // 鍥炬爣
+ title: string // 鏍囬
+ url: string // URL
+ snippet: string // 鍐呭鐨勭畝鐭弿杩�
+ summary: string // 鍐呭鐨勬枃鏈憳瑕�
+ }[]
+ createTime: Date // 鍒涘缓鏃堕棿
+ roleAvatar: string // 瑙掕壊澶村儚
+ userAvatar: string // 鐢ㄦ埛澶村儚
+}
+
+// AI chat 鑱婂ぉ
+export const ChatMessageApi = {
+ // 娑堟伅鍒楄〃
+ getChatMessageListByConversationId: async (conversationId: number | null) => {
+ return await request.get({
+ url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`
+ })
+ },
+
+ // 鍙戦�� Stream 娑堟伅
+ // 涓轰粈涔堜笉鐢� axios 鍛紵鍥犱负瀹冧笉鏀寔 SSE 璋冪敤
+ sendChatMessageStream: async (
+ conversationId: number,
+ content: string,
+ ctrl,
+ enableContext: boolean,
+ enableWebSearch: boolean,
+ onMessage,
+ onError,
+ onClose,
+ attachmentUrls?: string[]
+ ) => {
+ const token = getAccessToken()
+ return fetchEventSource(`${config.base_url}/ai/chat/message/send-stream`, {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`
+ },
+ openWhenHidden: true,
+ body: JSON.stringify({
+ conversationId,
+ content,
+ useContext: enableContext,
+ useSearch: enableWebSearch,
+ attachmentUrls: attachmentUrls || []
+ }),
+ onmessage: onMessage,
+ onerror: onError,
+ onclose: onClose,
+ signal: ctrl.signal
+ })
+ },
+
+ // 鍒犻櫎娑堟伅
+ deleteChatMessage: async (id: string) => {
+ return await request.delete({ url: `/ai/chat/message/delete?id=${id}` })
+ },
+
+ // 鍒犻櫎鎸囧畾瀵硅瘽鐨勬秷鎭�
+ deleteByConversationId: async (conversationId: number) => {
+ return await request.delete({
+ url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`
+ })
+ },
+
+ // 鑾峰緱娑堟伅鍒嗛〉
+ getChatMessagePage: async (params: any) => {
+ return await request.get({ url: '/ai/chat/message/page', params })
+ },
+
+ // 绠$悊鍛樺垹闄ゆ秷鎭�
+ deleteChatMessageByAdmin: async (id: number) => {
+ return await request.delete({ url: `/ai/chat/message/delete-by-admin?id=${id}` })
+ }
+}
diff --git a/src/api/ai/image/index.ts b/src/api/ai/image/index.ts
new file mode 100644
index 0000000..ab2bcf7
--- /dev/null
+++ b/src/api/ai/image/index.ts
@@ -0,0 +1,102 @@
+import request from '@/config/axios'
+
+// AI 缁樺浘 VO
+export interface ImageVO {
+ id: number // 缂栧彿
+ platform: string // 骞冲彴
+ model: string // 妯″瀷
+ prompt: string // 鎻愮ず璇�
+ width: number // 鍥剧墖瀹藉害
+ height: number // 鍥剧墖楂樺害
+ status: number // 鐘舵��
+ publicStatus: boolean // 鍏紑鐘舵��
+ picUrl: string // 浠诲姟鍦板潃
+ errorMessage: string // 閿欒淇℃伅
+ options: any // 閰嶇疆 Map<string, string>
+ taskId: number // 浠诲姟缂栧彿
+ buttons: ImageMidjourneyButtonsVO[] // mj 鎿嶄綔鎸夐挳
+ createTime: Date // 鍒涘缓鏃堕棿
+ finishTime: Date // 瀹屾垚鏃堕棿
+}
+
+export interface ImageDrawReqVO {
+ prompt: string // 鎻愮ず璇�
+ modelId: number // 妯″瀷
+ style: string // 鍥惧儚鐢熸垚鐨勯鏍�
+ width: string // 鍥剧墖瀹藉害
+ height: string // 鍥剧墖楂樺害
+ options: object // 缁樺埗鍙傛暟锛孧ap<String, String>
+}
+
+export interface ImageMidjourneyImagineReqVO {
+ prompt: string // 鎻愮ず璇�
+ modelId: number // 妯″瀷
+ base64Array: string[] // size涓嶈兘涓虹┖
+ width: string // 鍥剧墖瀹藉害
+ height: string // 鍥剧墖楂樺害
+ version: string // 鐗堟湰
+}
+
+export interface ImageMidjourneyActionVO {
+ id: number // 鍥剧墖缂栧彿
+ customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 鍔ㄤ綔鏍囪瘑
+}
+
+export interface ImageMidjourneyButtonsVO {
+ customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 鍔ㄤ綔鏍囪瘑
+ emoji: string // 鍥炬爣 emoji
+ label: string // Make Variations 鏂囨湰
+ style: number // 鏍峰紡: 2锛圥rimary锛夈��3锛圙reen锛�
+}
+
+// AI 鍥剧墖 API
+export const ImageApi = {
+ // 鑾峰彇銆愭垜鐨勩�戠粯鍥惧垎椤�
+ getImagePageMy: async (params: any) => {
+ return await request.get({ url: `/ai/image/my-page`, params })
+ },
+ // 鑾峰彇銆愭垜鐨勩�戠粯鍥捐褰�
+ getImageMy: async (id: number) => {
+ return await request.get({ url: `/ai/image/get-my?id=${id}` })
+ },
+ // 鑾峰彇銆愭垜鐨勩�戠粯鍥捐褰曞垪琛�
+ getImageListMyByIds: async (ids: number[]) => {
+ return await request.get({ url: `/ai/image/my-list-by-ids`, params: { ids: ids.join(',') } })
+ },
+ // 鐢熸垚鍥剧墖
+ drawImage: async (data: ImageDrawReqVO) => {
+ return await request.post({ url: `/ai/image/draw`, data })
+ },
+ // 鍒犻櫎銆愭垜鐨勩�戠粯鐢昏褰�
+ deleteImageMy: async (id: number) => {
+ return await request.delete({ url: `/ai/image/delete-my?id=${id}` })
+ },
+
+ // ================ midjourney 涓撳睘 ================
+
+ // 銆怣idjourney銆戠敓鎴愬浘鐗�
+ midjourneyImagine: async (data: ImageMidjourneyImagineReqVO) => {
+ return await request.post({ url: `/ai/image/midjourney/imagine`, data })
+ },
+ // 銆怣idjourney銆慉ction 鎿嶄綔锛堜簩娆$敓鎴愬浘鐗囷級
+ midjourneyAction: async (data: ImageMidjourneyActionVO) => {
+ return await request.post({ url: `/ai/image/midjourney/action`, data })
+ },
+
+ // ================ 缁樺浘绠$悊 ================
+
+ // 鏌ヨ缁樼敾鍒嗛〉
+ getImagePage: async (params: any) => {
+ return await request.get({ url: `/ai/image/page`, params })
+ },
+
+ // 鏇存柊缁樼敾鍙戝竷鐘舵��
+ updateImage: async (data: any) => {
+ return await request.put({ url: '/ai/image/update', data })
+ },
+
+ // 鍒犻櫎缁樼敾
+ deleteImage: async (id: number) => {
+ return await request.delete({ url: `/ai/image/delete?id=` + id })
+ }
+}
diff --git a/src/api/ai/knowledge/document/index.ts b/src/api/ai/knowledge/document/index.ts
new file mode 100644
index 0000000..62c24d5
--- /dev/null
+++ b/src/api/ai/knowledge/document/index.ts
@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+// AI 鐭ヨ瘑搴撴枃妗� VO
+export interface KnowledgeDocumentVO {
+ id: number // 缂栧彿
+ knowledgeId: number // 鐭ヨ瘑搴撶紪鍙�
+ name: string // 鏂囨。鍚嶇О
+ contentLength: number // 瀛楃鏁�
+ tokens: number // token 鏁�
+ segmentMaxTokens: number // 鍒嗙墖鏈�澶� token 鏁�
+ retrievalCount: number // 鍙洖娆℃暟
+ status: number // 鏄惁鍚敤
+}
+
+// AI 鐭ヨ瘑搴撴枃妗� API
+export const KnowledgeDocumentApi = {
+ // 鏌ヨ鐭ヨ瘑搴撴枃妗e垎椤�
+ getKnowledgeDocumentPage: async (params: any) => {
+ return await request.get({ url: `/ai/knowledge/document/page`, params })
+ },
+
+ // 鏌ヨ鐭ヨ瘑搴撴枃妗h鎯�
+ getKnowledgeDocument: async (id: number) => {
+ return await request.get({ url: `/ai/knowledge/document/get?id=` + id })
+ },
+
+ // 鏂板鐭ヨ瘑搴撴枃妗o紙鍗曚釜锛�
+ createKnowledgeDocument: async (data: any) => {
+ return await request.post({ url: `/ai/knowledge/document/create`, data })
+ },
+
+ // 鏂板鐭ヨ瘑搴撴枃妗o紙澶氫釜锛�
+ createKnowledgeDocumentList: async (data: any) => {
+ return await request.post({ url: `/ai/knowledge/document/create-list`, data })
+ },
+
+ // 淇敼鐭ヨ瘑搴撴枃妗�
+ updateKnowledgeDocument: async (data: any) => {
+ return await request.put({ url: `/ai/knowledge/document/update`, data })
+ },
+
+ // 淇敼鐭ヨ瘑搴撴枃妗g姸鎬�
+ updateKnowledgeDocumentStatus: async (data: any) => {
+ return await request.put({
+ url: `/ai/knowledge/document/update-status`,
+ data
+ })
+ },
+
+ // 鍒犻櫎鐭ヨ瘑搴撴枃妗�
+ deleteKnowledgeDocument: async (id: number) => {
+ return await request.delete({ url: `/ai/knowledge/document/delete?id=` + id })
+ }
+}
diff --git a/src/api/ai/knowledge/knowledge/index.ts b/src/api/ai/knowledge/knowledge/index.ts
new file mode 100644
index 0000000..f9d3683
--- /dev/null
+++ b/src/api/ai/knowledge/knowledge/index.ts
@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+// AI 鐭ヨ瘑搴� VO
+export interface KnowledgeVO {
+ id: number // 缂栧彿
+ name: string // 鐭ヨ瘑搴撳悕绉�
+ description: string // 鐭ヨ瘑搴撴弿杩�
+ embeddingModelId: number // 宓屽叆妯″瀷缂栧彿锛岄珮璐ㄩ噺妯″紡鏃剁淮鎶�
+ topK: number // topK
+ similarityThreshold: number // 鐩镐技搴﹂槇鍊�
+}
+
+// AI 鐭ヨ瘑搴� API
+export const KnowledgeApi = {
+ // 鏌ヨ鐭ヨ瘑搴撳垎椤�
+ getKnowledgePage: async (params: any) => {
+ return await request.get({ url: `/ai/knowledge/page`, params })
+ },
+
+ // 鏌ヨ鐭ヨ瘑搴撹鎯�
+ getKnowledge: async (id: number) => {
+ return await request.get({ url: `/ai/knowledge/get?id=` + id })
+ },
+
+ // 鏂板鐭ヨ瘑搴�
+ createKnowledge: async (data: KnowledgeVO) => {
+ return await request.post({ url: `/ai/knowledge/create`, data })
+ },
+
+ // 淇敼鐭ヨ瘑搴�
+ updateKnowledge: async (data: KnowledgeVO) => {
+ return await request.put({ url: `/ai/knowledge/update`, data })
+ },
+
+ // 鍒犻櫎鐭ヨ瘑搴�
+ deleteKnowledge: async (id: number) => {
+ return await request.delete({ url: `/ai/knowledge/delete?id=` + id })
+ },
+
+ // 鑾峰彇鐭ヨ瘑搴撶畝鍗曞垪琛�
+ getSimpleKnowledgeList: async () => {
+ return await request.get({ url: `/ai/knowledge/simple-list` })
+ }
+}
diff --git a/src/api/ai/knowledge/segment/index.ts b/src/api/ai/knowledge/segment/index.ts
new file mode 100644
index 0000000..d234d99
--- /dev/null
+++ b/src/api/ai/knowledge/segment/index.ts
@@ -0,0 +1,75 @@
+import request from '@/config/axios'
+
+// AI 鐭ヨ瘑搴撳垎娈� VO
+export interface KnowledgeSegmentVO {
+ id: number // 缂栧彿
+ documentId: number // 鏂囨。缂栧彿
+ knowledgeId: number // 鐭ヨ瘑搴撶紪鍙�
+ vectorId: string // 鍚戦噺搴撶紪鍙�
+ content: string // 鍒囩墖鍐呭
+ contentLength: number // 鍒囩墖鍐呭闀垮害
+ tokens: number // token 鏁伴噺
+ retrievalCount: number // 鍙洖娆℃暟
+ status: number // 鏂囨。鐘舵��
+ createTime: number // 鍒涘缓鏃堕棿
+}
+
+// AI 鐭ヨ瘑搴撳垎娈� API
+export const KnowledgeSegmentApi = {
+ // 鏌ヨ鐭ヨ瘑搴撳垎娈靛垎椤�
+ getKnowledgeSegmentPage: async (params: any) => {
+ return await request.get({ url: `/ai/knowledge/segment/page`, params })
+ },
+
+ // 鏌ヨ鐭ヨ瘑搴撳垎娈佃鎯�
+ getKnowledgeSegment: async (id: number) => {
+ return await request.get({ url: `/ai/knowledge/segment/get?id=` + id })
+ },
+
+ // 鍒犻櫎鐭ヨ瘑搴撳垎娈�
+ deleteKnowledgeSegment: async (id: number) => {
+ return await request.delete({ url: `/ai/knowledge/segment/delete?id=` + id })
+ },
+
+ // 鏂板鐭ヨ瘑搴撳垎娈�
+ createKnowledgeSegment: async (data: KnowledgeSegmentVO) => {
+ return await request.post({ url: `/ai/knowledge/segment/create`, data })
+ },
+
+ // 淇敼鐭ヨ瘑搴撳垎娈�
+ updateKnowledgeSegment: async (data: KnowledgeSegmentVO) => {
+ return await request.put({ url: `/ai/knowledge/segment/update`, data })
+ },
+
+ // 淇敼鐭ヨ瘑搴撳垎娈电姸鎬�
+ updateKnowledgeSegmentStatus: async (data: any) => {
+ return await request.put({
+ url: `/ai/knowledge/segment/update-status`,
+ data
+ })
+ },
+
+ // 鍒囩墖鍐呭
+ splitContent: async (url: string, segmentMaxTokens: number) => {
+ return await request.get({
+ url: `/ai/knowledge/segment/split`,
+ params: { url, segmentMaxTokens }
+ })
+ },
+
+ // 鑾峰彇鏂囨。澶勭悊鍒楄〃
+ getKnowledgeSegmentProcessList: async (documentIds: number[]) => {
+ return await request.get({
+ url: `/ai/knowledge/segment/get-process-list`,
+ params: { documentIds: documentIds.join(',') }
+ })
+ },
+
+ // 鎼滅储鐭ヨ瘑搴撳垎娈�
+ searchKnowledgeSegment: async (params: any) => {
+ return await request.get({
+ url: `/ai/knowledge/segment/search`,
+ params
+ })
+ }
+}
diff --git a/src/api/ai/mindmap/index.ts b/src/api/ai/mindmap/index.ts
new file mode 100644
index 0000000..59b4fd8
--- /dev/null
+++ b/src/api/ai/mindmap/index.ts
@@ -0,0 +1,60 @@
+import { getAccessToken } from '@/utils/auth'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+import { config } from '@/config/axios/config'
+import request from '@/config/axios' // AI 鎬濈淮瀵煎浘 VO
+
+// AI 鎬濈淮瀵煎浘 VO
+export interface MindMapVO {
+ id: number // 缂栧彿
+ userId: number // 鐢ㄦ埛缂栧彿
+ prompt: string // 鐢熸垚鍐呭鎻愮ず
+ generatedContent: string // 鐢熸垚鐨勬�濈淮瀵煎浘鍐呭
+ platform: string // 骞冲彴
+ model: string // 妯″瀷
+ errorMessage: string // 閿欒淇℃伅
+}
+
+// AI 鎬濈淮瀵煎浘鐢熸垚 VO
+export interface AiMindMapGenerateReqVO {
+ prompt: string
+}
+
+export const AiMindMapApi = {
+ generateMindMap: ({
+ data,
+ onClose,
+ onMessage,
+ onError,
+ ctrl
+ }: {
+ data: AiMindMapGenerateReqVO
+ onMessage?: (res: any) => void
+ onError?: (...args: any[]) => void
+ onClose?: (...args: any[]) => void
+ ctrl: AbortController
+ }) => {
+ const token = getAccessToken()
+ return fetchEventSource(`${config.base_url}/ai/mind-map/generate-stream`, {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`
+ },
+ openWhenHidden: true,
+ body: JSON.stringify(data),
+ onmessage: onMessage,
+ onerror: onError,
+ onclose: onClose,
+ signal: ctrl.signal
+ })
+ },
+
+ // 鏌ヨ鎬濈淮瀵煎浘鍒嗛〉
+ getMindMapPage: async (params: any) => {
+ return await request.get({ url: `/ai/mind-map/page`, params })
+ },
+ // 鍒犻櫎鎬濈淮瀵煎浘
+ deleteMindMap: async (id: number) => {
+ return await request.delete({ url: `/ai/mind-map/delete?id=` + id })
+ }
+}
diff --git a/src/api/ai/model/apiKey/index.ts b/src/api/ai/model/apiKey/index.ts
new file mode 100644
index 0000000..ed94836
--- /dev/null
+++ b/src/api/ai/model/apiKey/index.ts
@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+// AI API 瀵嗛挜 VO
+export interface ApiKeyVO {
+ id: number // 缂栧彿
+ name: string // 鍚嶇О
+ apiKey: string // 瀵嗛挜
+ platform: string // 骞冲彴
+ url: string // 鑷畾涔� API 鍦板潃
+ status: number // 鐘舵��
+}
+
+// AI API 瀵嗛挜 API
+export const ApiKeyApi = {
+ // 鏌ヨ API 瀵嗛挜鍒嗛〉
+ getApiKeyPage: async (params: any) => {
+ return await request.get({ url: `/ai/api-key/page`, params })
+ },
+
+ // 鑾峰緱 API 瀵嗛挜鍒楄〃
+ getApiKeySimpleList: async () => {
+ return await request.get({ url: `/ai/api-key/simple-list` })
+ },
+
+ // 鏌ヨ API 瀵嗛挜璇︽儏
+ getApiKey: async (id: number) => {
+ return await request.get({ url: `/ai/api-key/get?id=` + id })
+ },
+
+ // 鏂板 API 瀵嗛挜
+ createApiKey: async (data: ApiKeyVO) => {
+ return await request.post({ url: `/ai/api-key/create`, data })
+ },
+
+ // 淇敼 API 瀵嗛挜
+ updateApiKey: async (data: ApiKeyVO) => {
+ return await request.put({ url: `/ai/api-key/update`, data })
+ },
+
+ // 鍒犻櫎 API 瀵嗛挜
+ deleteApiKey: async (id: number) => {
+ return await request.delete({ url: `/ai/api-key/delete?id=` + id })
+ }
+}
diff --git a/src/api/ai/model/chatRole/index.ts b/src/api/ai/model/chatRole/index.ts
new file mode 100644
index 0000000..ffdd305
--- /dev/null
+++ b/src/api/ai/model/chatRole/index.ts
@@ -0,0 +1,83 @@
+import request from '@/config/axios'
+
+// AI 鑱婂ぉ瑙掕壊 VO
+export interface ChatRoleVO {
+ id: number // 瑙掕壊缂栧彿
+ modelId: number // 妯″瀷缂栧彿
+ name: string // 瑙掕壊鍚嶇О
+ avatar: string // 瑙掕壊澶村儚
+ category: string // 瑙掕壊绫诲埆
+ sort: number // 瑙掕壊鎺掑簭
+ description: string // 瑙掕壊鎻忚堪
+ systemMessage: string // 瑙掕壊璁惧畾
+ welcomeMessage: string // 瑙掕壊璁惧畾
+ publicStatus: boolean // 鏄惁鍏紑
+ status: number // 鐘舵��
+ knowledgeIds?: number[] // 寮曠敤鐨勭煡璇嗗簱 ID 鍒楄〃
+ toolIds?: number[] // 寮曠敤鐨勫伐鍏� ID 鍒楄〃
+ mcpClientNames?: string[] // 寮曠敤鐨� MCP Client 鍚嶅瓧鍒楄〃
+}
+
+// AI 鑱婂ぉ瑙掕壊 鍒嗛〉璇锋眰 vo
+export interface ChatRolePageReqVO {
+ name?: string // 瑙掕壊鍚嶇О
+ category?: string // 瑙掕壊绫诲埆
+ publicStatus: boolean // 鏄惁鍏紑
+ pageNo: number // 鏄惁鍏紑
+ pageSize: number // 鏄惁鍏紑
+}
+
+// AI 鑱婂ぉ瑙掕壊 API
+export const ChatRoleApi = {
+ // 鏌ヨ鑱婂ぉ瑙掕壊鍒嗛〉
+ getChatRolePage: async (params: any) => {
+ return await request.get({ url: `/ai/chat-role/page`, params })
+ },
+
+ // 鏌ヨ鑱婂ぉ瑙掕壊璇︽儏
+ getChatRole: async (id: number) => {
+ return await request.get({ url: `/ai/chat-role/get?id=` + id })
+ },
+
+ // 鏂板鑱婂ぉ瑙掕壊
+ createChatRole: async (data: ChatRoleVO) => {
+ return await request.post({ url: `/ai/chat-role/create`, data })
+ },
+
+ // 淇敼鑱婂ぉ瑙掕壊
+ updateChatRole: async (data: ChatRoleVO) => {
+ return await request.put({ url: `/ai/chat-role/update`, data })
+ },
+
+ // 鍒犻櫎鑱婂ぉ瑙掕壊
+ deleteChatRole: async (id: number) => {
+ return await request.delete({ url: `/ai/chat-role/delete?id=` + id })
+ },
+
+ // ======= chat 鑱婂ぉ
+
+ // 鑾峰彇 my role
+ getMyPage: async (params: ChatRolePageReqVO) => {
+ return await request.get({ url: `/ai/chat-role/my-page`, params })
+ },
+
+ // 鑾峰彇瑙掕壊鍒嗙被
+ getCategoryList: async () => {
+ return await request.get({ url: `/ai/chat-role/category-list` })
+ },
+
+ // 鍒涘缓瑙掕壊
+ createMy: async (data: ChatRoleVO) => {
+ return await request.post({ url: `/ai/chat-role/create-my`, data })
+ },
+
+ // 鏇存柊瑙掕壊
+ updateMy: async (data: ChatRoleVO) => {
+ return await request.put({ url: `/ai/chat-role/update-my`, data })
+ },
+
+ // 鍒犻櫎瑙掕壊 my
+ deleteMy: async (id: number) => {
+ return await request.delete({ url: `/ai/chat-role/delete-my?id=` + id })
+ }
+}
diff --git a/src/api/ai/model/model/index.ts b/src/api/ai/model/model/index.ts
new file mode 100644
index 0000000..7c485a0
--- /dev/null
+++ b/src/api/ai/model/model/index.ts
@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+// AI 妯″瀷 VO
+export interface ModelVO {
+ id: number // 缂栧彿
+ keyId: number // API 绉橀挜缂栧彿
+ name: string // 妯″瀷鍚嶅瓧
+ model: string // 妯″瀷鏍囪瘑
+ platform: string // 妯″瀷骞冲彴
+ type: number // 妯″瀷绫诲瀷
+ sort: number // 鎺掑簭
+ status: number // 鐘舵��
+ temperature?: number // 娓╁害鍙傛暟
+ maxTokens?: number // 鍗曟潯鍥炲鐨勬渶澶� Token 鏁伴噺
+ maxContexts?: number // 涓婁笅鏂囩殑鏈�澶� Message 鏁伴噺
+}
+
+// AI 妯″瀷 API
+export const ModelApi = {
+ // 鏌ヨ妯″瀷鍒嗛〉
+ getModelPage: async (params: any) => {
+ return await request.get({ url: `/ai/model/page`, params })
+ },
+
+ // 鑾峰緱妯″瀷鍒楄〃
+ getModelSimpleList: async (type?: number) => {
+ return await request.get({
+ url: `/ai/model/simple-list`,
+ params: {
+ type
+ }
+ })
+ },
+
+ // 鏌ヨ妯″瀷璇︽儏
+ getModel: async (id: number) => {
+ return await request.get({ url: `/ai/model/get?id=` + id })
+ },
+
+ // 鏂板妯″瀷
+ createModel: async (data: ModelVO) => {
+ return await request.post({ url: `/ai/model/create`, data })
+ },
+
+ // 淇敼妯″瀷
+ updateModel: async (data: ModelVO) => {
+ return await request.put({ url: `/ai/model/update`, data })
+ },
+
+ // 鍒犻櫎妯″瀷
+ deleteModel: async (id: number) => {
+ return await request.delete({ url: `/ai/model/delete?id=` + id })
+ }
+}
diff --git a/src/api/ai/model/tool/index.ts b/src/api/ai/model/tool/index.ts
new file mode 100644
index 0000000..bfb896a
--- /dev/null
+++ b/src/api/ai/model/tool/index.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+// AI 宸ュ叿 VO
+export interface ToolVO {
+ id: number // 宸ュ叿缂栧彿
+ name: string // 宸ュ叿鍚嶇О
+ description: string // 宸ュ叿鎻忚堪
+ status: number // 鐘舵��
+}
+
+// AI 宸ュ叿 API
+export const ToolApi = {
+ // 鏌ヨ宸ュ叿鍒嗛〉
+ getToolPage: async (params: any) => {
+ return await request.get({ url: `/ai/tool/page`, params })
+ },
+
+ // 鏌ヨ宸ュ叿璇︽儏
+ getTool: async (id: number) => {
+ return await request.get({ url: `/ai/tool/get?id=` + id })
+ },
+
+ // 鏂板宸ュ叿
+ createTool: async (data: ToolVO) => {
+ return await request.post({ url: `/ai/tool/create`, data })
+ },
+
+ // 淇敼宸ュ叿
+ updateTool: async (data: ToolVO) => {
+ return await request.put({ url: `/ai/tool/update`, data })
+ },
+
+ // 鍒犻櫎宸ュ叿
+ deleteTool: async (id: number) => {
+ return await request.delete({ url: `/ai/tool/delete?id=` + id })
+ },
+
+ // 鑾峰彇宸ュ叿绠�鍗曞垪琛�
+ getToolSimpleList: async () => {
+ return await request.get({ url: `/ai/tool/simple-list` })
+ }
+}
diff --git a/src/api/ai/music/index.ts b/src/api/ai/music/index.ts
new file mode 100644
index 0000000..74b8526
--- /dev/null
+++ b/src/api/ai/music/index.ts
@@ -0,0 +1,41 @@
+import request from '@/config/axios'
+
+// AI 闊充箰 VO
+export interface MusicVO {
+ id: number // 缂栧彿
+ userId: number // 鐢ㄦ埛缂栧彿
+ title: string // 闊充箰鍚嶇О
+ lyric: string // 姝岃瘝
+ imageUrl: string // 鍥剧墖鍦板潃
+ audioUrl: string // 闊抽鍦板潃
+ videoUrl: string // 瑙嗛鍦板潃
+ status: number // 闊充箰鐘舵��
+ gptDescriptionPrompt: string // 鎻忚堪璇�
+ prompt: string // 鎻愮ず璇�
+ platform: string // 妯″瀷骞冲彴
+ model: string // 妯″瀷
+ generateMode: number // 鐢熸垚妯″紡
+ tags: string // 闊充箰椋庢牸鏍囩
+ duration: number // 闊充箰鏃堕暱
+ publicStatus: boolean // 鏄惁鍙戝竷
+ taskId: string // 浠诲姟id
+ errorMessage: string // 閿欒淇℃伅
+}
+
+// AI 闊充箰 API
+export const MusicApi = {
+ // 鏌ヨ闊充箰鍒嗛〉
+ getMusicPage: async (params: any) => {
+ return await request.get({ url: `/ai/music/page`, params })
+ },
+
+ // 鏇存柊闊充箰
+ updateMusic: async (data: any) => {
+ return await request.put({ url: '/ai/music/update', data })
+ },
+
+ // 鍒犻櫎闊充箰
+ deleteMusic: async (id: number) => {
+ return await request.delete({ url: `/ai/music/delete?id=` + id })
+ }
+}
diff --git a/src/api/ai/workflow/index.ts b/src/api/ai/workflow/index.ts
new file mode 100644
index 0000000..5245911
--- /dev/null
+++ b/src/api/ai/workflow/index.ts
@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+export const getWorkflowPage = async (params) => {
+ return await request.get({ url: '/ai/workflow/page', params })
+}
+
+export const getWorkflow = async (id) => {
+ return await request.get({ url: '/ai/workflow/get?id=' + id })
+}
+
+export const createWorkflow = async (data) => {
+ return await request.post({ url: '/ai/workflow/create', data })
+}
+
+export const updateWorkflow = async (data) => {
+ return await request.put({ url: '/ai/workflow/update', data })
+}
+
+export const deleteWorkflow = async (id) => {
+ return await request.delete({ url: '/ai/workflow/delete?id=' + id })
+}
+
+export const testWorkflow = async (data) => {
+ return await request.post({ url: '/ai/workflow/test', data })
+}
diff --git a/src/api/ai/write/index.ts b/src/api/ai/write/index.ts
new file mode 100644
index 0000000..013f998
--- /dev/null
+++ b/src/api/ai/write/index.ts
@@ -0,0 +1,85 @@
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+
+import { getAccessToken } from '@/utils/auth'
+import { config } from '@/config/axios/config'
+import { AiWriteTypeEnum } from '@/views/ai/utils/constants'
+import request from '@/config/axios'
+
+export interface WriteVO {
+ type: AiWriteTypeEnum.WRITING | AiWriteTypeEnum.REPLY // 1:鎾板啓 2:鍥炲
+ prompt: string // 鍐欎綔鍐呭鎻愮ず 1銆傛挵鍐� 2鍥炲
+ originalContent: string // 鍘熸枃
+ length: number // 闀垮害
+ format: number // 鏍煎紡
+ tone: number // 璇皵
+ language: number // 璇█
+ userId?: number // 鐢ㄦ埛缂栧彿
+ platform?: string // 骞冲彴
+ model?: string // 妯″瀷
+ generatedContent?: string // 鐢熸垚鐨勫唴瀹�
+ errorMessage?: string // 閿欒淇℃伅
+ createTime?: Date // 鍒涘缓鏃堕棿
+}
+
+export interface AiWritePageReqVO extends PageParam {
+ userId?: number // 鐢ㄦ埛缂栧彿
+ type?: AiWriteTypeEnum // 鍐欎綔绫诲瀷
+ platform?: string // 骞冲彴
+ createTime?: [string, string] // 鍒涘缓鏃堕棿
+}
+
+export interface AiWriteRespVo {
+ id: number
+ userId: number
+ type: number
+ platform: string
+ model: string
+ prompt: string
+ generatedContent: string
+ originalContent: string
+ length: number
+ format: number
+ tone: number
+ language: number
+ errorMessage: string
+ createTime: string
+}
+
+export const WriteApi = {
+ writeStream: ({
+ data,
+ onClose,
+ onMessage,
+ onError,
+ ctrl
+ }: {
+ data: WriteVO
+ onMessage?: (res: any) => void
+ onError?: (...args: any[]) => void
+ onClose?: (...args: any[]) => void
+ ctrl: AbortController
+ }) => {
+ const token = getAccessToken()
+ return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {
+ method: 'post',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`
+ },
+ openWhenHidden: true,
+ body: JSON.stringify(data),
+ onmessage: onMessage,
+ onerror: onError,
+ onclose: onClose,
+ signal: ctrl.signal
+ })
+ },
+ // 鑾峰彇鍐欎綔鍒楄〃
+ getWritePage: (params: AiWritePageReqVO) => {
+ return request.get<PageResult<AiWriteRespVo[]>>({ url: `/ai/write/page`, params })
+ },
+ // 鍒犻櫎鍐欎綔
+ deleteWrite(id: number) {
+ return request.delete({ url: `/ai/write/delete`, params: { id } })
+ }
+}
diff --git a/src/api/bpm/category/index.ts b/src/api/bpm/category/index.ts
new file mode 100644
index 0000000..1854f31
--- /dev/null
+++ b/src/api/bpm/category/index.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// BPM 娴佺▼鍒嗙被 VO
+export interface CategoryVO {
+ id: number // 鍒嗙被缂栧彿
+ name: string // 鍒嗙被鍚�
+ code: string // 鍒嗙被鏍囧織
+ status: number // 鍒嗙被鐘舵��
+ sort: number // 鍒嗙被鎺掑簭
+}
+
+// BPM 娴佺▼鍒嗙被 API
+export const CategoryApi = {
+ // 鏌ヨ娴佺▼鍒嗙被鍒嗛〉
+ getCategoryPage: async (params: any) => {
+ return await request.get({ url: `/bpm/category/page`, params })
+ },
+
+ // 鏌ヨ娴佺▼鍒嗙被鍒楄〃
+ getCategorySimpleList: async () => {
+ return await request.get({ url: `/bpm/category/simple-list` })
+ },
+
+ // 鏌ヨ娴佺▼鍒嗙被璇︽儏
+ getCategory: async (id: number) => {
+ return await request.get({ url: `/bpm/category/get?id=` + id })
+ },
+
+ // 鏂板娴佺▼鍒嗙被
+ createCategory: async (data: CategoryVO) => {
+ return await request.post({ url: `/bpm/category/create`, data })
+ },
+
+ // 淇敼娴佺▼鍒嗙被
+ updateCategory: async (data: CategoryVO) => {
+ return await request.put({ url: `/bpm/category/update`, data })
+ },
+
+ // 鎵归噺淇敼娴佺▼鍒嗙被鐨勬帓搴�
+ updateCategorySortBatch: async (ids: number[]) => {
+ return await request.put({
+ url: `/bpm/category/update-sort-batch`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 鍒犻櫎娴佺▼鍒嗙被
+ deleteCategory: async (id: number) => {
+ return await request.delete({ url: `/bpm/category/delete?id=` + id })
+ }
+}
diff --git a/src/api/bpm/definition/index.ts b/src/api/bpm/definition/index.ts
new file mode 100644
index 0000000..c917787
--- /dev/null
+++ b/src/api/bpm/definition/index.ts
@@ -0,0 +1,28 @@
+import request from '@/config/axios'
+
+export const getProcessDefinition = async (id?: string, key?: string) => {
+ return await request.get({
+ url: '/bpm/process-definition/get',
+ params: { id, key }
+ })
+}
+
+export const getProcessDefinitionPage = async (params) => {
+ return await request.get({
+ url: '/bpm/process-definition/page',
+ params
+ })
+}
+
+export const getProcessDefinitionList = async (params) => {
+ return await request.get({
+ url: '/bpm/process-definition/list',
+ params
+ })
+}
+
+export const getSimpleProcessDefinitionList = async () => {
+ return await request.get({
+ url: '/bpm/process-definition/simple-list'
+ })
+}
diff --git a/src/api/bpm/form/index.ts b/src/api/bpm/form/index.ts
new file mode 100644
index 0000000..7fce11f
--- /dev/null
+++ b/src/api/bpm/form/index.ts
@@ -0,0 +1,56 @@
+import request from '@/config/axios'
+
+export type FormVO = {
+ id: number
+ name: string
+ conf: string
+ fields: string[]
+ status: number
+ remark: string
+ createTime: string
+}
+
+// 鍒涘缓宸ヤ綔娴佺殑琛ㄥ崟瀹氫箟
+export const createForm = async (data: FormVO) => {
+ return await request.post({
+ url: '/bpm/form/create',
+ data: data
+ })
+}
+
+// 鏇存柊宸ヤ綔娴佺殑琛ㄥ崟瀹氫箟
+export const updateForm = async (data: FormVO) => {
+ return await request.put({
+ url: '/bpm/form/update',
+ data: data
+ })
+}
+
+// 鍒犻櫎宸ヤ綔娴佺殑琛ㄥ崟瀹氫箟
+export const deleteForm = async (id: number) => {
+ return await request.delete({
+ url: '/bpm/form/delete?id=' + id
+ })
+}
+
+// 鑾峰緱宸ヤ綔娴佺殑琛ㄥ崟瀹氫箟
+export const getForm = async (id: number) => {
+ return await request.get({
+ url: '/bpm/form/get?id=' + id
+ })
+}
+
+// 鑾峰緱宸ヤ綔娴佺殑琛ㄥ崟瀹氫箟鍒嗛〉
+export const getFormPage = async (params) => {
+ return await request.get({
+ url: '/bpm/form/page',
+ params
+ })
+}
+
+// 鑾峰緱鍔ㄦ�佽〃鍗曠殑绮剧畝鍒楄〃
+export const getFormSimpleList = async () => {
+ return await request.get({
+ url: '/bpm/form/simple-list'
+ })
+}
diff --git a/src/api/bpm/leave/index.ts b/src/api/bpm/leave/index.ts
new file mode 100644
index 0000000..4f374b2
--- /dev/null
+++ b/src/api/bpm/leave/index.ts
@@ -0,0 +1,27 @@
+import request from '@/config/axios'
+
+export type LeaveVO = {
+ id: number
+ status: number
+ type: number
+ reason: string
+ processInstanceId: string
+ startTime: string
+ endTime: string
+ createTime: string
+}
+
+// 鍒涘缓璇峰亣鐢宠
+export const createLeave = async (data: LeaveVO) => {
+ return await request.post({ url: '/bpm/oa/leave/create', data: data })
+}
+
+// 鑾峰緱璇峰亣鐢宠
+export const getLeave = async (id: number) => {
+ return await request.get({ url: '/bpm/oa/leave/get?id=' + id })
+}
+
+// 鑾峰緱璇峰亣鐢宠鍒嗛〉
+export const getLeavePage = async (params: PageParam) => {
+ return await request.get({ url: '/bpm/oa/leave/page', params })
+}
diff --git a/src/api/bpm/model/index.ts b/src/api/bpm/model/index.ts
new file mode 100644
index 0000000..6d2b4d2
--- /dev/null
+++ b/src/api/bpm/model/index.ts
@@ -0,0 +1,79 @@
+import request from '@/config/axios'
+
+export type ProcessDefinitionVO = {
+ id: string
+ version: number
+ deploymentTIme: string
+ suspensionState: number
+ formType?: number
+ formCustomCreatePath?: string
+}
+
+export type ModelVO = {
+ id: number
+ formName: string
+ key: string
+ name: string
+ description: string
+ category: string
+ formType: number
+ formId: number
+ formCustomCreatePath: string
+ formCustomViewPath: string
+ processDefinition: ProcessDefinitionVO
+ status: number
+ remark: string
+ createTime: string
+ bpmnXml: string
+}
+
+export const getModelList = async (name: string | undefined) => {
+ return await request.get({ url: '/bpm/model/list', params: { name } })
+}
+
+export const getModel = async (id: string) => {
+ return await request.get({ url: '/bpm/model/get?id=' + id })
+}
+
+export const updateModel = async (data: ModelVO) => {
+ return await request.put({ url: '/bpm/model/update', data: data })
+}
+
+// 鎵归噺淇敼娴佺▼鍒嗙被鐨勬帓搴�
+export const updateModelSortBatch = async (ids: number[]) => {
+ return await request.put({
+ url: `/bpm/model/update-sort-batch`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+}
+
+export const updateModelBpmn = async (data: ModelVO) => {
+ return await request.put({ url: '/bpm/model/update-bpmn', data: data })
+}
+
+// 浠诲姟鐘舵�佷慨鏀�
+export const updateModelState = async (id: number, state: number) => {
+ const data = {
+ id: id,
+ state: state
+ }
+ return await request.put({ url: '/bpm/model/update-state', data: data })
+}
+
+export const createModel = async (data: ModelVO) => {
+ return await request.post({ url: '/bpm/model/create', data: data })
+}
+
+export const deleteModel = async (id: number) => {
+ return await request.delete({ url: '/bpm/model/delete?id=' + id })
+}
+
+export const deployModel = async (id: number) => {
+ return await request.post({ url: '/bpm/model/deploy?id=' + id })
+}
+
+export const cleanModel = async (id: number) => {
+ return await request.delete({ url: '/bpm/model/clean?id=' + id })
+}
diff --git a/src/api/bpm/processExpression/index.ts b/src/api/bpm/processExpression/index.ts
new file mode 100644
index 0000000..af6a737
--- /dev/null
+++ b/src/api/bpm/processExpression/index.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+// BPM 娴佺▼琛ㄨ揪寮� VO
+export interface ProcessExpressionVO {
+ id: number // 缂栧彿
+ name: string // 琛ㄨ揪寮忓悕瀛�
+ status: number // 琛ㄨ揪寮忕姸鎬�
+ expression: string // 琛ㄨ揪寮�
+}
+
+// BPM 娴佺▼琛ㄨ揪寮� API
+export const ProcessExpressionApi = {
+ // 鏌ヨBPM 娴佺▼琛ㄨ揪寮忓垎椤�
+ getProcessExpressionPage: async (params: any) => {
+ return await request.get({ url: `/bpm/process-expression/page`, params })
+ },
+
+ // 鏌ヨBPM 娴佺▼琛ㄨ揪寮忚鎯�
+ getProcessExpression: async (id: number) => {
+ return await request.get({ url: `/bpm/process-expression/get?id=` + id })
+ },
+
+ // 鏂板BPM 娴佺▼琛ㄨ揪寮�
+ createProcessExpression: async (data: ProcessExpressionVO) => {
+ return await request.post({ url: `/bpm/process-expression/create`, data })
+ },
+
+ // 淇敼BPM 娴佺▼琛ㄨ揪寮�
+ updateProcessExpression: async (data: ProcessExpressionVO) => {
+ return await request.put({ url: `/bpm/process-expression/update`, data })
+ },
+
+ // 鍒犻櫎BPM 娴佺▼琛ㄨ揪寮�
+ deleteProcessExpression: async (id: number) => {
+ return await request.delete({ url: `/bpm/process-expression/delete?id=` + id })
+ },
+
+ // 瀵煎嚭BPM 娴佺▼琛ㄨ揪寮� Excel
+ exportProcessExpression: async (params) => {
+ return await request.download({ url: `/bpm/process-expression/export-excel`, params })
+ }
+}
\ No newline at end of file
diff --git a/src/api/bpm/processInstance/index.ts b/src/api/bpm/processInstance/index.ts
new file mode 100644
index 0000000..1e8f04d
--- /dev/null
+++ b/src/api/bpm/processInstance/index.ts
@@ -0,0 +1,115 @@
+import request from '@/config/axios'
+import { ProcessDefinitionVO } from '@/api/bpm/model'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+export type Task = {
+ id: string
+ name: string
+}
+
+export type ProcessInstanceVO = {
+ id: number
+ name: string
+ processDefinitionId: string
+ category: string
+ result: number
+ tasks: Task[]
+ fields: string[]
+ status: number
+ remark: string
+ businessKey: string
+ createTime: string
+ endTime: string
+ processDefinition?: ProcessDefinitionVO
+}
+
+// 鐢ㄦ埛淇℃伅
+export type User = {
+ id: number
+ nickname: string
+ avatar: string
+}
+
+// 瀹℃壒浠诲姟淇℃伅
+export type ApprovalTaskInfo = {
+ id: number
+ ownerUser: User
+ assigneeUser: User
+ status: number
+ reason: string
+ signPicUrl: string
+}
+
+// 瀹℃壒鑺傜偣淇℃伅
+export type ApprovalNodeInfo = {
+ id: number
+ name: string
+ nodeType: NodeType
+ candidateStrategy?: CandidateStrategy
+ status: number
+ startTime?: Date
+ endTime?: Date
+ processInstanceId?: string
+ candidateUsers?: User[]
+ tasks: ApprovalTaskInfo[]
+}
+
+export const getProcessInstanceMyPage = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/my-page', params })
+}
+
+export const getProcessInstanceManagerPage = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/manager-page', params })
+}
+
+export const createProcessInstance = async (data) => {
+ return await request.post({ url: '/bpm/process-instance/create', data: data })
+}
+
+export const cancelProcessInstanceByStartUser = async (id: number, reason: string) => {
+ const data = {
+ id: id,
+ reason: reason
+ }
+ return await request.delete({ url: '/bpm/process-instance/cancel-by-start-user', data: data })
+}
+
+export const cancelProcessInstanceByAdmin = async (id: number, reason: string) => {
+ const data = {
+ id: id,
+ reason: reason
+ }
+ return await request.delete({ url: '/bpm/process-instance/cancel-by-admin', data: data })
+}
+
+export const getProcessInstance = async (id: string) => {
+ return await request.get({ url: '/bpm/process-instance/get?id=' + id })
+}
+
+export const getProcessInstanceCopyPage = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/copy/page', params })
+}
+
+// 鑾峰彇瀹℃壒璇︽儏
+export const getApprovalDetail = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/get-approval-detail', params })
+}
+
+// 鑾峰彇涓嬩竴涓墽琛岀殑娴佺▼鑺傜偣
+export const getNextApprovalNodes = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/get-next-approval-nodes', params })
+}
+
+// 鑾峰彇琛ㄥ崟瀛楁鏉冮檺
+export const getFormFieldsPermission = async (params: any) => {
+ return await request.get({ url: '/bpm/process-instance/get-form-fields-permission', params })
+}
+
+// 鑾峰彇娴佺▼瀹炰緥鐨� BPMN 妯″瀷瑙嗗浘
+export const getProcessInstanceBpmnModelView = async (id: string) => {
+ return await request.get({ url: '/bpm/process-instance/get-bpmn-model-view?id=' + id })
+}
+
+// 鑾峰彇娴佺▼瀹炰緥鎵撳嵃鏁版嵁
+export const getProcessInstancePrintData = async (id: string) => {
+ return await request.get({ url: '/bpm/process-instance/get-print-data?processInstanceId=' + id })
+}
diff --git a/src/api/bpm/processListener/index.ts b/src/api/bpm/processListener/index.ts
new file mode 100644
index 0000000..dabaa47
--- /dev/null
+++ b/src/api/bpm/processListener/index.ts
@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+// BPM 娴佺▼鐩戝惉鍣� VO
+export interface ProcessListenerVO {
+ id: number // 缂栧彿
+ name: string // 鐩戝惉鍣ㄥ悕瀛�
+ type: string // 鐩戝惉鍣ㄧ被鍨�
+ status: number // 鐩戝惉鍣ㄧ姸鎬�
+ event: string // 鐩戝惉浜嬩欢
+ valueType: string // 鐩戝惉鍣ㄥ�肩被鍨�
+ value: string // 鐩戝惉鍣ㄥ��
+}
+
+// BPM 娴佺▼鐩戝惉鍣� API
+export const ProcessListenerApi = {
+ // 鏌ヨ娴佺▼鐩戝惉鍣ㄥ垎椤�
+ getProcessListenerPage: async (params: any) => {
+ return await request.get({ url: `/bpm/process-listener/page`, params })
+ },
+
+ // 鏌ヨ娴佺▼鐩戝惉鍣ㄨ鎯�
+ getProcessListener: async (id: number) => {
+ return await request.get({ url: `/bpm/process-listener/get?id=` + id })
+ },
+
+ // 鏂板娴佺▼鐩戝惉鍣�
+ createProcessListener: async (data: ProcessListenerVO) => {
+ return await request.post({ url: `/bpm/process-listener/create`, data })
+ },
+
+ // 淇敼娴佺▼鐩戝惉鍣�
+ updateProcessListener: async (data: ProcessListenerVO) => {
+ return await request.put({ url: `/bpm/process-listener/update`, data })
+ },
+
+ // 鍒犻櫎娴佺▼鐩戝惉鍣�
+ deleteProcessListener: async (id: number) => {
+ return await request.delete({ url: `/bpm/process-listener/delete?id=` + id })
+ }
+}
diff --git a/src/api/bpm/simple/index.ts b/src/api/bpm/simple/index.ts
new file mode 100644
index 0000000..6e1e995
--- /dev/null
+++ b/src/api/bpm/simple/index.ts
@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+
+
+export const updateBpmSimpleModel = async (data) => {
+ return await request.post({
+ url: '/bpm/model/simple/update',
+ data: data
+ })
+}
+
+export const getBpmSimpleModel = async (id) => {
+ return await request.get({
+ url: '/bpm/model/simple/get?id=' + id
+ })
+}
diff --git a/src/api/bpm/task/index.ts b/src/api/bpm/task/index.ts
new file mode 100644
index 0000000..713400f
--- /dev/null
+++ b/src/api/bpm/task/index.ts
@@ -0,0 +1,122 @@
+import request from '@/config/axios'
+
+/**
+ * 浠诲姟鐘舵�佹灇涓�
+ */
+export enum TaskStatusEnum {
+ /**
+ * 璺宠繃
+ */
+ SKIP = -2,
+ /**
+ * 鏈紑濮�
+ */
+ NOT_START = -1,
+
+ /**
+ * 寰呭鎵�
+ */
+ WAIT = 0,
+ /**
+ * 瀹℃壒涓�
+ */
+ RUNNING = 1,
+ /**
+ * 瀹℃壒閫氳繃
+ */
+ APPROVE = 2,
+
+ /**
+ * 瀹℃壒涓嶉�氳繃
+ */
+ REJECT = 3,
+
+ /**
+ * 宸插彇娑�
+ */
+ CANCEL = 4,
+ /**
+ * 宸查��鍥�
+ */
+ RETURN = 5,
+ /**
+ * 瀹℃壒閫氳繃涓�
+ */
+ APPROVING = 7
+}
+
+export const getTaskTodoPage = async (params: any) => {
+ return await request.get({ url: '/bpm/task/todo-page', params })
+}
+
+export const getTaskDonePage = async (params: any) => {
+ return await request.get({ url: '/bpm/task/done-page', params })
+}
+
+export const getTaskManagerPage = async (params: any) => {
+ return await request.get({ url: '/bpm/task/manager-page', params })
+}
+
+export const approveTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/approve', data })
+}
+
+export const rejectTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/reject', data })
+}
+
+export const getTaskListByProcessInstanceId = async (processInstanceId: string) => {
+ return await request.get({
+ url: '/bpm/task/list-by-process-instance-id?processInstanceId=' + processInstanceId
+ })
+}
+
+// 鑾峰彇鎵�鏈夊彲閫�鍥炵殑鑺傜偣
+export const getTaskListByReturn = async (id: string) => {
+ return await request.get({ url: '/bpm/task/list-by-return', params: { id } })
+}
+
+// 閫�鍥�
+export const returnTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/return', data })
+}
+
+// 濮旀淳
+export const delegateTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/delegate', data })
+}
+
+// 杞淳
+export const transferTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/transfer', data })
+}
+
+// 鍔犵
+export const signCreateTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/create-sign', data })
+}
+
+// 鍑忕
+export const signDeleteTask = async (data: any) => {
+ return await request.delete({ url: '/bpm/task/delete-sign', data })
+}
+
+// 鎶勯��
+export const copyTask = async (data: any) => {
+ return await request.put({ url: '/bpm/task/copy', data })
+}
+
+// 鎾ゅ洖
+export const withdrawTask = async (taskId: string) => {
+ return await request.put({ url: '/bpm/task/withdraw', params: { taskId } })
+}
+
+// 鑾峰彇鎴戠殑寰呭姙浠诲姟
+export const myTodoTask = async (processInstanceId: string) => {
+ return await request.get({ url: '/bpm/task/my-todo?processInstanceId=' + processInstanceId })
+}
+
+// 鑾峰彇鍑忕浠诲姟鍒楄〃
+export const getChildrenTaskList = async (id: string) => {
+ return await request.get({ url: '/bpm/task/list-by-parent-task-id?parentTaskId=' + id })
+}
diff --git a/src/api/bpm/userGroup/index.ts b/src/api/bpm/userGroup/index.ts
new file mode 100644
index 0000000..7d12755
--- /dev/null
+++ b/src/api/bpm/userGroup/index.ts
@@ -0,0 +1,47 @@
+import request from '@/config/axios'
+
+export type UserGroupVO = {
+ id: number
+ name: string
+ description: string
+ userIds: number[]
+ status: number
+ remark: string
+ createTime: string
+}
+
+// 鍒涘缓鐢ㄦ埛缁�
+export const createUserGroup = async (data: UserGroupVO) => {
+ return await request.post({
+ url: '/bpm/user-group/create',
+ data: data
+ })
+}
+
+// 鏇存柊鐢ㄦ埛缁�
+export const updateUserGroup = async (data: UserGroupVO) => {
+ return await request.put({
+ url: '/bpm/user-group/update',
+ data: data
+ })
+}
+
+// 鍒犻櫎鐢ㄦ埛缁�
+export const deleteUserGroup = async (id: number) => {
+ return await request.delete({ url: '/bpm/user-group/delete?id=' + id })
+}
+
+// 鑾峰緱鐢ㄦ埛缁�
+export const getUserGroup = async (id: number) => {
+ return await request.get({ url: '/bpm/user-group/get?id=' + id })
+}
+
+// 鑾峰緱鐢ㄦ埛缁勫垎椤�
+export const getUserGroupPage = async (params) => {
+ return await request.get({ url: '/bpm/user-group/page', params })
+}
+
+// 鑾峰彇鐢ㄦ埛缁勭簿绠�淇℃伅鍒楄〃
+export const getUserGroupSimpleList = async (): Promise<UserGroupVO[]> => {
+ return await request.get({ url: '/bpm/user-group/simple-list' })
+}
diff --git a/src/api/crm/business/index.ts b/src/api/crm/business/index.ts
new file mode 100644
index 0000000..2420425
--- /dev/null
+++ b/src/api/crm/business/index.ts
@@ -0,0 +1,98 @@
+import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/permission'
+
+export interface BusinessVO {
+ id: number
+ name: string
+ customerId: number
+ customerName?: string
+ followUpStatus: boolean
+ contactLastTime: Date
+ contactNextTime: Date
+ ownerUserId: number
+ ownerUserName?: string // 璐熻矗浜虹殑鐢ㄦ埛鍚嶇О
+ ownerUserDept?: string // 璐熻矗浜虹殑閮ㄩ棬鍚嶇О
+ statusTypeId: number
+ statusTypeName?: string
+ statusId: number
+ statusName?: string
+ endStatus: number
+ endRemark: string
+ dealTime: Date
+ totalProductPrice: number
+ totalPrice: number
+ discountPercent: number
+ remark: string
+ creator: string // 鍒涘缓浜�
+ creatorName?: string // 鍒涘缓浜哄悕绉�
+ createTime: Date // 鍒涘缓鏃堕棿
+ updateTime: Date // 鏇存柊鏃堕棿
+ products?: [
+ {
+ id: number
+ productId: number
+ productName: string
+ productNo: string
+ productUnit: number
+ productPrice: number
+ businessPrice: number
+ count: number
+ totalPrice: number
+ }
+ ]
+}
+
+// 鏌ヨ CRM 鍟嗘満鍒楄〃
+export const getBusinessPage = async (params) => {
+ return await request.get({ url: `/crm/business/page`, params })
+}
+
+// 鏌ヨ CRM 鍟嗘満鍒楄〃锛屽熀浜庢寚瀹氬鎴�
+export const getBusinessPageByCustomer = async (params) => {
+ return await request.get({ url: `/crm/business/page-by-customer`, params })
+}
+
+// 鏌ヨ CRM 鍟嗘満璇︽儏
+export const getBusiness = async (id: number) => {
+ return await request.get({ url: `/crm/business/get?id=` + id })
+}
+
+// 鑾峰緱 CRM 鍟嗘満鍒楄〃锛堢簿绠�锛�
+export const getSimpleBusinessList = async () => {
+ return await request.get({ url: `/crm/business/simple-all-list` })
+}
+
+// 鏂板 CRM 鍟嗘満
+export const createBusiness = async (data: BusinessVO) => {
+ return await request.post({ url: `/crm/business/create`, data })
+}
+
+// 淇敼 CRM 鍟嗘満
+export const updateBusiness = async (data: BusinessVO) => {
+ return await request.put({ url: `/crm/business/update`, data })
+}
+
+// 淇敼 CRM 鍟嗘満鐘舵��
+export const updateBusinessStatus = async (data: BusinessVO) => {
+ return await request.put({ url: `/crm/business/update-status`, data })
+}
+
+// 鍒犻櫎 CRM 鍟嗘満
+export const deleteBusiness = async (id: number) => {
+ return await request.delete({ url: `/crm/business/delete?id=` + id })
+}
+
+// 瀵煎嚭 CRM 鍟嗘満 Excel
+export const exportBusiness = async (params) => {
+ return await request.download({ url: `/crm/business/export-excel`, params })
+}
+
+// 鑱旂郴浜哄叧鑱斿晢鏈哄垪琛�
+export const getBusinessPageByContact = async (params) => {
+ return await request.get({ url: `/crm/business/page-by-contact`, params })
+}
+
+// 鍟嗘満杞Щ
+export const transferBusiness = async (data: TransferReqVO) => {
+ return await request.put({ url: '/crm/business/transfer', data })
+}
diff --git a/src/api/crm/business/status/index.ts b/src/api/crm/business/status/index.ts
new file mode 100644
index 0000000..cddaa5a
--- /dev/null
+++ b/src/api/crm/business/status/index.ts
@@ -0,0 +1,68 @@
+import request from '@/config/axios'
+
+export interface BusinessStatusTypeVO {
+ id: number
+ name: string
+ deptIds: number[]
+ statuses?: {
+ id: number
+ name: string
+ percent: number
+ }
+}
+
+export const DEFAULT_STATUSES = [
+ {
+ endStatus: 1,
+ key: '缁撴潫',
+ name: '璧㈠崟',
+ percent: 100
+ },
+ {
+ endStatus: 2,
+ key: '缁撴潫',
+ name: '杈撳崟',
+ percent: 0
+ },
+ {
+ endStatus: 3,
+ key: '缁撴潫',
+ name: '鏃犳晥',
+ percent: 0
+ }
+]
+
+// 鏌ヨ鍟嗘満鐘舵�佺粍鍒楄〃
+export const getBusinessStatusPage = async (params: any) => {
+ return await request.get({ url: `/crm/business-status/page`, params })
+}
+
+// 鏂板鍟嗘満鐘舵�佺粍
+export const createBusinessStatus = async (data: BusinessStatusTypeVO) => {
+ return await request.post({ url: `/crm/business-status/create`, data })
+}
+
+// 淇敼鍟嗘満鐘舵�佺粍
+export const updateBusinessStatus = async (data: BusinessStatusTypeVO) => {
+ return await request.put({ url: `/crm/business-status/update`, data })
+}
+
+// 鏌ヨ鍟嗘満鐘舵�佺被鍨嬭鎯�
+export const getBusinessStatus = async (id: number) => {
+ return await request.get({ url: `/crm/business-status/get?id=` + id })
+}
+
+// 鍒犻櫎鍟嗘満鐘舵��
+export const deleteBusinessStatus = async (id: number) => {
+ return await request.delete({ url: `/crm/business-status/delete?id=` + id })
+}
+
+// 鑾峰緱鍟嗘満鐘舵�佺粍鍒楄〃
+export const getBusinessStatusTypeSimpleList = async () => {
+ return await request.get({ url: `/crm/business-status/type-simple-list` })
+}
+
+// 鑾峰緱鍟嗘満闃舵鍒楄〃
+export const getBusinessStatusSimpleList = async (typeId: number) => {
+ return await request.get({ url: `/crm/business-status/status-simple-list`, params: { typeId } })
+}
diff --git a/src/api/crm/clue/index.ts b/src/api/crm/clue/index.ts
new file mode 100644
index 0000000..9736514
--- /dev/null
+++ b/src/api/crm/clue/index.ts
@@ -0,0 +1,78 @@
+import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/permission'
+
+export interface ClueVO {
+ id: number // 缂栧彿
+ name: string // 绾跨储鍚嶇О
+ followUpStatus: boolean // 璺熻繘鐘舵��
+ contactLastTime: Date // 鏈�鍚庤窡杩涙椂闂�
+ contactLastContent: string // 鏈�鍚庤窡杩涘唴瀹�
+ contactNextTime: Date // 涓嬫鑱旂郴鏃堕棿
+ ownerUserId: number // 璐熻矗浜虹殑鐢ㄦ埛缂栧彿
+ ownerUserName?: string // 璐熻矗浜虹殑鐢ㄦ埛鍚嶇О
+ ownerUserDept?: string // 璐熻矗浜虹殑閮ㄩ棬鍚嶇О
+ transformStatus: boolean // 杞寲鐘舵��
+ customerId: number // 瀹㈡埛缂栧彿
+ customerName?: string // 瀹㈡埛鍚嶇О
+ mobile: string // 鎵嬫満鍙�
+ telephone: string // 鐢佃瘽
+ qq: string // QQ
+ wechat: string // wechat
+ email: string // email
+ areaId: number // 鎵�鍦ㄥ湴
+ areaName?: string // 鎵�鍦ㄥ湴鍚嶇О
+ detailAddress: string // 璇︾粏鍦板潃
+ industryId: number // 鎵�灞炶涓�
+ level: number // 瀹㈡埛绛夌骇
+ source: number // 瀹㈡埛鏉ユ簮
+ remark: string // 澶囨敞
+ creator: string // 鍒涘缓浜�
+ creatorName?: string // 鍒涘缓浜哄悕绉�
+ createTime: Date // 鍒涘缓鏃堕棿
+ updateTime: Date // 鏇存柊鏃堕棿
+}
+
+// 鏌ヨ绾跨储鍒楄〃
+export const getCluePage = async (params: any) => {
+ return await request.get({ url: `/crm/clue/page`, params })
+}
+
+// 鏌ヨ绾跨储璇︽儏
+export const getClue = async (id: number) => {
+ return await request.get({ url: `/crm/clue/get?id=` + id })
+}
+
+// 鏂板绾跨储
+export const createClue = async (data: ClueVO) => {
+ return await request.post({ url: `/crm/clue/create`, data })
+}
+
+// 淇敼绾跨储
+export const updateClue = async (data: ClueVO) => {
+ return await request.put({ url: `/crm/clue/update`, data })
+}
+
+// 鍒犻櫎绾跨储
+export const deleteClue = async (id: number) => {
+ return await request.delete({ url: `/crm/clue/delete?id=` + id })
+}
+
+// 瀵煎嚭绾跨储 Excel
+export const exportClue = async (params) => {
+ return await request.download({ url: `/crm/clue/export-excel`, params })
+}
+
+// 绾跨储杞Щ
+export const transferClue = async (data: TransferReqVO) => {
+ return await request.put({ url: '/crm/clue/transfer', data })
+}
+
+// 绾跨储杞寲涓哄鎴�
+export const transformClue = async (id: number) => {
+ return await request.put({ url: '/crm/clue/transform', params: { id } })
+}
+
+// 鑾峰緱鍒嗛厤缁欐垜鐨勩�佸緟璺熻繘鐨勭嚎绱㈡暟閲�
+export const getFollowClueCount = async () => {
+ return await request.get({ url: '/crm/clue/follow-count' })
+}
diff --git a/src/api/crm/contact/index.ts b/src/api/crm/contact/index.ts
new file mode 100644
index 0000000..7c24dfa
--- /dev/null
+++ b/src/api/crm/contact/index.ts
@@ -0,0 +1,113 @@
+import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/permission'
+
+export interface ContactVO {
+ id: number // 缂栧彿
+ name: string // 鑱旂郴浜哄悕绉�
+ customerId: number // 瀹㈡埛缂栧彿
+ customerName?: string // 瀹㈡埛鍚嶇О
+ contactLastTime: Date // 鏈�鍚庤窡杩涙椂闂�
+ contactLastContent: string // 鏈�鍚庤窡杩涘唴瀹�
+ contactNextTime: Date // 涓嬫鑱旂郴鏃堕棿
+ ownerUserId: number // 璐熻矗浜虹殑鐢ㄦ埛缂栧彿
+ ownerUserName?: string // 璐熻矗浜虹殑鐢ㄦ埛鍚嶇О
+ ownerUserDept?: string // 璐熻矗浜虹殑閮ㄩ棬鍚嶇О
+ mobile: string // 鎵嬫満鍙�
+ telephone: string // 鐢佃瘽
+ qq: string // QQ
+ wechat: string // wechat
+ email: string // email
+ areaId: number // 鎵�鍦ㄥ湴
+ areaName?: string // 鎵�鍦ㄥ湴鍚嶇О
+ detailAddress: string // 璇︾粏鍦板潃
+ sex: number // 鎬у埆
+ master: boolean // 鏄惁涓昏仈绯讳汉
+ post: string // 鑱屽姟
+ parentId: number // 涓婄骇鑱旂郴浜虹紪鍙�
+ parentName?: string // 涓婄骇鑱旂郴浜哄悕绉�
+ remark: string // 澶囨敞
+ creator: string // 鍒涘缓浜�
+ creatorName?: string // 鍒涘缓浜哄悕绉�
+ createTime: Date // 鍒涘缓鏃堕棿
+ updateTime: Date // 鏇存柊鏃堕棿
+}
+
+export interface ContactBusinessReqVO {
+ contactId: number
+ businessIds: number[]
+}
+
+export interface ContactBusiness2ReqVO {
+ businessId: number
+ contactIds: number[]
+}
+
+// 鏌ヨ CRM 鑱旂郴浜哄垪琛�
+export const getContactPage = async (params) => {
+ return await request.get({ url: `/crm/contact/page`, params })
+}
+
+// 鏌ヨ CRM 鑱旂郴浜哄垪琛紝鍩轰簬鎸囧畾瀹㈡埛
+export const getContactPageByCustomer = async (params: any) => {
+ return await request.get({ url: `/crm/contact/page-by-customer`, params })
+}
+
+// 鏌ヨ CRM 鑱旂郴浜哄垪琛紝鍩轰簬鎸囧畾鍟嗘満
+export const getContactPageByBusiness = async (params: any) => {
+ return await request.get({ url: `/crm/contact/page-by-business`, params })
+}
+
+// 鏌ヨ CRM 鑱旂郴浜鸿鎯�
+export const getContact = async (id: number) => {
+ return await request.get({ url: `/crm/contact/get?id=` + id })
+}
+
+// 鏂板 CRM 鑱旂郴浜�
+export const createContact = async (data: ContactVO) => {
+ return await request.post({ url: `/crm/contact/create`, data })
+}
+
+// 淇敼 CRM 鑱旂郴浜�
+export const updateContact = async (data: ContactVO) => {
+ return await request.put({ url: `/crm/contact/update`, data })
+}
+
+// 鍒犻櫎 CRM 鑱旂郴浜�
+export const deleteContact = async (id: number) => {
+ return await request.delete({ url: `/crm/contact/delete?id=` + id })
+}
+
+// 瀵煎嚭 CRM 鑱旂郴浜� Excel
+export const exportContact = async (params) => {
+ return await request.download({ url: `/crm/contact/export-excel`, params })
+}
+
+// 鑾峰緱 CRM 鑱旂郴浜哄垪琛紙绮剧畝锛�
+export const getSimpleContactList = async () => {
+ return await request.get({ url: `/crm/contact/simple-all-list` })
+}
+
+// 鎵归噺鏂板鑱旂郴浜哄晢鏈哄叧鑱�
+export const createContactBusinessList = async (data: ContactBusinessReqVO) => {
+ return await request.post({ url: `/crm/contact/create-business-list`, data })
+}
+
+// 鎵归噺鏂板鑱旂郴浜哄晢鏈哄叧鑱�
+export const createContactBusinessList2 = async (data: ContactBusiness2ReqVO) => {
+ return await request.post({ url: `/crm/contact/create-business-list2`, data })
+}
+
+// 瑙i櫎鑱旂郴浜哄晢鏈哄叧鑱�
+export const deleteContactBusinessList = async (data: ContactBusinessReqVO) => {
+ return await request.delete({ url: `/crm/contact/delete-business-list`, data })
+}
+
+// 瑙i櫎鑱旂郴浜哄晢鏈哄叧鑱�
+export const deleteContactBusinessList2 = async (data: ContactBusiness2ReqVO) => {
+ return await request.delete({ url: `/crm/contact/delete-business-list2`, data })
+}
+
+// 鑱旂郴浜鸿浆绉�
+export const transferContact = async (data: TransferReqVO) => {
+ return await request.put({ url: '/crm/contact/transfer', data })
+}
diff --git a/src/api/crm/contract/config/index.ts b/src/api/crm/contract/config/index.ts
new file mode 100644
index 0000000..0c7ad20
--- /dev/null
+++ b/src/api/crm/contract/config/index.ts
@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+export interface ContractConfigVO {
+ notifyEnabled?: boolean
+ notifyDays?: number
+}
+
+// 鑾峰彇鍚堝悓閰嶇疆
+export const getContractConfig = async () => {
+ return await request.get({ url: `/crm/contract-config/get` })
+}
+
+// 鏇存柊鍚堝悓閰嶇疆
+export const saveContractConfig = async (data: ContractConfigVO) => {
+ return await request.put({ url: `/crm/contract-config/save`, data })
+}
diff --git a/src/api/crm/contract/index.ts b/src/api/crm/contract/index.ts
new file mode 100644
index 0000000..7028b77
--- /dev/null
+++ b/src/api/crm/contract/index.ts
@@ -0,0 +1,114 @@
+import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/permission'
+
+export interface ContractVO {
+ id: number
+ name: string
+ no: string
+ customerId: number
+ customerName?: string
+ businessId: number
+ businessName: string
+ contactLastTime: Date
+ ownerUserId: number
+ ownerUserName?: string
+ ownerUserDeptName?: string
+ processInstanceId: number
+ auditStatus: number
+ orderDate: Date
+ startTime: Date
+ endTime: Date
+ totalProductPrice: number
+ discountPercent: number
+ totalPrice: number
+ totalReceivablePrice: number
+ signContactId: number
+ signContactName?: string
+ signUserId: number
+ signUserName: string
+ remark: string
+ createTime?: Date
+ creator: string
+ creatorName: string
+ updateTime?: Date
+ products?: [
+ {
+ id: number
+ productId: number
+ productName: string
+ productNo: string
+ productUnit: number
+ productPrice: number
+ contractPrice: number
+ count: number
+ totalPrice: number
+ }
+ ]
+}
+
+// 鏌ヨ CRM 鍚堝悓鍒楄〃
+export const getContractPage = async (params) => {
+ return await request.get({ url: `/crm/contract/page`, params })
+}
+
+// 鏌ヨ CRM 鑱旂郴浜哄垪琛紝鍩轰簬鎸囧畾瀹㈡埛
+export const getContractPageByCustomer = async (params: any) => {
+ return await request.get({ url: `/crm/contract/page-by-customer`, params })
+}
+
+// 鏌ヨ CRM 鑱旂郴浜哄垪琛紝鍩轰簬鎸囧畾鍟嗘満
+export const getContractPageByBusiness = async (params: any) => {
+ return await request.get({ url: `/crm/contract/page-by-business`, params })
+}
+
+// 鏌ヨ CRM 鍚堝悓璇︽儏
+export const getContract = async (id: number) => {
+ return await request.get({ url: `/crm/contract/get?id=` + id })
+}
+
+// 鏌ヨ CRM 鍚堝悓涓嬫媺鍒楄〃
+export const getContractSimpleList = async (customerId: number) => {
+ return await request.get({
+ url: `/crm/contract/simple-list?customerId=${customerId}`
+ })
+}
+
+// 鏂板 CRM 鍚堝悓
+export const createContract = async (data: ContractVO) => {
+ return await request.post({ url: `/crm/contract/create`, data })
+}
+
+// 淇敼 CRM 鍚堝悓
+export const updateContract = async (data: ContractVO) => {
+ return await request.put({ url: `/crm/contract/update`, data })
+}
+
+// 鍒犻櫎 CRM 鍚堝悓
+export const deleteContract = async (id: number) => {
+ return await request.delete({ url: `/crm/contract/delete?id=` + id })
+}
+
+// 瀵煎嚭 CRM 鍚堝悓 Excel
+export const exportContract = async (params) => {
+ return await request.download({ url: `/crm/contract/export-excel`, params })
+}
+
+// 鎻愪氦瀹℃牳
+export const submitContract = async (id: number) => {
+ return await request.put({ url: `/crm/contract/submit?id=${id}` })
+}
+
+// 鍚堝悓杞Щ
+export const transferContract = async (data: TransferReqVO) => {
+ return await request.put({ url: '/crm/contract/transfer', data })
+}
+
+// 鑾峰緱寰呭鏍稿悎鍚屾暟閲�
+export const getAuditContractCount = async () => {
+ return await request.get({ url: '/crm/contract/audit-count' })
+}
+
+// 鑾峰緱鍗冲皢鍒版湡锛堟彁閱掞級鐨勫悎鍚屾暟閲�
+export const getRemindContractCount = async () => {
+ return await request.get({ url: '/crm/contract/remind-count' })
+}
diff --git a/src/api/crm/customer/index.ts b/src/api/crm/customer/index.ts
new file mode 100644
index 0000000..d149d4e
--- /dev/null
+++ b/src/api/crm/customer/index.ts
@@ -0,0 +1,132 @@
+import request from '@/config/axios'
+import { TransferReqVO } from '@/api/crm/permission'
+
+export interface CustomerVO {
+ id: number // 缂栧彿
+ name: string // 瀹㈡埛鍚嶇О
+ followUpStatus: boolean // 璺熻繘鐘舵��
+ contactLastTime: Date // 鏈�鍚庤窡杩涙椂闂�
+ contactLastContent: string // 鏈�鍚庤窡杩涘唴瀹�
+ contactNextTime: Date // 涓嬫鑱旂郴鏃堕棿
+ ownerUserId: number // 璐熻矗浜虹殑鐢ㄦ埛缂栧彿
+ ownerUserName?: string // 璐熻矗浜虹殑鐢ㄦ埛鍚嶇О
+ ownerUserDept?: string // 璐熻矗浜虹殑閮ㄩ棬鍚嶇О
+ lockStatus?: boolean
+ dealStatus?: boolean
+ mobile: string // 鎵嬫満鍙�
+ telephone: string // 鐢佃瘽
+ qq: string // QQ
+ wechat: string // wechat
+ email: string // email
+ areaId: number // 鎵�鍦ㄥ湴
+ areaName?: string // 鎵�鍦ㄥ湴鍚嶇О
+ detailAddress: string // 璇︾粏鍦板潃
+ industryId: number // 鎵�灞炶涓�
+ level: number // 瀹㈡埛绛夌骇
+ source: number // 瀹㈡埛鏉ユ簮
+ remark: string // 澶囨敞
+ creator: string // 鍒涘缓浜�
+ creatorName?: string // 鍒涘缓浜哄悕绉�
+ createTime: Date // 鍒涘缓鏃堕棿
+ updateTime: Date // 鏇存柊鏃堕棿
+}
+
+// 鏌ヨ瀹㈡埛鍒楄〃
+export const getCustomerPage = async (params) => {
+ return await request.get({ url: `/crm/customer/page`, params })
+}
+
+// 杩涘叆鍏捣瀹㈡埛鎻愰啋鐨勫鎴峰垪琛�
+export const getPutPoolRemindCustomerPage = async (params) => {
+ return await request.get({ url: `/crm/customer/put-pool-remind-page`, params })
+}
+
+// 鑾峰緱寰呰繘鍏ュ叕娴峰鎴锋暟閲�
+export const getPutPoolRemindCustomerCount = async () => {
+ return await request.get({ url: `/crm/customer/put-pool-remind-count` })
+}
+
+// 鑾峰緱浠婃棩闇�鑱旂郴瀹㈡埛鏁伴噺
+export const getTodayContactCustomerCount = async () => {
+ return await request.get({ url: `/crm/customer/today-contact-count` })
+}
+
+// 鑾峰緱鍒嗛厤缁欐垜銆佸緟璺熻繘鐨勭嚎绱㈡暟閲忕殑瀹㈡埛鏁伴噺
+export const getFollowCustomerCount = async () => {
+ return await request.get({ url: `/crm/customer/follow-count` })
+}
+
+// 鏌ヨ瀹㈡埛璇︽儏
+export const getCustomer = async (id: number) => {
+ return await request.get({ url: `/crm/customer/get?id=` + id })
+}
+
+// 鏂板瀹㈡埛
+export const createCustomer = async (data: CustomerVO) => {
+ return await request.post({ url: `/crm/customer/create`, data })
+}
+
+// 淇敼瀹㈡埛
+export const updateCustomer = async (data: CustomerVO) => {
+ return await request.put({ url: `/crm/customer/update`, data })
+}
+
+// 鏇存柊瀹㈡埛鐨勬垚浜ょ姸鎬�
+export const updateCustomerDealStatus = async (id: number, dealStatus: boolean) => {
+ return await request.put({ url: `/crm/customer/update-deal-status`, params: { id, dealStatus } })
+}
+
+// 鍒犻櫎瀹㈡埛
+export const deleteCustomer = async (id: number) => {
+ return await request.delete({ url: `/crm/customer/delete?id=` + id })
+}
+
+// 瀵煎嚭瀹㈡埛 Excel
+export const exportCustomer = async (params: any) => {
+ return await request.download({ url: `/crm/customer/export-excel`, params })
+}
+
+// 涓嬭浇瀹㈡埛瀵煎叆妯℃澘
+export const importCustomerTemplate = () => {
+ return request.download({ url: '/crm/customer/get-import-template' })
+}
+
+// 瀵煎叆瀹㈡埛
+export const handleImport = async (formData) => {
+ return await request.upload({ url: `/crm/customer/import`, data: formData })
+}
+
+// 瀹㈡埛鍒楄〃
+export const getCustomerSimpleList = async () => {
+ return await request.get({ url: `/crm/customer/simple-list` })
+}
+
+// ======================= 涓氬姟鎿嶄綔 =======================
+
+// 瀹㈡埛杞Щ
+export const transferCustomer = async (data: TransferReqVO) => {
+ return await request.put({ url: '/crm/customer/transfer', data })
+}
+
+// 閿佸畾/瑙i攣瀹㈡埛
+export const lockCustomer = async (id: number, lockStatus: boolean) => {
+ return await request.put({ url: `/crm/customer/lock`, data: { id, lockStatus } })
+}
+
+// 棰嗗彇鍏捣瀹㈡埛
+export const receiveCustomer = async (ids: any[]) => {
+ return await request.put({ url: '/crm/customer/receive', params: { ids: ids.join(',') } })
+}
+
+// 鍒嗛厤鍏捣缁欏搴旇礋璐d汉
+export const distributeCustomer = async (ids: any[], ownerUserId: number) => {
+ return await request.put({
+ url: '/crm/customer/distribute',
+ data: { ids: ids, ownerUserId }
+ })
+}
+
+// 瀹㈡埛鏀惧叆鍏捣
+export const putCustomerPool = async (id: number) => {
+ return await request.put({ url: `/crm/customer/put-pool?id=${id}` })
+}
diff --git a/src/api/crm/customer/limitConfig/index.ts b/src/api/crm/customer/limitConfig/index.ts
new file mode 100644
index 0000000..8677632
--- /dev/null
+++ b/src/api/crm/customer/limitConfig/index.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface CustomerLimitConfigVO {
+ id?: number
+ type?: number
+ userIds?: string
+ deptIds?: string
+ maxCount?: number
+ dealCountEnabled?: boolean
+}
+
+/**
+ * 瀹㈡埛闄愬埗閰嶇疆绫诲瀷
+ */
+export enum LimitConfType {
+ /**
+ * 鎷ユ湁瀹㈡埛鏁伴檺鍒�
+ */
+ CUSTOMER_QUANTITY_LIMIT = 1,
+ /**
+ * 閿佸畾瀹㈡埛鏁伴檺鍒�
+ */
+ CUSTOMER_LOCK_LIMIT = 2
+}
+
+// 鏌ヨ瀹㈡埛闄愬埗閰嶇疆鍒楄〃
+export const getCustomerLimitConfigPage = async (params) => {
+ return await request.get({ url: `/crm/customer-limit-config/page`, params })
+}
+
+// 鏌ヨ瀹㈡埛闄愬埗閰嶇疆璇︽儏
+export const getCustomerLimitConfig = async (id: number) => {
+ return await request.get({ url: `/crm/customer-limit-config/get?id=` + id })
+}
+
+// 鏂板瀹㈡埛闄愬埗閰嶇疆
+export const createCustomerLimitConfig = async (data: CustomerLimitConfigVO) => {
+ return await request.post({ url: `/crm/customer-limit-config/create`, data })
+}
+
+// 淇敼瀹㈡埛闄愬埗閰嶇疆
+export const updateCustomerLimitConfig = async (data: CustomerLimitConfigVO) => {
+ return await request.put({ url: `/crm/customer-limit-config/update`, data })
+}
+
+// 鍒犻櫎瀹㈡埛闄愬埗閰嶇疆
+export const deleteCustomerLimitConfig = async (id: number) => {
+ return await request.delete({ url: `/crm/customer-limit-config/delete?id=` + id })
+}
diff --git a/src/api/crm/customer/poolConfig/index.ts b/src/api/crm/customer/poolConfig/index.ts
new file mode 100644
index 0000000..b96e61f
--- /dev/null
+++ b/src/api/crm/customer/poolConfig/index.ts
@@ -0,0 +1,19 @@
+import request from '@/config/axios'
+
+export interface CustomerPoolConfigVO {
+ enabled?: boolean
+ contactExpireDays?: number
+ dealExpireDays?: number
+ notifyEnabled?: boolean
+ notifyDays?: number
+}
+
+// 鑾峰彇瀹㈡埛鍏捣瑙勫垯璁剧疆
+export const getCustomerPoolConfig = async () => {
+ return await request.get({ url: `/crm/customer-pool-config/get` })
+}
+
+// 鏇存柊瀹㈡埛鍏捣瑙勫垯璁剧疆
+export const saveCustomerPoolConfig = async (data: CustomerPoolConfigVO) => {
+ return await request.put({ url: `/crm/customer-pool-config/save`, data })
+}
diff --git a/src/api/crm/followup/index.ts b/src/api/crm/followup/index.ts
new file mode 100644
index 0000000..414f3f7
--- /dev/null
+++ b/src/api/crm/followup/index.ts
@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// 璺熻繘璁板綍 VO
+export interface FollowUpRecordVO {
+ id: number // 缂栧彿
+ bizType: number // 鏁版嵁绫诲瀷
+ bizId: number // 鏁版嵁缂栧彿
+ type: number // 璺熻繘绫诲瀷
+ content: string // 璺熻繘鍐呭
+ picUrls: string[] // 鍥剧墖
+ fileUrls: string[] // 闄勪欢
+ nextTime: Date // 涓嬫鑱旂郴鏃堕棿
+ businessIds: number[] // 鍏宠仈鐨勫晢鏈虹紪鍙锋暟缁�
+ businesses: {
+ id: number
+ name: string
+ }[] // 鍏宠仈鐨勫晢鏈烘暟缁�
+ contactIds: number[] // 鍏宠仈鐨勮仈绯讳汉缂栧彿鏁扮粍
+ contacts: {
+ id: number
+ name: string
+ }[] // 鍏宠仈鐨勮仈绯讳汉鏁扮粍
+ creator: string
+ creatorName?: string
+}
+
+// 璺熻繘璁板綍 API
+export const FollowUpRecordApi = {
+ // 鏌ヨ璺熻繘璁板綍鍒嗛〉
+ getFollowUpRecordPage: async (params: any) => {
+ return await request.get({ url: `/crm/follow-up-record/page`, params })
+ },
+
+ // 鏂板璺熻繘璁板綍
+ createFollowUpRecord: async (data: FollowUpRecordVO) => {
+ return await request.post({ url: `/crm/follow-up-record/create`, data })
+ },
+
+ // 鍒犻櫎璺熻繘璁板綍
+ deleteFollowUpRecord: async (id: number) => {
+ return await request.delete({ url: `/crm/follow-up-record/delete?id=` + id })
+ }
+}
diff --git a/src/api/crm/operateLog/index.ts b/src/api/crm/operateLog/index.ts
new file mode 100644
index 0000000..d0f25b6
--- /dev/null
+++ b/src/api/crm/operateLog/index.ts
@@ -0,0 +1,11 @@
+import request from '@/config/axios'
+
+export interface OperateLogVO extends PageParam {
+ bizType: number
+ bizId: number
+}
+
+// 鑾峰緱鎿嶄綔鏃ュ織
+export const getOperateLogPage = async (params: OperateLogVO) => {
+ return await request.get({ url: `/crm/operate-log/page`, params })
+}
diff --git a/src/api/crm/permission/index.ts b/src/api/crm/permission/index.ts
new file mode 100644
index 0000000..4f88b14
--- /dev/null
+++ b/src/api/crm/permission/index.ts
@@ -0,0 +1,72 @@
+import request from '@/config/axios'
+
+export interface PermissionVO {
+ id?: number // 鏁版嵁鏉冮檺缂栧彿
+ userId: number // 鐢ㄦ埛缂栧彿
+ bizType: number // Crm 绫诲瀷
+ bizId: number // Crm 绫诲瀷鏁版嵁缂栧彿
+ level: number // 鏉冮檺绾у埆
+ toBizTypes?: number[] // 鍚屾椂娣诲姞鑷�
+ deptName?: string // 閮ㄩ棬鍚嶇О
+ nickname?: string // 鐢ㄦ埛鏄电О
+ postNames?: string[] // 宀椾綅鍚嶇О鏁扮粍
+ createTime?: Date
+ ids?: number[]
+}
+
+export interface TransferReqVO {
+ id: number // 妯″潡缂栧彿
+ newOwnerUserId: number // 鏂拌礋璐d汉鐨勭敤鎴风紪鍙�
+ oldOwnerPermissionLevel?: number // 鑰佽礋璐d汉鍔犲叆鍥㈤槦鍚庣殑鏉冮檺绾у埆
+ toBizTypes?: number[] // 杞Щ瀹㈡埛鏃讹紝闇�瑕侀澶栨湁銆愯仈绯讳汉銆戙�愬晢鏈恒�戙�愬悎鍚屻�戠殑 checkbox 閫夋嫨
+}
+
+/**
+ * CRM 涓氬姟绫诲瀷鏋氫妇
+ *
+ * @author HUIHUI
+ */
+export enum BizTypeEnum {
+ CRM_CLUE = 1, // 绾跨储
+ CRM_CUSTOMER = 2, // 瀹㈡埛
+ CRM_CONTACT = 3, // 鑱旂郴浜�
+ CRM_BUSINESS = 4, // 鍟嗘満
+ CRM_CONTRACT = 5, // 鍚堝悓
+ CRM_PRODUCT = 6, // 浜у搧
+ CRM_RECEIVABLE = 7, // 鍥炴
+ CRM_RECEIVABLE_PLAN = 8 // 鍥炴璁″垝
+}
+
+/**
+ * CRM 鏁版嵁鏉冮檺绾у埆鏋氫妇
+ */
+export enum PermissionLevelEnum {
+ OWNER = 1, // 璐熻矗浜�
+ READ = 2, // 鍙
+ WRITE = 3 // 璇诲啓
+}
+
+// 鑾峰緱鏁版嵁鏉冮檺鍒楄〃锛堟煡璇㈠洟闃熸垚鍛樺垪琛級
+export const getPermissionList = async (params) => {
+ return await request.get({ url: `/crm/permission/list`, params })
+}
+
+// 鍒涘缓鏁版嵁鏉冮檺锛堟柊澧炲洟闃熸垚鍛橈級
+export const createPermission = async (data: PermissionVO) => {
+ return await request.post({ url: `/crm/permission/create`, data })
+}
+
+// 缂栬緫鏁版嵁鏉冮檺锛堜慨鏀瑰洟闃熸垚鍛樻潈闄愮骇鍒級
+export const updatePermission = async (data) => {
+ return await request.put({ url: `/crm/permission/update`, data })
+}
+
+// 鍒犻櫎鏁版嵁鏉冮檺锛堝垹闄ゅ洟闃熸垚鍛橈級
+export const deletePermissionBatch = async (val: number[]) => {
+ return await request.delete({ url: '/crm/permission/delete?ids=' + val.join(',') })
+}
+
+// 鍒犻櫎鑷繁鐨勬暟鎹潈闄愶紙閫�鍑哄洟闃燂級
+export const deleteSelfPermission = async (id: number) => {
+ return await request.delete({ url: '/crm/permission/delete-self?id=' + id })
+}
diff --git a/src/api/crm/product/category/index.ts b/src/api/crm/product/category/index.ts
new file mode 100644
index 0000000..6341d1b
--- /dev/null
+++ b/src/api/crm/product/category/index.ts
@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+// TODO @zange锛氭尓鍒� product 涓嬶紝寤轰釜 category 鍖咃紝鎸繘鍘诲搱锛�
+export interface ProductCategoryVO {
+ id: number
+ name: string
+ parentId: number
+}
+
+// 鏌ヨ浜у搧鍒嗙被璇︽儏
+export const getProductCategory = async (id: number) => {
+ return await request.get({ url: `/crm/product-category/get?id=` + id })
+}
+
+// 鏂板浜у搧鍒嗙被
+export const createProductCategory = async (data: ProductCategoryVO) => {
+ return await request.post({ url: `/crm/product-category/create`, data })
+}
+
+// 淇敼浜у搧鍒嗙被
+export const updateProductCategory = async (data: ProductCategoryVO) => {
+ return await request.put({ url: `/crm/product-category/update`, data })
+}
+
+// 鍒犻櫎浜у搧鍒嗙被
+export const deleteProductCategory = async (id: number) => {
+ return await request.delete({ url: `/crm/product-category/delete?id=` + id })
+}
+
+// 浜у搧鍒嗙被鍒楄〃
+export const getProductCategoryList = async (params) => {
+ return await request.get({ url: `/crm/product-category/list`, params })
+}
diff --git a/src/api/crm/product/index.ts b/src/api/crm/product/index.ts
new file mode 100644
index 0000000..f0c2328
--- /dev/null
+++ b/src/api/crm/product/index.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface ProductVO {
+ id: number
+ name: string
+ no: string
+ unit: number
+ price: number
+ status: number
+ categoryId: number
+ categoryName?: string
+ description: string
+ ownerUserId: number
+}
+
+// 鏌ヨ浜у搧鍒楄〃
+export const getProductPage = async (params) => {
+ return await request.get({ url: `/crm/product/page`, params })
+}
+
+// 鑾峰緱浜у搧绮剧畝鍒楄〃
+export const getProductSimpleList = async () => {
+ return await request.get({ url: `/crm/product/simple-list` })
+}
+
+// 鏌ヨ浜у搧璇︽儏
+export const getProduct = async (id: number) => {
+ return await request.get({ url: `/crm/product/get?id=` + id })
+}
+
+// 鏂板浜у搧
+export const createProduct = async (data: ProductVO) => {
+ return await request.post({ url: `/crm/product/create`, data })
+}
+
+// 淇敼浜у搧
+export const updateProduct = async (data: ProductVO) => {
+ return await request.put({ url: `/crm/product/update`, data })
+}
+
+// 鍒犻櫎浜у搧
+export const deleteProduct = async (id: number) => {
+ return await request.delete({ url: `/crm/product/delete?id=` + id })
+}
+
+// 瀵煎嚭浜у搧 Excel
+export const exportProduct = async (params) => {
+ return await request.download({ url: `/crm/product/export-excel`, params })
+}
diff --git a/src/api/crm/receivable/index.ts b/src/api/crm/receivable/index.ts
new file mode 100644
index 0000000..32ecd25
--- /dev/null
+++ b/src/api/crm/receivable/index.ts
@@ -0,0 +1,73 @@
+import request from '@/config/axios'
+
+export interface ReceivableVO {
+ id: number
+ no: string
+ planId?: number
+ customerId?: number
+ customerName?: string
+ contractId?: number
+ contract?: {
+ id?: number
+ name?: string
+ no: string
+ totalPrice: number
+ }
+ auditStatus: number
+ processInstanceId: number
+ returnTime: Date
+ returnType: number
+ price: number
+ ownerUserId: number
+ ownerUserName?: string
+ remark: string
+ creator: string // 鍒涘缓浜�
+ creatorName?: string // 鍒涘缓浜哄悕绉�
+ createTime: Date // 鍒涘缓鏃堕棿
+ updateTime: Date // 鏇存柊鏃堕棿
+}
+
+// 鏌ヨ鍥炴鍒楄〃
+export const getReceivablePage = async (params) => {
+ return await request.get({ url: `/crm/receivable/page`, params })
+}
+
+// 鏌ヨ鍥炴鍒楄〃
+export const getReceivablePageByCustomer = async (params) => {
+ return await request.get({ url: `/crm/receivable/page-by-customer`, params })
+}
+
+// 鏌ヨ鍥炴璇︽儏
+export const getReceivable = async (id: number) => {
+ return await request.get({ url: `/crm/receivable/get?id=` + id })
+}
+
+// 鏂板鍥炴
+export const createReceivable = async (data: ReceivableVO) => {
+ return await request.post({ url: `/crm/receivable/create`, data })
+}
+
+// 淇敼鍥炴
+export const updateReceivable = async (data: ReceivableVO) => {
+ return await request.put({ url: `/crm/receivable/update`, data })
+}
+
+// 鍒犻櫎鍥炴
+export const deleteReceivable = async (id: number) => {
+ return await request.delete({ url: `/crm/receivable/delete?id=` + id })
+}
+
+// 瀵煎嚭鍥炴 Excel
+export const exportReceivable = async (params) => {
+ return await request.download({ url: `/crm/receivable/export-excel`, params })
+}
+
+// 鎻愪氦瀹℃牳
+export const submitReceivable = async (id: number) => {
+ return await request.put({ url: `/crm/receivable/submit?id=${id}` })
+}
+
+// 鑾峰緱寰呭鏍稿洖娆炬暟閲�
+export const getAuditReceivableCount = async () => {
+ return await request.get({ url: '/crm/receivable/audit-count' })
+}
diff --git a/src/api/crm/receivable/plan/index.ts b/src/api/crm/receivable/plan/index.ts
new file mode 100644
index 0000000..770b347
--- /dev/null
+++ b/src/api/crm/receivable/plan/index.ts
@@ -0,0 +1,74 @@
+import request from '@/config/axios'
+
+export interface ReceivablePlanVO {
+ id: number
+ period: number
+ receivableId: number
+ price: number
+ returnTime: Date
+ remindDays: number
+ returnType: number
+ remindTime: Date
+ customerId: number
+ customerName?: string
+ contractId?: number
+ contractNo?: string
+ ownerUserId: number
+ ownerUserName?: string
+ remark: string
+ creator: string // 鍒涘缓浜�
+ creatorName?: string // 鍒涘缓浜哄悕绉�
+ createTime: Date // 鍒涘缓鏃堕棿
+ updateTime: Date // 鏇存柊鏃堕棿
+ receivable?: {
+ price: number
+ returnTime: Date
+ }
+}
+
+// 鏌ヨ鍥炴璁″垝鍒楄〃
+export const getReceivablePlanPage = async (params) => {
+ return await request.get({ url: `/crm/receivable-plan/page`, params })
+}
+
+// 鏌ヨ鍥炴璁″垝鍒楄〃
+export const getReceivablePlanPageByCustomer = async (params) => {
+ return await request.get({ url: `/crm/receivable-plan/page-by-customer`, params })
+}
+
+// 鏌ヨ鍥炴璁″垝璇︽儏
+export const getReceivablePlan = async (id: number) => {
+ return await request.get({ url: `/crm/receivable-plan/get?id=` + id })
+}
+
+// 鏌ヨ鍥炴璁″垝涓嬫媺鏁版嵁
+export const getReceivablePlanSimpleList = async (customerId: number, contractId: number) => {
+ return await request.get({
+ url: `/crm/receivable-plan/simple-list?customerId=${customerId}&contractId=${contractId}`
+ })
+}
+
+// 鏂板鍥炴璁″垝
+export const createReceivablePlan = async (data: ReceivablePlanVO) => {
+ return await request.post({ url: `/crm/receivable-plan/create`, data })
+}
+
+// 淇敼鍥炴璁″垝
+export const updateReceivablePlan = async (data: ReceivablePlanVO) => {
+ return await request.put({ url: `/crm/receivable-plan/update`, data })
+}
+
+// 鍒犻櫎鍥炴璁″垝
+export const deleteReceivablePlan = async (id: number) => {
+ return await request.delete({ url: `/crm/receivable-plan/delete?id=` + id })
+}
+
+// 瀵煎嚭鍥炴璁″垝 Excel
+export const exportReceivablePlan = async (params) => {
+ return await request.download({ url: `/crm/receivable-plan/export-excel`, params })
+}
+
+// 鑾峰緱寰呭洖娆炬彁閱掓暟閲�
+export const getReceivablePlanRemindCount = async () => {
+ return await request.get({ url: '/crm/receivable-plan/remind-count' })
+}
diff --git a/src/api/crm/statistics/customer.ts b/src/api/crm/statistics/customer.ts
new file mode 100644
index 0000000..c2092e4
--- /dev/null
+++ b/src/api/crm/statistics/customer.ts
@@ -0,0 +1,168 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticsCustomerSummaryByDateRespVO {
+ time: string
+ customerCreateCount: number
+ customerDealCount: number
+}
+
+export interface CrmStatisticsCustomerSummaryByUserRespVO {
+ ownerUserName: string
+ customerCreateCount: number
+ customerDealCount: number
+ contractPrice: number
+ receivablePrice: number
+}
+
+export interface CrmStatisticsFollowUpSummaryByDateRespVO {
+ time: string
+ followUpRecordCount: number
+ followUpCustomerCount: number
+}
+
+export interface CrmStatisticsFollowUpSummaryByUserRespVO {
+ ownerUserName: string
+ followupRecordCount: number
+ followupCustomerCount: number
+}
+
+export interface CrmStatisticsFollowUpSummaryByTypeRespVO {
+ followUpType: string
+ followUpRecordCount: number
+}
+
+export interface CrmStatisticsCustomerContractSummaryRespVO {
+ customerName: string
+ contractName: string
+ totalPrice: number
+ receivablePrice: number
+ customerType: string
+ customerSource: string
+ ownerUserName: string
+ creatorUserName: string
+ createTime: Date
+ orderDate: Date
+}
+
+export interface CrmStatisticsPoolSummaryByDateRespVO {
+ time: string
+ customerPutCount: number
+ customerTakeCount: number
+}
+
+export interface CrmStatisticsPoolSummaryByUserRespVO {
+ ownerUserName: string
+ customerPutCount: number
+ customerTakeCount: number
+}
+
+export interface CrmStatisticsCustomerDealCycleByDateRespVO {
+ time: string
+ customerDealCycle: number
+}
+
+export interface CrmStatisticsCustomerDealCycleByUserRespVO {
+ ownerUserName: string
+ customerDealCycle: number
+ customerDealCount: number
+}
+
+export interface CrmStatisticsCustomerDealCycleByAreaRespVO {
+ areaName: string
+ customerDealCycle: number
+ customerDealCount: number
+}
+
+export interface CrmStatisticsCustomerDealCycleByProductRespVO {
+ productName: string
+ customerDealCycle: number
+ customerDealCount: number
+}
+
+// 瀹㈡埛鍒嗘瀽 API
+export const StatisticsCustomerApi = {
+ // 1.1 瀹㈡埛鎬婚噺鍒嗘瀽(鎸夋棩鏈�)
+ getCustomerSummaryByDate: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-customer-summary-by-date',
+ params
+ })
+ },
+ // 1.2 瀹㈡埛鎬婚噺鍒嗘瀽(鎸夌敤鎴�)
+ getCustomerSummaryByUser: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-customer-summary-by-user',
+ params
+ })
+ },
+ // 2.1 瀹㈡埛璺熻繘娆℃暟鍒嗘瀽(鎸夋棩鏈�)
+ getFollowUpSummaryByDate: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-follow-up-summary-by-date',
+ params
+ })
+ },
+ // 2.2 瀹㈡埛璺熻繘娆℃暟鍒嗘瀽(鎸夌敤鎴�)
+ getFollowUpSummaryByUser: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-follow-up-summary-by-user',
+ params
+ })
+ },
+ // 3.1 鑾峰彇瀹㈡埛璺熻繘鏂瑰紡缁熻鏁�
+ getFollowUpSummaryByType: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-follow-up-summary-by-type',
+ params
+ })
+ },
+ // 4.1 鍚堝悓鎽樿淇℃伅(瀹㈡埛杞寲鐜囬〉闈�)
+ getContractSummary: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-contract-summary',
+ params
+ })
+ },
+ // 5.1 鑾峰彇瀹㈡埛鍏捣鍒嗘瀽(鎸夋棩鏈�)
+ getPoolSummaryByDate: (param: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-pool-summary-by-date',
+ params: param
+ })
+ },
+ // 5.2 鑾峰彇瀹㈡埛鍏捣鍒嗘瀽(鎸夌敤鎴�)
+ getPoolSummaryByUser: (param: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-pool-summary-by-user',
+ params: param
+ })
+ },
+ // 6.1 鑾峰彇瀹㈡埛鎴愪氦鍛ㄦ湡(鎸夋棩鏈�)
+ getCustomerDealCycleByDate: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-customer-deal-cycle-by-date',
+ params
+ })
+ },
+ // 6.2 鑾峰彇瀹㈡埛鎴愪氦鍛ㄦ湡(鎸夌敤鎴�)
+ getCustomerDealCycleByUser: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-customer-deal-cycle-by-user',
+ params
+ })
+ },
+ // 6.2 鑾峰彇瀹㈡埛鎴愪氦鍛ㄦ湡(鎸夌敤鎴�)
+ getCustomerDealCycleByArea: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-customer-deal-cycle-by-area',
+ params
+ })
+ },
+ // 6.2 鑾峰彇瀹㈡埛鎴愪氦鍛ㄦ湡(鎸夌敤鎴�)
+ getCustomerDealCycleByProduct: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-customer/get-customer-deal-cycle-by-product',
+ params
+ })
+ }
+}
diff --git a/src/api/crm/statistics/funnel.ts b/src/api/crm/statistics/funnel.ts
new file mode 100644
index 0000000..574a5f4
--- /dev/null
+++ b/src/api/crm/statistics/funnel.ts
@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticFunnelRespVO {
+ customerCount: number // 瀹㈡埛鏁�
+ businessCount: number // 鍟嗘満鏁�
+ businessWinCount: number // 璧㈠崟鏁�
+}
+
+export interface CrmStatisticsBusinessSummaryByDateRespVO {
+ time: string // 鏃堕棿
+ businessCreateCount: number // 鍟嗘満鏁�
+ totalPrice: number | string // 鍟嗘満閲戦
+}
+
+export interface CrmStatisticsBusinessInversionRateSummaryByDateRespVO {
+ time: string // 鏃堕棿
+ businessCount: number // 鍟嗘満鏁伴噺
+ businessWinCount: number // 璧㈠崟鍟嗘満鏁�
+}
+
+// 瀹㈡埛鍒嗘瀽 API
+export const StatisticFunnelApi = {
+ // 1. 鑾峰彇閿�鍞紡鏂楃粺璁℃暟鎹�
+ getFunnelSummary: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-funnel/get-funnel-summary',
+ params
+ })
+ },
+ // 2. 鑾峰彇鍟嗘満缁撴潫鐘舵�佺粺璁�
+ getBusinessSummaryByEndStatus: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-funnel/get-business-summary-by-end-status',
+ params
+ })
+ },
+ // 3. 鑾峰彇鏂板鍟嗘満鍒嗘瀽(鎸夋棩鏈�)
+ getBusinessSummaryByDate: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-funnel/get-business-summary-by-date',
+ params
+ })
+ },
+ // 4. 鑾峰彇鍟嗘満杞寲鐜囧垎鏋�(鎸夋棩鏈�)
+ getBusinessInversionRateSummaryByDate: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-funnel/get-business-inversion-rate-summary-by-date',
+ params
+ })
+ },
+ // 5. 鑾峰彇鍟嗘満鍒楄〃(鎸夋棩鏈�)
+ getBusinessPageByDate: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-funnel/get-business-page-by-date',
+ params
+ })
+ }
+}
diff --git a/src/api/crm/statistics/performance.ts b/src/api/crm/statistics/performance.ts
new file mode 100644
index 0000000..2318505
--- /dev/null
+++ b/src/api/crm/statistics/performance.ts
@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+export interface StatisticsPerformanceRespVO {
+ time: string
+ currentMonthCount: number
+ lastMonthCount: number
+ lastYearCount: number
+}
+
+// 鎺掕 API
+export const StatisticsPerformanceApi = {
+ // 鍛樺伐鑾峰緱鍚堝悓閲戦缁熻
+ getContractPricePerformance: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-performance/get-contract-price-performance',
+ params
+ })
+ },
+ // 鍛樺伐鑾峰緱鍥炴缁熻
+ getReceivablePricePerformance: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-performance/get-receivable-price-performance',
+ params
+ })
+ },
+ //鍛樺伐鑾峰緱绛剧害鍚堝悓鏁伴噺缁熻
+ getContractCountPerformance: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-performance/get-contract-count-performance',
+ params
+ })
+ }
+}
diff --git a/src/api/crm/statistics/portrait.ts b/src/api/crm/statistics/portrait.ts
new file mode 100644
index 0000000..c7a2572
--- /dev/null
+++ b/src/api/crm/statistics/portrait.ts
@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+
+export interface CrmStatisticCustomerBaseRespVO {
+ customerCount: number
+ dealCount: number
+ dealPortion: string | number
+}
+
+export interface CrmStatisticCustomerIndustryRespVO extends CrmStatisticCustomerBaseRespVO {
+ industryId: number
+ industryPortion: string | number
+}
+
+export interface CrmStatisticCustomerSourceRespVO extends CrmStatisticCustomerBaseRespVO {
+ source: number
+ sourcePortion: string | number
+}
+
+export interface CrmStatisticCustomerLevelRespVO extends CrmStatisticCustomerBaseRespVO {
+ level: number
+ levelPortion: string | number
+}
+
+export interface CrmStatisticCustomerAreaRespVO extends CrmStatisticCustomerBaseRespVO {
+ areaId: number
+ areaName: string
+ areaPortion: string | number
+}
+
+// 瀹㈡埛鍒嗘瀽 API
+export const StatisticsPortraitApi = {
+ // 1. 鑾峰彇瀹㈡埛琛屼笟缁熻鏁版嵁
+ getCustomerIndustry: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-portrait/get-customer-industry-summary',
+ params
+ })
+ },
+ // 2. 鑾峰彇瀹㈡埛鏉ユ簮缁熻鏁版嵁
+ getCustomerSource: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-portrait/get-customer-source-summary',
+ params
+ })
+ },
+ // 3. 鑾峰彇瀹㈡埛绾у埆缁熻鏁版嵁
+ getCustomerLevel: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-portrait/get-customer-level-summary',
+ params
+ })
+ },
+ // 4. 鑾峰彇瀹㈡埛鍦板尯缁熻鏁版嵁
+ getCustomerArea: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-portrait/get-customer-area-summary',
+ params
+ })
+ }
+}
diff --git a/src/api/crm/statistics/rank.ts b/src/api/crm/statistics/rank.ts
new file mode 100644
index 0000000..a9b355e
--- /dev/null
+++ b/src/api/crm/statistics/rank.ts
@@ -0,0 +1,67 @@
+import request from '@/config/axios'
+
+export interface StatisticsRankRespVO {
+ count: number
+ nickname: string
+ deptName: string
+}
+
+// 鎺掕 API
+export const StatisticsRankApi = {
+ // 鑾峰緱鍚堝悓鎺掕姒�
+ getContractPriceRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-contract-price-rank',
+ params
+ })
+ },
+ // 鑾峰緱鍥炴鎺掕姒�
+ getReceivablePriceRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-receivable-price-rank',
+ params
+ })
+ },
+ // 绛剧害鍚堝悓鎺掕
+ getContractCountRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-contract-count-rank',
+ params
+ })
+ },
+ // 浜у搧閿�閲忔帓琛�
+ getProductSalesRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-product-sales-rank',
+ params
+ })
+ },
+ // 鏂板瀹㈡埛鏁版帓琛�
+ getCustomerCountRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-customer-count-rank',
+ params
+ })
+ },
+ // 鏂板鑱旂郴浜烘暟鎺掕
+ getContactsCountRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-contacts-count-rank',
+ params
+ })
+ },
+ // 璺熻繘娆℃暟鎺掕
+ getFollowCountRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-follow-count-rank',
+ params
+ })
+ },
+ // 璺熻繘瀹㈡埛鏁版帓琛�
+ getFollowCustomerCountRank: (params: any) => {
+ return request.get({
+ url: '/crm/statistics-rank/get-follow-customer-count-rank',
+ params
+ })
+ }
+}
diff --git a/src/api/erp/finance/account/index.ts b/src/api/erp/finance/account/index.ts
new file mode 100644
index 0000000..a62b180
--- /dev/null
+++ b/src/api/erp/finance/account/index.ts
@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+// ERP 缁撶畻璐︽埛 VO
+export interface AccountVO {
+ id: number // 缁撶畻璐︽埛缂栧彿
+ no: string // 璐︽埛缂栫爜
+ remark: string // 澶囨敞
+ status: number // 寮�鍚姸鎬�
+ sort: number // 鎺掑簭
+ defaultStatus: boolean // 鏄惁榛樿
+ name: string // 璐︽埛鍚嶇О
+}
+
+// ERP 缁撶畻璐︽埛 API
+export const AccountApi = {
+ // 鏌ヨ缁撶畻璐︽埛鍒嗛〉
+ getAccountPage: async (params: any) => {
+ return await request.get({ url: `/erp/account/page`, params })
+ },
+
+ // 鏌ヨ缁撶畻璐︽埛绮剧畝鍒楄〃
+ getAccountSimpleList: async () => {
+ return await request.get({ url: `/erp/account/simple-list` })
+ },
+
+ // 鏌ヨ缁撶畻璐︽埛璇︽儏
+ getAccount: async (id: number) => {
+ return await request.get({ url: `/erp/account/get?id=` + id })
+ },
+
+ // 鏂板缁撶畻璐︽埛
+ createAccount: async (data: AccountVO) => {
+ return await request.post({ url: `/erp/account/create`, data })
+ },
+
+ // 淇敼缁撶畻璐︽埛
+ updateAccount: async (data: AccountVO) => {
+ return await request.put({ url: `/erp/account/update`, data })
+ },
+
+ // 淇敼缁撶畻璐︽埛榛樿鐘舵��
+ updateAccountDefaultStatus: async (id: number, defaultStatus: boolean) => {
+ return await request.put({
+ url: `/erp/account/update-default-status`,
+ params: {
+ id,
+ defaultStatus
+ }
+ })
+ },
+
+ // 鍒犻櫎缁撶畻璐︽埛
+ deleteAccount: async (id: number) => {
+ return await request.delete({ url: `/erp/account/delete?id=` + id })
+ },
+
+ // 瀵煎嚭缁撶畻璐︽埛 Excel
+ exportAccount: async (params: any) => {
+ return await request.download({ url: `/erp/account/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/finance/payment/index.ts b/src/api/erp/finance/payment/index.ts
new file mode 100644
index 0000000..c6749db
--- /dev/null
+++ b/src/api/erp/finance/payment/index.ts
@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+// ERP 浠樻鍗� VO
+export interface FinancePaymentVO {
+ id: number // 浠樻鍗曠紪鍙�
+ no: string // 浠樻鍗曞彿
+ supplierId: number // 渚涘簲鍟嗙紪鍙�
+ paymentTime: Date // 浠樻鏃堕棿
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 浠樻鍗� API
+export const FinancePaymentApi = {
+ // 鏌ヨ浠樻鍗曞垎椤�
+ getFinancePaymentPage: async (params: any) => {
+ return await request.get({ url: `/erp/finance-payment/page`, params })
+ },
+
+ // 鏌ヨ浠樻鍗曡鎯�
+ getFinancePayment: async (id: number) => {
+ return await request.get({ url: `/erp/finance-payment/get?id=` + id })
+ },
+
+ // 鏂板浠樻鍗�
+ createFinancePayment: async (data: FinancePaymentVO) => {
+ return await request.post({ url: `/erp/finance-payment/create`, data })
+ },
+
+ // 淇敼浠樻鍗�
+ updateFinancePayment: async (data: FinancePaymentVO) => {
+ return await request.put({ url: `/erp/finance-payment/update`, data })
+ },
+
+ // 鏇存柊浠樻鍗曠殑鐘舵��
+ updateFinancePaymentStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/finance-payment/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎浠樻鍗�
+ deleteFinancePayment: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/finance-payment/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭浠樻鍗� Excel
+ exportFinancePayment: async (params: any) => {
+ return await request.download({ url: `/erp/finance-payment/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/finance/receipt/index.ts b/src/api/erp/finance/receipt/index.ts
new file mode 100644
index 0000000..4de28ca
--- /dev/null
+++ b/src/api/erp/finance/receipt/index.ts
@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+// ERP 鏀舵鍗� VO
+export interface FinanceReceiptVO {
+ id: number // 鏀舵鍗曠紪鍙�
+ no: string // 鏀舵鍗曞彿
+ customerId: number // 瀹㈡埛缂栧彿
+ receiptTime: Date // 鏀舵鏃堕棿
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 鏀舵鍗� API
+export const FinanceReceiptApi = {
+ // 鏌ヨ鏀舵鍗曞垎椤�
+ getFinanceReceiptPage: async (params: any) => {
+ return await request.get({ url: `/erp/finance-receipt/page`, params })
+ },
+
+ // 鏌ヨ鏀舵鍗曡鎯�
+ getFinanceReceipt: async (id: number) => {
+ return await request.get({ url: `/erp/finance-receipt/get?id=` + id })
+ },
+
+ // 鏂板鏀舵鍗�
+ createFinanceReceipt: async (data: FinanceReceiptVO) => {
+ return await request.post({ url: `/erp/finance-receipt/create`, data })
+ },
+
+ // 淇敼鏀舵鍗�
+ updateFinanceReceipt: async (data: FinanceReceiptVO) => {
+ return await request.put({ url: `/erp/finance-receipt/update`, data })
+ },
+
+ // 鏇存柊鏀舵鍗曠殑鐘舵��
+ updateFinanceReceiptStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/finance-receipt/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎鏀舵鍗�
+ deleteFinanceReceipt: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/finance-receipt/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭鏀舵鍗� Excel
+ exportFinanceReceipt: async (params: any) => {
+ return await request.download({ url: `/erp/finance-receipt/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/product/category/index.ts b/src/api/erp/product/category/index.ts
new file mode 100644
index 0000000..d67ccff
--- /dev/null
+++ b/src/api/erp/product/category/index.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+// ERP 浜у搧鍒嗙被 VO
+export interface ProductCategoryVO {
+ id: number // 鍒嗙被缂栧彿
+ parentId: number // 鐖跺垎绫荤紪鍙�
+ name: string // 鍒嗙被鍚嶇О
+ code: string // 鍒嗙被缂栫爜
+ sort: number // 鍒嗙被鎺掑簭
+ status: number // 寮�鍚姸鎬�
+}
+
+// ERP 浜у搧鍒嗙被 API
+export const ProductCategoryApi = {
+ // 鏌ヨ浜у搧鍒嗙被鍒楄〃
+ getProductCategoryList: async () => {
+ return await request.get({ url: `/erp/product-category/list` })
+ },
+
+ // 鏌ヨ浜у搧鍒嗙被绮剧畝鍒楄〃
+ getProductCategorySimpleList: async () => {
+ return await request.get({ url: `/erp/product-category/simple-list` })
+ },
+
+ // 鏌ヨ浜у搧鍒嗙被璇︽儏
+ getProductCategory: async (id: number) => {
+ return await request.get({ url: `/erp/product-category/get?id=` + id })
+ },
+
+ // 鏂板浜у搧鍒嗙被
+ createProductCategory: async (data: ProductCategoryVO) => {
+ return await request.post({ url: `/erp/product-category/create`, data })
+ },
+
+ // 淇敼浜у搧鍒嗙被
+ updateProductCategory: async (data: ProductCategoryVO) => {
+ return await request.put({ url: `/erp/product-category/update`, data })
+ },
+
+ // 鍒犻櫎浜у搧鍒嗙被
+ deleteProductCategory: async (id: number) => {
+ return await request.delete({ url: `/erp/product-category/delete?id=` + id })
+ },
+
+ // 瀵煎嚭浜у搧鍒嗙被 Excel
+ exportProductCategory: async (params) => {
+ return await request.download({ url: `/erp/product-category/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/product/product/index.ts b/src/api/erp/product/product/index.ts
new file mode 100644
index 0000000..1136282
--- /dev/null
+++ b/src/api/erp/product/product/index.ts
@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+// ERP 浜у搧 VO
+export interface ProductVO {
+ id: number // 浜у搧缂栧彿
+ name: string // 浜у搧鍚嶇О
+ barCode: string // 浜у搧鏉$爜
+ categoryId: number // 浜у搧绫诲瀷缂栧彿
+ unitId: number // 鍗曚綅缂栧彿
+ unitName?: string // 鍗曚綅鍚嶅瓧
+ status: number // 浜у搧鐘舵��
+ standard: string // 浜у搧瑙勬牸
+ remark: string // 浜у搧澶囨敞
+ expiryDay: number // 淇濊川鏈熷ぉ鏁�
+ weight: number // 閲嶉噺锛坘g锛�
+ purchasePrice: number // 閲囪喘浠锋牸锛屽崟浣嶏細鍏�
+ salePrice: number // 閿�鍞环鏍硷紝鍗曚綅锛氬厓
+ minPrice: number // 鏈�浣庝环鏍硷紝鍗曚綅锛氬厓
+}
+
+// ERP 浜у搧 API
+export const ProductApi = {
+ // 鏌ヨ浜у搧鍒嗛〉
+ getProductPage: async (params: any) => {
+ return await request.get({ url: `/erp/product/page`, params })
+ },
+
+ // 鏌ヨ浜у搧绮剧畝鍒楄〃
+ getProductSimpleList: async () => {
+ return await request.get({ url: `/erp/product/simple-list` })
+ },
+
+ // 鏌ヨ浜у搧璇︽儏
+ getProduct: async (id: number) => {
+ return await request.get({ url: `/erp/product/get?id=` + id })
+ },
+
+ // 鏂板浜у搧
+ createProduct: async (data: ProductVO) => {
+ return await request.post({ url: `/erp/product/create`, data })
+ },
+
+ // 淇敼浜у搧
+ updateProduct: async (data: ProductVO) => {
+ return await request.put({ url: `/erp/product/update`, data })
+ },
+
+ // 鍒犻櫎浜у搧
+ deleteProduct: async (id: number) => {
+ return await request.delete({ url: `/erp/product/delete?id=` + id })
+ },
+
+ // 瀵煎嚭浜у搧 Excel
+ exportProduct: async (params) => {
+ return await request.download({ url: `/erp/product/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/product/unit/index.ts b/src/api/erp/product/unit/index.ts
new file mode 100644
index 0000000..1e1c8ac
--- /dev/null
+++ b/src/api/erp/product/unit/index.ts
@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+// ERP 浜у搧鍗曚綅 VO
+export interface ProductUnitVO {
+ id: number // 鍗曚綅缂栧彿
+ name: string // 鍗曚綅鍚嶅瓧
+ status: number // 鍗曚綅鐘舵��
+}
+
+// ERP 浜у搧鍗曚綅 API
+export const ProductUnitApi = {
+ // 鏌ヨ浜у搧鍗曚綅鍒嗛〉
+ getProductUnitPage: async (params: any) => {
+ return await request.get({ url: `/erp/product-unit/page`, params })
+ },
+
+ // 鏌ヨ浜у搧鍗曚綅绮剧畝鍒楄〃
+ getProductUnitSimpleList: async () => {
+ return await request.get({ url: `/erp/product-unit/simple-list` })
+ },
+
+ // 鏌ヨ浜у搧鍗曚綅璇︽儏
+ getProductUnit: async (id: number) => {
+ return await request.get({ url: `/erp/product-unit/get?id=` + id })
+ },
+
+ // 鏂板浜у搧鍗曚綅
+ createProductUnit: async (data: ProductUnitVO) => {
+ return await request.post({ url: `/erp/product-unit/create`, data })
+ },
+
+ // 淇敼浜у搧鍗曚綅
+ updateProductUnit: async (data: ProductUnitVO) => {
+ return await request.put({ url: `/erp/product-unit/update`, data })
+ },
+
+ // 鍒犻櫎浜у搧鍗曚綅
+ deleteProductUnit: async (id: number) => {
+ return await request.delete({ url: `/erp/product-unit/delete?id=` + id })
+ },
+
+ // 瀵煎嚭浜у搧鍗曚綅 Excel
+ exportProductUnit: async (params) => {
+ return await request.download({ url: `/erp/product-unit/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/purchase/in/index.ts b/src/api/erp/purchase/in/index.ts
new file mode 100644
index 0000000..f94708d
--- /dev/null
+++ b/src/api/erp/purchase/in/index.ts
@@ -0,0 +1,64 @@
+import request from '@/config/axios'
+
+// ERP 閲囪喘鍏ュ簱 VO
+export interface PurchaseInVO {
+ id: number // 鍏ュ簱宸ュ崟缂栧彿
+ no: string // 閲囪喘鍏ュ簱鍙�
+ customerId: number // 瀹㈡埛缂栧彿
+ inTime: Date // 鍏ュ簱鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+ outCount: number // 閲囪喘鍑哄簱鏁伴噺
+ returnCount: number // 閲囪喘閫�璐ф暟閲�
+}
+
+// ERP 閲囪喘鍏ュ簱 API
+export const PurchaseInApi = {
+ // 鏌ヨ閲囪喘鍏ュ簱鍒嗛〉
+ getPurchaseInPage: async (params: any) => {
+ return await request.get({ url: `/erp/purchase-in/page`, params })
+ },
+
+ // 鏌ヨ閲囪喘鍏ュ簱璇︽儏
+ getPurchaseIn: async (id: number) => {
+ return await request.get({ url: `/erp/purchase-in/get?id=` + id })
+ },
+
+ // 鏂板閲囪喘鍏ュ簱
+ createPurchaseIn: async (data: PurchaseInVO) => {
+ return await request.post({ url: `/erp/purchase-in/create`, data })
+ },
+
+ // 淇敼閲囪喘鍏ュ簱
+ updatePurchaseIn: async (data: PurchaseInVO) => {
+ return await request.put({ url: `/erp/purchase-in/update`, data })
+ },
+
+ // 鏇存柊閲囪喘鍏ュ簱鐨勭姸鎬�
+ updatePurchaseInStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/purchase-in/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎閲囪喘鍏ュ簱
+ deletePurchaseIn: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/purchase-in/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭閲囪喘鍏ュ簱 Excel
+ exportPurchaseIn: async (params: any) => {
+ return await request.download({ url: `/erp/purchase-in/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/purchase/order/index.ts b/src/api/erp/purchase/order/index.ts
new file mode 100644
index 0000000..ad3222f
--- /dev/null
+++ b/src/api/erp/purchase/order/index.ts
@@ -0,0 +1,64 @@
+import request from '@/config/axios'
+
+// ERP 閲囪喘璁㈠崟 VO
+export interface PurchaseOrderVO {
+ id: number // 璁㈠崟宸ュ崟缂栧彿
+ no: string // 閲囪喘璁㈠崟鍙�
+ customerId: number // 瀹㈡埛缂栧彿
+ orderTime: Date // 璁㈠崟鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+ outCount: number // 閲囪喘鍑哄簱鏁伴噺
+ returnCount: number // 閲囪喘閫�璐ф暟閲�
+}
+
+// ERP 閲囪喘璁㈠崟 API
+export const PurchaseOrderApi = {
+ // 鏌ヨ閲囪喘璁㈠崟鍒嗛〉
+ getPurchaseOrderPage: async (params: any) => {
+ return await request.get({ url: `/erp/purchase-order/page`, params })
+ },
+
+ // 鏌ヨ閲囪喘璁㈠崟璇︽儏
+ getPurchaseOrder: async (id: number) => {
+ return await request.get({ url: `/erp/purchase-order/get?id=` + id })
+ },
+
+ // 鏂板閲囪喘璁㈠崟
+ createPurchaseOrder: async (data: PurchaseOrderVO) => {
+ return await request.post({ url: `/erp/purchase-order/create`, data })
+ },
+
+ // 淇敼閲囪喘璁㈠崟
+ updatePurchaseOrder: async (data: PurchaseOrderVO) => {
+ return await request.put({ url: `/erp/purchase-order/update`, data })
+ },
+
+ // 鏇存柊閲囪喘璁㈠崟鐨勭姸鎬�
+ updatePurchaseOrderStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/purchase-order/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎閲囪喘璁㈠崟
+ deletePurchaseOrder: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/purchase-order/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭閲囪喘璁㈠崟 Excel
+ exportPurchaseOrder: async (params: any) => {
+ return await request.download({ url: `/erp/purchase-order/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/purchase/return/index.ts b/src/api/erp/purchase/return/index.ts
new file mode 100644
index 0000000..182e04e
--- /dev/null
+++ b/src/api/erp/purchase/return/index.ts
@@ -0,0 +1,62 @@
+import request from '@/config/axios'
+
+// ERP 閲囪喘閫�璐� VO
+export interface PurchaseReturnVO {
+ id: number // 閲囪喘閫�璐х紪鍙�
+ no: string // 閲囪喘閫�璐у彿
+ customerId: number // 瀹㈡埛缂栧彿
+ returnTime: Date // 閫�璐ф椂闂�
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 閲囪喘閫�璐� API
+export const PurchaseReturnApi = {
+ // 鏌ヨ閲囪喘閫�璐у垎椤�
+ getPurchaseReturnPage: async (params: any) => {
+ return await request.get({ url: `/erp/purchase-return/page`, params })
+ },
+
+ // 鏌ヨ閲囪喘閫�璐ц鎯�
+ getPurchaseReturn: async (id: number) => {
+ return await request.get({ url: `/erp/purchase-return/get?id=` + id })
+ },
+
+ // 鏂板閲囪喘閫�璐�
+ createPurchaseReturn: async (data: PurchaseReturnVO) => {
+ return await request.post({ url: `/erp/purchase-return/create`, data })
+ },
+
+ // 淇敼閲囪喘閫�璐�
+ updatePurchaseReturn: async (data: PurchaseReturnVO) => {
+ return await request.put({ url: `/erp/purchase-return/update`, data })
+ },
+
+ // 鏇存柊閲囪喘閫�璐х殑鐘舵��
+ updatePurchaseReturnStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/purchase-return/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎閲囪喘閫�璐�
+ deletePurchaseReturn: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/purchase-return/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭閲囪喘閫�璐� Excel
+ exportPurchaseReturn: async (params: any) => {
+ return await request.download({ url: `/erp/purchase-return/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/purchase/supplier/index.ts b/src/api/erp/purchase/supplier/index.ts
new file mode 100644
index 0000000..34729a5
--- /dev/null
+++ b/src/api/erp/purchase/supplier/index.ts
@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+
+// ERP 渚涘簲鍟� VO
+export interface SupplierVO {
+ id: number // 渚涘簲鍟嗙紪鍙�
+ name: string // 渚涘簲鍟嗗悕绉�
+ contact: string // 鑱旂郴浜�
+ mobile: string // 鎵嬫満鍙风爜
+ telephone: string // 鑱旂郴鐢佃瘽
+ email: string // 鐢靛瓙閭
+ fax: string // 浼犵湡
+ remark: string // 澶囨敞
+ status: number // 寮�鍚姸鎬�
+ sort: number // 鎺掑簭
+ taxNo: string // 绾崇◣浜鸿瘑鍒彿
+ taxPercent: number // 绋庣巼
+ bankName: string // 寮�鎴疯
+ bankAccount: string // 寮�鎴疯处鍙�
+ bankAddress: string // 寮�鎴峰湴鍧�
+}
+
+// ERP 渚涘簲鍟� API
+export const SupplierApi = {
+ // 鏌ヨ渚涘簲鍟嗗垎椤�
+ getSupplierPage: async (params: any) => {
+ return await request.get({ url: `/erp/supplier/page`, params })
+ },
+
+ // 鑾峰緱渚涘簲鍟嗙簿绠�鍒楄〃
+ getSupplierSimpleList: async () => {
+ return await request.get({ url: `/erp/supplier/simple-list` })
+ },
+
+ // 鏌ヨ渚涘簲鍟嗚鎯�
+ getSupplier: async (id: number) => {
+ return await request.get({ url: `/erp/supplier/get?id=` + id })
+ },
+
+ // 鏂板渚涘簲鍟�
+ createSupplier: async (data: SupplierVO) => {
+ return await request.post({ url: `/erp/supplier/create`, data })
+ },
+
+ // 淇敼渚涘簲鍟�
+ updateSupplier: async (data: SupplierVO) => {
+ return await request.put({ url: `/erp/supplier/update`, data })
+ },
+
+ // 鍒犻櫎渚涘簲鍟�
+ deleteSupplier: async (id: number) => {
+ return await request.delete({ url: `/erp/supplier/delete?id=` + id })
+ },
+
+ // 瀵煎嚭渚涘簲鍟� Excel
+ exportSupplier: async (params) => {
+ return await request.download({ url: `/erp/supplier/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/sale/customer/index.ts b/src/api/erp/sale/customer/index.ts
new file mode 100644
index 0000000..3aaefb5
--- /dev/null
+++ b/src/api/erp/sale/customer/index.ts
@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+
+// ERP 瀹㈡埛 VO
+export interface CustomerVO {
+ id: number // 瀹㈡埛缂栧彿
+ name: string // 瀹㈡埛鍚嶇О
+ contact: string // 鑱旂郴浜�
+ mobile: string // 鎵嬫満鍙风爜
+ telephone: string // 鑱旂郴鐢佃瘽
+ email: string // 鐢靛瓙閭
+ fax: string // 浼犵湡
+ remark: string // 澶囨敞
+ status: number // 寮�鍚姸鎬�
+ sort: number // 鎺掑簭
+ taxNo: string // 绾崇◣浜鸿瘑鍒彿
+ taxPercent: number // 绋庣巼
+ bankName: string // 寮�鎴疯
+ bankAccount: string // 寮�鎴疯处鍙�
+ bankAddress: string // 寮�鎴峰湴鍧�
+}
+
+// ERP 瀹㈡埛 API
+export const CustomerApi = {
+ // 鏌ヨ瀹㈡埛鍒嗛〉
+ getCustomerPage: async (params: any) => {
+ return await request.get({ url: `/erp/customer/page`, params })
+ },
+
+ // 鏌ヨ瀹㈡埛绮剧畝鍒楄〃
+ getCustomerSimpleList: async () => {
+ return await request.get({ url: `/erp/customer/simple-list` })
+ },
+
+ // 鏌ヨ瀹㈡埛璇︽儏
+ getCustomer: async (id: number) => {
+ return await request.get({ url: `/erp/customer/get?id=` + id })
+ },
+
+ // 鏂板瀹㈡埛
+ createCustomer: async (data: CustomerVO) => {
+ return await request.post({ url: `/erp/customer/create`, data })
+ },
+
+ // 淇敼瀹㈡埛
+ updateCustomer: async (data: CustomerVO) => {
+ return await request.put({ url: `/erp/customer/update`, data })
+ },
+
+ // 鍒犻櫎瀹㈡埛
+ deleteCustomer: async (id: number) => {
+ return await request.delete({ url: `/erp/customer/delete?id=` + id })
+ },
+
+ // 瀵煎嚭瀹㈡埛 Excel
+ exportCustomer: async (params) => {
+ return await request.download({ url: `/erp/customer/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/sale/order/index.ts b/src/api/erp/sale/order/index.ts
new file mode 100644
index 0000000..2d2ac53
--- /dev/null
+++ b/src/api/erp/sale/order/index.ts
@@ -0,0 +1,64 @@
+import request from '@/config/axios'
+
+// ERP 閿�鍞鍗� VO
+export interface SaleOrderVO {
+ id: number // 璁㈠崟宸ュ崟缂栧彿
+ no: string // 閿�鍞鍗曞彿
+ customerId: number // 瀹㈡埛缂栧彿
+ orderTime: Date // 璁㈠崟鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+ outCount: number // 閿�鍞嚭搴撴暟閲�
+ returnCount: number // 閿�鍞��璐ф暟閲�
+}
+
+// ERP 閿�鍞鍗� API
+export const SaleOrderApi = {
+ // 鏌ヨ閿�鍞鍗曞垎椤�
+ getSaleOrderPage: async (params: any) => {
+ return await request.get({ url: `/erp/sale-order/page`, params })
+ },
+
+ // 鏌ヨ閿�鍞鍗曡鎯�
+ getSaleOrder: async (id: number) => {
+ return await request.get({ url: `/erp/sale-order/get?id=` + id })
+ },
+
+ // 鏂板閿�鍞鍗�
+ createSaleOrder: async (data: SaleOrderVO) => {
+ return await request.post({ url: `/erp/sale-order/create`, data })
+ },
+
+ // 淇敼閿�鍞鍗�
+ updateSaleOrder: async (data: SaleOrderVO) => {
+ return await request.put({ url: `/erp/sale-order/update`, data })
+ },
+
+ // 鏇存柊閿�鍞鍗曠殑鐘舵��
+ updateSaleOrderStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/sale-order/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎閿�鍞鍗�
+ deleteSaleOrder: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/sale-order/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭閿�鍞鍗� Excel
+ exportSaleOrder: async (params: any) => {
+ return await request.download({ url: `/erp/sale-order/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/sale/out/index.ts b/src/api/erp/sale/out/index.ts
new file mode 100644
index 0000000..cbc605e
--- /dev/null
+++ b/src/api/erp/sale/out/index.ts
@@ -0,0 +1,62 @@
+import request from '@/config/axios'
+
+// ERP 閿�鍞嚭搴� VO
+export interface SaleOutVO {
+ id: number // 閿�鍞嚭搴撶紪鍙�
+ no: string // 閿�鍞嚭搴撳彿
+ customerId: number // 瀹㈡埛缂栧彿
+ outTime: Date // 鍑哄簱鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 閿�鍞嚭搴� API
+export const SaleOutApi = {
+ // 鏌ヨ閿�鍞嚭搴撳垎椤�
+ getSaleOutPage: async (params: any) => {
+ return await request.get({ url: `/erp/sale-out/page`, params })
+ },
+
+ // 鏌ヨ閿�鍞嚭搴撹鎯�
+ getSaleOut: async (id: number) => {
+ return await request.get({ url: `/erp/sale-out/get?id=` + id })
+ },
+
+ // 鏂板閿�鍞嚭搴�
+ createSaleOut: async (data: SaleOutVO) => {
+ return await request.post({ url: `/erp/sale-out/create`, data })
+ },
+
+ // 淇敼閿�鍞嚭搴�
+ updateSaleOut: async (data: SaleOutVO) => {
+ return await request.put({ url: `/erp/sale-out/update`, data })
+ },
+
+ // 鏇存柊閿�鍞嚭搴撶殑鐘舵��
+ updateSaleOutStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/sale-out/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎閿�鍞嚭搴�
+ deleteSaleOut: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/sale-out/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭閿�鍞嚭搴� Excel
+ exportSaleOut: async (params: any) => {
+ return await request.download({ url: `/erp/sale-out/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/sale/return/index.ts b/src/api/erp/sale/return/index.ts
new file mode 100644
index 0000000..160ac01
--- /dev/null
+++ b/src/api/erp/sale/return/index.ts
@@ -0,0 +1,62 @@
+import request from '@/config/axios'
+
+// ERP 閿�鍞��璐� VO
+export interface SaleReturnVO {
+ id: number // 閿�鍞��璐х紪鍙�
+ no: string // 閿�鍞��璐у彿
+ customerId: number // 瀹㈡埛缂栧彿
+ returnTime: Date // 閫�璐ф椂闂�
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 閿�鍞��璐� API
+export const SaleReturnApi = {
+ // 鏌ヨ閿�鍞��璐у垎椤�
+ getSaleReturnPage: async (params: any) => {
+ return await request.get({ url: `/erp/sale-return/page`, params })
+ },
+
+ // 鏌ヨ閿�鍞��璐ц鎯�
+ getSaleReturn: async (id: number) => {
+ return await request.get({ url: `/erp/sale-return/get?id=` + id })
+ },
+
+ // 鏂板閿�鍞��璐�
+ createSaleReturn: async (data: SaleReturnVO) => {
+ return await request.post({ url: `/erp/sale-return/create`, data })
+ },
+
+ // 淇敼閿�鍞��璐�
+ updateSaleReturn: async (data: SaleReturnVO) => {
+ return await request.put({ url: `/erp/sale-return/update`, data })
+ },
+
+ // 鏇存柊閿�鍞��璐х殑鐘舵��
+ updateSaleReturnStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/sale-return/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎閿�鍞��璐�
+ deleteSaleReturn: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/sale-return/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭閿�鍞��璐� Excel
+ exportSaleReturn: async (params: any) => {
+ return await request.download({ url: `/erp/sale-return/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/statistics/purchase/index.ts b/src/api/erp/statistics/purchase/index.ts
new file mode 100644
index 0000000..80d907a
--- /dev/null
+++ b/src/api/erp/statistics/purchase/index.ts
@@ -0,0 +1,28 @@
+import request from '@/config/axios'
+
+// ERP 閲囪喘鍏ㄥ眬缁熻 VO
+export interface ErpPurchaseSummaryRespVO {
+ todayPrice: number // 浠婃棩閲囪喘閲戦
+ yesterdayPrice: number // 鏄ㄦ棩閲囪喘閲戦
+ monthPrice: number // 鏈湀閲囪喘閲戦
+ yearPrice: number // 浠婂勾閲囪喘閲戦
+}
+
+// ERP 閲囪喘鏃堕棿娈电粺璁� VO
+export interface ErpPurchaseTimeSummaryRespVO {
+ time: string // 鏃堕棿
+ price: number // 閲囪喘閲戦
+}
+
+// ERP 閲囪喘缁熻 API
+export const PurchaseStatisticsApi = {
+ // 鑾峰緱閲囪喘缁熻
+ getPurchaseSummary: async (): Promise<ErpPurchaseSummaryRespVO> => {
+ return await request.get({ url: `/erp/purchase-statistics/summary` })
+ },
+
+ // 鑾峰緱閲囪喘鏃堕棿娈电粺璁�
+ getPurchaseTimeSummary: async (): Promise<ErpPurchaseTimeSummaryRespVO[]> => {
+ return await request.get({ url: `/erp/purchase-statistics/time-summary` })
+ }
+}
diff --git a/src/api/erp/statistics/sale/index.ts b/src/api/erp/statistics/sale/index.ts
new file mode 100644
index 0000000..09d8500
--- /dev/null
+++ b/src/api/erp/statistics/sale/index.ts
@@ -0,0 +1,28 @@
+import request from '@/config/axios'
+
+// ERP 閿�鍞叏灞�缁熻 VO
+export interface ErpSaleSummaryRespVO {
+ todayPrice: number // 浠婃棩閿�鍞噾棰�
+ yesterdayPrice: number // 鏄ㄦ棩閿�鍞噾棰�
+ monthPrice: number // 鏈湀閿�鍞噾棰�
+ yearPrice: number // 浠婂勾閿�鍞噾棰�
+}
+
+// ERP 閿�鍞椂闂存缁熻 VO
+export interface ErpSaleTimeSummaryRespVO {
+ time: string // 鏃堕棿
+ price: number // 閿�鍞噾棰�
+}
+
+// ERP 閿�鍞粺璁� API
+export const SaleStatisticsApi = {
+ // 鑾峰緱閿�鍞粺璁�
+ getSaleSummary: async (): Promise<ErpSaleSummaryRespVO> => {
+ return await request.get({ url: `/erp/sale-statistics/summary` })
+ },
+
+ // 鑾峰緱閿�鍞椂闂存缁熻
+ getSaleTimeSummary: async (): Promise<ErpSaleTimeSummaryRespVO[]> => {
+ return await request.get({ url: `/erp/sale-statistics/time-summary` })
+ }
+}
diff --git a/src/api/erp/stock/check/index.ts b/src/api/erp/stock/check/index.ts
new file mode 100644
index 0000000..4a3e653
--- /dev/null
+++ b/src/api/erp/stock/check/index.ts
@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+// ERP 搴撳瓨鐩樼偣鍗� VO
+export interface StockCheckVO {
+ id: number // 鍑哄簱缂栧彿
+ no: string // 鍑哄簱鍗曞彿
+ outTime: Date // 鍑哄簱鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 搴撳瓨鐩樼偣鍗� API
+export const StockCheckApi = {
+ // 鏌ヨ搴撳瓨鐩樼偣鍗曞垎椤�
+ getStockCheckPage: async (params: any) => {
+ return await request.get({ url: `/erp/stock-check/page`, params })
+ },
+
+ // 鏌ヨ搴撳瓨鐩樼偣鍗曡鎯�
+ getStockCheck: async (id: number) => {
+ return await request.get({ url: `/erp/stock-check/get?id=` + id })
+ },
+
+ // 鏂板搴撳瓨鐩樼偣鍗�
+ createStockCheck: async (data: StockCheckVO) => {
+ return await request.post({ url: `/erp/stock-check/create`, data })
+ },
+
+ // 淇敼搴撳瓨鐩樼偣鍗�
+ updateStockCheck: async (data: StockCheckVO) => {
+ return await request.put({ url: `/erp/stock-check/update`, data })
+ },
+
+ // 鏇存柊搴撳瓨鐩樼偣鍗曠殑鐘舵��
+ updateStockCheckStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/stock-check/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎搴撳瓨鐩樼偣鍗�
+ deleteStockCheck: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/stock-check/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭搴撳瓨鐩樼偣鍗� Excel
+ exportStockCheck: async (params) => {
+ return await request.download({ url: `/erp/stock-check/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/stock/in/index.ts b/src/api/erp/stock/in/index.ts
new file mode 100644
index 0000000..148b64f
--- /dev/null
+++ b/src/api/erp/stock/in/index.ts
@@ -0,0 +1,62 @@
+import request from '@/config/axios'
+
+// ERP 鍏跺畠鍏ュ簱鍗� VO
+export interface StockInVO {
+ id: number // 鍏ュ簱缂栧彿
+ no: string // 鍏ュ簱鍗曞彿
+ supplierId: number // 渚涘簲鍟嗙紪鍙�
+ inTime: Date // 鍏ュ簱鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 鍏跺畠鍏ュ簱鍗� API
+export const StockInApi = {
+ // 鏌ヨ鍏跺畠鍏ュ簱鍗曞垎椤�
+ getStockInPage: async (params: any) => {
+ return await request.get({ url: `/erp/stock-in/page`, params })
+ },
+
+ // 鏌ヨ鍏跺畠鍏ュ簱鍗曡鎯�
+ getStockIn: async (id: number) => {
+ return await request.get({ url: `/erp/stock-in/get?id=` + id })
+ },
+
+ // 鏂板鍏跺畠鍏ュ簱鍗�
+ createStockIn: async (data: StockInVO) => {
+ return await request.post({ url: `/erp/stock-in/create`, data })
+ },
+
+ // 淇敼鍏跺畠鍏ュ簱鍗�
+ updateStockIn: async (data: StockInVO) => {
+ return await request.put({ url: `/erp/stock-in/update`, data })
+ },
+
+ // 鏇存柊鍏跺畠鍏ュ簱鍗曠殑鐘舵��
+ updateStockInStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/stock-in/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎鍏跺畠鍏ュ簱鍗�
+ deleteStockIn: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/stock-in/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭鍏跺畠鍏ュ簱鍗� Excel
+ exportStockIn: async (params) => {
+ return await request.download({ url: `/erp/stock-in/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/stock/move/index.ts b/src/api/erp/stock/move/index.ts
new file mode 100644
index 0000000..398568e
--- /dev/null
+++ b/src/api/erp/stock/move/index.ts
@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+// ERP 搴撳瓨璋冨害鍗� VO
+export interface StockMoveVO {
+ id: number // 鍑哄簱缂栧彿
+ no: string // 鍑哄簱鍗曞彿
+ outTime: Date // 鍑哄簱鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 搴撳瓨璋冨害鍗� API
+export const StockMoveApi = {
+ // 鏌ヨ搴撳瓨璋冨害鍗曞垎椤�
+ getStockMovePage: async (params: any) => {
+ return await request.get({ url: `/erp/stock-move/page`, params })
+ },
+
+ // 鏌ヨ搴撳瓨璋冨害鍗曡鎯�
+ getStockMove: async (id: number) => {
+ return await request.get({ url: `/erp/stock-move/get?id=` + id })
+ },
+
+ // 鏂板搴撳瓨璋冨害鍗�
+ createStockMove: async (data: StockMoveVO) => {
+ return await request.post({ url: `/erp/stock-move/create`, data })
+ },
+
+ // 淇敼搴撳瓨璋冨害鍗�
+ updateStockMove: async (data: StockMoveVO) => {
+ return await request.put({ url: `/erp/stock-move/update`, data })
+ },
+
+ // 鏇存柊搴撳瓨璋冨害鍗曠殑鐘舵��
+ updateStockMoveStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/stock-move/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎搴撳瓨璋冨害鍗�
+ deleteStockMove: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/stock-move/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭搴撳瓨璋冨害鍗� Excel
+ exportStockMove: async (params) => {
+ return await request.download({ url: `/erp/stock-move/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/stock/out/index.ts b/src/api/erp/stock/out/index.ts
new file mode 100644
index 0000000..f0f40d3
--- /dev/null
+++ b/src/api/erp/stock/out/index.ts
@@ -0,0 +1,62 @@
+import request from '@/config/axios'
+
+// ERP 鍏跺畠鍑哄簱鍗� VO
+export interface StockOutVO {
+ id: number // 鍑哄簱缂栧彿
+ no: string // 鍑哄簱鍗曞彿
+ customerId: number // 瀹㈡埛缂栧彿
+ outTime: Date // 鍑哄簱鏃堕棿
+ totalCount: number // 鍚堣鏁伴噺
+ totalPrice: number // 鍚堣閲戦锛屽崟浣嶏細鍏�
+ status: number // 鐘舵��
+ remark: string // 澶囨敞
+}
+
+// ERP 鍏跺畠鍑哄簱鍗� API
+export const StockOutApi = {
+ // 鏌ヨ鍏跺畠鍑哄簱鍗曞垎椤�
+ getStockOutPage: async (params: any) => {
+ return await request.get({ url: `/erp/stock-out/page`, params })
+ },
+
+ // 鏌ヨ鍏跺畠鍑哄簱鍗曡鎯�
+ getStockOut: async (id: number) => {
+ return await request.get({ url: `/erp/stock-out/get?id=` + id })
+ },
+
+ // 鏂板鍏跺畠鍑哄簱鍗�
+ createStockOut: async (data: StockOutVO) => {
+ return await request.post({ url: `/erp/stock-out/create`, data })
+ },
+
+ // 淇敼鍏跺畠鍑哄簱鍗�
+ updateStockOut: async (data: StockOutVO) => {
+ return await request.put({ url: `/erp/stock-out/update`, data })
+ },
+
+ // 鏇存柊鍏跺畠鍑哄簱鍗曠殑鐘舵��
+ updateStockOutStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/erp/stock-out/update-status`,
+ params: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎鍏跺畠鍑哄簱鍗�
+ deleteStockOut: async (ids: number[]) => {
+ return await request.delete({
+ url: `/erp/stock-out/delete`,
+ params: {
+ ids: ids.join(',')
+ }
+ })
+ },
+
+ // 瀵煎嚭鍏跺畠鍑哄簱鍗� Excel
+ exportStockOut: async (params) => {
+ return await request.download({ url: `/erp/stock-out/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/stock/record/index.ts b/src/api/erp/stock/record/index.ts
new file mode 100644
index 0000000..a758eb4
--- /dev/null
+++ b/src/api/erp/stock/record/index.ts
@@ -0,0 +1,32 @@
+import request from '@/config/axios'
+
+// ERP 浜у搧搴撳瓨鏄庣粏 VO
+export interface StockRecordVO {
+ id: number // 缂栧彿
+ productId: number // 浜у搧缂栧彿
+ warehouseId: number // 浠撳簱缂栧彿
+ count: number // 鍑哄叆搴撴暟閲�
+ totalCount: number // 鎬诲簱瀛橀噺
+ bizType: number // 涓氬姟绫诲瀷
+ bizId: number // 涓氬姟缂栧彿
+ bizItemId: number // 涓氬姟椤圭紪鍙�
+ bizNo: string // 涓氬姟鍗曞彿
+}
+
+// ERP 浜у搧搴撳瓨鏄庣粏 API
+export const StockRecordApi = {
+ // 鏌ヨ浜у搧搴撳瓨鏄庣粏鍒嗛〉
+ getStockRecordPage: async (params: any) => {
+ return await request.get({ url: `/erp/stock-record/page`, params })
+ },
+
+ // 鏌ヨ浜у搧搴撳瓨鏄庣粏璇︽儏
+ getStockRecord: async (id: number) => {
+ return await request.get({ url: `/erp/stock-record/get?id=` + id })
+ },
+
+ // 瀵煎嚭浜у搧搴撳瓨鏄庣粏 Excel
+ exportStockRecord: async (params) => {
+ return await request.download({ url: `/erp/stock-record/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/stock/stock/index.ts b/src/api/erp/stock/stock/index.ts
new file mode 100644
index 0000000..4de86fb
--- /dev/null
+++ b/src/api/erp/stock/stock/index.ts
@@ -0,0 +1,41 @@
+import request from '@/config/axios'
+
+// ERP 浜у搧搴撳瓨 VO
+export interface StockVO {
+ // 缂栧彿
+ id: number
+ // 浜у搧缂栧彿
+ productId: number
+ // 浠撳簱缂栧彿
+ warehouseId: number
+ // 搴撳瓨鏁伴噺
+ count: number
+}
+
+// ERP 浜у搧搴撳瓨 API
+export const StockApi = {
+ // 鏌ヨ浜у搧搴撳瓨鍒嗛〉
+ getStockPage: async (params: any) => {
+ return await request.get({ url: `/erp/stock/page`, params })
+ },
+
+ // 鏌ヨ浜у搧搴撳瓨璇︽儏
+ getStock: async (id: number) => {
+ return await request.get({ url: `/erp/stock/get?id=` + id })
+ },
+
+ // 鏌ヨ浜у搧搴撳瓨璇︽儏
+ getStock2: async (productId: number, warehouseId: number) => {
+ return await request.get({ url: `/erp/stock/get`, params: { productId, warehouseId } })
+ },
+
+ // 鑾峰緱浜у搧搴撳瓨鏁伴噺
+ getStockCount: async (productId: number) => {
+ return await request.get({ url: `/erp/stock/get-count`, params: { productId } })
+ },
+
+ // 瀵煎嚭浜у搧搴撳瓨 Excel
+ exportStock: async (params) => {
+ return await request.download({ url: `/erp/stock/export-excel`, params })
+ }
+}
diff --git a/src/api/erp/stock/warehouse/index.ts b/src/api/erp/stock/warehouse/index.ts
new file mode 100644
index 0000000..598824b
--- /dev/null
+++ b/src/api/erp/stock/warehouse/index.ts
@@ -0,0 +1,64 @@
+import request from '@/config/axios'
+
+// ERP 浠撳簱 VO
+export interface WarehouseVO {
+ id: number // 浠撳簱缂栧彿
+ name: string // 浠撳簱鍚嶇О
+ address: string // 浠撳簱鍦板潃
+ sort: number // 鎺掑簭
+ remark: string // 澶囨敞
+ principal: string // 璐熻矗浜�
+ warehousePrice: number // 浠撳偍璐癸紝鍗曚綅锛氬厓
+ truckagePrice: number // 鎼繍璐癸紝鍗曚綅锛氬厓
+ status: number // 寮�鍚姸鎬�
+ defaultStatus: boolean // 鏄惁榛樿
+}
+
+// ERP 浠撳簱 API
+export const WarehouseApi = {
+ // 鏌ヨ浠撳簱鍒嗛〉
+ getWarehousePage: async (params: any) => {
+ return await request.get({ url: `/erp/warehouse/page`, params })
+ },
+
+ // 鏌ヨ浠撳簱绮剧畝鍒楄〃
+ getWarehouseSimpleList: async () => {
+ return await request.get({ url: `/erp/warehouse/simple-list` })
+ },
+
+ // 鏌ヨ浠撳簱璇︽儏
+ getWarehouse: async (id: number) => {
+ return await request.get({ url: `/erp/warehouse/get?id=` + id })
+ },
+
+ // 鏂板浠撳簱
+ createWarehouse: async (data: WarehouseVO) => {
+ return await request.post({ url: `/erp/warehouse/create`, data })
+ },
+
+ // 淇敼浠撳簱
+ updateWarehouse: async (data: WarehouseVO) => {
+ return await request.put({ url: `/erp/warehouse/update`, data })
+ },
+
+ // 淇敼浠撳簱榛樿鐘舵��
+ updateWarehouseDefaultStatus: async (id: number, defaultStatus: boolean) => {
+ return await request.put({
+ url: `/erp/warehouse/update-default-status`,
+ params: {
+ id,
+ defaultStatus
+ }
+ })
+ },
+
+ // 鍒犻櫎浠撳簱
+ deleteWarehouse: async (id: number) => {
+ return await request.delete({ url: `/erp/warehouse/delete?id=` + id })
+ },
+
+ // 瀵煎嚭浠撳簱 Excel
+ exportWarehouse: async (params) => {
+ return await request.download({ url: `/erp/warehouse/export-excel`, params })
+ }
+}
diff --git a/src/api/infra/apiAccessLog/index.ts b/src/api/infra/apiAccessLog/index.ts
new file mode 100644
index 0000000..4fa50e1
--- /dev/null
+++ b/src/api/infra/apiAccessLog/index.ts
@@ -0,0 +1,34 @@
+import request from '@/config/axios'
+
+export interface ApiAccessLogVO {
+ id: number
+ traceId: string
+ userId: number
+ userType: number
+ applicationName: string
+ requestMethod: string
+ requestParams: string
+ responseBody: string
+ requestUrl: string
+ userIp: string
+ userAgent: string
+ operateModule: string
+ operateName: string
+ operateType: number
+ beginTime: Date
+ endTime: Date
+ duration: number
+ resultCode: number
+ resultMsg: string
+ createTime: Date
+}
+
+// 鏌ヨ鍒楄〃API 璁块棶鏃ュ織
+export const getApiAccessLogPage = (params: PageParam) => {
+ return request.get({ url: '/infra/api-access-log/page', params })
+}
+
+// 瀵煎嚭API 璁块棶鏃ュ織
+export const exportApiAccessLog = (params) => {
+ return request.download({ url: '/infra/api-access-log/export-excel', params })
+}
diff --git a/src/api/infra/apiErrorLog/index.ts b/src/api/infra/apiErrorLog/index.ts
new file mode 100644
index 0000000..59ee214
--- /dev/null
+++ b/src/api/infra/apiErrorLog/index.ts
@@ -0,0 +1,48 @@
+import request from '@/config/axios'
+
+export interface ApiErrorLogVO {
+ id: number
+ traceId: string
+ userId: number
+ userType: number
+ applicationName: string
+ requestMethod: string
+ requestParams: string
+ requestUrl: string
+ userIp: string
+ userAgent: string
+ exceptionTime: Date
+ exceptionName: string
+ exceptionMessage: string
+ exceptionRootCauseMessage: string
+ exceptionStackTrace: string
+ exceptionClassName: string
+ exceptionFileName: string
+ exceptionMethodName: string
+ exceptionLineNumber: number
+ processUserId: number
+ processStatus: number
+ processTime: Date
+ resultCode: number
+ createTime: Date
+}
+
+// 鏌ヨ鍒楄〃API 璁块棶鏃ュ織
+export const getApiErrorLogPage = (params: PageParam) => {
+ return request.get({ url: '/infra/api-error-log/page', params })
+}
+
+// 鏇存柊 API 閿欒鏃ュ織鐨勫鐞嗙姸鎬�
+export const updateApiErrorLogPage = (id: number, processStatus: number) => {
+ return request.put({
+ url: '/infra/api-error-log/update-status?id=' + id + '&processStatus=' + processStatus
+ })
+}
+
+// 瀵煎嚭API 璁块棶鏃ュ織
+export const exportApiErrorLog = (params) => {
+ return request.download({
+ url: '/infra/api-error-log/export-excel',
+ params
+ })
+}
diff --git a/src/api/infra/codegen/index.ts b/src/api/infra/codegen/index.ts
new file mode 100644
index 0000000..b0e93cd
--- /dev/null
+++ b/src/api/infra/codegen/index.ts
@@ -0,0 +1,112 @@
+import request from '@/config/axios'
+
+export type CodegenTableVO = {
+ id: number
+ tableId: number
+ isParentMenuIdValid: boolean
+ dataSourceConfigId: number
+ scene: number
+ tableName: string
+ tableComment: string
+ remark: string
+ moduleName: string
+ businessName: string
+ className: string
+ classComment: string
+ author: string
+ createTime: Date
+ updateTime: Date
+ templateType: number
+ parentMenuId: number
+}
+
+export type CodegenColumnVO = {
+ id: number
+ tableId: number
+ columnName: string
+ dataType: string
+ columnComment: string
+ nullable: number
+ primaryKey: number
+ ordinalPosition: number
+ javaType: string
+ javaField: string
+ dictType: string
+ example: string
+ createOperation: number
+ updateOperation: number
+ listOperation: number
+ listOperationCondition: string
+ listOperationResult: number
+ htmlType: string
+}
+
+export type DatabaseTableVO = {
+ name: string
+ comment: string
+}
+
+export type CodegenPreviewVO = {
+ filePath: string
+ code: string
+}
+
+export type CodegenUpdateReqVO = {
+ table: CodegenTableVO | any
+ columns: CodegenColumnVO[]
+}
+
+// 鏌ヨ鍒楄〃浠g爜鐢熸垚琛ㄥ畾涔�
+export const getCodegenTableList = (dataSourceConfigId: number) => {
+ return request.get({ url: '/infra/codegen/table/list?dataSourceConfigId=' + dataSourceConfigId })
+}
+
+// 鏌ヨ鍒楄〃浠g爜鐢熸垚琛ㄥ畾涔�
+export const getCodegenTablePage = (params: PageParam) => {
+ return request.get({ url: '/infra/codegen/table/page', params })
+}
+
+// 鏌ヨ璇︽儏浠g爜鐢熸垚琛ㄥ畾涔�
+export const getCodegenTable = (id: number) => {
+ return request.get({ url: '/infra/codegen/detail?tableId=' + id })
+}
+
+// 淇敼浠g爜鐢熸垚琛ㄥ畾涔�
+export const updateCodegenTable = (data: CodegenUpdateReqVO) => {
+ return request.put({ url: '/infra/codegen/update', data })
+}
+
+// 鍩轰簬鏁版嵁搴撶殑琛ㄧ粨鏋勶紝鍚屾鏁版嵁搴撶殑琛ㄥ拰瀛楁瀹氫箟
+export const syncCodegenFromDB = (id: number) => {
+ return request.put({ url: '/infra/codegen/sync-from-db?tableId=' + id })
+}
+
+// 棰勮鐢熸垚浠g爜
+export const previewCodegen = (id: number) => {
+ return request.get({ url: '/infra/codegen/preview?tableId=' + id })
+}
+
+// 涓嬭浇鐢熸垚浠g爜
+export const downloadCodegen = (id: number) => {
+ return request.download({ url: '/infra/codegen/download?tableId=' + id })
+}
+
+// 鑾峰緱琛ㄥ畾涔�
+export const getSchemaTableList = (params) => {
+ return request.get({ url: '/infra/codegen/db/table/list', params })
+}
+
+// 鍩轰簬鏁版嵁搴撶殑琛ㄧ粨鏋勶紝鍒涘缓浠g爜鐢熸垚鍣ㄧ殑琛ㄥ畾涔�
+export const createCodegenList = (data) => {
+ return request.post({ url: '/infra/codegen/create-list', data })
+}
+
+// 鍒犻櫎浠g爜鐢熸垚琛ㄥ畾涔�
+export const deleteCodegenTable = (id: number) => {
+ return request.delete({ url: '/infra/codegen/delete?tableId=' + id })
+}
+
+// 鎵归噺鍒犻櫎浠g爜鐢熸垚琛ㄥ畾涔�
+export const deleteCodegenTableList = (ids: number[]) => {
+ return request.delete({ url: '/infra/codegen/delete-list', params: { tableIds: ids.join(',') } })
+}
diff --git a/src/api/infra/config/index.ts b/src/api/infra/config/index.ts
new file mode 100644
index 0000000..c78c2c0
--- /dev/null
+++ b/src/api/infra/config/index.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+export interface ConfigVO {
+ id: number | undefined
+ category: string
+ name: string
+ key: string
+ value: string
+ type: number
+ visible: boolean
+ remark: string
+ createTime: Date
+}
+
+// 鏌ヨ鍙傛暟鍒楄〃
+export const getConfigPage = (params: PageParam) => {
+ return request.get({ url: '/infra/config/page', params })
+}
+
+// 鏌ヨ鍙傛暟璇︽儏
+export const getConfig = (id: number) => {
+ return request.get({ url: '/infra/config/get?id=' + id })
+}
+
+// 鏍规嵁鍙傛暟閿悕鏌ヨ鍙傛暟鍊�
+export const getConfigKey = (configKey: string) => {
+ return request.get({ url: '/infra/config/get-value-by-key?key=' + configKey })
+}
+
+// 鏂板鍙傛暟
+export const createConfig = (data: ConfigVO) => {
+ return request.post({ url: '/infra/config/create', data })
+}
+
+// 淇敼鍙傛暟
+export const updateConfig = (data: ConfigVO) => {
+ return request.put({ url: '/infra/config/update', data })
+}
+
+// 鍒犻櫎鍙傛暟
+export const deleteConfig = (id: number) => {
+ return request.delete({ url: '/infra/config/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鍙傛暟
+export const deleteConfigList = (ids: number[]) => {
+ return request.delete({ url: '/infra/config/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭鍙傛暟
+export const exportConfig = (params) => {
+ return request.download({ url: '/infra/config/export-excel', params })
+}
diff --git a/src/api/infra/dataSourceConfig/index.ts b/src/api/infra/dataSourceConfig/index.ts
new file mode 100644
index 0000000..55bd6a3
--- /dev/null
+++ b/src/api/infra/dataSourceConfig/index.ts
@@ -0,0 +1,40 @@
+import request from '@/config/axios'
+
+export interface DataSourceConfigVO {
+ id: number | undefined
+ name: string
+ url: string
+ username: string
+ password: string
+ createTime?: Date
+}
+
+// 鏂板鏁版嵁婧愰厤缃�
+export const createDataSourceConfig = (data: DataSourceConfigVO) => {
+ return request.post({ url: '/infra/data-source-config/create', data })
+}
+
+// 淇敼鏁版嵁婧愰厤缃�
+export const updateDataSourceConfig = (data: DataSourceConfigVO) => {
+ return request.put({ url: '/infra/data-source-config/update', data })
+}
+
+// 鍒犻櫎鏁版嵁婧愰厤缃�
+export const deleteDataSourceConfig = (id: number) => {
+ return request.delete({ url: '/infra/data-source-config/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鏁版嵁婧愰厤缃�
+export const deleteDataSourceConfigList = (ids: number[]) => {
+ return request.delete({ url: '/infra/data-source-config/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 鏌ヨ鏁版嵁婧愰厤缃鎯�
+export const getDataSourceConfig = (id: number) => {
+ return request.get({ url: '/infra/data-source-config/get?id=' + id })
+}
+
+// 鏌ヨ鏁版嵁婧愰厤缃垪琛�
+export const getDataSourceConfigList = () => {
+ return request.get({ url: '/infra/data-source-config/list' })
+}
diff --git a/src/api/infra/demo/demo01/index.ts b/src/api/infra/demo/demo01/index.ts
new file mode 100644
index 0000000..982049c
--- /dev/null
+++ b/src/api/infra/demo/demo01/index.ts
@@ -0,0 +1,50 @@
+import request from '@/config/axios'
+import type { Dayjs } from 'dayjs'
+
+/** 绀轰緥鑱旂郴浜轰俊鎭� */
+export interface Demo01Contact {
+ id: number // 缂栧彿
+ name?: string // 鍚嶅瓧
+ sex?: number // 鎬у埆
+ birthday?: string | Dayjs // 鍑虹敓骞�
+ description?: string // 绠�浠�
+ avatar: string // 澶村儚
+}
+
+// 绀轰緥鑱旂郴浜� API
+export const Demo01ContactApi = {
+ // 鏌ヨ绀轰緥鑱旂郴浜哄垎椤�
+ getDemo01ContactPage: async (params: any) => {
+ return await request.get({ url: `/infra/demo01-contact/page`, params })
+ },
+
+ // 鏌ヨ绀轰緥鑱旂郴浜鸿鎯�
+ getDemo01Contact: async (id: number) => {
+ return await request.get({ url: `/infra/demo01-contact/get?id=` + id })
+ },
+
+ // 鏂板绀轰緥鑱旂郴浜�
+ createDemo01Contact: async (data: Demo01Contact) => {
+ return await request.post({ url: `/infra/demo01-contact/create`, data })
+ },
+
+ // 淇敼绀轰緥鑱旂郴浜�
+ updateDemo01Contact: async (data: Demo01Contact) => {
+ return await request.put({ url: `/infra/demo01-contact/update`, data })
+ },
+
+ // 鍒犻櫎绀轰緥鑱旂郴浜�
+ deleteDemo01Contact: async (id: number) => {
+ return await request.delete({ url: `/infra/demo01-contact/delete?id=` + id })
+ },
+
+ /** 鎵归噺鍒犻櫎绀轰緥鑱旂郴浜� */
+ deleteDemo01ContactList: async (ids: number[]) => {
+ return await request.delete({ url: `/infra/demo01-contact/delete-list?ids=${ids.join(',')}` })
+ },
+
+ // 瀵煎嚭绀轰緥鑱旂郴浜� Excel
+ exportDemo01Contact: async (params) => {
+ return await request.download({ url: `/infra/demo01-contact/export-excel`, params })
+ }
+}
diff --git a/src/api/infra/demo/demo02/index.ts b/src/api/infra/demo/demo02/index.ts
new file mode 100644
index 0000000..736a123
--- /dev/null
+++ b/src/api/infra/demo/demo02/index.ts
@@ -0,0 +1,37 @@
+import request from '@/config/axios'
+
+export interface Demo02CategoryVO {
+ id: number
+ name: string
+ parentId: number
+}
+
+// 鏌ヨ绀轰緥鍒嗙被鍒楄〃
+export const getDemo02CategoryList = async () => {
+ return await request.get({ url: `/infra/demo02-category/list` })
+}
+
+// 鏌ヨ绀轰緥鍒嗙被璇︽儏
+export const getDemo02Category = async (id: number) => {
+ return await request.get({ url: `/infra/demo02-category/get?id=` + id })
+}
+
+// 鏂板绀轰緥鍒嗙被
+export const createDemo02Category = async (data: Demo02CategoryVO) => {
+ return await request.post({ url: `/infra/demo02-category/create`, data })
+}
+
+// 淇敼绀轰緥鍒嗙被
+export const updateDemo02Category = async (data: Demo02CategoryVO) => {
+ return await request.put({ url: `/infra/demo02-category/update`, data })
+}
+
+// 鍒犻櫎绀轰緥鍒嗙被
+export const deleteDemo02Category = async (id: number) => {
+ return await request.delete({ url: `/infra/demo02-category/delete?id=` + id })
+}
+
+// 瀵煎嚭绀轰緥鍒嗙被 Excel
+export const exportDemo02Category = async (params) => {
+ return await request.download({ url: `/infra/demo02-category/export-excel`, params })
+}
diff --git a/src/api/infra/demo/demo03/erp/index.ts b/src/api/infra/demo/demo03/erp/index.ts
new file mode 100644
index 0000000..c641ed0
--- /dev/null
+++ b/src/api/infra/demo/demo03/erp/index.ts
@@ -0,0 +1,127 @@
+import request from '@/config/axios'
+import type { Dayjs } from 'dayjs';
+
+/** 瀛︾敓璇剧▼淇℃伅 */
+export interface Demo03Course {
+ id: number; // 缂栧彿
+ studentId?: number; // 瀛︾敓缂栧彿
+ name?: string; // 鍚嶅瓧
+ score?: number; // 鍒嗘暟
+}
+
+/** 瀛︾敓鐝骇淇℃伅 */
+export interface Demo03Grade {
+ id: number; // 缂栧彿
+ studentId?: number; // 瀛︾敓缂栧彿
+ name?: string; // 鍚嶅瓧
+ teacher?: string; // 鐝富浠�
+}
+
+/** 瀛︾敓淇℃伅 */
+export interface Demo03Student {
+ id: number; // 缂栧彿
+ name?: string; // 鍚嶅瓧
+ sex?: number; // 鎬у埆
+ birthday?: string | Dayjs; // 鍑虹敓鏃ユ湡
+ description?: string; // 绠�浠�
+}
+
+// 瀛︾敓 API
+export const Demo03StudentApi = {
+ // 鏌ヨ瀛︾敓鍒嗛〉
+ getDemo03StudentPage: async (params: any) => {
+ return await request.get({ url: `/infra/demo03-student-erp/page`, params })
+ },
+
+ // 鏌ヨ瀛︾敓璇︽儏
+ getDemo03Student: async (id: number) => {
+ return await request.get({ url: `/infra/demo03-student-erp/get?id=` + id })
+ },
+
+ // 鏂板瀛︾敓
+ createDemo03Student: async (data: Demo03Student) => {
+ return await request.post({ url: `/infra/demo03-student-erp/create`, data })
+ },
+
+ // 淇敼瀛︾敓
+ updateDemo03Student: async (data: Demo03Student) => {
+ return await request.put({ url: `/infra/demo03-student-erp/update`, data })
+ },
+
+ // 鍒犻櫎瀛︾敓
+ deleteDemo03Student: async (id: number) => {
+ return await request.delete({ url: `/infra/demo03-student-erp/delete?id=` + id })
+ },
+
+ /** 鎵归噺鍒犻櫎瀛︾敓 */
+ deleteDemo03StudentList: async (ids: number[]) => {
+ return await request.delete({ url: `/infra/demo03-student-erp/delete-list?ids=${ids.join(',')}` })
+ },
+
+ // 瀵煎嚭瀛︾敓 Excel
+ exportDemo03Student: async (params) => {
+ return await request.download({ url: `/infra/demo03-student-erp/export-excel`, params })
+ },
+
+// ==================== 瀛愯〃锛堝鐢熻绋嬶級 ====================
+
+ // 鑾峰緱瀛︾敓璇剧▼鍒嗛〉
+ getDemo03CoursePage: async (params) => {
+ return await request.get({ url: `/infra/demo03-student-erp/demo03-course/page`, params })
+ },
+ // 鏂板瀛︾敓璇剧▼
+ createDemo03Course: async (data: Demo03Course) => {
+ return await request.post({ url: `/infra/demo03-student-erp/demo03-course/create`, data })
+ },
+
+ // 淇敼瀛︾敓璇剧▼
+ updateDemo03Course: async (data: Demo03Course) => {
+ return await request.put({ url: `/infra/demo03-student-erp/demo03-course/update`, data })
+ },
+
+ // 鍒犻櫎瀛︾敓璇剧▼
+ deleteDemo03Course: async (id: number) => {
+ return await request.delete({ url: `/infra/demo03-student-erp/demo03-course/delete?id=` + id })
+ },
+
+ /** 鎵归噺鍒犻櫎瀛︾敓璇剧▼ */
+ deleteDemo03CourseList: async (ids: number[]) => {
+ return await request.delete({ url: `/infra/demo03-student-erp/demo03-course/delete-list?ids=${ids.join(',')}` })
+ },
+
+ // 鑾峰緱瀛︾敓璇剧▼
+ getDemo03Course: async (id: number) => {
+ return await request.get({ url: `/infra/demo03-student-erp/demo03-course/get?id=` + id })
+ },
+
+// ==================== 瀛愯〃锛堝鐢熺彮绾э級 ====================
+
+ // 鑾峰緱瀛︾敓鐝骇鍒嗛〉
+ getDemo03GradePage: async (params) => {
+ return await request.get({ url: `/infra/demo03-student-erp/demo03-grade/page`, params })
+ },
+ // 鏂板瀛︾敓鐝骇
+ createDemo03Grade: async (data: Demo03Grade) => {
+ return await request.post({ url: `/infra/demo03-student-erp/demo03-grade/create`, data })
+ },
+
+ // 淇敼瀛︾敓鐝骇
+ updateDemo03Grade: async (data: Demo03Grade) => {
+ return await request.put({ url: `/infra/demo03-student-erp/demo03-grade/update`, data })
+ },
+
+ // 鍒犻櫎瀛︾敓鐝骇
+ deleteDemo03Grade: async (id: number) => {
+ return await request.delete({ url: `/infra/demo03-student-erp/demo03-grade/delete?id=` + id })
+ },
+
+ /** 鎵归噺鍒犻櫎瀛︾敓鐝骇 */
+ deleteDemo03GradeList: async (ids: number[]) => {
+ return await request.delete({ url: `/infra/demo03-student-erp/demo03-grade/delete-list?ids=${ids.join(',')}` })
+ },
+
+ // 鑾峰緱瀛︾敓鐝骇
+ getDemo03Grade: async (id: number) => {
+ return await request.get({ url: `/infra/demo03-student-erp/demo03-grade/get?id=` + id })
+ },
+}
diff --git a/src/api/infra/demo/demo03/inner/index.ts b/src/api/infra/demo/demo03/inner/index.ts
new file mode 100644
index 0000000..1000d9c
--- /dev/null
+++ b/src/api/infra/demo/demo03/inner/index.ts
@@ -0,0 +1,81 @@
+import request from '@/config/axios'
+import type { Dayjs } from 'dayjs';
+
+/** 瀛︾敓璇剧▼淇℃伅 */
+export interface Demo03Course {
+ id: number; // 缂栧彿
+ studentId?: number; // 瀛︾敓缂栧彿
+ name?: string; // 鍚嶅瓧
+ score?: number; // 鍒嗘暟
+}
+
+/** 瀛︾敓鐝骇淇℃伅 */
+export interface Demo03Grade {
+ id: number; // 缂栧彿
+ studentId?: number; // 瀛︾敓缂栧彿
+ name?: string; // 鍚嶅瓧
+ teacher?: string; // 鐝富浠�
+}
+
+/** 瀛︾敓淇℃伅 */
+export interface Demo03Student {
+ id: number; // 缂栧彿
+ name?: string; // 鍚嶅瓧
+ sex?: number; // 鎬у埆
+ birthday?: string | Dayjs; // 鍑虹敓鏃ユ湡
+ description?: string; // 绠�浠�
+ demo03courses?: Demo03Course[]
+ demo03grade?: Demo03Grade
+}
+
+// 瀛︾敓 API
+export const Demo03StudentApi = {
+ // 鏌ヨ瀛︾敓鍒嗛〉
+ getDemo03StudentPage: async (params: any) => {
+ return await request.get({ url: `/infra/demo03-student-inner/page`, params })
+ },
+
+ // 鏌ヨ瀛︾敓璇︽儏
+ getDemo03Student: async (id: number) => {
+ return await request.get({ url: `/infra/demo03-student-inner/get?id=` + id })
+ },
+
+ // 鏂板瀛︾敓
+ createDemo03Student: async (data: Demo03Student) => {
+ return await request.post({ url: `/infra/demo03-student-inner/create`, data })
+ },
+
+ // 淇敼瀛︾敓
+ updateDemo03Student: async (data: Demo03Student) => {
+ return await request.put({ url: `/infra/demo03-student-inner/update`, data })
+ },
+
+ // 鍒犻櫎瀛︾敓
+ deleteDemo03Student: async (id: number) => {
+ return await request.delete({ url: `/infra/demo03-student-inner/delete?id=` + id })
+ },
+
+ /** 鎵归噺鍒犻櫎瀛︾敓 */
+ deleteDemo03StudentList: async (ids: number[]) => {
+ return await request.delete({ url: `/infra/demo03-student-inner/delete-list?ids=${ids.join(',')}` })
+ },
+
+ // 瀵煎嚭瀛︾敓 Excel
+ exportDemo03Student: async (params) => {
+ return await request.download({ url: `/infra/demo03-student-inner/export-excel`, params })
+ },
+
+// ==================== 瀛愯〃锛堝鐢熻绋嬶級 ====================
+
+ // 鑾峰緱瀛︾敓璇剧▼鍒楄〃
+ getDemo03CourseListByStudentId: async (studentId) => {
+ return await request.get({ url: `/infra/demo03-student-inner/demo03-course/list-by-student-id?studentId=` + studentId })
+ },
+
+// ==================== 瀛愯〃锛堝鐢熺彮绾э級 ====================
+
+ // 鑾峰緱瀛︾敓鐝骇
+ getDemo03GradeByStudentId: async (studentId) => {
+ return await request.get({ url: `/infra/demo03-student-inner/demo03-grade/get-by-student-id?studentId=` + studentId })
+ },
+}
diff --git a/src/api/infra/demo/demo03/normal/index.ts b/src/api/infra/demo/demo03/normal/index.ts
new file mode 100644
index 0000000..56a824d
--- /dev/null
+++ b/src/api/infra/demo/demo03/normal/index.ts
@@ -0,0 +1,81 @@
+import request from '@/config/axios'
+import type { Dayjs } from 'dayjs';
+
+/** 瀛︾敓璇剧▼淇℃伅 */
+export interface Demo03Course {
+ id: number; // 缂栧彿
+ studentId?: number; // 瀛︾敓缂栧彿
+ name?: string; // 鍚嶅瓧
+ score?: number; // 鍒嗘暟
+}
+
+/** 瀛︾敓鐝骇淇℃伅 */
+export interface Demo03Grade {
+ id: number; // 缂栧彿
+ studentId?: number; // 瀛︾敓缂栧彿
+ name?: string; // 鍚嶅瓧
+ teacher?: string; // 鐝富浠�
+}
+
+/** 瀛︾敓淇℃伅 */
+export interface Demo03Student {
+ id: number; // 缂栧彿
+ name?: string; // 鍚嶅瓧
+ sex?: number; // 鎬у埆
+ birthday?: string | Dayjs; // 鍑虹敓鏃ユ湡
+ description?: string; // 绠�浠�
+ demo03courses?: Demo03Course[]
+ demo03grade?: Demo03Grade
+}
+
+// 瀛︾敓 API
+export const Demo03StudentApi = {
+ // 鏌ヨ瀛︾敓鍒嗛〉
+ getDemo03StudentPage: async (params: any) => {
+ return await request.get({ url: `/infra/demo03-student-normal/page`, params })
+ },
+
+ // 鏌ヨ瀛︾敓璇︽儏
+ getDemo03Student: async (id: number) => {
+ return await request.get({ url: `/infra/demo03-student-normal/get?id=` + id })
+ },
+
+ // 鏂板瀛︾敓
+ createDemo03Student: async (data: Demo03Student) => {
+ return await request.post({ url: `/infra/demo03-student-normal/create`, data })
+ },
+
+ // 淇敼瀛︾敓
+ updateDemo03Student: async (data: Demo03Student) => {
+ return await request.put({ url: `/infra/demo03-student-normal/update`, data })
+ },
+
+ // 鍒犻櫎瀛︾敓
+ deleteDemo03Student: async (id: number) => {
+ return await request.delete({ url: `/infra/demo03-student-normal/delete?id=` + id })
+ },
+
+ /** 鎵归噺鍒犻櫎瀛︾敓 */
+ deleteDemo03StudentList: async (ids: number[]) => {
+ return await request.delete({ url: `/infra/demo03-student-normal/delete-list?ids=${ids.join(',')}` })
+ },
+
+ // 瀵煎嚭瀛︾敓 Excel
+ exportDemo03Student: async (params) => {
+ return await request.download({ url: `/infra/demo03-student-normal/export-excel`, params })
+ },
+
+// ==================== 瀛愯〃锛堝鐢熻绋嬶級 ====================
+
+ // 鑾峰緱瀛︾敓璇剧▼鍒楄〃
+ getDemo03CourseListByStudentId: async (studentId) => {
+ return await request.get({ url: `/infra/demo03-student-normal/demo03-course/list-by-student-id?studentId=` + studentId })
+ },
+
+// ==================== 瀛愯〃锛堝鐢熺彮绾э級 ====================
+
+ // 鑾峰緱瀛︾敓鐝骇
+ getDemo03GradeByStudentId: async (studentId) => {
+ return await request.get({ url: `/infra/demo03-student-normal/demo03-grade/get-by-student-id?studentId=` + studentId })
+ },
+}
diff --git a/src/api/infra/file/index.ts b/src/api/infra/file/index.ts
new file mode 100644
index 0000000..01009f2
--- /dev/null
+++ b/src/api/infra/file/index.ts
@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+// 鏂囦欢棰勭鍚嶅湴鍧� Response VO
+export interface FilePresignedUrlRespVO {
+ // 鏂囦欢閰嶇疆缂栧彿
+ configId: number
+ // 鏂囦欢涓婁紶 URL
+ uploadUrl: string
+ // 鏂囦欢 URL
+ url: string
+ // 鏂囦欢璺緞
+ path: string
+}
+
+// 鏌ヨ鏂囦欢鍒楄〃
+export const getFilePage = (params: PageParam) => {
+ return request.get({ url: '/infra/file/page', params })
+}
+
+// 鍒犻櫎鏂囦欢
+export const deleteFile = (id: number) => {
+ return request.delete({ url: '/infra/file/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鏂囦欢
+export const deleteFileList = (ids: number[]) => {
+ return request.delete({ url: '/infra/file/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 鑾峰彇鏂囦欢棰勭鍚嶅湴鍧�
+export const getFilePresignedUrl = (name: string, directory?: string) => {
+ return request.get<FilePresignedUrlRespVO>({
+ url: '/infra/file/presigned-url',
+ params: { name, directory }
+ })
+}
+
+// 鍒涘缓鏂囦欢
+export const createFile = (data: any) => {
+ return request.post({ url: '/infra/file/create', data })
+}
+
+// 涓婁紶鏂囦欢
+export const updateFile = (data: any, onUploadProgress?: Function) => {
+ return request.upload({ url: '/infra/file/upload', data, onUploadProgress })
+}
diff --git a/src/api/infra/fileConfig/index.ts b/src/api/infra/fileConfig/index.ts
new file mode 100644
index 0000000..cd1dbe6
--- /dev/null
+++ b/src/api/infra/fileConfig/index.ts
@@ -0,0 +1,69 @@
+import request from '@/config/axios'
+
+export interface FileClientConfig {
+ basePath: string
+ host?: string
+ port?: number
+ username?: string
+ password?: string
+ mode?: string
+ endpoint?: string
+ bucket?: string
+ accessKey?: string
+ accessSecret?: string
+ enablePathStyleAccess?: boolean
+ enablePublicAccess?: boolean
+ region?: string
+ domain: string
+}
+
+export interface FileConfigVO {
+ id: number
+ name: string
+ storage?: number
+ master: boolean
+ visible: boolean
+ config: FileClientConfig
+ remark: string
+ createTime: Date
+}
+
+// 鏌ヨ鏂囦欢閰嶇疆鍒楄〃
+export const getFileConfigPage = (params: PageParam) => {
+ return request.get({ url: '/infra/file-config/page', params })
+}
+
+// 鏌ヨ鏂囦欢閰嶇疆璇︽儏
+export const getFileConfig = (id: number) => {
+ return request.get({ url: '/infra/file-config/get?id=' + id })
+}
+
+// 鏇存柊鏂囦欢閰嶇疆涓轰富閰嶇疆
+export const updateFileConfigMaster = (id: number) => {
+ return request.put({ url: '/infra/file-config/update-master?id=' + id })
+}
+
+// 鏂板鏂囦欢閰嶇疆
+export const createFileConfig = (data: FileConfigVO) => {
+ return request.post({ url: '/infra/file-config/create', data })
+}
+
+// 淇敼鏂囦欢閰嶇疆
+export const updateFileConfig = (data: FileConfigVO) => {
+ return request.put({ url: '/infra/file-config/update', data })
+}
+
+// 鍒犻櫎鏂囦欢閰嶇疆
+export const deleteFileConfig = (id: number) => {
+ return request.delete({ url: '/infra/file-config/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鏂囦欢閰嶇疆
+export const deleteFileConfigList = (ids: number[]) => {
+ return request.delete({ url: '/infra/file-config/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 娴嬭瘯鏂囦欢閰嶇疆
+export const testFileConfig = (id: number) => {
+ return request.get({ url: '/infra/file-config/test?id=' + id })
+}
diff --git a/src/api/infra/job/index.ts b/src/api/infra/job/index.ts
new file mode 100644
index 0000000..ce05c54
--- /dev/null
+++ b/src/api/infra/job/index.ts
@@ -0,0 +1,68 @@
+import request from '@/config/axios'
+
+export interface JobVO {
+ id: number
+ name: string
+ status: number
+ handlerName: string
+ handlerParam: string
+ cronExpression: string
+ retryCount: number
+ retryInterval: number
+ monitorTimeout: number
+ createTime: Date
+}
+
+// 浠诲姟鍒楄〃
+export const getJobPage = (params: PageParam) => {
+ return request.get({ url: '/infra/job/page', params })
+}
+
+// 浠诲姟璇︽儏
+export const getJob = (id: number) => {
+ return request.get({ url: '/infra/job/get?id=' + id })
+}
+
+// 鏂板浠诲姟
+export const createJob = (data: JobVO) => {
+ return request.post({ url: '/infra/job/create', data })
+}
+
+// 淇敼瀹氭椂浠诲姟璋冨害
+export const updateJob = (data: JobVO) => {
+ return request.put({ url: '/infra/job/update', data })
+}
+
+// 鍒犻櫎瀹氭椂浠诲姟璋冨害
+export const deleteJob = (id: number) => {
+ return request.delete({ url: '/infra/job/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎瀹氭椂浠诲姟璋冨害
+export const deleteJobList = (ids: number[]) => {
+ return request.delete({ url: '/infra/job/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭瀹氭椂浠诲姟璋冨害
+export const exportJob = (params) => {
+ return request.download({ url: '/infra/job/export-excel', params })
+}
+
+// 浠诲姟鐘舵�佷慨鏀�
+export const updateJobStatus = (id: number, status: number) => {
+ const params = {
+ id,
+ status
+ }
+ return request.put({ url: '/infra/job/update-status', params })
+}
+
+// 瀹氭椂浠诲姟绔嬪嵆鎵ц涓�娆�
+export const runJob = (id: number) => {
+ return request.put({ url: '/infra/job/trigger?id=' + id })
+}
+
+// 鑾峰緱瀹氭椂浠诲姟鐨勪笅 n 娆℃墽琛屾椂闂�
+export const getJobNextTimes = (id: number) => {
+ return request.get({ url: '/infra/job/get_next_times?id=' + id })
+}
diff --git a/src/api/infra/jobLog/index.ts b/src/api/infra/jobLog/index.ts
new file mode 100644
index 0000000..ed54761
--- /dev/null
+++ b/src/api/infra/jobLog/index.ts
@@ -0,0 +1,34 @@
+import request from '@/config/axios'
+
+export interface JobLogVO {
+ id: number
+ jobId: number
+ handlerName: string
+ handlerParam: string
+ cronExpression: string
+ executeIndex: string
+ beginTime: Date
+ endTime: Date
+ duration: string
+ status: number
+ createTime: string
+ result: string
+}
+
+// 浠诲姟鏃ュ織鍒楄〃
+export const getJobLogPage = (params: PageParam) => {
+ return request.get({ url: '/infra/job-log/page', params })
+}
+
+// 浠诲姟鏃ュ織璇︽儏
+export const getJobLog = (id: number) => {
+ return request.get({ url: '/infra/job-log/get?id=' + id })
+}
+
+// 瀵煎嚭瀹氭椂浠诲姟鏃ュ織
+export const exportJobLog = (params) => {
+ return request.download({
+ url: '/infra/job-log/export-excel',
+ params
+ })
+}
diff --git a/src/api/infra/redis/index.ts b/src/api/infra/redis/index.ts
new file mode 100644
index 0000000..f27be77
--- /dev/null
+++ b/src/api/infra/redis/index.ts
@@ -0,0 +1,8 @@
+import request from '@/config/axios'
+
+/**
+ * 鑾峰彇redis 鐩戞帶淇℃伅
+ */
+export const getCache = () => {
+ return request.get({ url: '/infra/redis/get-monitor-info' })
+}
diff --git a/src/api/infra/redis/types.ts b/src/api/infra/redis/types.ts
new file mode 100644
index 0000000..548bfe9
--- /dev/null
+++ b/src/api/infra/redis/types.ts
@@ -0,0 +1,176 @@
+export interface RedisMonitorInfoVO {
+ info: RedisInfoVO
+ dbSize: number
+ commandStats: RedisCommandStatsVO[]
+}
+
+export interface RedisInfoVO {
+ io_threaded_reads_processed: string
+ tracking_clients: string
+ uptime_in_seconds: string
+ cluster_connections: string
+ current_cow_size: string
+ maxmemory_human: string
+ aof_last_cow_size: string
+ master_replid2: string
+ mem_replication_backlog: string
+ aof_rewrite_scheduled: string
+ total_net_input_bytes: string
+ rss_overhead_ratio: string
+ hz: string
+ current_cow_size_age: string
+ redis_build_id: string
+ errorstat_BUSYGROUP: string
+ aof_last_bgrewrite_status: string
+ multiplexing_api: string
+ client_recent_max_output_buffer: string
+ allocator_resident: string
+ mem_fragmentation_bytes: string
+ aof_current_size: string
+ repl_backlog_first_byte_offset: string
+ tracking_total_prefixes: string
+ redis_mode: string
+ redis_git_dirty: string
+ aof_delayed_fsync: string
+ allocator_rss_bytes: string
+ repl_backlog_histlen: string
+ io_threads_active: string
+ rss_overhead_bytes: string
+ total_system_memory: string
+ loading: string
+ evicted_keys: string
+ maxclients: string
+ cluster_enabled: string
+ redis_version: string
+ repl_backlog_active: string
+ mem_aof_buffer: string
+ allocator_frag_bytes: string
+ io_threaded_writes_processed: string
+ instantaneous_ops_per_sec: string
+ used_memory_human: string
+ total_error_replies: string
+ role: string
+ maxmemory: string
+ used_memory_lua: string
+ rdb_current_bgsave_time_sec: string
+ used_memory_startup: string
+ used_cpu_sys_main_thread: string
+ lazyfree_pending_objects: string
+ aof_pending_bio_fsync: string
+ used_memory_dataset_perc: string
+ allocator_frag_ratio: string
+ arch_bits: string
+ used_cpu_user_main_thread: string
+ mem_clients_normal: string
+ expired_time_cap_reached_count: string
+ unexpected_error_replies: string
+ mem_fragmentation_ratio: string
+ aof_last_rewrite_time_sec: string
+ master_replid: string
+ aof_rewrite_in_progress: string
+ lru_clock: string
+ maxmemory_policy: string
+ run_id: string
+ latest_fork_usec: string
+ tracking_total_items: string
+ total_commands_processed: string
+ expired_keys: string
+ errorstat_ERR: string
+ used_memory: string
+ module_fork_in_progress: string
+ errorstat_WRONGPASS: string
+ aof_buffer_length: string
+ dump_payload_sanitizations: string
+ mem_clients_slaves: string
+ keyspace_misses: string
+ server_time_usec: string
+ executable: string
+ lazyfreed_objects: string
+ db0: string
+ used_memory_peak_human: string
+ keyspace_hits: string
+ rdb_last_cow_size: string
+ aof_pending_rewrite: string
+ used_memory_overhead: string
+ active_defrag_hits: string
+ tcp_port: string
+ uptime_in_days: string
+ used_memory_peak_perc: string
+ current_save_keys_processed: string
+ blocked_clients: string
+ total_reads_processed: string
+ expire_cycle_cpu_milliseconds: string
+ sync_partial_err: string
+ used_memory_scripts_human: string
+ aof_current_rewrite_time_sec: string
+ aof_enabled: string
+ process_supervised: string
+ master_repl_offset: string
+ used_memory_dataset: string
+ used_cpu_user: string
+ rdb_last_bgsave_status: string
+ tracking_total_keys: string
+ atomicvar_api: string
+ allocator_rss_ratio: string
+ client_recent_max_input_buffer: string
+ clients_in_timeout_table: string
+ aof_last_write_status: string
+ mem_allocator: string
+ used_memory_scripts: string
+ used_memory_peak: string
+ process_id: string
+ master_failover_state: string
+ errorstat_NOAUTH: string
+ used_cpu_sys: string
+ repl_backlog_size: string
+ connected_slaves: string
+ current_save_keys_total: string
+ gcc_version: string
+ total_system_memory_human: string
+ sync_full: string
+ connected_clients: string
+ module_fork_last_cow_size: string
+ total_writes_processed: string
+ allocator_active: string
+ total_net_output_bytes: string
+ pubsub_channels: string
+ current_fork_perc: string
+ active_defrag_key_hits: string
+ rdb_changes_since_last_save: string
+ instantaneous_input_kbps: string
+ used_memory_rss_human: string
+ configured_hz: string
+ expired_stale_perc: string
+ active_defrag_misses: string
+ used_cpu_sys_children: string
+ number_of_cached_scripts: string
+ sync_partial_ok: string
+ used_memory_lua_human: string
+ rdb_last_save_time: string
+ pubsub_patterns: string
+ slave_expires_tracked_keys: string
+ redis_git_sha1: string
+ used_memory_rss: string
+ rdb_last_bgsave_time_sec: string
+ os: string
+ mem_not_counted_for_evict: string
+ active_defrag_running: string
+ rejected_connections: string
+ aof_rewrite_buffer_length: string
+ total_forks: string
+ active_defrag_key_misses: string
+ allocator_allocated: string
+ aof_base_size: string
+ instantaneous_output_kbps: string
+ second_repl_offset: string
+ rdb_bgsave_in_progress: string
+ used_cpu_user_children: string
+ total_connections_received: string
+ migrate_cached_sockets: string
+}
+
+export interface RedisCommandStatsVO {
+ command: string
+ calls: number
+ usec: number
+}
diff --git a/src/api/iot/alert/config/index.ts b/src/api/iot/alert/config/index.ts
new file mode 100644
index 0000000..e3ddc2a
--- /dev/null
+++ b/src/api/iot/alert/config/index.ts
@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+/** IoT 鍛婅閰嶇疆淇℃伅 */
+export interface AlertConfig {
+ id: number // 閰嶇疆缂栧彿
+ name?: string // 閰嶇疆鍚嶇О
+ description: string // 閰嶇疆鎻忚堪
+ level?: number // 鍛婅绾у埆
+ status?: number // 閰嶇疆鐘舵��
+ sceneRuleIds: string // 鍏宠仈鐨勫満鏅仈鍔ㄨ鍒欑紪鍙锋暟缁�
+ receiveUserIds: string // 鎺ユ敹鐨勭敤鎴风紪鍙锋暟缁�
+ receiveTypes: string // 鎺ユ敹鐨勭被鍨嬫暟缁�
+}
+
+// IoT 鍛婅閰嶇疆 API
+export const AlertConfigApi = {
+ // 鏌ヨ鍛婅閰嶇疆鍒嗛〉
+ getAlertConfigPage: async (params: any) => {
+ return await request.get({ url: `/iot/alert-config/page`, params })
+ },
+
+ // 鏌ヨ鍛婅閰嶇疆璇︽儏
+ getAlertConfig: async (id: number) => {
+ return await request.get({ url: `/iot/alert-config/get?id=` + id })
+ },
+
+ // 鏂板鍛婅閰嶇疆
+ createAlertConfig: async (data: AlertConfig) => {
+ return await request.post({ url: `/iot/alert-config/create`, data })
+ },
+
+ // 淇敼鍛婅閰嶇疆
+ updateAlertConfig: async (data: AlertConfig) => {
+ return await request.put({ url: `/iot/alert-config/update`, data })
+ },
+
+ // 鍒犻櫎鍛婅閰嶇疆
+ deleteAlertConfig: async (id: number) => {
+ return await request.delete({ url: `/iot/alert-config/delete?id=` + id })
+ },
+
+ // 鑾峰彇鍛婅閰嶇疆绠�鍗曞垪琛�
+ getSimpleAlertConfigList: async () => {
+ return await request.get({ url: `/iot/alert-config/simple-list` })
+ }
+}
diff --git a/src/api/iot/alert/record/index.ts b/src/api/iot/alert/record/index.ts
new file mode 100644
index 0000000..b124a9c
--- /dev/null
+++ b/src/api/iot/alert/record/index.ts
@@ -0,0 +1,35 @@
+import request from '@/config/axios'
+
+/** IoT 鍛婅璁板綍淇℃伅 */
+export interface AlertRecord {
+ id: number // 璁板綍缂栧彿
+ configId: number // 鍛婅閰嶇疆缂栧彿
+ configName: string // 鍛婅鍚嶇О
+ configLevel: number // 鍛婅绾у埆
+ productId: number // 浜у搧缂栧彿
+ deviceId: number // 璁惧缂栧彿
+ deviceMessage: any // 瑙﹀彂鐨勮澶囨秷鎭�
+ processStatus?: boolean // 鏄惁澶勭悊
+ processRemark: string // 澶勭悊缁撴灉锛堝娉級
+}
+
+// IoT 鍛婅璁板綍 API
+export const AlertRecordApi = {
+ // 鏌ヨ鍛婅璁板綍鍒嗛〉
+ getAlertRecordPage: async (params: any) => {
+ return await request.get({ url: `/iot/alert-record/page`, params })
+ },
+
+ // 鏌ヨ鍛婅璁板綍璇︽儏
+ getAlertRecord: async (id: number) => {
+ return await request.get({ url: `/iot/alert-record/get?id=` + id })
+ },
+
+ // 澶勭悊鍛婅璁板綍
+ processAlertRecord: async (id: number, processRemark: string) => {
+ return await request.put({
+ url: `/iot/alert-record/process`,
+ data: { id, processRemark }
+ })
+ }
+}
diff --git a/src/api/iot/device/device/index.ts b/src/api/iot/device/device/index.ts
new file mode 100644
index 0000000..2311a9a
--- /dev/null
+++ b/src/api/iot/device/device/index.ts
@@ -0,0 +1,165 @@
+import request from '@/config/axios'
+
+// IoT 璁惧 VO
+export interface DeviceVO {
+ id: number // 璁惧 ID锛屼富閿紝鑷
+ deviceName: string // 璁惧鍚嶇О
+ productId: number // 浜у搧缂栧彿
+ productKey: string // 浜у搧鏍囪瘑
+ deviceType: number // 璁惧绫诲瀷
+ nickname: string // 璁惧澶囨敞鍚嶇О
+ gatewayId: number // 缃戝叧璁惧 ID
+ state: number // 璁惧鐘舵��
+ onlineTime: Date // 鏈�鍚庝笂绾挎椂闂�
+ offlineTime: Date // 鏈�鍚庣绾挎椂闂�
+ activeTime: Date // 璁惧婵�娲绘椂闂�
+ createTime: Date // 鍒涘缓鏃堕棿
+ ip: string // 璁惧鐨� IP 鍦板潃
+ firmwareVersion: string // 璁惧鐨勫浐浠剁増鏈�
+ deviceSecret: string // 璁惧瀵嗛挜锛岀敤浜庤澶囪璇侊紝闇�瀹夊叏瀛樺偍
+ mqttClientId: string // MQTT 瀹㈡埛绔� ID
+ mqttUsername: string // MQTT 鐢ㄦ埛鍚�
+ mqttPassword: string // MQTT 瀵嗙爜
+ authType: string // 璁よ瘉绫诲瀷
+ locationType: number // 瀹氫綅绫诲瀷
+ latitude?: number // 璁惧浣嶇疆鐨勭含搴�
+ longitude?: number // 璁惧浣嶇疆鐨勭粡搴�
+ areaId: number // 鍦板尯缂栫爜
+ address: string // 璁惧璇︾粏鍦板潃
+ serialNumber: string // 璁惧搴忓垪鍙�
+ config: string // 璁惧閰嶇疆
+ groupIds?: number[] // 娣诲姞鍒嗙粍 ID
+}
+
+// IoT 璁惧灞炴�ц缁� VO
+export interface IotDevicePropertyDetailRespVO {
+ identifier: string // 灞炴�ф爣璇嗙
+ value: string // 鏈�鏂板��
+ updateTime: Date // 鏇存柊鏃堕棿
+ name: string // 灞炴�у悕绉�
+ dataType: string // 鏁版嵁绫诲瀷
+ dataSpecs: any // 鏁版嵁瀹氫箟
+ dataSpecsList: any[] // 鏁版嵁瀹氫箟鍒楄〃
+}
+
+// IoT 璁惧灞炴�� VO
+export interface IotDevicePropertyRespVO {
+ identifier: string // 灞炴�ф爣璇嗙
+ value: string // 鏈�鏂板��
+ updateTime: Date // 鏇存柊鏃堕棿
+}
+
+// TODO @鑺嬭壙锛氳皟鏁村埌 constants
+// IoT 璁惧鐘舵�佹灇涓�
+export enum DeviceStateEnum {
+ INACTIVE = 0, // 鏈縺娲�
+ ONLINE = 1, // 鍦ㄧ嚎
+ OFFLINE = 2 // 绂荤嚎
+}
+
+// 璁惧璁よ瘉鍙傛暟 VO
+export interface IotDeviceAuthInfoVO {
+ clientId: string // 瀹㈡埛绔� ID
+ username: string // 鐢ㄦ埛鍚�
+ password: string // 瀵嗙爜
+}
+
+// IoT 璁惧鍙戦�佹秷鎭� Request VO
+export interface IotDeviceMessageSendReqVO {
+ deviceId: number // 璁惧缂栧彿
+ method: string // 璇锋眰鏂规硶
+ params?: any // 璇锋眰鍙傛暟
+}
+
+// 璁惧 API
+export const DeviceApi = {
+ // 鏌ヨ璁惧鍒嗛〉
+ getDevicePage: async (params: any) => {
+ return await request.get({ url: `/iot/device/page`, params })
+ },
+
+ // 鏌ヨ璁惧璇︽儏
+ getDevice: async (id: number) => {
+ return await request.get({ url: `/iot/device/get?id=` + id })
+ },
+
+ // 鏂板璁惧
+ createDevice: async (data: DeviceVO) => {
+ return await request.post({ url: `/iot/device/create`, data })
+ },
+
+ // 淇敼璁惧
+ updateDevice: async (data: DeviceVO) => {
+ return await request.put({ url: `/iot/device/update`, data })
+ },
+
+ // 淇敼璁惧鍒嗙粍
+ updateDeviceGroup: async (data: { ids: number[]; groupIds: number[] }) => {
+ return await request.put({ url: `/iot/device/update-group`, data })
+ },
+
+ // 鍒犻櫎鍗曚釜璁惧
+ deleteDevice: async (id: number) => {
+ return await request.delete({ url: `/iot/device/delete?id=` + id })
+ },
+
+ // 鍒犻櫎澶氫釜璁惧
+ deleteDeviceList: async (ids: number[]) => {
+ return await request.delete({ url: `/iot/device/delete-list`, params: { ids: ids.join(',') } })
+ },
+
+ // 瀵煎嚭璁惧
+ exportDeviceExcel: async (params: any) => {
+ return await request.download({ url: `/iot/device/export-excel`, params })
+ },
+
+ // 鑾峰彇璁惧鏁伴噺
+ getDeviceCount: async (productId: number) => {
+ return await request.get({ url: `/iot/device/count?productId=` + productId })
+ },
+
+ // 鑾峰彇璁惧鐨勭簿绠�淇℃伅鍒楄〃
+ getSimpleDeviceList: async (deviceType?: number, productId?: number) => {
+ return await request.get({ url: `/iot/device/simple-list?`, params: { deviceType, productId } })
+ },
+
+ // 鏍规嵁浜у搧缂栧彿锛岃幏鍙栬澶囩殑绮剧畝淇℃伅鍒楄〃
+ getDeviceListByProductId: async (productId: number) => {
+ return await request.get({ url: `/iot/device/simple-list?`, params: { productId } })
+ },
+
+ // 鑾峰彇瀵煎叆妯℃澘
+ importDeviceTemplate: async () => {
+ return await request.download({ url: `/iot/device/get-import-template` })
+ },
+
+ // 鑾峰彇璁惧灞炴�ф渶鏂版暟鎹�
+ getLatestDeviceProperties: async (params: any) => {
+ return await request.get({ url: `/iot/device/property/get-latest`, params })
+ },
+
+ // 鑾峰彇璁惧灞炴�у巻鍙叉暟鎹�
+ getHistoryDevicePropertyList: async (params: any) => {
+ return await request.get({ url: `/iot/device/property/history-list`, params })
+ },
+
+ // 鑾峰彇璁惧璁よ瘉淇℃伅
+ getDeviceAuthInfo: async (id: number) => {
+ return await request.get({ url: `/iot/device/get-auth-info`, params: { id } })
+ },
+
+ // 鏌ヨ璁惧娑堟伅鍒嗛〉
+ getDeviceMessagePage: async (params: any) => {
+ return await request.get({ url: `/iot/device/message/page`, params })
+ },
+
+ // 鏌ヨ璁惧娑堟伅閰嶅鍒嗛〉
+ getDeviceMessagePairPage: async (params: any) => {
+ return await request.get({ url: `/iot/device/message/pair-page`, params })
+ },
+
+ // 鍙戦�佽澶囨秷鎭�
+ sendDeviceMessage: async (params: IotDeviceMessageSendReqVO) => {
+ return await request.post({ url: `/iot/device/message/send`, data: params })
+ }
+}
diff --git a/src/api/iot/device/group/index.ts b/src/api/iot/device/group/index.ts
new file mode 100644
index 0000000..4debe8b
--- /dev/null
+++ b/src/api/iot/device/group/index.ts
@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// IoT 璁惧鍒嗙粍 VO
+export interface DeviceGroupVO {
+ id: number // 鍒嗙粍 ID
+ name: string // 鍒嗙粍鍚嶅瓧
+ status: number // 鍒嗙粍鐘舵��
+ description: string // 鍒嗙粍鎻忚堪
+ deviceCount?: number // 璁惧鏁伴噺
+}
+
+// IoT 璁惧鍒嗙粍 API
+export const DeviceGroupApi = {
+ // 鏌ヨ璁惧鍒嗙粍鍒嗛〉
+ getDeviceGroupPage: async (params: any) => {
+ return await request.get({ url: `/iot/device-group/page`, params })
+ },
+
+ // 鏌ヨ璁惧鍒嗙粍璇︽儏
+ getDeviceGroup: async (id: number) => {
+ return await request.get({ url: `/iot/device-group/get?id=` + id })
+ },
+
+ // 鏂板璁惧鍒嗙粍
+ createDeviceGroup: async (data: DeviceGroupVO) => {
+ return await request.post({ url: `/iot/device-group/create`, data })
+ },
+
+ // 淇敼璁惧鍒嗙粍
+ updateDeviceGroup: async (data: DeviceGroupVO) => {
+ return await request.put({ url: `/iot/device-group/update`, data })
+ },
+
+ // 鍒犻櫎璁惧鍒嗙粍
+ deleteDeviceGroup: async (id: number) => {
+ return await request.delete({ url: `/iot/device-group/delete?id=` + id })
+ },
+
+ // 鑾峰彇璁惧鍒嗙粍鐨勭簿绠�淇℃伅鍒楄〃
+ getSimpleDeviceGroupList: async () => {
+ return await request.get({ url: `/iot/device-group/simple-list` })
+ }
+}
diff --git a/src/api/iot/ota/firmware/index.ts b/src/api/iot/ota/firmware/index.ts
new file mode 100644
index 0000000..97e6d05
--- /dev/null
+++ b/src/api/iot/ota/firmware/index.ts
@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+/** IoT OTA 鍥轰欢淇℃伅 */
+export interface IoTOtaFirmware {
+ id?: number // 鍥轰欢缂栧彿
+ name?: string // 鍥轰欢鍚嶇О
+ description?: string // 鍥轰欢鎻忚堪
+ version?: string // 鐗堟湰鍙�
+ productId?: number // 浜у搧缂栧彿
+ productName?: string // 浜у搧鍚嶇О
+ fileUrl?: string // 鍥轰欢鏂囦欢 URL
+ fileSize?: number // 鍥轰欢鏂囦欢澶у皬
+ fileDigestAlgorithm?: string // 鍥轰欢鏂囦欢绛惧悕绠楁硶
+ fileDigestValue?: string // 鍥轰欢鏂囦欢绛惧悕缁撴灉
+ createTime?: Date // 鍒涘缓鏃堕棿
+}
+
+// IoT OTA 鍥轰欢 API
+export const IoTOtaFirmwareApi = {
+ // 鏌ヨ OTA 鍥轰欢鍒嗛〉
+ getOtaFirmwarePage: async (params: any) => {
+ return await request.get({ url: `/iot/ota/firmware/page`, params })
+ },
+
+ // 鏌ヨ OTA 鍥轰欢璇︽儏
+ getOtaFirmware: async (id: number) => {
+ return await request.get({ url: `/iot/ota/firmware/get?id=` + id })
+ },
+
+ // 鏂板 OTA 鍥轰欢
+ createOtaFirmware: async (data: IoTOtaFirmware) => {
+ return await request.post({ url: `/iot/ota/firmware/create`, data })
+ },
+
+ // 淇敼 OTA 鍥轰欢
+ updateOtaFirmware: async (data: IoTOtaFirmware) => {
+ return await request.put({ url: `/iot/ota/firmware/update`, data })
+ },
+
+ // 鍒犻櫎 OTA 鍥轰欢
+ deleteOtaFirmware: async (id: number) => {
+ return await request.delete({ url: `/iot/ota/firmware/delete?id=` + id })
+ }
+}
diff --git a/src/api/iot/ota/task/index.ts b/src/api/iot/ota/task/index.ts
new file mode 100644
index 0000000..454405c
--- /dev/null
+++ b/src/api/iot/ota/task/index.ts
@@ -0,0 +1,38 @@
+import request from '@/config/axios'
+
+/** IoT OTA 浠诲姟淇℃伅 */
+export interface OtaTask {
+ id?: number // 浠诲姟缂栧彿
+ name: string // 浠诲姟鍚嶇О
+ description?: string // 浠诲姟鎻忚堪
+ firmwareId?: number // 鍥轰欢缂栧彿
+ status: number // 浠诲姟鐘舵��
+ deviceScope?: number // 鍗囩骇鑼冨洿
+ deviceIds?: number[] // 鎸囧畾璁惧ID鍒楄〃锛堝綋鍗囩骇鑼冨洿涓烘寚瀹氳澶囨椂浣跨敤锛�
+ deviceTotalCount?: number // 璁惧鎬诲叡鏁伴噺
+ deviceSuccessCount?: number // 璁惧鎴愬姛鏁伴噺
+ createTime?: Date // 鍒涘缓鏃堕棿
+}
+
+// IoT OTA 浠诲姟 API
+export const IoTOtaTaskApi = {
+ // 鏌ヨ OTA 鍗囩骇浠诲姟鍒嗛〉
+ getOtaTaskPage: async (params: any) => {
+ return await request.get({ url: `/iot/ota/task/page`, params })
+ },
+
+ // 鏌ヨ OTA 鍗囩骇浠诲姟璇︽儏
+ getOtaTask: async (id: number) => {
+ return await request.get({ url: `/iot/ota/task/get?id=` + id })
+ },
+
+ // 鍒涘缓 OTA 鍗囩骇浠诲姟
+ createOtaTask: async (data: OtaTask) => {
+ return await request.post({ url: `/iot/ota/task/create`, data })
+ },
+
+ // 鍙栨秷 OTA 鍗囩骇浠诲姟
+ cancelOtaTask: async (id: number) => {
+ return await request.post({ url: `/iot/ota/task/cancel?id=` + id })
+ }
+}
diff --git a/src/api/iot/ota/task/record/index.ts b/src/api/iot/ota/task/record/index.ts
new file mode 100644
index 0000000..aedc0b9
--- /dev/null
+++ b/src/api/iot/ota/task/record/index.ts
@@ -0,0 +1,38 @@
+import request from '@/config/axios'
+
+/** IoT OTA 浠诲姟璁板綍淇℃伅 */
+export interface OtaTaskRecord {
+ id?: number // 鍗囩骇璁板綍缂栧彿
+ firmwareId?: number // 鍥轰欢缂栧彿
+ firmwareVersion?: string // 鍥轰欢鐗堟湰
+ taskId?: number // 浠诲姟缂栧彿
+ deviceId?: string // 璁惧缂栧彿
+ deviceName?: string // 璁惧鍚嶇О
+ currentVersion?: string // 褰撳墠鐗堟湰
+ fromFirmwareId?: number // 鏉ユ簮鐨勫浐浠剁紪鍙�
+ fromFirmwareVersion?: string // 鏉ユ簮鐨勫浐浠剁増鏈�
+ status?: number // 鍗囩骇鐘舵��
+ progress?: number // 鍗囩骇杩涘害锛岀櫨鍒嗘瘮
+ description?: string // 鍗囩骇杩涘害鎻忚堪
+ updateTime?: Date // 鏇存柊鏃堕棿
+}
+
+// IoT OTA 浠诲姟璁板綍 API
+export const IoTOtaTaskRecordApi = {
+ getOtaTaskRecordStatusStatistics: async (firmwareId?: number, taskId?: number) => {
+ const params: any = {}
+ if (firmwareId) params.firmwareId = firmwareId
+ if (taskId) params.taskId = taskId
+ return await request.get({ url: `/iot/ota/task/record/get-status-statistics`, params })
+ },
+
+ // 鏌ヨ OTA 浠诲姟璁板綍鍒嗛〉
+ getOtaTaskRecordPage: async (params: any) => {
+ return await request.get({ url: `/iot/ota/task/record/page`, params })
+ },
+
+ // 鍙栨秷 OTA 浠诲姟璁板綍
+ cancelOtaTaskRecord: async (id: number) => {
+ return await request.put({ url: `/iot/ota/task/record/cancel?id=` + id })
+ }
+}
diff --git a/src/api/iot/product/category/index.ts b/src/api/iot/product/category/index.ts
new file mode 100644
index 0000000..cad17f5
--- /dev/null
+++ b/src/api/iot/product/category/index.ts
@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+// IoT 浜у搧鍒嗙被 VO
+export interface ProductCategoryVO {
+ id: number // 鍒嗙被 ID
+ name: string // 鍒嗙被鍚嶅瓧
+ sort: number // 鍒嗙被鎺掑簭
+ status: number // 鍒嗙被鐘舵��
+ description: string // 鍒嗙被鎻忚堪
+}
+
+// IoT 浜у搧鍒嗙被 API
+export const ProductCategoryApi = {
+ // 鏌ヨ浜у搧鍒嗙被鍒嗛〉
+ getProductCategoryPage: async (params: any) => {
+ return await request.get({ url: `/iot/product-category/page`, params })
+ },
+
+ // 鏌ヨ浜у搧鍒嗙被璇︽儏
+ getProductCategory: async (id: number) => {
+ return await request.get({ url: `/iot/product-category/get?id=` + id })
+ },
+
+ // 鏂板浜у搧鍒嗙被
+ createProductCategory: async (data: ProductCategoryVO) => {
+ return await request.post({ url: `/iot/product-category/create`, data })
+ },
+
+ // 淇敼浜у搧鍒嗙被
+ updateProductCategory: async (data: ProductCategoryVO) => {
+ return await request.put({ url: `/iot/product-category/update`, data })
+ },
+
+ // 鍒犻櫎浜у搧鍒嗙被
+ deleteProductCategory: async (id: number) => {
+ return await request.delete({ url: `/iot/product-category/delete?id=` + id })
+ },
+
+ /** 鑾峰彇浜у搧鍒嗙被绮剧畝鍒楄〃 */
+ getSimpleProductCategoryList: () => {
+ return request.get({ url: '/iot/product-category/simple-list' })
+ }
+}
diff --git a/src/api/iot/product/product/index.ts b/src/api/iot/product/product/index.ts
new file mode 100644
index 0000000..c9f273e
--- /dev/null
+++ b/src/api/iot/product/product/index.ts
@@ -0,0 +1,86 @@
+import request from '@/config/axios'
+
+// IoT 浜у搧 VO
+export interface ProductVO {
+ id: number // 浜у搧缂栧彿
+ name: string // 浜у搧鍚嶇О
+ productKey: string // 浜у搧鏍囪瘑
+ protocolId: number // 鍗忚缂栧彿
+ categoryId: number // 浜у搧鎵�灞炲搧绫绘爣璇嗙
+ categoryName?: string // 浜у搧鎵�灞炲搧绫诲悕绉�
+ icon: string // 浜у搧鍥炬爣
+ picUrl: string // 浜у搧鍥剧墖
+ description: string // 浜у搧鎻忚堪
+ status: number // 浜у搧鐘舵��
+ deviceType: number // 璁惧绫诲瀷
+ locationType: number // 璁惧绫诲瀷
+ netType: number // 鑱旂綉鏂瑰紡
+ codecType: string // 鏁版嵁鏍煎紡锛堢紪瑙g爜鍣ㄧ被鍨嬶級
+ deviceCount: number // 璁惧鏁伴噺
+ createTime: Date // 鍒涘缓鏃堕棿
+}
+
+// IOT 浜у搧璁惧绫诲瀷鏋氫妇绫� 0: 鐩磋繛璁惧, 1: 缃戝叧瀛愯澶�, 2: 缃戝叧璁惧
+export enum DeviceTypeEnum {
+ DEVICE = 0, // 鐩磋繛璁惧
+ GATEWAY_SUB = 1, // 缃戝叧瀛愯澶�
+ GATEWAY = 2 // 缃戝叧璁惧
+}
+// IOT 浜у搧瀹氫綅绫诲瀷鏋氫妇绫� 0: 鎵嬪姩瀹氫綅, 1: IP 瀹氫綅, 2: 瀹氫綅妯″潡瀹氫綅
+export enum LocationTypeEnum {
+ IP = 1, // IP 瀹氫綅
+ MODULE = 2, // 璁惧瀹氫綅
+ MANUAL = 3 // 鎵嬪姩瀹氫綅
+}
+// IOT 鏁版嵁鏍煎紡锛堢紪瑙g爜鍣ㄧ被鍨嬶級鏋氫妇绫�
+export enum CodecTypeEnum {
+ ALINK = 'Alink' // 闃块噷浜� Alink 鍗忚
+}
+
+// IoT 浜у搧 API
+export const ProductApi = {
+ // 鏌ヨ浜у搧鍒嗛〉
+ getProductPage: async (params: any) => {
+ return await request.get({ url: `/iot/product/page`, params })
+ },
+
+ // 鏌ヨ浜у搧璇︽儏
+ getProduct: async (id: number) => {
+ return await request.get({ url: `/iot/product/get?id=` + id })
+ },
+
+ // 鏂板浜у搧
+ createProduct: async (data: ProductVO) => {
+ return await request.post({ url: `/iot/product/create`, data })
+ },
+
+ // 淇敼浜у搧
+ updateProduct: async (data: ProductVO) => {
+ return await request.put({ url: `/iot/product/update`, data })
+ },
+
+ // 鍒犻櫎浜у搧
+ deleteProduct: async (id: number) => {
+ return await request.delete({ url: `/iot/product/delete?id=` + id })
+ },
+
+ // 瀵煎嚭浜у搧 Excel
+ exportProduct: async (params) => {
+ return await request.download({ url: `/iot/product/export-excel`, params })
+ },
+
+ // 鏇存柊浜у搧鐘舵��
+ updateProductStatus: async (id: number, status: number) => {
+ return await request.put({ url: `/iot/product/update-status?id=` + id + `&status=` + status })
+ },
+
+ // 鏌ヨ浜у搧锛堢簿绠�锛夊垪琛�
+ getSimpleProductList() {
+ return request.get({ url: '/iot/product/simple-list' })
+ },
+
+ // 鏍规嵁 ProductKey 鑾峰彇浜у搧淇℃伅
+ getProductByKey: async (productKey: string) => {
+ return await request.get({ url: `/iot/product/get-by-key`, params: { productKey } })
+ }
+}
diff --git a/src/api/iot/rule/data/rule/index.ts b/src/api/iot/rule/data/rule/index.ts
new file mode 100644
index 0000000..f805961
--- /dev/null
+++ b/src/api/iot/rule/data/rule/index.ts
@@ -0,0 +1,39 @@
+import request from '@/config/axios'
+
+/** IoT 鏁版嵁娴佽浆瑙勫垯淇℃伅 */
+export interface DataRule {
+ id: number // 鍦烘櫙缂栧彿
+ name?: string // 鍦烘櫙鍚嶇О
+ description: string // 鍦烘櫙鎻忚堪
+ status?: number // 鍦烘櫙鐘舵��
+ sourceConfigs?: any[] // 鏁版嵁婧愰厤缃暟缁�
+ sinkIds?: number[] // 鏁版嵁鐩殑缂栧彿鏁扮粍
+}
+
+// IoT 鏁版嵁娴佽浆瑙勫垯 API
+export const DataRuleApi = {
+ // 鏌ヨ鏁版嵁娴佽浆瑙勫垯鍒嗛〉
+ getDataRulePage: async (params: any) => {
+ return await request.get({ url: `/iot/data-rule/page`, params })
+ },
+
+ // 鏌ヨ鏁版嵁娴佽浆瑙勫垯璇︽儏
+ getDataRule: async (id: number) => {
+ return await request.get({ url: `/iot/data-rule/get?id=` + id })
+ },
+
+ // 鏂板鏁版嵁娴佽浆瑙勫垯
+ createDataRule: async (data: DataRule) => {
+ return await request.post({ url: `/iot/data-rule/create`, data })
+ },
+
+ // 淇敼鏁版嵁娴佽浆瑙勫垯
+ updateDataRule: async (data: DataRule) => {
+ return await request.put({ url: `/iot/data-rule/update`, data })
+ },
+
+ // 鍒犻櫎鏁版嵁娴佽浆瑙勫垯
+ deleteDataRule: async (id: number) => {
+ return await request.delete({ url: `/iot/data-rule/delete?id=` + id })
+ }
+}
diff --git a/src/api/iot/rule/data/sink/index.ts b/src/api/iot/rule/data/sink/index.ts
new file mode 100644
index 0000000..3e2755e
--- /dev/null
+++ b/src/api/iot/rule/data/sink/index.ts
@@ -0,0 +1,126 @@
+import request from '@/config/axios'
+
+// IoT 鏁版嵁娴佽浆鐩殑 VO
+export interface DataSinkVO {
+ id?: number // 妗ユ缂栧彿
+ name?: string // 妗ユ鍚嶇О
+ description?: string // 妗ユ鎻忚堪
+ status?: number // 妗ユ鐘舵��
+ direction?: number // 妗ユ鏂瑰悜
+ type?: number // 妗ユ绫诲瀷
+ config?:
+ | HttpConfig
+ | MqttConfig
+ | RocketMQConfig
+ | KafkaMQConfig
+ | RabbitMQConfig
+ | RedisStreamMQConfig // 妗ユ閰嶇疆
+}
+
+interface Config {
+ type: string
+}
+
+/** HTTP 閰嶇疆 */
+export interface HttpConfig extends Config {
+ url: string
+ method: string
+ headers: Record<string, string>
+ query: Record<string, string>
+ body: string
+}
+
+/** MQTT 閰嶇疆 */
+export interface MqttConfig extends Config {
+ url: string
+ username: string
+ password: string
+ clientId: string
+ topic: string
+}
+
+/** RocketMQ 閰嶇疆 */
+export interface RocketMQConfig extends Config {
+ nameServer: string
+ accessKey: string
+ secretKey: string
+ group: string
+ topic: string
+ tags: string
+}
+
+/** Kafka 閰嶇疆 */
+export interface KafkaMQConfig extends Config {
+ bootstrapServers: string
+ username: string
+ password: string
+ ssl: boolean
+ topic: string
+}
+
+/** RabbitMQ 閰嶇疆 */
+export interface RabbitMQConfig extends Config {
+ host: string
+ port: number
+ virtualHost: string
+ username: string
+ password: string
+ exchange: string
+ routingKey: string
+ queue: string
+}
+
+/** Redis Stream MQ 閰嶇疆 */
+export interface RedisStreamMQConfig extends Config {
+ host: string
+ port: number
+ password: string
+ database: number
+ topic: string
+}
+
+/** 鏁版嵁娴佽浆鐩殑绫诲瀷 */
+export const IotDataSinkTypeEnum = {
+ HTTP: 1,
+ TCP: 2,
+ WEBSOCKET: 3,
+ MQTT: 10,
+ DATABASE: 20,
+ REDIS_STREAM: 21,
+ ROCKETMQ: 30,
+ RABBITMQ: 31,
+ KAFKA: 32
+} as const
+
+// 鏁版嵁娴佽浆鐩殑 API
+export const DataSinkApi = {
+ // 鏌ヨ鏁版嵁娴佽浆鐩殑鍒嗛〉
+ getDataSinkPage: async (params: any) => {
+ return await request.get({ url: `/iot/data-sink/page`, params })
+ },
+
+ // 鏌ヨ鏁版嵁娴佽浆鐩殑璇︽儏
+ getDataSink: async (id: number) => {
+ return await request.get({ url: `/iot/data-sink/get?id=` + id })
+ },
+
+ // 鏂板鏁版嵁娴佽浆鐩殑
+ createDataSink: async (data: DataSinkVO) => {
+ return await request.post({ url: `/iot/data-sink/create`, data })
+ },
+
+ // 淇敼鏁版嵁娴佽浆鐩殑
+ updateDataSink: async (data: DataSinkVO) => {
+ return await request.put({ url: `/iot/data-sink/update`, data })
+ },
+
+ // 鍒犻櫎鏁版嵁娴佽浆鐩殑
+ deleteDataSink: async (id: number) => {
+ return await request.delete({ url: `/iot/data-sink/delete?id=` + id })
+ },
+
+ // 鏌ヨ鏁版嵁娴佽浆鐩殑锛堢簿绠�锛夊垪琛�
+ getDataSinkSimpleList() {
+ return request.get({ url: '/iot/data-sink/simple-list' })
+ }
+}
diff --git a/src/api/iot/rule/scene/index.ts b/src/api/iot/rule/scene/index.ts
new file mode 100644
index 0000000..0fbde26
--- /dev/null
+++ b/src/api/iot/rule/scene/index.ts
@@ -0,0 +1,87 @@
+import request from '@/config/axios'
+
+// 鍦烘櫙鑱斿姩
+export interface IotSceneRule {
+ id?: number // 鍦烘櫙缂栧彿
+ name: string // 鍦烘櫙鍚嶇О
+ description?: string // 鍦烘櫙鎻忚堪
+ status: number // 鍦烘櫙鐘舵�侊細0-寮�鍚紝1-鍏抽棴
+ triggers: Trigger[] // 瑙﹀彂鍣ㄦ暟缁�
+ actions: Action[] // 鎵ц鍣ㄦ暟缁�
+}
+
+// 瑙﹀彂鍣ㄧ粨鏋�
+export interface Trigger {
+ type: number // 瑙﹀彂绫诲瀷
+ productId?: number // 浜у搧缂栧彿
+ deviceId?: number // 璁惧缂栧彿
+ identifier?: string // 鐗╂ā鍨嬫爣璇嗙
+ operator?: string // 鎿嶄綔绗�
+ value?: string // 鍙傛暟鍊�
+ cronExpression?: string // CRON 琛ㄨ揪寮�
+ conditionGroups?: TriggerCondition[][] // 鏉′欢缁勶紙浜岀淮鏁扮粍锛�
+}
+
+// 瑙﹀彂鏉′欢缁撴瀯
+export interface TriggerCondition {
+ type: number // 鏉′欢绫诲瀷锛�1-璁惧鐘舵�侊紝2-璁惧灞炴�э紝3-褰撳墠鏃堕棿
+ productId?: number // 浜у搧缂栧彿
+ deviceId?: number // 璁惧缂栧彿
+ identifier?: string // 鏍囪瘑绗�
+ operator: string // 鎿嶄綔绗�
+ param: string // 鍙傛暟
+}
+
+// 鎵ц鍣ㄧ粨鏋�
+export interface Action {
+ type: number // 鎵ц绫诲瀷
+ productId?: number // 浜у搧缂栧彿
+ deviceId?: number // 璁惧缂栧彿
+ identifier?: string // 鐗╂ā鍨嬫爣璇嗙锛堟湇鍔¤皟鐢ㄦ椂浣跨敤锛�
+ params?: string // 璇锋眰鍙傛暟
+ alertConfigId?: number // 鍛婅閰嶇疆缂栧彿
+}
+
+// IoT 鍦烘櫙鑱斿姩 API
+export const RuleSceneApi = {
+ // 鏌ヨ鍦烘櫙鑱斿姩鍒嗛〉
+ getRuleScenePage: async (params: any) => {
+ return await request.get({ url: `/iot/scene-rule/page`, params })
+ },
+
+ // 鏌ヨ鍦烘櫙鑱斿姩璇︽儏
+ getRuleScene: async (id: number) => {
+ return await request.get({ url: `/iot/scene-rule/get?id=` + id })
+ },
+
+ // 鏂板鍦烘櫙鑱斿姩
+ createRuleScene: async (data: IotSceneRule) => {
+ return await request.post({ url: `/iot/scene-rule/create`, data })
+ },
+
+ // 淇敼鍦烘櫙鑱斿姩
+ updateRuleScene: async (data: IotSceneRule) => {
+ return await request.put({ url: `/iot/scene-rule/update`, data })
+ },
+
+ // 淇敼鍦烘櫙鑱斿姩
+ updateRuleSceneStatus: async (id: number, status: number) => {
+ return await request.put({
+ url: `/iot/scene-rule/update-status`,
+ data: {
+ id,
+ status
+ }
+ })
+ },
+
+ // 鍒犻櫎鍦烘櫙鑱斿姩
+ deleteRuleScene: async (id: number) => {
+ return await request.delete({ url: `/iot/scene-rule/delete?id=` + id })
+ },
+
+ // 鑾峰彇鍦烘櫙鑱斿姩绠�鍗曞垪琛�
+ getSimpleRuleSceneList: async () => {
+ return await request.get({ url: `/iot/scene-rule/simple-list` })
+ }
+}
diff --git a/src/api/iot/statistics/index.ts b/src/api/iot/statistics/index.ts
new file mode 100644
index 0000000..cdcb94d
--- /dev/null
+++ b/src/api/iot/statistics/index.ts
@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+
+/** IoT 缁熻鏁版嵁绫诲瀷 */
+export interface IotStatisticsSummaryRespVO {
+ productCategoryCount: number
+ productCount: number
+ deviceCount: number
+ deviceMessageCount: number
+ productCategoryTodayCount: number
+ productTodayCount: number
+ deviceTodayCount: number
+ deviceMessageTodayCount: number
+ deviceOnlineCount: number
+ deviceOfflineCount: number
+ deviceInactiveCount: number
+ productCategoryDeviceCounts: Record<string, number>
+}
+
+/** 鏃堕棿鎴�-鏁板�肩殑閿�煎绫诲瀷 */
+interface TimeValueItem {
+ [key: string]: number
+}
+
+/** IoT 娑堟伅缁熻鏁版嵁绫诲瀷 */
+export interface IotStatisticsDeviceMessageSummaryRespVO {
+ statType: number
+ upstreamCounts: TimeValueItem[]
+ downstreamCounts: TimeValueItem[]
+}
+
+/** 鏂扮殑娑堟伅缁熻鏁版嵁椤� */
+export interface IotStatisticsDeviceMessageSummaryByDateRespVO {
+ time: string
+ upstreamCount: number
+ downstreamCount: number
+}
+
+/** 鏂扮殑娑堟伅缁熻鎺ュ彛鍙傛暟 */
+export interface IotStatisticsDeviceMessageReqVO {
+ interval: number
+ times?: string[]
+}
+
+// IoT 鏁版嵁缁熻 API
+export const StatisticsApi = {
+ // 鏌ヨ鍏ㄥ眬鐨勬暟鎹粺璁�
+ getStatisticsSummary: async () => {
+ return await request.get<IotStatisticsSummaryRespVO>({
+ url: `/iot/statistics/get-summary`
+ })
+ },
+
+ // 鑾峰彇璁惧娑堟伅鐨勬暟鎹粺璁�
+ getDeviceMessageSummaryByDate: async (params: IotStatisticsDeviceMessageReqVO) => {
+ return await request.get<IotStatisticsDeviceMessageSummaryByDateRespVO[]>({
+ url: `/iot/statistics/get-device-message-summary-by-date`,
+ params
+ })
+ }
+}
diff --git a/src/api/iot/thingmodel/index.ts b/src/api/iot/thingmodel/index.ts
new file mode 100644
index 0000000..bcf9e07
--- /dev/null
+++ b/src/api/iot/thingmodel/index.ts
@@ -0,0 +1,301 @@
+import request from '@/config/axios'
+import { isEmpty } from '@/utils/is'
+
+/**
+ * IoT 浜у搧鐗╂ā鍨�
+ */
+export interface ThingModelData {
+ id?: number // 鐗╂ā鍨嬪姛鑳界紪鍙�
+ identifier?: string // 鍔熻兘鏍囪瘑
+ name?: string // 鍔熻兘鍚嶇О
+ description?: string // 鍔熻兘鎻忚堪
+ productId?: number // 浜у搧缂栧彿
+ productKey?: string // 浜у搧鏍囪瘑
+ dataType: string // 鏁版嵁绫诲瀷锛屼笌 dataSpecs 鐨� dataType 淇濇寔涓�鑷�
+ type: number // 鍔熻兘绫诲瀷
+ property: ThingModelProperty // 灞炴��
+ event?: ThingModelEvent // 浜嬩欢
+ service?: ThingModelService // 鏈嶅姟
+}
+
+/**
+ * ThingModelProperty 绫诲瀷
+ */
+export interface ThingModelProperty {
+ [key: string]: any
+}
+
+/**
+ * ThingModelEvent 绫诲瀷
+ */
+export interface ThingModelEvent {
+ [key: string]: any
+}
+
+/**
+ * ThingModelService 绫诲瀷
+ */
+export interface ThingModelService {
+ [key: string]: any
+}
+
+/** dataSpecs 鏁板�煎瀷鏁版嵁缁撴瀯 */
+export interface DataSpecsNumberData {
+ dataType: 'int' | 'float' | 'double' // 鏁版嵁绫诲瀷锛屽彇鍊间负 INT銆丗LOAT 鎴� DOUBLE
+ max: string // 鏈�澶у�硷紝蹇呴』涓� dataType 璁剧疆涓�鑷达紝涓斾负 STRING 绫诲瀷
+ min: string // 鏈�灏忓�硷紝蹇呴』涓� dataType 璁剧疆涓�鑷达紝涓斾负 STRING 绫诲瀷
+ step: string // 姝ラ暱锛屽繀椤讳笌 dataType 璁剧疆涓�鑷达紝涓斾负 STRING 绫诲瀷
+ precise?: string // 绮惧害锛屽綋 dataType 涓� FLOAT 鎴� DOUBLE 鏃跺彲閫�
+ defaultValue?: string // 榛樿鍊硷紝鍙��
+ unit: string // 鍗曚綅鐨勭鍙�
+ unitName: string // 鍗曚綅鐨勫悕绉�
+}
+
+/** dataSpecs 鏋氫妇鍨嬫暟鎹粨鏋� */
+export interface DataSpecsEnumOrBoolData {
+ dataType: 'enum' | 'bool'
+ defaultValue?: string // 榛樿鍊硷紝鍙��
+ name: string // 鏋氫妇椤圭殑鍚嶇О
+ value: number | undefined // 鏋氫妇鍊�
+}
+
+/** 鐗╂ā鍨婽SL鍝嶅簲鏁版嵁缁撴瀯 */
+export interface IotThingModelTSLResp {
+ productId: number
+ productKey: string
+ properties: ThingModelProperty[]
+ events: ThingModelEvent[]
+ services: ThingModelService[]
+}
+
+/** 鐗╂ā鍨嬪睘鎬� */
+export interface ThingModelProperty {
+ identifier: string
+ name: string
+ accessMode: string
+ required?: boolean
+ dataType: string
+ description?: string
+ dataSpecs?: ThingModelProperty
+ dataSpecsList?: ThingModelProperty[]
+}
+
+/** 鐗╂ā鍨嬩簨浠� */
+export interface ThingModelEvent {
+ identifier: string
+ name: string
+ required?: boolean
+ type: string
+ description?: string
+ outputParams?: ThingModelParam[]
+ method?: string
+}
+
+/** 鐗╂ā鍨嬫湇鍔� */
+export interface ThingModelService {
+ identifier: string
+ name: string
+ required?: boolean
+ callType: string
+ description?: string
+ inputParams?: ThingModelParam[]
+ outputParams?: ThingModelParam[]
+ method?: string
+}
+
+/** 鐗╂ā鍨嬪弬鏁� */
+export interface ThingModelParam {
+ identifier: string
+ name: string
+ direction: string
+ paraOrder?: number
+ dataType: string
+ dataSpecs?: ThingModelProperty
+ dataSpecsList?: ThingModelProperty[]
+}
+
+/** 鏁板�煎瀷鏁版嵁瑙勮寖 */
+export interface ThingModelNumericDataSpec {
+ dataType: 'int' | 'float' | 'double'
+ max: string
+ min: string
+ step: string
+ precise?: string
+ defaultValue?: string
+ unit?: string
+ unitName?: string
+}
+
+/** 甯冨皵/鏋氫妇鍨嬫暟鎹鑼� */
+export interface ThingModelBoolOrEnumDataSpecs {
+ dataType: 'bool' | 'enum'
+ name: string
+ value: number
+}
+
+/** 鏂囨湰/鏃堕棿鍨嬫暟鎹鑼� */
+export interface ThingModelDateOrTextDataSpecs {
+ dataType: 'text' | 'date'
+ length?: number
+ defaultValue?: string
+}
+
+/** 鏁扮粍鍨嬫暟鎹鑼� */
+export interface ThingModelArrayDataSpecs {
+ dataType: 'array'
+ size: number
+ childDataType: string
+ dataSpecsList?: ThingModelProperty[]
+}
+
+/** 缁撴瀯浣撳瀷鏁版嵁瑙勮寖 */
+export interface ThingModelStructDataSpecs {
+ dataType: 'struct'
+ identifier: string
+ name: string
+ accessMode: string
+ required?: boolean
+ childDataType: string
+ dataSpecs?: ThingModelProperty
+ dataSpecsList?: ThingModelProperty[]
+}
+
+// IoT 浜у搧鐗╂ā鍨� API
+export const ThingModelApi = {
+ // 鏌ヨ浜у搧鐗╂ā鍨嬪垎椤�
+ getThingModelPage: async (params: any) => {
+ return await request.get({ url: `/iot/thing-model/page`, params })
+ },
+
+ // 鑾峰緱浜у搧鐗╂ā鍨嬪垪琛�
+ getThingModelList: async (params: any) => {
+ return await request.get({ url: `/iot/thing-model/list`, params })
+ },
+
+ // 鑾峰緱浜у搧鐗╂ā鍨� TSL
+ getThingModelTSLByProductId: async (productId: number) => {
+ return await request.get({
+ url: `/iot/thing-model/get-tsl?productId=${productId}`
+ })
+ },
+
+ // 鏌ヨ浜у搧鐗╂ā鍨嬭鎯�
+ getThingModel: async (id: number) => {
+ return await request.get({ url: `/iot/thing-model/get?id=` + id })
+ },
+
+ // 鏂板浜у搧鐗╂ā鍨�
+ createThingModel: async (data: ThingModelData) => {
+ return await request.post({ url: `/iot/thing-model/create`, data })
+ },
+
+ // 淇敼浜у搧鐗╂ā鍨�
+ updateThingModel: async (data: ThingModelData) => {
+ return await request.put({ url: `/iot/thing-model/update`, data })
+ },
+
+ // 鍒犻櫎浜у搧鐗╂ā鍨�
+ deleteThingModel: async (id: number) => {
+ return await request.delete({ url: `/iot/thing-model/delete?id=` + id })
+ }
+}
+
+/** 鍏叡鏍¢獙瑙勫垯 */
+export const ThingModelFormRules = {
+ name: [
+ { required: true, message: '鍔熻兘鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' },
+ {
+ pattern: /^[\u4e00-\u9fa5a-zA-Z0-9][\u4e00-\u9fa5a-zA-Z0-9\-_/\.]{0,29}$/,
+ message:
+ '鏀寔涓枃銆佸ぇ灏忓啓瀛楁瘝銆佹棩鏂囥�佹暟瀛椼�佺煭鍒掔嚎銆佷笅鍒掔嚎銆佹枩鏉犲拰灏忔暟鐐癸紝蹇呴』浠ヤ腑鏂囥�佽嫳鏂囨垨鏁板瓧寮�澶达紝涓嶈秴杩� 30 涓瓧绗�',
+ trigger: 'blur'
+ }
+ ],
+ type: [{ required: true, message: '鍔熻兘绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ identifier: [
+ { required: true, message: '鏍囪瘑绗︿笉鑳戒负绌�', trigger: 'blur' },
+ {
+ pattern: /^[a-zA-Z0-9_]{1,50}$/,
+ message: '鏀寔澶у皬鍐欏瓧姣嶃�佹暟瀛楀拰涓嬪垝绾匡紝涓嶈秴杩� 50 涓瓧绗�',
+ trigger: 'blur'
+ },
+ {
+ validator: (_: any, value: string, callback: any) => {
+ const reservedKeywords = ['set', 'get', 'post', 'property', 'event', 'time', 'value']
+ if (reservedKeywords.includes(value)) {
+ callback(
+ new Error(
+ 'set, get, post, property, event, time, value 鏄郴缁熶繚鐣欏瓧娈碉紝涓嶈兘鐢ㄤ簬鏍囪瘑绗﹀畾涔�'
+ )
+ )
+ } else if (/^\d+$/.test(value)) {
+ callback(new Error('鏍囪瘑绗︿笉鑳芥槸绾暟瀛�'))
+ } else {
+ callback()
+ }
+ },
+ trigger: 'blur'
+ }
+ ],
+ 'property.dataSpecs.childDataType': [{ required: true, message: '鍏冪礌绫诲瀷涓嶈兘涓虹┖' }],
+ 'property.dataSpecs.size': [
+ { required: true, message: '鍏冪礌涓暟涓嶈兘涓虹┖' },
+ {
+ validator: (_: any, value: any, callback: any) => {
+ if (isEmpty(value)) {
+ callback(new Error('鍏冪礌涓暟涓嶈兘涓虹┖'))
+ return
+ }
+ if (isNaN(Number(value))) {
+ callback(new Error('鍏冪礌涓暟蹇呴』鏄暟瀛�'))
+ return
+ }
+ callback()
+ },
+ trigger: 'blur'
+ }
+ ],
+ 'property.dataSpecs.length': [
+ { required: true, message: '璇疯緭鍏ユ枃鏈瓧鑺傞暱搴�', trigger: 'blur' },
+ {
+ validator: (_: any, value: any, callback: any) => {
+ if (isEmpty(value)) {
+ callback(new Error('鏂囨湰闀垮害涓嶈兘涓虹┖'))
+ return
+ }
+ if (isNaN(Number(value))) {
+ callback(new Error('鏂囨湰闀垮害蹇呴』鏄暟瀛�'))
+ return
+ }
+ callback()
+ },
+ trigger: 'blur'
+ }
+ ],
+ 'property.accessMode': [{ required: true, message: '璇烽�夋嫨璇诲啓绫诲瀷', trigger: 'change' }]
+}
+
+/** 鏍¢獙甯冨皵鍊煎悕绉� */
+export const validateBoolName = (_: any, value: string, callback: any) => {
+ if (isEmpty(value)) {
+ callback(new Error('甯冨皵鍊煎悕绉颁笉鑳戒负绌�'))
+ return
+ }
+ // 妫�鏌ュ紑澶村瓧绗�
+ if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) {
+ callback(new Error('甯冨皵鍊煎悕绉板繀椤讳互涓枃銆佽嫳鏂囧瓧姣嶆垨鏁板瓧寮�澶�'))
+ return
+ }
+ // 妫�鏌ユ暣浣撴牸寮�
+ if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) {
+ callback(new Error('甯冨皵鍊煎悕绉板彧鑳藉寘鍚腑鏂囥�佽嫳鏂囧瓧姣嶃�佹暟瀛椼�佷笅鍒掔嚎鍜岀煭鍒掔嚎'))
+ return
+ }
+ // 妫�鏌ラ暱搴︼紙涓�涓腑鏂囩畻涓�涓瓧绗︼級
+ if (value.length > 20) {
+ callback(new Error('甯冨皵鍊煎悕绉伴暱搴︿笉鑳借秴杩� 20 涓瓧绗�'))
+ return
+ }
+
+ callback()
+}
diff --git a/src/api/login/index.ts b/src/api/login/index.ts
new file mode 100644
index 0000000..37eb491
--- /dev/null
+++ b/src/api/login/index.ts
@@ -0,0 +1,91 @@
+import request from '@/config/axios'
+import type { RegisterVO, UserLoginVO } from './types'
+
+export interface SmsCodeVO {
+ mobile: string
+ scene: number
+}
+
+export interface SmsLoginVO {
+ mobile: string
+ code: string
+}
+
+// 鐧诲綍
+export const login = (data: UserLoginVO) => {
+ return request.post({
+ url: '/system/auth/login',
+ data,
+ headers: {
+ isEncrypt: false
+ }
+ })
+}
+
+// 娉ㄥ唽
+export const register = (data: RegisterVO) => {
+ return request.post({ url: '/system/auth/register', data })
+}
+
+// 浣跨敤绉熸埛鍚嶏紝鑾峰緱绉熸埛缂栧彿
+export const getTenantIdByName = (name: string) => {
+ return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })
+}
+
+// 浣跨敤绉熸埛鍩熷悕锛岃幏寰楃鎴蜂俊鎭�
+export const getTenantByWebsite = (website: string) => {
+ return request.get({ url: '/system/tenant/get-by-website?website=' + website })
+}
+
+// 鐧诲嚭
+export const loginOut = () => {
+ return request.post({ url: '/system/auth/logout' })
+}
+
+// 鑾峰彇鐢ㄦ埛鏉冮檺淇℃伅
+export const getInfo = () => {
+ return request.get({ url: '/system/auth/get-permission-info' })
+}
+
+//鑾峰彇鐧诲綍楠岃瘉鐮�
+export const sendSmsCode = (data: SmsCodeVO) => {
+ return request.post({ url: '/system/auth/send-sms-code', data })
+}
+
+// 鐭俊楠岃瘉鐮佺櫥褰�
+export const smsLogin = (data: SmsLoginVO) => {
+ return request.post({ url: '/system/auth/sms-login', data })
+}
+
+// 绀句氦蹇嵎鐧诲綍锛屼娇鐢� code 鎺堟潈鐮�
+export function socialLogin(type: string, code: string, state: string) {
+ return request.post({
+ url: '/system/auth/social-login',
+ data: {
+ type,
+ code,
+ state
+ }
+ })
+}
+
+// 绀句氦鎺堟潈鐨勮烦杞�
+export const socialAuthRedirect = (type: number, redirectUri: string) => {
+ return request.get({
+ url: '/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri
+ })
+}
+// 鑾峰彇楠岃瘉鍥剧墖浠ュ強 token
+export const getCode = (data: any) => {
+ return request.postOriginal({ url: 'system/captcha/get', data })
+}
+
+// 婊戝姩鎴栬�呯偣閫夐獙璇�
+export const reqCheck = (data: any) => {
+ return request.postOriginal({ url: 'system/captcha/check', data })
+}
+
+// 閫氳繃鐭俊閲嶇疆瀵嗙爜
+export const smsResetPassword = (data: any) => {
+ return request.post({ url: '/system/auth/reset-password', data })
+}
diff --git a/src/api/login/oauth2/index.ts b/src/api/login/oauth2/index.ts
new file mode 100644
index 0000000..f4a67fb
--- /dev/null
+++ b/src/api/login/oauth2/index.ts
@@ -0,0 +1,41 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鎺堟潈淇℃伅
+export const getAuthorize = (clientId: string) => {
+ return request.get({ url: '/system/oauth2/authorize?clientId=' + clientId })
+}
+
+// 鍙戣捣鎺堟潈
+export const authorize = (
+ responseType: string,
+ clientId: string,
+ redirectUri: string,
+ state: string,
+ autoApprove: boolean,
+ checkedScopes: string[],
+ uncheckedScopes: string[]
+) => {
+ // 鏋勫缓 scopes
+ const scopes = {}
+ for (const scope of checkedScopes) {
+ scopes[scope] = true
+ }
+ for (const scope of uncheckedScopes) {
+ scopes[scope] = false
+ }
+ // 鍙戣捣璇锋眰
+ return request.post({
+ url: '/system/oauth2/authorize',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ params: {
+ response_type: responseType,
+ client_id: clientId,
+ redirect_uri: redirectUri,
+ state: state,
+ auto_approve: autoApprove,
+ scope: JSON.stringify(scopes)
+ }
+ })
+}
diff --git a/src/api/login/types.ts b/src/api/login/types.ts
new file mode 100644
index 0000000..b5790e6
--- /dev/null
+++ b/src/api/login/types.ts
@@ -0,0 +1,38 @@
+export type UserLoginVO = {
+ username: string
+ password: string
+ captchaVerification: string
+ socialType?: string
+ socialCode?: string
+ socialState?: string
+}
+
+export type TokenType = {
+ id: number // 缂栧彿
+ accessToken: string // 璁块棶浠ょ墝
+ refreshToken: string // 鍒锋柊浠ょ墝
+ userId: number // 鐢ㄦ埛缂栧彿
+ userType: number //鐢ㄦ埛绫诲瀷
+ clientId: string //瀹㈡埛绔紪鍙�
+ expiresTime: number //杩囨湡鏃堕棿
+}
+
+export type UserVO = {
+ id: number
+ username: string
+ nickname: string
+ deptId: number
+ email: string
+ mobile: string
+ sex: number
+ avatar: string
+ loginIp: string
+ loginDate: string
+}
+
+export type RegisterVO = {
+ tenantName: string
+ username: string
+ password: string
+ captchaVerification: string
+}
diff --git a/src/api/mall/market/banner/index.ts b/src/api/mall/market/banner/index.ts
new file mode 100644
index 0000000..ee65024
--- /dev/null
+++ b/src/api/mall/market/banner/index.ts
@@ -0,0 +1,37 @@
+import request from '@/config/axios'
+
+export interface BannerVO {
+ id: number
+ title: string
+ picUrl: string
+ status: number
+ url: string
+ position: number
+ sort: number
+ memo: string
+}
+
+// 鏌ヨBanner绠$悊鍒楄〃
+export const getBannerPage = async (params) => {
+ return await request.get({ url: `/promotion/banner/page`, params })
+}
+
+// 鏌ヨBanner绠$悊璇︽儏
+export const getBanner = async (id: number) => {
+ return await request.get({ url: `/promotion/banner/get?id=` + id })
+}
+
+// 鏂板Banner绠$悊
+export const createBanner = async (data: BannerVO) => {
+ return await request.post({ url: `/promotion/banner/create`, data })
+}
+
+// 淇敼Banner绠$悊
+export const updateBanner = async (data: BannerVO) => {
+ return await request.put({ url: `/promotion/banner/update`, data })
+}
+
+// 鍒犻櫎Banner绠$悊
+export const deleteBanner = async (id: number) => {
+ return await request.delete({ url: `/promotion/banner/delete?id=` + id })
+}
diff --git a/src/api/mall/product/brand.ts b/src/api/mall/product/brand.ts
new file mode 100644
index 0000000..94d5370
--- /dev/null
+++ b/src/api/mall/product/brand.ts
@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+/**
+ * 鍟嗗搧鍝佺墝
+ */
+export interface BrandVO {
+ /**
+ * 鍝佺墝缂栧彿
+ */
+ id?: number
+ /**
+ * 鍝佺墝鍚嶇О
+ */
+ name: string
+ /**
+ * 鍝佺墝鍥剧墖
+ */
+ picUrl: string
+ /**
+ * 鍝佺墝鎺掑簭
+ */
+ sort?: number
+ /**
+ * 鍝佺墝鎻忚堪
+ */
+ description?: string
+ /**
+ * 寮�鍚姸鎬�
+ */
+ status: number
+}
+
+// 鍒涘缓鍟嗗搧鍝佺墝
+export const createBrand = (data: BrandVO) => {
+ return request.post({ url: '/product/brand/create', data })
+}
+
+// 鏇存柊鍟嗗搧鍝佺墝
+export const updateBrand = (data: BrandVO) => {
+ return request.put({ url: '/product/brand/update', data })
+}
+
+// 鍒犻櫎鍟嗗搧鍝佺墝
+export const deleteBrand = (id: number) => {
+ return request.delete({ url: `/product/brand/delete?id=${id}` })
+}
+
+// 鑾峰緱鍟嗗搧鍝佺墝
+export const getBrand = (id: number) => {
+ return request.get({ url: `/product/brand/get?id=${id}` })
+}
+
+// 鑾峰緱鍟嗗搧鍝佺墝鍒楄〃
+export const getBrandParam = (params: PageParam) => {
+ return request.get({ url: '/product/brand/page', params })
+}
+
+// 鑾峰緱鍟嗗搧鍝佺墝绮剧畝淇℃伅鍒楄〃
+export const getSimpleBrandList = () => {
+ return request.get({ url: '/product/brand/list-all-simple' })
+}
diff --git a/src/api/mall/product/category.ts b/src/api/mall/product/category.ts
new file mode 100644
index 0000000..7e80b76
--- /dev/null
+++ b/src/api/mall/product/category.ts
@@ -0,0 +1,56 @@
+import request from '@/config/axios'
+
+/**
+ * 浜у搧鍒嗙被
+ */
+export interface CategoryVO {
+ /**
+ * 鍒嗙被缂栧彿
+ */
+ id?: number
+ /**
+ * 鐖跺垎绫荤紪鍙�
+ */
+ parentId?: number
+ /**
+ * 鍒嗙被鍚嶇О
+ */
+ name: string
+ /**
+ * 绉诲姩绔垎绫诲浘
+ */
+ picUrl: string
+ /**
+ * 鍒嗙被鎺掑簭
+ */
+ sort: number
+ /**
+ * 寮�鍚姸鎬�
+ */
+ status: number
+}
+
+// 鍒涘缓鍟嗗搧鍒嗙被
+export const createCategory = (data: CategoryVO) => {
+ return request.post({ url: '/product/category/create', data })
+}
+
+// 鏇存柊鍟嗗搧鍒嗙被
+export const updateCategory = (data: CategoryVO) => {
+ return request.put({ url: '/product/category/update', data })
+}
+
+// 鍒犻櫎鍟嗗搧鍒嗙被
+export const deleteCategory = (id: number) => {
+ return request.delete({ url: `/product/category/delete?id=${id}` })
+}
+
+// 鑾峰緱鍟嗗搧鍒嗙被
+export const getCategory = (id: number) => {
+ return request.get({ url: `/product/category/get?id=${id}` })
+}
+
+// 鑾峰緱鍟嗗搧鍒嗙被鍒楄〃
+export const getCategoryList = (params: any) => {
+ return request.get({ url: '/product/category/list', params })
+}
diff --git a/src/api/mall/product/comment.ts b/src/api/mall/product/comment.ts
new file mode 100644
index 0000000..defdbb9
--- /dev/null
+++ b/src/api/mall/product/comment.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface CommentVO {
+ id: number
+ userId: number
+ userNickname: string
+ userAvatar: string
+ anonymous: boolean
+ orderId: number
+ orderItemId: number
+ spuId: number
+ spuName: string
+ skuId: number
+ visible: boolean
+ scores: number
+ descriptionScores: number
+ benefitScores: number
+ content: string
+ picUrls: string
+ replyStatus: boolean
+ replyUserId: number
+ replyContent: string
+ replyTime: Date
+}
+
+// 鏌ヨ鍟嗗搧璇勮鍒楄〃
+export const getCommentPage = async (params) => {
+ return await request.get({ url: `/product/comment/page`, params })
+}
+
+// 鏌ヨ鍟嗗搧璇勮璇︽儏
+export const getComment = async (id: number) => {
+ return await request.get({ url: `/product/comment/get?id=` + id })
+}
+
+// 娣诲姞鑷瘎
+export const createComment = async (data: CommentVO) => {
+ return await request.post({ url: `/product/comment/create`, data })
+}
+
+// 鏄剧ず / 闅愯棌璇勮
+export const updateCommentVisible = async (data: any) => {
+ return await request.put({ url: `/product/comment/update-visible`, data })
+}
+
+// 鍟嗗鍥炲
+export const replyComment = async (data: any) => {
+ return await request.put({ url: `/product/comment/reply`, data })
+}
diff --git a/src/api/mall/product/favorite.ts b/src/api/mall/product/favorite.ts
new file mode 100644
index 0000000..3834eed
--- /dev/null
+++ b/src/api/mall/product/favorite.ts
@@ -0,0 +1,12 @@
+import request from '@/config/axios'
+
+export interface Favorite {
+ id?: number
+ userId?: string // 鐢ㄦ埛缂栧彿
+ spuId?: number | null // 鍟嗗搧 SPU 缂栧彿
+}
+
+// 鑾峰緱 ProductFavorite 鍒楄〃
+export const getFavoritePage = (params: PageParam) => {
+ return request.get({ url: '/product/favorite/page', params })
+}
diff --git a/src/api/mall/product/history.ts b/src/api/mall/product/history.ts
new file mode 100644
index 0000000..0aa45bd
--- /dev/null
+++ b/src/api/mall/product/history.ts
@@ -0,0 +1,10 @@
+import request from '@/config/axios'
+
+/**
+ * 鑾峰緱鍟嗗搧娴忚璁板綍鍒嗛〉
+ *
+ * @param params 璇锋眰鍙傛暟
+ */
+export const getBrowseHistoryPage = (params: any) => {
+ return request.get({ url: '/product/browse-history/page', params })
+}
diff --git a/src/api/mall/product/property.ts b/src/api/mall/product/property.ts
new file mode 100644
index 0000000..a191d82
--- /dev/null
+++ b/src/api/mall/product/property.ts
@@ -0,0 +1,89 @@
+import request from '@/config/axios'
+
+/**
+ * 鍟嗗搧灞炴��
+ */
+export interface PropertyVO {
+ id?: number
+ /** 鍚嶇О */
+ name: string
+ /** 澶囨敞 */
+ remark?: string
+}
+
+/**
+ * 灞炴�у��
+ */
+export interface PropertyValueVO {
+ id?: number
+ /** 灞炴�ч」鐨勭紪鍙� */
+ propertyId?: number
+ /** 鍚嶇О */
+ name: string
+ /** 澶囨敞 */
+ remark?: string
+}
+
+// ------------------------ 灞炴�ч」 -------------------
+
+// 鍒涘缓灞炴�ч」
+export const createProperty = (data: PropertyVO) => {
+ return request.post({ url: '/product/property/create', data })
+}
+
+// 鏇存柊灞炴�ч」
+export const updateProperty = (data: PropertyVO) => {
+ return request.put({ url: '/product/property/update', data })
+}
+
+// 鍒犻櫎灞炴�ч」
+export const deleteProperty = (id: number) => {
+ return request.delete({ url: `/product/property/delete?id=${id}` })
+}
+
+// 鑾峰緱灞炴�ч」
+export const getProperty = (id: number): Promise<PropertyVO> => {
+ return request.get({ url: `/product/property/get?id=${id}` })
+}
+
+// 鑾峰緱灞炴�ч」鍒嗛〉
+export const getPropertyPage = (params: PageParam) => {
+ return request.get({ url: '/product/property/page', params })
+}
+
+// 鑾峰緱灞炴�ч」绮剧畝鍒楄〃
+export const getPropertySimpleList = (): Promise<PropertyVO[]> => {
+ return request.get({ url: '/product/property/simple-list' })
+}
+
+// ------------------------ 灞炴�у�� -------------------
+
+// 鑾峰緱灞炴�у�煎垎椤�
+export const getPropertyValuePage = (params: PageParam & any) => {
+ return request.get({ url: '/product/property/value/page', params })
+}
+
+// 鑾峰緱灞炴�у��
+export const getPropertyValue = (id: number): Promise<PropertyValueVO> => {
+ return request.get({ url: `/product/property/value/get?id=${id}` })
+}
+
+// 鍒涘缓灞炴�у��
+export const createPropertyValue = (data: PropertyValueVO) => {
+ return request.post({ url: '/product/property/value/create', data })
+}
+
+// 鏇存柊灞炴�у��
+export const updatePropertyValue = (data: PropertyValueVO) => {
+ return request.put({ url: '/product/property/value/update', data })
+}
+
+// 鍒犻櫎灞炴�у��
+export const deletePropertyValue = (id: number) => {
+ return request.delete({ url: `/product/property/value/delete?id=${id}` })
+}
+
+// 鑾峰緱灞炴�у�肩簿绠�鍒楄〃
+export const getPropertyValueSimpleList = (propertyId: number): Promise<PropertyValueVO[]> => {
+ return request.get({ url: '/product/property/value/simple-list', params: { propertyId } })
+}
diff --git a/src/api/mall/product/spu.ts b/src/api/mall/product/spu.ts
new file mode 100644
index 0000000..df72386
--- /dev/null
+++ b/src/api/mall/product/spu.ts
@@ -0,0 +1,111 @@
+import request from '@/config/axios'
+
+export interface Property {
+ propertyId?: number // 灞炴�х紪鍙�
+ propertyName?: string // 灞炴�у悕绉�
+ valueId?: number // 灞炴�у�肩紪鍙�
+ valueName?: string // 灞炴�у�煎悕绉�
+}
+
+export interface Sku {
+ id?: number // 鍟嗗搧 SKU 缂栧彿
+ name?: string // 鍟嗗搧 SKU 鍚嶇О
+ spuId?: number // SPU 缂栧彿
+ properties?: Property[] // 灞炴�ф暟缁�
+ price?: number | string // 鍟嗗搧浠锋牸
+ marketPrice?: number | string // 甯傚満浠�
+ costPrice?: number | string // 鎴愭湰浠�
+ barCode?: string // 鍟嗗搧鏉$爜
+ picUrl?: string // 鍥剧墖鍦板潃
+ stock?: number // 搴撳瓨
+ weight?: number // 鍟嗗搧閲嶉噺锛屽崟浣嶏細kg 鍗冨厠
+ volume?: number // 鍟嗗搧浣撶Н锛屽崟浣嶏細m^3 骞崇背
+ firstBrokeragePrice?: number | string // 涓�绾у垎閿�鐨勪剑閲�
+ secondBrokeragePrice?: number | string // 浜岀骇鍒嗛攢鐨勪剑閲�
+ salesCount?: number // 鍟嗗搧閿�閲�
+}
+
+export interface GiveCouponTemplate {
+ id?: number
+ name?: string // 浼樻儬鍒稿悕绉�
+}
+
+export interface Spu {
+ id?: number
+ name?: string // 鍟嗗搧鍚嶇О
+ categoryId?: number // 鍟嗗搧鍒嗙被
+ keyword?: string // 鍏抽敭瀛�
+ unit?: number | undefined // 鍗曚綅
+ picUrl?: string // 鍟嗗搧灏侀潰鍥�
+ sliderPicUrls?: string[] // 鍟嗗搧杞挱鍥�
+ introduction?: string // 鍟嗗搧绠�浠�
+ deliveryTypes?: number[] // 閰嶉�佹柟寮�
+ deliveryTemplateId?: number | undefined // 杩愯垂妯$増
+ brandId?: number // 鍟嗗搧鍝佺墝缂栧彿
+ specType?: boolean // 鍟嗗搧瑙勬牸
+ subCommissionType?: boolean // 鍒嗛攢绫诲瀷
+ skus?: Sku[] // sku鏁扮粍
+ description?: string // 鍟嗗搧璇︽儏
+ sort?: number // 鍟嗗搧鎺掑簭
+ giveIntegral?: number // 璧犻�佺Н鍒�
+ virtualSalesCount?: number // 铏氭嫙閿�閲�
+ price?: number // 鍟嗗搧浠锋牸
+ combinationPrice?: number // 鍟嗗搧鎷煎洟浠锋牸
+ seckillPrice?: number // 鍟嗗搧绉掓潃浠锋牸
+ salesCount?: number // 鍟嗗搧閿�閲�
+ marketPrice?: number // 甯傚満浠�
+ costPrice?: number // 鎴愭湰浠�
+ stock?: number // 鍟嗗搧搴撳瓨
+ createTime?: Date // 鍟嗗搧鍒涘缓鏃堕棿
+ status?: number // 鍟嗗搧鐘舵��
+}
+
+// 鑾峰緱 Spu 鍒楄〃
+export const getSpuPage = (params: PageParam) => {
+ return request.get({ url: '/product/spu/page', params })
+}
+
+// 鑾峰緱 Spu 鍒楄〃 tabsCount
+export const getTabsCount = () => {
+ return request.get({ url: '/product/spu/get-count' })
+}
+
+// 鍒涘缓鍟嗗搧 Spu
+export const createSpu = (data: Spu) => {
+ return request.post({ url: '/product/spu/create', data })
+}
+
+// 鏇存柊鍟嗗搧 Spu
+export const updateSpu = (data: Spu) => {
+ return request.put({ url: '/product/spu/update', data })
+}
+
+// 鏇存柊鍟嗗搧 Spu status
+export const updateStatus = (data: { id: number; status: number }) => {
+ return request.put({ url: '/product/spu/update-status', data })
+}
+
+// 鑾峰緱鍟嗗搧 Spu
+export const getSpu = (id: number) => {
+ return request.get({ url: `/product/spu/get-detail?id=${id}` })
+}
+
+// 鑾峰緱鍟嗗搧 Spu 璇︽儏鍒楄〃
+export const getSpuDetailList = (ids: number[]) => {
+ return request.get({ url: `/product/spu/list?spuIds=${ids}` })
+}
+
+// 鍒犻櫎鍟嗗搧 Spu
+export const deleteSpu = (id: number) => {
+ return request.delete({ url: `/product/spu/delete?id=${id}` })
+}
+
+// 瀵煎嚭鍟嗗搧 Spu Excel
+export const exportSpu = async (params: any) => {
+ return await request.download({ url: '/product/spu/export-excel', params })
+}
+
+// 鑾峰緱鍟嗗搧 SPU 绮剧畝鍒楄〃
+export const getSpuSimpleList = async () => {
+ return request.get({ url: '/product/spu/list-all-simple' })
+}
diff --git a/src/api/mall/promotion/article/index.ts b/src/api/mall/promotion/article/index.ts
new file mode 100644
index 0000000..9184c7a
--- /dev/null
+++ b/src/api/mall/promotion/article/index.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+export interface ArticleVO {
+ id: number
+ categoryId: number
+ title: string
+ author: string
+ picUrl: string
+ introduction: string
+ browseCount: string
+ sort: number
+ status: number
+ spuId: number
+ recommendHot: boolean
+ recommendBanner: boolean
+ content: string
+}
+
+// 鏌ヨ鏂囩珷绠$悊鍒楄〃
+export const getArticlePage = async (params: any) => {
+ return await request.get({ url: `/promotion/article/page`, params })
+}
+
+// 鏌ヨ鏂囩珷绠$悊璇︽儏
+export const getArticle = async (id: number) => {
+ return await request.get({ url: `/promotion/article/get?id=` + id })
+}
+
+// 鏂板鏂囩珷绠$悊
+export const createArticle = async (data: ArticleVO) => {
+ return await request.post({ url: `/promotion/article/create`, data })
+}
+
+// 淇敼鏂囩珷绠$悊
+export const updateArticle = async (data: ArticleVO) => {
+ return await request.put({ url: `/promotion/article/update`, data })
+}
+
+// 鍒犻櫎鏂囩珷绠$悊
+export const deleteArticle = async (id: number) => {
+ return await request.delete({ url: `/promotion/article/delete?id=` + id })
+}
diff --git a/src/api/mall/promotion/articleCategory/index.ts b/src/api/mall/promotion/articleCategory/index.ts
new file mode 100644
index 0000000..47f5e93
--- /dev/null
+++ b/src/api/mall/promotion/articleCategory/index.ts
@@ -0,0 +1,39 @@
+import request from '@/config/axios'
+
+export interface ArticleCategoryVO {
+ id: number
+ name: string
+ picUrl: string
+ status: number
+ sort: number
+}
+
+// 鏌ヨ鏂囩珷鍒嗙被鍒楄〃
+export const getArticleCategoryPage = async (params) => {
+ return await request.get({ url: `/promotion/article-category/page`, params })
+}
+
+// 鏌ヨ鏂囩珷鍒嗙被绮剧畝淇℃伅鍒楄〃
+export const getSimpleArticleCategoryList = async () => {
+ return await request.get({ url: `/promotion/article-category/list-all-simple` })
+}
+
+// 鏌ヨ鏂囩珷鍒嗙被璇︽儏
+export const getArticleCategory = async (id: number) => {
+ return await request.get({ url: `/promotion/article-category/get?id=` + id })
+}
+
+// 鏂板鏂囩珷鍒嗙被
+export const createArticleCategory = async (data: ArticleCategoryVO) => {
+ return await request.post({ url: `/promotion/article-category/create`, data })
+}
+
+// 淇敼鏂囩珷鍒嗙被
+export const updateArticleCategory = async (data: ArticleCategoryVO) => {
+ return await request.put({ url: `/promotion/article-category/update`, data })
+}
+
+// 鍒犻櫎鏂囩珷鍒嗙被
+export const deleteArticleCategory = async (id: number) => {
+ return await request.delete({ url: `/promotion/article-category/delete?id=` + id })
+}
diff --git a/src/api/mall/promotion/bargain/bargainActivity.ts b/src/api/mall/promotion/bargain/bargainActivity.ts
new file mode 100644
index 0000000..9ad219a
--- /dev/null
+++ b/src/api/mall/promotion/bargain/bargainActivity.ts
@@ -0,0 +1,68 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface BargainActivityVO {
+ id?: number
+ name?: string
+ startTime?: Date
+ endTime?: Date
+ status?: number
+ helpMaxCount?: number // 杈惧埌璇ヤ汉鏁帮紝鎵嶈兘鐮嶅埌浣庝环
+ bargainCount?: number // 鏈�澶у府鐮嶆鏁�
+ totalLimitCount?: number // 鏈�澶ц喘涔版鏁�
+ spuId: number
+ skuId: number
+ bargainFirstPrice: number // 鐮嶄环璧峰浠锋牸锛屽崟浣嶅垎
+ bargainMinPrice: number // 鐮嶄环搴曚环
+ stock: number // 娲诲姩搴撳瓨
+ randomMinPrice?: number // 鐢ㄦ埛姣忔鐮嶄环鐨勬渶灏忛噾棰濓紝鍗曚綅锛氬垎
+ randomMaxPrice?: number // 鐢ㄦ埛姣忔鐮嶄环鐨勬渶澶ч噾棰濓紝鍗曚綅锛氬垎
+}
+
+// 鐮嶄环娲诲姩鎵�闇�灞炴�с�傞�夋嫨鐨勫晢鍝佸拰灞炴�х殑鏃跺�欎娇鐢ㄦ柟渚夸娇鐢ㄦ椿鍔ㄧ殑閫氱敤灏佽
+export interface BargainProductVO {
+ spuId: number
+ skuId: number
+ bargainFirstPrice: number // 鐮嶄环璧峰浠锋牸锛屽崟浣嶅垎
+ bargainMinPrice: number // 鐮嶄环搴曚环
+ stock: number // 娲诲姩搴撳瓨
+}
+
+// 鎵╁睍 Sku 閰嶇疆
+export type SkuExtension = Sku & {
+ productConfig: BargainProductVO
+}
+
+export interface SpuExtension extends Spu {
+ skus: SkuExtension[] // 閲嶅啓绫诲瀷
+}
+
+// 鏌ヨ鐮嶄环娲诲姩鍒楄〃
+export const getBargainActivityPage = async (params: any) => {
+ return await request.get({ url: '/promotion/bargain-activity/page', params })
+}
+
+// 鏌ヨ鐮嶄环娲诲姩璇︽儏
+export const getBargainActivity = async (id: number) => {
+ return await request.get({ url: '/promotion/bargain-activity/get?id=' + id })
+}
+
+// 鏂板鐮嶄环娲诲姩
+export const createBargainActivity = async (data: BargainActivityVO) => {
+ return await request.post({ url: '/promotion/bargain-activity/create', data })
+}
+
+// 淇敼鐮嶄环娲诲姩
+export const updateBargainActivity = async (data: BargainActivityVO) => {
+ return await request.put({ url: '/promotion/bargain-activity/update', data })
+}
+
+// 鍏抽棴鐮嶄环娲诲姩
+export const closeBargainActivity = async (id: number) => {
+ return await request.put({ url: '/promotion/bargain-activity/close?id=' + id })
+}
+
+// 鍒犻櫎鐮嶄环娲诲姩
+export const deleteBargainActivity = async (id: number) => {
+ return await request.delete({ url: '/promotion/bargain-activity/delete?id=' + id })
+}
diff --git a/src/api/mall/promotion/bargain/bargainHelp.ts b/src/api/mall/promotion/bargain/bargainHelp.ts
new file mode 100644
index 0000000..4308ae6
--- /dev/null
+++ b/src/api/mall/promotion/bargain/bargainHelp.ts
@@ -0,0 +1,14 @@
+import request from '@/config/axios'
+
+export interface BargainHelpVO {
+ id: number
+ record: number
+ userId: number
+ reducePrice: number
+ endTime: Date
+}
+
+// 鏌ヨ鐮嶄环璁板綍鍒楄〃
+export const getBargainHelpPage = async (params) => {
+ return await request.get({ url: `/promotion/bargain-help/page`, params })
+}
diff --git a/src/api/mall/promotion/bargain/bargainRecord.ts b/src/api/mall/promotion/bargain/bargainRecord.ts
new file mode 100644
index 0000000..f90b784
--- /dev/null
+++ b/src/api/mall/promotion/bargain/bargainRecord.ts
@@ -0,0 +1,19 @@
+import request from '@/config/axios'
+
+export interface BargainRecordVO {
+ id: number
+ activityId: number
+ userId: number
+ spuId: number
+ skuId: number
+ bargainFirstPrice: number
+ bargainPrice: number
+ status: number
+ orderId: number
+ endTime: Date
+}
+
+// 鏌ヨ鐮嶄环璁板綍鍒楄〃
+export const getBargainRecordPage = async (params) => {
+ return await request.get({ url: `/promotion/bargain-record/page`, params })
+}
diff --git a/src/api/mall/promotion/combination/combinationActivity.ts b/src/api/mall/promotion/combination/combinationActivity.ts
new file mode 100644
index 0000000..6400267
--- /dev/null
+++ b/src/api/mall/promotion/combination/combinationActivity.ts
@@ -0,0 +1,72 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface CombinationActivityVO {
+ id?: number
+ name?: string
+ spuId?: number
+ totalLimitCount?: number
+ singleLimitCount?: number
+ startTime?: Date
+ endTime?: Date
+ userSize?: number
+ totalCount?: number
+ successCount?: number
+ orderUserCount?: number
+ virtualGroup?: number
+ status?: number
+ limitDuration?: number
+ combinationPrice?: number
+ products: CombinationProductVO[]
+}
+
+// 鎷煎洟娲诲姩鎵�闇�灞炴��
+export interface CombinationProductVO {
+ spuId: number
+ skuId: number
+ combinationPrice: number // 鎷煎洟浠锋牸
+}
+
+// 鎵╁睍 Sku 閰嶇疆
+export type SkuExtension = Sku & {
+ productConfig: CombinationProductVO
+}
+
+export interface SpuExtension extends Spu {
+ skus: SkuExtension[] // 閲嶅啓绫诲瀷
+}
+
+// 鏌ヨ鎷煎洟娲诲姩鍒楄〃
+export const getCombinationActivityPage = async (params: any) => {
+ return await request.get({ url: '/promotion/combination-activity/page', params })
+}
+
+// 鏌ヨ鎷煎洟娲诲姩璇︽儏
+export const getCombinationActivity = async (id: number) => {
+ return await request.get({ url: '/promotion/combination-activity/get?id=' + id })
+}
+
+// 鑾峰緱鎷煎洟娲诲姩鍒楄〃锛屽熀浜庢椿鍔ㄧ紪鍙锋暟缁�
+export const getCombinationActivityListByIds = (ids: number[]) => {
+ return request.get({ url: `/promotion/combination-activity/list-by-ids?ids=${ids}` })
+}
+
+// 鏂板鎷煎洟娲诲姩
+export const createCombinationActivity = async (data: CombinationActivityVO) => {
+ return await request.post({ url: '/promotion/combination-activity/create', data })
+}
+
+// 淇敼鎷煎洟娲诲姩
+export const updateCombinationActivity = async (data: CombinationActivityVO) => {
+ return await request.put({ url: '/promotion/combination-activity/update', data })
+}
+
+// 鍏抽棴鎷煎洟娲诲姩
+export const closeCombinationActivity = async (id: number) => {
+ return await request.put({ url: '/promotion/combination-activity/close?id=' + id })
+}
+
+// 鍒犻櫎鎷煎洟娲诲姩
+export const deleteCombinationActivity = async (id: number) => {
+ return await request.delete({ url: '/promotion/combination-activity/delete?id=' + id })
+}
diff --git a/src/api/mall/promotion/combination/combinationRecord.ts b/src/api/mall/promotion/combination/combinationRecord.ts
new file mode 100644
index 0000000..b2b7d75
--- /dev/null
+++ b/src/api/mall/promotion/combination/combinationRecord.ts
@@ -0,0 +1,28 @@
+import request from '@/config/axios'
+
+export interface CombinationRecordVO {
+ id: number // 鎷煎洟璁板綍缂栧彿
+ activityId: number // 鎷煎洟娲诲姩缂栧彿
+ nickname: string // 鐢ㄦ埛鏄电О
+ avatar: string // 鐢ㄦ埛澶村儚
+ headId: number // 鍥㈤暱缂栧彿
+ expireTime: string // 杩囨湡鏃堕棿
+ userSize: number // 鍙弬鍥汉鏁�
+ userCount: number // 宸插弬鍥汉鏁�
+ status: number // 鎷煎洟鐘舵��
+ spuName: string // 鍟嗗搧鍚嶅瓧
+ picUrl: string // 鍟嗗搧鍥剧墖
+ virtualGroup: boolean // 鏄惁铏氭嫙鎴愬洟
+ startTime: string // 寮�濮嬫椂闂� (璁㈠崟浠樻鍚庡紑濮嬬殑鏃堕棿)
+ endTime: string // 缁撴潫鏃堕棿锛堟垚鍥㈡椂闂�/澶辫触鏃堕棿锛�
+}
+
+// 鏌ヨ鎷煎洟璁板綍鍒楄〃
+export const getCombinationRecordPage = async (params: any) => {
+ return await request.get({ url: '/promotion/combination-record/page', params })
+}
+
+// 鑾峰緱鎷煎洟璁板綍鐨勬瑕佷俊鎭�
+export const getCombinationRecordSummary = async () => {
+ return await request.get({ url: '/promotion/combination-record/get-summary' })
+}
diff --git a/src/api/mall/promotion/coupon/coupon.ts b/src/api/mall/promotion/coupon/coupon.ts
new file mode 100644
index 0000000..2ebff5d
--- /dev/null
+++ b/src/api/mall/promotion/coupon/coupon.ts
@@ -0,0 +1,26 @@
+import request from '@/config/axios'
+
+// TODO @dhb52锛歷o 缂哄皯
+
+// 鍒犻櫎浼樻儬鍔�
+export const deleteCoupon = async (id: number) => {
+ return request.delete({
+ url: `/promotion/coupon/delete?id=${id}`
+ })
+}
+
+// 鑾峰緱浼樻儬鍔靛垎椤�
+export const getCouponPage = async (params: PageParam) => {
+ return request.get({
+ url: '/promotion/coupon/page',
+ params: params
+ })
+}
+
+// 鍙戦�佷紭鎯犲埜
+export const sendCoupon = async (data: any) => {
+ return request.post({
+ url: '/promotion/coupon/send',
+ data: data
+ })
+}
diff --git a/src/api/mall/promotion/coupon/couponTemplate.ts b/src/api/mall/promotion/coupon/couponTemplate.ts
new file mode 100644
index 0000000..7e0a68c
--- /dev/null
+++ b/src/api/mall/promotion/coupon/couponTemplate.ts
@@ -0,0 +1,90 @@
+import request from '@/config/axios'
+
+export interface CouponTemplateVO {
+ id: number
+ name: string
+ status: number
+ totalCount: number
+ takeLimitCount: number
+ takeType: number
+ usePrice: number
+ productScope: number
+ productScopeValues: number[]
+ validityType: number
+ validStartTime: Date
+ validEndTime: Date
+ fixedStartTerm: number
+ fixedEndTerm: number
+ discountType: number
+ discountPercent: number
+ discountPrice: number
+ discountLimitPrice: number
+ takeCount: number
+ useCount: number
+}
+
+// 鍒涘缓浼樻儬鍔垫ā鏉�
+export function createCouponTemplate(data: CouponTemplateVO) {
+ return request.post({
+ url: '/promotion/coupon-template/create',
+ data: data
+ })
+}
+
+// 鏇存柊浼樻儬鍔垫ā鏉�
+export function updateCouponTemplate(data: CouponTemplateVO) {
+ return request.put({
+ url: '/promotion/coupon-template/update',
+ data: data
+ })
+}
+
+// 鏇存柊浼樻儬鍔垫ā鏉跨殑鐘舵��
+export function updateCouponTemplateStatus(id: number, status: [0, 1]) {
+ const data = {
+ id,
+ status
+ }
+ return request.put({
+ url: '/promotion/coupon-template/update-status',
+ data: data
+ })
+}
+
+// 鍒犻櫎浼樻儬鍔垫ā鏉�
+export function deleteCouponTemplate(id: number) {
+ return request.delete({
+ url: '/promotion/coupon-template/delete?id=' + id
+ })
+}
+
+// 鑾峰緱浼樻儬鍔垫ā鏉�
+export function getCouponTemplate(id: number) {
+ return request.get({
+ url: '/promotion/coupon-template/get?id=' + id
+ })
+}
+
+// 鑾峰緱浼樻儬鍔垫ā鏉垮垎椤�
+export function getCouponTemplatePage(params: PageParam) {
+ return request.get({
+ url: '/promotion/coupon-template/page',
+ params: params
+ })
+}
+
+// 鑾峰緱浼樻儬鍔垫ā鏉垮垎椤�
+export function getCouponTemplateList(ids: number[]): Promise<CouponTemplateVO[]> {
+ return request.get({
+ url: `/promotion/coupon-template/list?ids=${ids}`
+ })
+}
+
+// 瀵煎嚭浼樻儬鍔垫ā鏉� Excel
+export function exportCouponTemplateExcel(params: PageParam) {
+ return request.get({
+ url: '/promotion/coupon-template/export-excel',
+ params: params,
+ responseType: 'blob'
+ })
+}
diff --git a/src/api/mall/promotion/discount/discountActivity.ts b/src/api/mall/promotion/discount/discountActivity.ts
new file mode 100644
index 0000000..e755c1b
--- /dev/null
+++ b/src/api/mall/promotion/discount/discountActivity.ts
@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface DiscountActivityVO {
+ id?: number
+ spuId?: number
+ name?: string
+ status?: number
+ remark?: string
+ startTime?: Date
+ endTime?: Date
+ products?: DiscountProductVO[]
+}
+// 闄愭椂鎶樻墸鐩稿叧 灞炴��
+export interface DiscountProductVO {
+ spuId: number
+ skuId: number
+ discountType: number
+ discountPercent: number
+ discountPrice: number
+}
+
+// 鎵╁睍 Sku 閰嶇疆
+export type SkuExtension = Sku & {
+ productConfig: DiscountProductVO
+}
+
+export interface SpuExtension extends Spu {
+ skus: SkuExtension[] // 閲嶅啓绫诲瀷
+}
+
+// 鏌ヨ闄愭椂鎶樻墸娲诲姩鍒楄〃
+export const getDiscountActivityPage = async (params) => {
+ return await request.get({ url: '/promotion/discount-activity/page', params })
+}
+
+// 鏌ヨ闄愭椂鎶樻墸娲诲姩璇︽儏
+export const getDiscountActivity = async (id: number) => {
+ return await request.get({ url: '/promotion/discount-activity/get?id=' + id })
+}
+
+// 鏂板闄愭椂鎶樻墸娲诲姩
+export const createDiscountActivity = async (data: DiscountActivityVO) => {
+ return await request.post({ url: '/promotion/discount-activity/create', data })
+}
+
+// 淇敼闄愭椂鎶樻墸娲诲姩
+export const updateDiscountActivity = async (data: DiscountActivityVO) => {
+ return await request.put({ url: '/promotion/discount-activity/update', data })
+}
+
+// 鍏抽棴闄愭椂鎶樻墸娲诲姩
+export const closeDiscountActivity = async (id: number) => {
+ return await request.put({ url: '/promotion/discount-activity/close?id=' + id })
+}
+
+// 鍒犻櫎闄愭椂鎶樻墸娲诲姩
+export const deleteDiscountActivity = async (id: number) => {
+ return await request.delete({ url: '/promotion/discount-activity/delete?id=' + id })
+}
diff --git a/src/api/mall/promotion/diy/page.ts b/src/api/mall/promotion/diy/page.ts
new file mode 100644
index 0000000..a834b24
--- /dev/null
+++ b/src/api/mall/promotion/diy/page.ts
@@ -0,0 +1,45 @@
+import request from '@/config/axios'
+
+export interface DiyPageVO {
+ id?: number
+ templateId?: number
+ name: string
+ remark: string
+ previewPicUrls: string[]
+ property: string
+}
+
+// 鏌ヨ瑁呬慨椤甸潰鍒楄〃
+export const getDiyPagePage = async (params: any) => {
+ return await request.get({ url: `/promotion/diy-page/page`, params })
+}
+
+// 鏌ヨ瑁呬慨椤甸潰璇︽儏
+export const getDiyPage = async (id: number) => {
+ return await request.get({ url: `/promotion/diy-page/get?id=` + id })
+}
+
+// 鏂板瑁呬慨椤甸潰
+export const createDiyPage = async (data: DiyPageVO) => {
+ return await request.post({ url: `/promotion/diy-page/create`, data })
+}
+
+// 淇敼瑁呬慨椤甸潰
+export const updateDiyPage = async (data: DiyPageVO) => {
+ return await request.put({ url: `/promotion/diy-page/update`, data })
+}
+
+// 鍒犻櫎瑁呬慨椤甸潰
+export const deleteDiyPage = async (id: number) => {
+ return await request.delete({ url: `/promotion/diy-page/delete?id=` + id })
+}
+
+// 鑾峰緱瑁呬慨椤甸潰灞炴��
+export const getDiyPageProperty = async (id: number) => {
+ return await request.get({ url: `/promotion/diy-page/get-property?id=` + id })
+}
+
+// 鏇存柊瑁呬慨椤甸潰灞炴��
+export const updateDiyPageProperty = async (data: DiyPageVO) => {
+ return await request.put({ url: `/promotion/diy-page/update-property`, data })
+}
diff --git a/src/api/mall/promotion/diy/template.ts b/src/api/mall/promotion/diy/template.ts
new file mode 100644
index 0000000..87134c9
--- /dev/null
+++ b/src/api/mall/promotion/diy/template.ts
@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+import { DiyPageVO } from '@/api/mall/promotion/diy/page'
+
+export interface DiyTemplateVO {
+ id?: number
+ name: string
+ used: boolean
+ usedTime?: Date
+ remark: string
+ previewPicUrls: string[]
+ property: string
+}
+
+export interface DiyTemplatePropertyVO extends DiyTemplateVO {
+ pages: DiyPageVO[]
+}
+
+// 鏌ヨ瑁呬慨妯℃澘鍒楄〃
+export const getDiyTemplatePage = async (params: any) => {
+ return await request.get({ url: `/promotion/diy-template/page`, params })
+}
+
+// 鏌ヨ瑁呬慨妯℃澘璇︽儏
+export const getDiyTemplate = async (id: number) => {
+ return await request.get({ url: `/promotion/diy-template/get?id=` + id })
+}
+
+// 鏂板瑁呬慨妯℃澘
+export const createDiyTemplate = async (data: DiyTemplateVO) => {
+ return await request.post({ url: `/promotion/diy-template/create`, data })
+}
+
+// 淇敼瑁呬慨妯℃澘
+export const updateDiyTemplate = async (data: DiyTemplateVO) => {
+ return await request.put({ url: `/promotion/diy-template/update`, data })
+}
+
+// 鍒犻櫎瑁呬慨妯℃澘
+export const deleteDiyTemplate = async (id: number) => {
+ return await request.delete({ url: `/promotion/diy-template/delete?id=` + id })
+}
+
+// 浣跨敤瑁呬慨妯℃澘
+export const useDiyTemplate = async (id: number) => {
+ return await request.put({ url: `/promotion/diy-template/use?id=` + id })
+}
+
+// 鑾峰緱瑁呬慨妯℃澘灞炴��
+export const getDiyTemplateProperty = async (id: number) => {
+ return await request.get<DiyTemplatePropertyVO>({
+ url: `/promotion/diy-template/get-property?id=` + id
+ })
+}
+
+// 鏇存柊瑁呬慨妯℃澘灞炴��
+export const updateDiyTemplateProperty = async (data: DiyTemplateVO) => {
+ return await request.put({ url: `/promotion/diy-template/update-property`, data })
+}
diff --git a/src/api/mall/promotion/kefu/conversation/index.ts b/src/api/mall/promotion/kefu/conversation/index.ts
new file mode 100644
index 0000000..eb6eb9c
--- /dev/null
+++ b/src/api/mall/promotion/kefu/conversation/index.ts
@@ -0,0 +1,39 @@
+import request from '@/config/axios'
+
+export interface KeFuConversationRespVO {
+ id: number // 缂栧彿
+ userId: number // 浼氳瘽鎵�灞炵敤鎴�
+ userAvatar: string // 浼氳瘽鎵�灞炵敤鎴峰ご鍍�
+ userNickname: string // 浼氳瘽鎵�灞炵敤鎴锋樀绉�
+ lastMessageTime: Date // 鏈�鍚庤亰澶╂椂闂�
+ lastMessageContent: string // 鏈�鍚庤亰澶╁唴瀹�
+ lastMessageContentType: number // 鏈�鍚庡彂閫佺殑娑堟伅绫诲瀷
+ adminPinned: boolean // 绠$悊绔疆椤�
+ userDeleted: boolean // 鐢ㄦ埛鏄惁鍙
+ adminDeleted: boolean // 绠$悊鍛樻槸鍚﹀彲瑙�
+ adminUnreadMessageCount: number // 绠$悊鍛樻湭璇绘秷鎭暟
+ createTime?: string // 鍒涘缓鏃堕棿
+}
+
+// 瀹㈡湇浼氳瘽 API
+export const KeFuConversationApi = {
+ // 鑾峰緱瀹㈡湇浼氳瘽鍒楄〃
+ getConversationList: async () => {
+ return await request.get({ url: '/promotion/kefu-conversation/list' })
+ },
+ // 鑾峰緱瀹㈡湇浼氳瘽
+ getConversation: async (id: number) => {
+ return await request.get({ url: `/promotion/kefu-conversation/get?id=` + id })
+ },
+ // 瀹㈡湇浼氳瘽缃《
+ updateConversationPinned: async (data: any) => {
+ return await request.put({
+ url: '/promotion/kefu-conversation/update-conversation-pinned',
+ data
+ })
+ },
+ // 鍒犻櫎瀹㈡湇浼氳瘽
+ deleteConversation: async (id: number) => {
+ return await request.delete({ url: `/promotion/kefu-conversation/delete?id=${id}` })
+ }
+}
diff --git a/src/api/mall/promotion/kefu/message/index.ts b/src/api/mall/promotion/kefu/message/index.ts
new file mode 100644
index 0000000..4c3bed8
--- /dev/null
+++ b/src/api/mall/promotion/kefu/message/index.ts
@@ -0,0 +1,36 @@
+import request from '@/config/axios'
+
+export interface KeFuMessageRespVO {
+ id: number // 缂栧彿
+ conversationId: number // 浼氳瘽缂栧彿
+ senderId: number // 鍙戦�佷汉缂栧彿
+ senderAvatar: string // 鍙戦�佷汉澶村儚
+ senderType: number // 鍙戦�佷汉绫诲瀷
+ receiverId: number // 鎺ユ敹浜虹紪鍙�
+ receiverType: number // 鎺ユ敹浜虹被鍨�
+ contentType: number // 娑堟伅绫诲瀷
+ content: string // 娑堟伅
+ readStatus: boolean // 鏄惁宸茶
+ createTime: Date // 鍒涘缓鏃堕棿
+}
+
+// 瀹㈡湇浼氳瘽 API
+export const KeFuMessageApi = {
+ // 鍙戦�佸鏈嶆秷鎭�
+ sendKeFuMessage: async (data: any) => {
+ return await request.post({
+ url: '/promotion/kefu-message/send',
+ data
+ })
+ },
+ // 鏇存柊瀹㈡湇娑堟伅宸茶鐘舵��
+ updateKeFuMessageReadStatus: async (conversationId: number) => {
+ return await request.put({
+ url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
+ })
+ },
+ // 鑾峰緱娑堟伅鍒楄〃锛堟祦寮忓姞杞斤級
+ getKeFuMessageList: async (params: any) => {
+ return await request.get({ url: '/promotion/kefu-message/list', params })
+ }
+}
diff --git a/src/api/mall/promotion/point/index.ts b/src/api/mall/promotion/point/index.ts
new file mode 100644
index 0000000..38254c2
--- /dev/null
+++ b/src/api/mall/promotion/point/index.ts
@@ -0,0 +1,91 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu' // 绉垎鍟嗗煄娲诲姩 VO
+
+// 绉垎鍟嗗煄娲诲姩 VO
+export interface PointActivityVO {
+ id: number // 绉垎鍟嗗煄娲诲姩缂栧彿
+ spuId: number // 绉垎鍟嗗煄娲诲姩鍟嗗搧
+ status: number // 娲诲姩鐘舵��
+ stock: number // 绉垎鍟嗗煄娲诲姩搴撳瓨
+ totalStock: number // 绉垎鍟嗗煄娲诲姩鎬诲簱瀛�
+ remark?: string // 澶囨敞
+ sort: number // 鎺掑簭
+ createTime: string // 鍒涘缓鏃堕棿
+ products: PointProductVO[] // 绉垎鍟嗗煄鍟嗗搧
+
+ // ========== 鍟嗗搧瀛楁 ==========
+ spuName: string // 鍟嗗搧鍚嶇О
+ picUrl: string // 鍟嗗搧涓诲浘
+ marketPrice: number // 鍟嗗搧甯傚満浠凤紝鍗曚綅锛氬垎
+
+ //======================= 鏄剧ず鎵�闇�鍏戞崲绉垎鏈�灏戠殑 sku 淇℃伅 =======================
+ point: number // 鍏戞崲绉垎
+ price: number // 鍏戞崲閲戦锛屽崟浣嶏細鍒�
+}
+
+// 绉掓潃娲诲姩鎵�闇�灞炴��
+export interface PointProductVO {
+ id?: number // 绉垎鍟嗗煄鍟嗗搧缂栧彿
+ activityId?: number // 绉垎鍟嗗煄娲诲姩 id
+ spuId?: number // 鍟嗗搧 SPU 缂栧彿
+ skuId: number // 鍟嗗搧 SKU 缂栧彿
+ count: number // 鍙厬鎹㈡暟閲�
+ point: number // 鍏戞崲绉垎
+ price: number // 鍏戞崲閲戦锛屽崟浣嶏細鍒�
+ stock: number // 绉垎鍟嗗煄鍟嗗搧搴撳瓨
+ activityStatus?: number // 绉垎鍟嗗煄鍟嗗搧鐘舵��
+}
+
+// 鎵╁睍 Sku 閰嶇疆
+export type SkuExtension = Sku & {
+ productConfig: PointProductVO
+}
+
+export interface SpuExtension extends Spu {
+ skus: SkuExtension[] // 閲嶅啓绫诲瀷
+}
+
+export interface SpuExtension0 extends Spu {
+ pointStock: number // 绉垎鍟嗗煄娲诲姩搴撳瓨
+ pointTotalStock: number // 绉垎鍟嗗煄娲诲姩鎬诲簱瀛�
+ point: number // 鍏戞崲绉垎
+ pointPrice: number // 鍏戞崲閲戦锛屽崟浣嶏細鍒�
+}
+
+// 绉垎鍟嗗煄娲诲姩 API
+export const PointActivityApi = {
+ // 鏌ヨ绉垎鍟嗗煄娲诲姩鍒嗛〉
+ getPointActivityPage: async (params: any) => {
+ return await request.get({ url: `/promotion/point-activity/page`, params })
+ },
+
+ // 鏌ヨ绉垎鍟嗗煄娲诲姩璇︽儏
+ getPointActivity: async (id: number) => {
+ return await request.get({ url: `/promotion/point-activity/get?id=` + id })
+ },
+
+ // 鏌ヨ绉垎鍟嗗煄娲诲姩鍒楄〃锛屽熀浜庢椿鍔ㄧ紪鍙锋暟缁�
+ getPointActivityListByIds: async (ids: number[]) => {
+ return request.get({ url: `/promotion/point-activity/list-by-ids?ids=${ids}` })
+ },
+
+ // 鏂板绉垎鍟嗗煄娲诲姩
+ createPointActivity: async (data: PointActivityVO) => {
+ return await request.post({ url: `/promotion/point-activity/create`, data })
+ },
+
+ // 淇敼绉垎鍟嗗煄娲诲姩
+ updatePointActivity: async (data: PointActivityVO) => {
+ return await request.put({ url: `/promotion/point-activity/update`, data })
+ },
+
+ // 鍒犻櫎绉垎鍟嗗煄娲诲姩
+ deletePointActivity: async (id: number) => {
+ return await request.delete({ url: `/promotion/point-activity/delete?id=` + id })
+ },
+
+ // 鍏抽棴绉掓潃娲诲姩
+ closePointActivity: async (id: number) => {
+ return await request.put({ url: '/promotion/point-activity/close?id=' + id })
+ }
+}
diff --git a/src/api/mall/promotion/reward/rewardActivity.ts b/src/api/mall/promotion/reward/rewardActivity.ts
new file mode 100644
index 0000000..e9f95ed
--- /dev/null
+++ b/src/api/mall/promotion/reward/rewardActivity.ts
@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+
+export interface RewardActivityVO {
+ id?: number
+ name?: string
+ startTime?: Date
+ endTime?: Date
+ startAndEndTime?: Date[] // 鍙墠绔娇鐢�
+ remark?: string
+ conditionType?: number
+ productScope?: number
+ rules: RewardRule[]
+ // 濡備笅浠呯敤浜庤〃鍗曪紝涓嶆彁浜�
+ productScopeValues?: number[] // 鍟嗗搧鑼冨洿锛氬�间负鍝佺被缂栧彿鍒楄〃銆佸晢鍝佺紪鍙峰垪琛�
+ productCategoryIds?: number[]
+ productSpuIds?: number[]
+}
+
+// 浼樻儬瑙勫垯
+export interface RewardRule {
+ limit?: number
+ discountPrice?: number
+ freeDelivery?: boolean
+ point: number
+ giveCouponTemplateCounts?: {
+ [key: number]: number
+ }
+}
+
+// 鏂板婊″噺閫佹椿鍔�
+export const createRewardActivity = async (data: RewardActivityVO) => {
+ return await request.post({ url: '/promotion/reward-activity/create', data })
+}
+
+// 鏇存柊婊″噺閫佹椿鍔�
+export const updateRewardActivity = async (data: RewardActivityVO) => {
+ return await request.put({ url: '/promotion/reward-activity/update', data })
+}
+
+// 鏌ヨ婊″噺閫佹椿鍔ㄥ垪琛�
+export const getRewardActivityPage = async (params) => {
+ return await request.get({ url: '/promotion/reward-activity/page', params })
+}
+
+// 鏌ヨ婊″噺閫佹椿鍔ㄨ鎯�
+export const getReward = async (id: number) => {
+ return await request.get({ url: '/promotion/reward-activity/get?id=' + id })
+}
+
+// 鍒犻櫎婊″噺閫佹椿鍔�
+export const deleteRewardActivity = async (id: number) => {
+ return await request.delete({ url: '/promotion/reward-activity/delete?id=' + id })
+}
+
+// 鍏抽棴婊″噺閫佹椿鍔�
+export const closeRewardActivity = async (id: number) => {
+ return await request.put({ url: '/promotion/reward-activity/close?id=' + id })
+}
diff --git a/src/api/mall/promotion/seckill/seckillActivity.ts b/src/api/mall/promotion/seckill/seckillActivity.ts
new file mode 100644
index 0000000..dc5a350
--- /dev/null
+++ b/src/api/mall/promotion/seckill/seckillActivity.ts
@@ -0,0 +1,75 @@
+import request from '@/config/axios'
+import { Sku, Spu } from '@/api/mall/product/spu'
+
+export interface SeckillActivityVO {
+ id?: number
+ spuId?: number
+ name?: string
+ status?: number
+ remark?: string
+ startTime?: Date
+ endTime?: Date
+ sort?: number
+ configIds?: string
+ orderCount?: number
+ userCount?: number
+ totalPrice?: number
+ totalLimitCount?: number
+ singleLimitCount?: number
+ stock?: number
+ totalStock?: number
+ seckillPrice?: number
+ products?: SeckillProductVO[]
+}
+
+// 绉掓潃娲诲姩鎵�闇�灞炴��
+export interface SeckillProductVO {
+ skuId: number
+ spuId: number
+ seckillPrice: number
+ stock: number
+}
+
+// 鎵╁睍 Sku 閰嶇疆
+export type SkuExtension = Sku & {
+ productConfig: SeckillProductVO
+}
+
+export interface SpuExtension extends Spu {
+ skus: SkuExtension[] // 閲嶅啓绫诲瀷
+}
+
+// 鏌ヨ绉掓潃娲诲姩鍒楄〃
+export const getSeckillActivityPage = async (params) => {
+ return await request.get({ url: '/promotion/seckill-activity/page', params })
+}
+
+// 鏌ヨ绉掓潃娲诲姩鍒楄〃锛屽熀浜庢椿鍔ㄧ紪鍙锋暟缁�
+export const getSeckillActivityListByIds = (ids: number[]) => {
+ return request.get({ url: `/promotion/seckill-activity/list-by-ids?ids=${ids}` })
+}
+
+// 鏌ヨ绉掓潃娲诲姩璇︽儏
+export const getSeckillActivity = async (id: number) => {
+ return await request.get({ url: '/promotion/seckill-activity/get?id=' + id })
+}
+
+// 鏂板绉掓潃娲诲姩
+export const createSeckillActivity = async (data: SeckillActivityVO) => {
+ return await request.post({ url: '/promotion/seckill-activity/create', data })
+}
+
+// 淇敼绉掓潃娲诲姩
+export const updateSeckillActivity = async (data: SeckillActivityVO) => {
+ return await request.put({ url: '/promotion/seckill-activity/update', data })
+}
+
+// 鍏抽棴绉掓潃娲诲姩
+export const closeSeckillActivity = async (id: number) => {
+ return await request.put({ url: '/promotion/seckill-activity/close?id=' + id })
+}
+
+// 鍒犻櫎绉掓潃娲诲姩
+export const deleteSeckillActivity = async (id: number) => {
+ return await request.delete({ url: '/promotion/seckill-activity/delete?id=' + id })
+}
diff --git a/src/api/mall/promotion/seckill/seckillConfig.ts b/src/api/mall/promotion/seckill/seckillConfig.ts
new file mode 100644
index 0000000..37d9b54
--- /dev/null
+++ b/src/api/mall/promotion/seckill/seckillConfig.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// 绉掓潃鏃舵 VO
+export interface SeckillConfigVO {
+ id: number // 缂栧彿
+ name: string // 绉掓潃鏃舵鍚嶇О
+ startTime: string // 寮�濮嬫椂闂寸偣
+ endTime: string // 缁撴潫鏃堕棿鐐�
+ sliderPicUrls: string[] // 绉掓潃杞挱鍥�
+ status: number // 娲诲姩鐘舵��
+}
+
+// 绉掓潃鏃舵 API
+export const SeckillConfigApi = {
+ // 鏌ヨ绉掓潃鏃舵鍒嗛〉
+ getSeckillConfigPage: async (params: any) => {
+ return await request.get({ url: `/promotion/seckill-config/page`, params })
+ },
+
+ // 鏌ヨ绉掓潃鏃舵鍒楄〃
+ getSimpleSeckillConfigList: async () => {
+ return await request.get({ url: `/promotion/seckill-config/list` })
+ },
+
+ // 鏌ヨ绉掓潃鏃舵璇︽儏
+ getSeckillConfig: async (id: number) => {
+ return await request.get({ url: `/promotion/seckill-config/get?id=` + id })
+ },
+
+ // 鏂板绉掓潃鏃舵
+ createSeckillConfig: async (data: SeckillConfigVO) => {
+ return await request.post({ url: `/promotion/seckill-config/create`, data })
+ },
+
+ // 淇敼绉掓潃鏃舵
+ updateSeckillConfig: async (data: SeckillConfigVO) => {
+ return await request.put({ url: `/promotion/seckill-config/update`, data })
+ },
+
+ // 鍒犻櫎绉掓潃鏃舵
+ deleteSeckillConfig: async (id: number) => {
+ return await request.delete({ url: `/promotion/seckill-config/delete?id=` + id })
+ },
+
+ // 淇敼鏃舵閰嶇疆鐘舵��
+ updateSeckillConfigStatus: async (id: number, status: number) => {
+ const data = {
+ id,
+ status
+ }
+ return request.put({ url: '/promotion/seckill-config/update-status', data: data })
+ }
+}
diff --git a/src/api/mall/statistics/common.ts b/src/api/mall/statistics/common.ts
new file mode 100644
index 0000000..3d96439
--- /dev/null
+++ b/src/api/mall/statistics/common.ts
@@ -0,0 +1,5 @@
+/** 鏁版嵁瀵圭収 Response VO */
+export interface DataComparisonRespVO<T> {
+ value: T
+ reference: T
+}
diff --git a/src/api/mall/statistics/member.ts b/src/api/mall/statistics/member.ts
new file mode 100644
index 0000000..d9accf9
--- /dev/null
+++ b/src/api/mall/statistics/member.ts
@@ -0,0 +1,123 @@
+import request from '@/config/axios'
+import dayjs from 'dayjs'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+import { formatDate } from '@/utils/formatTime'
+
+/** 浼氬憳鍒嗘瀽 Request VO */
+export interface MemberAnalyseReqVO {
+ times: dayjs.ConfigType[]
+}
+
+/** 浼氬憳鍒嗘瀽 Response VO */
+export interface MemberAnalyseRespVO {
+ visitUserCount: number
+ orderUserCount: number
+ payUserCount: number
+ atv: number
+ comparison: DataComparisonRespVO<MemberAnalyseComparisonRespVO>
+}
+
+/** 浼氬憳鍒嗘瀽瀵圭収鏁版嵁 Response VO */
+export interface MemberAnalyseComparisonRespVO {
+ registerUserCount: number
+ visitUserCount: number
+ rechargeUserCount: number
+}
+
+/** 浼氬憳鍦板尯缁熻 Response VO */
+export interface MemberAreaStatisticsRespVO {
+ areaId: number
+ areaName: string
+ userCount: number
+ orderCreateUserCount: number
+ orderPayUserCount: number
+ orderPayPrice: number
+}
+
+/** 浼氬憳鎬у埆缁熻 Response VO */
+export interface MemberSexStatisticsRespVO {
+ sex: number
+ userCount: number
+}
+
+/** 浼氬憳缁熻 Response VO */
+export interface MemberSummaryRespVO {
+ userCount: number
+ rechargeUserCount: number
+ rechargePrice: number
+ expensePrice: number
+}
+
+/** 浼氬憳缁堢缁熻 Response VO */
+export interface MemberTerminalStatisticsRespVO {
+ terminal: number
+ userCount: number
+}
+
+/** 浼氬憳鏁伴噺缁熻 Response VO */
+export interface MemberCountRespVO {
+ /** 鐢ㄦ埛璁块棶閲� */
+ visitUserCount: string
+ /** 娉ㄥ唽鐢ㄦ埛鏁伴噺 */
+ registerUserCount: number
+}
+
+/** 浼氬憳娉ㄥ唽鏁伴噺 Response VO */
+export interface MemberRegisterCountRespVO {
+ date: string
+ count: number
+}
+
+// 鏌ヨ浼氬憳缁熻
+export const getMemberSummary = () => {
+ return request.get<MemberSummaryRespVO>({
+ url: '/statistics/member/summary'
+ })
+}
+
+// 鏌ヨ浼氬憳鍒嗘瀽鏁版嵁
+export const getMemberAnalyse = (params: MemberAnalyseReqVO) => {
+ return request.get<MemberAnalyseRespVO>({
+ url: '/statistics/member/analyse',
+ params: { times: [formatDate(params.times[0]), formatDate(params.times[1])] }
+ })
+}
+
+// 鎸夌収鐪佷唤锛屾煡璇細鍛樼粺璁″垪琛�
+export const getMemberAreaStatisticsList = () => {
+ return request.get<MemberAreaStatisticsRespVO[]>({
+ url: '/statistics/member/area-statistics-list'
+ })
+}
+
+// 鎸夌収鎬у埆锛屾煡璇細鍛樼粺璁″垪琛�
+export const getMemberSexStatisticsList = () => {
+ return request.get<MemberSexStatisticsRespVO[]>({
+ url: '/statistics/member/sex-statistics-list'
+ })
+}
+
+// 鎸夌収缁堢锛屾煡璇細鍛樼粺璁″垪琛�
+export const getMemberTerminalStatisticsList = () => {
+ return request.get<MemberTerminalStatisticsRespVO[]>({
+ url: '/statistics/member/terminal-statistics-list'
+ })
+}
+
+// 鑾峰緱鐢ㄦ埛鏁伴噺閲忓鐓�
+export const getUserCountComparison = () => {
+ return request.get<DataComparisonRespVO<MemberCountRespVO>>({
+ url: '/statistics/member/user-count-comparison'
+ })
+}
+
+// 鑾峰緱浼氬憳娉ㄥ唽鏁伴噺鍒楄〃
+export const getMemberRegisterCountList = (
+ beginTime: dayjs.ConfigType,
+ endTime: dayjs.ConfigType
+) => {
+ return request.get<MemberRegisterCountRespVO[]>({
+ url: '/statistics/member/register-count-list',
+ params: { times: [formatDate(beginTime), formatDate(endTime)] }
+ })
+}
diff --git a/src/api/mall/statistics/pay.ts b/src/api/mall/statistics/pay.ts
new file mode 100644
index 0000000..f5d14c9
--- /dev/null
+++ b/src/api/mall/statistics/pay.ts
@@ -0,0 +1,12 @@
+import request from '@/config/axios'
+
+/** 鏀粯缁熻 */
+export interface PaySummaryRespVO {
+ /** 鍏呭�奸噾棰濓紝鍗曚綅鍒� */
+ rechargePrice: number
+}
+
+/** 鑾峰彇閽卞寘鍏呭�奸噾棰� */
+export const getWalletRechargePrice = async () => {
+ return await request.get<PaySummaryRespVO>({ url: `/statistics/pay/summary` })
+}
diff --git a/src/api/mall/statistics/product.ts b/src/api/mall/statistics/product.ts
new file mode 100644
index 0000000..798a2fa
--- /dev/null
+++ b/src/api/mall/statistics/product.ts
@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+
+export interface ProductStatisticsVO {
+ id: number
+ day: string
+ spuId: number
+ spuName: string
+ spuPicUrl: string
+ browseCount: number
+ browseUserCount: number
+ favoriteCount: number
+ cartCount: number
+ orderCount: number
+ orderPayCount: number
+ orderPayPrice: number
+ afterSaleCount: number
+ afterSaleRefundPrice: number
+ browseConvertPercent: number
+}
+
+// 鍟嗗搧缁熻 API
+export const ProductStatisticsApi = {
+ // 鑾峰緱鍟嗗搧缁熻鍒嗘瀽
+ getProductStatisticsAnalyse: (params: any) => {
+ return request.get<DataComparisonRespVO<ProductStatisticsVO>>({
+ url: '/statistics/product/analyse',
+ params
+ })
+ },
+ // 鑾峰緱鍟嗗搧鐘跺喌鏄庣粏
+ getProductStatisticsList: (params: any) => {
+ return request.get<ProductStatisticsVO[]>({
+ url: '/statistics/product/list',
+ params
+ })
+ },
+ // 瀵煎嚭鑾峰緱鍟嗗搧鐘跺喌鏄庣粏 Excel
+ exportProductStatisticsExcel: (params: any) => {
+ return request.download({
+ url: '/statistics/product/export-excel',
+ params
+ })
+ },
+ // 鑾峰緱鍟嗗搧鎺掕姒滃垎椤�
+ getProductStatisticsRankPage: async (params: any) => {
+ return await request.get({
+ url: `/statistics/product/rank-page`,
+ params
+ })
+ }
+}
diff --git a/src/api/mall/statistics/trade.ts b/src/api/mall/statistics/trade.ts
new file mode 100644
index 0000000..e59952a
--- /dev/null
+++ b/src/api/mall/statistics/trade.ts
@@ -0,0 +1,119 @@
+import request from '@/config/axios'
+import dayjs from 'dayjs'
+import { formatDate } from '@/utils/formatTime'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+
+/** 浜ゆ槗缁熻 Response VO */
+export interface TradeSummaryRespVO {
+ yesterdayOrderCount: number
+ monthOrderCount: number
+ yesterdayPayPrice: number
+ monthPayPrice: number
+}
+
+/** 浜ゆ槗鐘跺喌 Request VO */
+export interface TradeTrendReqVO {
+ times: [dayjs.ConfigType, dayjs.ConfigType]
+}
+
+/** 浜ゆ槗鐘跺喌缁熻 Response VO */
+export interface TradeTrendSummaryRespVO {
+ time: string
+ turnoverPrice: number
+ orderPayPrice: number
+ rechargePrice: number
+ expensePrice: number
+ walletPayPrice: number
+ brokerageSettlementPrice: number
+ afterSaleRefundPrice: number
+}
+
+/** 浜ゆ槗璁㈠崟鏁伴噺 Response VO */
+export interface TradeOrderCountRespVO {
+ /** 寰呭彂璐� */
+ undelivered?: number
+ /** 寰呮牳閿� */
+ pickUp?: number
+ /** 閫�娆句腑 */
+ afterSaleApply?: number
+ /** 鎻愮幇寰呭鏍� */
+ auditingWithdraw?: number
+}
+
+/** 浜ゆ槗璁㈠崟缁熻 Response VO */
+export interface TradeOrderSummaryRespVO {
+ /** 鏀粯璁㈠崟鍟嗗搧鏁� */
+ orderPayCount?: number
+ /** 鎬绘敮浠橀噾棰濓紝鍗曚綅锛氬垎 */
+ orderPayPrice?: number
+}
+
+/** 璁㈠崟閲忚秼鍔跨粺璁� Response VO */
+export interface TradeOrderTrendRespVO {
+ /** 鏃ユ湡 */
+ date: string
+ /** 璁㈠崟鏁伴噺 */
+ orderPayCount: number
+ /** 璁㈠崟鏀粯閲戦 */
+ orderPayPrice: number
+}
+
+// 鏌ヨ浜ゆ槗缁熻
+export const getTradeStatisticsSummary = () => {
+ return request.get<DataComparisonRespVO<TradeSummaryRespVO>>({
+ url: '/statistics/trade/summary'
+ })
+}
+
+// 鑾峰緱浜ゆ槗鐘跺喌缁熻
+export const getTradeStatisticsAnalyse = (params: TradeTrendReqVO) => {
+ return request.get<DataComparisonRespVO<TradeTrendSummaryRespVO>>({
+ url: '/statistics/trade/analyse',
+ params: formatDateParam(params)
+ })
+}
+
+// 鑾峰緱浜ゆ槗鐘跺喌鏄庣粏
+export const getTradeStatisticsList = (params: TradeTrendReqVO) => {
+ return request.get<TradeTrendSummaryRespVO[]>({
+ url: '/statistics/trade/list',
+ params: formatDateParam(params)
+ })
+}
+
+// 瀵煎嚭浜ゆ槗鐘跺喌鏄庣粏
+export const exportTradeStatisticsExcel = (params: TradeTrendReqVO) => {
+ return request.download({
+ url: '/statistics/trade/export-excel',
+ params: formatDateParam(params)
+ })
+}
+
+// 鑾峰緱浜ゆ槗璁㈠崟鏁伴噺
+export const getOrderCount = async () => {
+ return await request.get<TradeOrderCountRespVO>({ url: `/statistics/trade/order-count` })
+}
+
+// 鑾峰緱浜ゆ槗璁㈠崟鏁伴噺瀵圭収
+export const getOrderComparison = async () => {
+ return await request.get<DataComparisonRespVO<TradeOrderSummaryRespVO>>({
+ url: `/statistics/trade/order-comparison`
+ })
+}
+
+// 鑾峰緱璁㈠崟閲忚秼鍔跨粺璁�
+export const getOrderCountTrendComparison = (
+ type: number,
+ beginTime: dayjs.ConfigType,
+ endTime: dayjs.ConfigType
+) => {
+ return request.get<DataComparisonRespVO<TradeOrderTrendRespVO>[]>({
+ url: '/statistics/trade/order-count-trend',
+ params: { type, beginTime: formatDate(beginTime), endTime: formatDate(endTime) }
+ })
+}
+
+/** 鏃堕棿鍙傛暟闇�瑕佹牸寮忓寲, 纭繚鎺ュ彛鑳借瘑鍒� */
+const formatDateParam = (params: TradeTrendReqVO) => {
+ return { times: [formatDate(params.times[0]), formatDate(params.times[1])] } as TradeTrendReqVO
+}
diff --git a/src/api/mall/trade/afterSale/index.ts b/src/api/mall/trade/afterSale/index.ts
new file mode 100644
index 0000000..a109ee6
--- /dev/null
+++ b/src/api/mall/trade/afterSale/index.ts
@@ -0,0 +1,75 @@
+import request from '@/config/axios'
+
+export interface TradeAfterSaleVO {
+ id?: number | null // 鍞悗缂栧彿锛屼富閿嚜澧�
+ no?: string // 鍞悗鍗曞彿
+ status?: number | null // 閫�娆剧姸鎬�
+ way?: number | null // 鍞悗鏂瑰紡
+ type?: number | null // 鍞悗绫诲瀷
+ userId?: number | null // 鐢ㄦ埛缂栧彿
+ applyReason?: string // 鐢宠鍘熷洜
+ applyDescription?: string // 琛ュ厖鎻忚堪
+ applyPicUrls?: string[] // 琛ュ厖鍑瘉鍥剧墖
+ orderId?: number | null // 浜ゆ槗璁㈠崟缂栧彿
+ orderNo?: string // 璁㈠崟娴佹按鍙�
+ orderItemId?: number | null // 浜ゆ槗璁㈠崟椤圭紪鍙�
+ spuId?: number | null // 鍟嗗搧 SPU 缂栧彿
+ spuName?: string // 鍟嗗搧 SPU 鍚嶇О
+ skuId?: number | null // 鍟嗗搧 SKU 缂栧彿
+ properties?: ProductPropertiesVO[] // 灞炴�ф暟缁�
+ picUrl?: string // 鍟嗗搧鍥剧墖
+ count?: number | null // 閫�璐у晢鍝佹暟閲�
+ auditTime?: Date // 瀹℃壒鏃堕棿
+ auditUserId?: number | null // 瀹℃壒浜�
+ auditReason?: string // 瀹℃壒澶囨敞
+ refundPrice?: number | null // 閫�娆鹃噾棰濓紝鍗曚綅锛氬垎銆�
+ payRefundId?: number | null // 鏀粯閫�娆剧紪鍙�
+ refundTime?: Date // 閫�娆炬椂闂�
+ logisticsId?: number | null // 閫�璐х墿娴佸叕鍙哥紪鍙�
+ logisticsNo?: string // 閫�璐х墿娴佸崟鍙�
+ deliveryTime?: Date // 閫�璐ф椂闂�
+ receiveTime?: Date // 鏀惰揣鏃堕棿
+ receiveReason?: string // 鏀惰揣澶囨敞
+}
+
+export interface ProductPropertiesVO {
+ propertyId?: number | null // 灞炴�х殑缂栧彿
+ propertyName?: string // 灞炴�х殑鍚嶇О
+ valueId?: number | null //灞炴�у�肩殑缂栧彿
+ valueName?: string // 灞炴�у�肩殑鍚嶇О
+}
+
+// 鑾峰緱浜ゆ槗鍞悗鍒嗛〉
+export const getAfterSalePage = async (params) => {
+ return await request.get({ url: `/trade/after-sale/page`, params })
+}
+
+// 鑾峰緱浜ゆ槗鍞悗璇︽儏
+export const getAfterSale = async (id: any) => {
+ return await request.get({ url: `/trade/after-sale/get-detail?id=${id}` })
+}
+
+// 鍚屾剰鍞悗
+export const agree = async (id: any) => {
+ return await request.put({ url: `/trade/after-sale/agree?id=${id}` })
+}
+
+// 鎷掔粷鍞悗
+export const disagree = async (data: any) => {
+ return await request.put({ url: `/trade/after-sale/disagree`, data })
+}
+
+// 纭鏀惰揣
+export const receive = async (id: any) => {
+ return await request.put({ url: `/trade/after-sale/receive?id=${id}` })
+}
+
+// 鎷掔粷鏀惰揣
+export const refuse = async (id: any) => {
+ return await request.put({ url: `/trade/after-sale/refuse?id=${id}` })
+}
+
+// 纭閫�娆�
+export const refund = async (id: any) => {
+ return await request.put({ url: `/trade/after-sale/refund?id=${id}` })
+}
diff --git a/src/api/mall/trade/brokerage/record/index.ts b/src/api/mall/trade/brokerage/record/index.ts
new file mode 100644
index 0000000..7df9a22
--- /dev/null
+++ b/src/api/mall/trade/brokerage/record/index.ts
@@ -0,0 +1,11 @@
+import request from '@/config/axios'
+
+// 鏌ヨ浣i噾璁板綍鍒楄〃
+export const getBrokerageRecordPage = async (params: any) => {
+ return await request.get({ url: `/trade/brokerage-record/page`, params })
+}
+
+// 鏌ヨ浣i噾璁板綍璇︽儏
+export const getBrokerageRecord = async (id: number) => {
+ return await request.get({ url: `/trade/brokerage-record/get?id=` + id })
+}
diff --git a/src/api/mall/trade/brokerage/user/index.ts b/src/api/mall/trade/brokerage/user/index.ts
new file mode 100644
index 0000000..8ed6977
--- /dev/null
+++ b/src/api/mall/trade/brokerage/user/index.ts
@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+export interface BrokerageUserVO {
+ id: number
+ bindUserId: number
+ bindUserTime: Date
+ brokerageEnabled: boolean
+ brokerageTime: Date
+ price: number
+ frozenPrice: number
+
+ nickname: string
+ avatar: string
+}
+
+// 鍒涘缓鍒嗛攢鐢ㄦ埛
+export const createBrokerageUser = (data: any) => {
+ return request.post({ url: '/trade/brokerage-user/create', data })
+}
+
+// 鏌ヨ鍒嗛攢鐢ㄦ埛鍒楄〃
+export const getBrokerageUserPage = async (params: any) => {
+ return await request.get({ url: `/trade/brokerage-user/page`, params })
+}
+
+// 鏌ヨ鍒嗛攢鐢ㄦ埛璇︽儏
+export const getBrokerageUser = async (id: number) => {
+ return await request.get({ url: `/trade/brokerage-user/get?id=` + id })
+}
+
+// 淇敼鎺ㄥ箍鍛�
+export const updateBindUser = async (data: any) => {
+ return await request.put({ url: `/trade/brokerage-user/update-bind-user`, data })
+}
+
+// 娓呴櫎鎺ㄥ箍鍛�
+export const clearBindUser = async (data: any) => {
+ return await request.put({ url: `/trade/brokerage-user/clear-bind-user`, data })
+}
+
+// 淇敼鎺ㄥ箍璧勬牸
+export const updateBrokerageEnabled = async (data: any) => {
+ return await request.put({ url: `/trade/brokerage-user/update-brokerage-enable`, data })
+}
diff --git a/src/api/mall/trade/brokerage/withdraw/index.ts b/src/api/mall/trade/brokerage/withdraw/index.ts
new file mode 100644
index 0000000..8175272
--- /dev/null
+++ b/src/api/mall/trade/brokerage/withdraw/index.ts
@@ -0,0 +1,43 @@
+import request from '@/config/axios'
+
+export interface BrokerageWithdrawVO {
+ id: number
+ userId: number
+ price: number
+ feePrice: number
+ totalPrice: number
+ type: number
+ userName: string
+ userAccount: string
+ bankName: string
+ bankAddress: string
+ qrCodeUrl: string
+ status: number
+ auditReason: string
+ auditTime: Date
+ remark: string
+ payTransferId?: number
+ transferChannelCode?: string
+ transferTime?: Date
+ transferErrorMsg?: string
+}
+
+// 鏌ヨ浣i噾鎻愮幇鍒楄〃
+export const getBrokerageWithdrawPage = async (params: any) => {
+ return await request.get({ url: `/trade/brokerage-withdraw/page`, params })
+}
+
+// 鏌ヨ浣i噾鎻愮幇璇︽儏
+export const getBrokerageWithdraw = async (id: number) => {
+ return await request.get({ url: `/trade/brokerage-withdraw/get?id=` + id })
+}
+
+// 浣i噾鎻愮幇 - 閫氳繃鐢宠
+export const approveBrokerageWithdraw = async (id: number) => {
+ return await request.put({ url: `/trade/brokerage-withdraw/approve?id=` + id })
+}
+
+// 瀹℃牳浣i噾鎻愮幇 - 椹冲洖鐢宠
+export const rejectBrokerageWithdraw = async (data: BrokerageWithdrawVO) => {
+ return await request.put({ url: `/trade/brokerage-withdraw/reject`, data })
+}
diff --git a/src/api/mall/trade/config/index.ts b/src/api/mall/trade/config/index.ts
new file mode 100644
index 0000000..43fdbdf
--- /dev/null
+++ b/src/api/mall/trade/config/index.ts
@@ -0,0 +1,23 @@
+import request from '@/config/axios'
+
+export interface ConfigVO {
+ brokerageEnabled: boolean
+ brokerageEnabledCondition: number
+ brokerageBindMode: number
+ brokeragePosterUrls: string
+ brokerageFirstPercent: number
+ brokerageSecondPercent: number
+ brokerageWithdrawMinPrice: number
+ brokerageFrozenDays: number
+ brokerageWithdrawTypes: string
+}
+
+// 鏌ヨ浜ゆ槗涓績閰嶇疆璇︽儏
+export const getTradeConfig = async () => {
+ return await request.get({ url: `/trade/config/get` })
+}
+
+// 淇濆瓨浜ゆ槗涓績閰嶇疆
+export const saveTradeConfig = async (data: ConfigVO) => {
+ return await request.put({ url: `/trade/config/save`, data })
+}
diff --git a/src/api/mall/trade/delivery/express/index.ts b/src/api/mall/trade/delivery/express/index.ts
new file mode 100644
index 0000000..0070bcd
--- /dev/null
+++ b/src/api/mall/trade/delivery/express/index.ts
@@ -0,0 +1,45 @@
+import request from '@/config/axios'
+
+export interface DeliveryExpressVO {
+ id: number
+ code: string
+ name: string
+ logo: string
+ sort: number
+ status: number
+}
+
+// 鏌ヨ蹇�掑叕鍙稿垪琛�
+export const getDeliveryExpressPage = async (params: PageParam) => {
+ return await request.get({ url: '/trade/delivery/express/page', params })
+}
+
+// 鏌ヨ蹇�掑叕鍙歌鎯�
+export const getDeliveryExpress = async (id: number) => {
+ return await request.get({ url: '/trade/delivery/express/get?id=' + id })
+}
+
+// 鑾峰緱蹇�掑叕鍙哥簿绠�淇℃伅鍒楄〃
+export const getSimpleDeliveryExpressList = () => {
+ return request.get({ url: '/trade/delivery/express/list-all-simple' })
+}
+
+// 鏂板蹇�掑叕鍙�
+export const createDeliveryExpress = async (data: DeliveryExpressVO) => {
+ return await request.post({ url: '/trade/delivery/express/create', data })
+}
+
+// 淇敼蹇�掑叕鍙�
+export const updateDeliveryExpress = async (data: DeliveryExpressVO) => {
+ return await request.put({ url: '/trade/delivery/express/update', data })
+}
+
+// 鍒犻櫎蹇�掑叕鍙�
+export const deleteDeliveryExpress = async (id: number) => {
+ return await request.delete({ url: '/trade/delivery/express/delete?id=' + id })
+}
+
+// 瀵煎嚭蹇�掑叕鍙� Excel
+export const exportDeliveryExpressApi = async (params) => {
+ return await request.download({ url: '/trade/delivery/express/export-excel', params })
+}
diff --git a/src/api/mall/trade/delivery/expressTemplate/index.ts b/src/api/mall/trade/delivery/expressTemplate/index.ts
new file mode 100644
index 0000000..9ed23bc
--- /dev/null
+++ b/src/api/mall/trade/delivery/expressTemplate/index.ts
@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+export interface DeliveryExpressTemplateVO {
+ id: number
+ name: string
+ chargeMode: number
+ sort: number
+ templateCharge: ExpressTemplateChargeVO[]
+ templateFree: ExpressTemplateFreeVO[]
+}
+
+export declare type ExpressTemplateChargeVO = {
+ areaIds: number[]
+ startCount: number
+ startPrice: number
+ extraCount: number
+ extraPrice: number
+}
+
+export declare type ExpressTemplateFreeVO = {
+ areaIds: number[]
+ freeCount: number
+ freePrice: number
+}
+
+// 鏌ヨ蹇�掕繍璐规ā鏉垮垪琛�
+export const getDeliveryExpressTemplatePage = async (params: PageParam) => {
+ return await request.get({ url: '/trade/delivery/express-template/page', params })
+}
+
+// 鏌ヨ蹇�掕繍璐规ā鏉胯鎯�
+export const getDeliveryExpressTemplate = async (id: number) => {
+ return await request.get({ url: '/trade/delivery/express-template/get?id=' + id })
+}
+
+// 鏌ヨ蹇�掕繍璐规ā鏉胯鎯�
+export const getSimpleTemplateList = async () => {
+ return await request.get({ url: '/trade/delivery/express-template/list-all-simple' })
+}
+
+// 鏂板蹇�掕繍璐规ā鏉�
+export const createDeliveryExpressTemplate = async (data: DeliveryExpressTemplateVO) => {
+ return await request.post({ url: '/trade/delivery/express-template/create', data })
+}
+
+// 淇敼蹇�掕繍璐规ā鏉�
+export const updateDeliveryExpressTemplate = async (data: DeliveryExpressTemplateVO) => {
+ return await request.put({ url: '/trade/delivery/express-template/update', data })
+}
+
+// 鍒犻櫎蹇�掕繍璐规ā鏉�
+export const deleteDeliveryExpressTemplate = async (id: number) => {
+ return await request.delete({ url: '/trade/delivery/express-template/delete?id=' + id })
+}
diff --git a/src/api/mall/trade/delivery/pickUpStore/index.ts b/src/api/mall/trade/delivery/pickUpStore/index.ts
new file mode 100644
index 0000000..ea6c852
--- /dev/null
+++ b/src/api/mall/trade/delivery/pickUpStore/index.ts
@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface DeliveryPickUpStoreVO {
+ id: number
+ name: string
+ introduction: string
+ phone: string
+ areaId: number
+ detailAddress: string
+ logo: string
+ openingTime: string
+ closingTime: string
+ latitude: number
+ longitude: number
+ status: number
+ verifyUserIds: number[] // 缁戝畾鐢ㄦ埛缂栧彿缁勬暟
+}
+
+// 鏌ヨ鑷彁闂ㄥ簵鍒楄〃
+export const getDeliveryPickUpStorePage = async (params: any) => {
+ return await request.get({ url: '/trade/delivery/pick-up-store/page', params })
+}
+
+// 鏌ヨ鑷彁闂ㄥ簵璇︽儏
+export const getDeliveryPickUpStore = async (id: number) => {
+ return await request.get({ url: '/trade/delivery/pick-up-store/get?id=' + id })
+}
+
+// 鏌ヨ鑷彁闂ㄥ簵绮剧畝鍒楄〃
+export const getSimpleDeliveryPickUpStoreList = async (): Promise<DeliveryPickUpStoreVO[]> => {
+ return await request.get({ url: '/trade/delivery/pick-up-store/simple-list' })
+}
+
+// 鏂板鑷彁闂ㄥ簵
+export const createDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => {
+ return await request.post({ url: '/trade/delivery/pick-up-store/create', data })
+}
+
+// 淇敼鑷彁闂ㄥ簵
+export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => {
+ return await request.put({ url: '/trade/delivery/pick-up-store/update', data })
+}
+
+// 鍒犻櫎鑷彁闂ㄥ簵
+export const deleteDeliveryPickUpStore = async (id: number) => {
+ return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id })
+}
+
+// 缁戝畾鑷彁搴楀憳
+export const bindStoreStaffId = async (data: any) => {
+ return await request.post({ url: '/trade/delivery/pick-up-store/bind', data })
+}
diff --git a/src/api/mall/trade/order/index.ts b/src/api/mall/trade/order/index.ts
new file mode 100644
index 0000000..37fee8c
--- /dev/null
+++ b/src/api/mall/trade/order/index.ts
@@ -0,0 +1,188 @@
+import request from '@/config/axios'
+
+export interface OrderVO {
+ // ========== 璁㈠崟鍩烘湰淇℃伅 ==========
+ id?: number | null // 璁㈠崟缂栧彿
+ no?: string // 璁㈠崟娴佹按鍙�
+ createTime?: Date | null // 涓嬪崟鏃堕棿
+ type?: number | null // 璁㈠崟绫诲瀷
+ terminal?: number | null // 璁㈠崟鏉ユ簮
+ userId?: number | null // 鐢ㄦ埛缂栧彿
+ userIp?: string // 鐢ㄦ埛 IP
+ userRemark?: string // 鐢ㄦ埛澶囨敞
+ status?: number | null // 璁㈠崟鐘舵��
+ productCount?: number | null // 璐拱鐨勫晢鍝佹暟閲�
+ finishTime?: Date | null // 璁㈠崟瀹屾垚鏃堕棿
+ cancelTime?: Date | null // 璁㈠崟鍙栨秷鏃堕棿
+ cancelType?: number | null // 鍙栨秷绫诲瀷
+ remark?: string // 鍟嗗澶囨敞
+
+ // ========== 浠锋牸 + 鏀粯鍩烘湰淇℃伅 ==========
+ payOrderId?: number | null // 鏀粯璁㈠崟缂栧彿
+ payStatus?: boolean // 鏄惁宸叉敮浠�
+ payTime?: Date | null // 浠樻鏃堕棿
+ payChannelCode?: string // 鏀粯娓犻亾
+ totalPrice?: number | null // 鍟嗗搧鍘熶环锛堟�伙級
+ discountPrice?: number | null // 璁㈠崟浼樻儬锛堟�伙級
+ deliveryPrice?: number | null // 杩愯垂閲戦
+ adjustPrice?: number | null // 璁㈠崟璋冧环锛堟�伙級
+ payPrice?: number | null // 搴斾粯閲戦锛堟�伙級
+ // ========== 鏀朵欢 + 鐗╂祦鍩烘湰淇℃伅 ==========
+ deliveryType?: number | null // 鍙戣揣鏂瑰紡
+ pickUpStoreId?: number // 鑷彁闂ㄥ簵缂栧彿
+ pickUpVerifyCode?: string // 鑷彁鏍搁攢鐮�
+ deliveryTemplateId?: number | null // 閰嶉�佹ā鏉跨紪鍙�
+ logisticsId?: number | null // 鍙戣揣鐗╂祦鍏徃缂栧彿
+ logisticsNo?: string // 鍙戣揣鐗╂祦鍗曞彿
+ deliveryTime?: Date | null // 鍙戣揣鏃堕棿
+ receiveTime?: Date | null // 鏀惰揣鏃堕棿
+ receiverName?: string // 鏀朵欢浜哄悕绉�
+ receiverMobile?: string // 鏀朵欢浜烘墜鏈�
+ receiverPostCode?: number | null // 鏀朵欢浜洪偖缂�
+ receiverAreaId?: number | null // 鏀朵欢浜哄湴鍖虹紪鍙�
+ receiverAreaName?: string //鏀朵欢浜哄湴鍖哄悕瀛�
+ receiverDetailAddress?: string // 鏀朵欢浜鸿缁嗗湴鍧�
+
+ // ========== 鍞悗鍩烘湰淇℃伅 ==========
+ afterSaleStatus?: number | null // 鍞悗鐘舵��
+ refundPrice?: number | null // 閫�娆鹃噾棰�
+
+ // ========== 钀ラ攢鍩烘湰淇℃伅 ==========
+ couponId?: number | null // 浼樻儬鍔电紪鍙�
+ couponPrice?: number | null // 浼樻儬鍔靛噺鍏嶉噾棰�
+ pointPrice?: number | null // 绉垎鎶垫墸鐨勯噾棰�
+ vipPrice?: number | null // VIP 鍑忓厤閲戦
+
+ items?: OrderItemRespVO[] // 璁㈠崟椤瑰垪琛�
+ // 涓嬪崟鐢ㄦ埛淇℃伅
+ user?: {
+ id?: number | null
+ nickname?: string
+ avatar?: string
+ }
+ // 鎺ㄥ箍鐢ㄦ埛淇℃伅
+ brokerageUser?: {
+ id?: number | null
+ nickname?: string
+ avatar?: string
+ }
+ // 璁㈠崟鎿嶄綔鏃ュ織
+ logs?: OrderLogRespVO[]
+}
+
+export interface OrderLogRespVO {
+ content?: string
+ createTime?: Date
+ userType?: number
+}
+
+export interface OrderItemRespVO {
+ // ========== 璁㈠崟椤瑰熀鏈俊鎭� ==========
+ id?: number | null // 缂栧彿
+ userId?: number | null // 鐢ㄦ埛缂栧彿
+ orderId?: number | null // 璁㈠崟缂栧彿
+ // ========== 鍟嗗搧鍩烘湰淇℃伅 ==========
+ spuId?: number | null // 鍟嗗搧 SPU 缂栧彿
+ spuName?: string //鍟嗗搧 SPU 鍚嶇О
+ skuId?: number | null // 鍟嗗搧 SKU 缂栧彿
+ picUrl?: string //鍟嗗搧鍥剧墖
+ count?: number | null //璐拱鏁伴噺
+ // ========== 浠锋牸 + 鏀粯鍩烘湰淇℃伅 ==========
+ originalPrice?: number | null //鍟嗗搧鍘熶环锛堟�伙級
+ originalUnitPrice?: number | null //鍟嗗搧鍘熶环锛堝崟锛�
+ discountPrice?: number | null //鍟嗗搧浼樻儬锛堟�伙級
+ payPrice?: number | null //鍟嗗搧瀹炰粯閲戦锛堟�伙級
+ orderPartPrice?: number | null //瀛愯鍗曞垎鎽婇噾棰濓紙鎬伙級
+ orderDividePrice?: number | null //鍒嗘憡鍚庡瓙璁㈠崟瀹炰粯閲戦锛堟�伙級
+ // ========== 钀ラ攢鍩烘湰淇℃伅 ==========
+ // TODO 鑺嬭壙锛氬湪鎹夋懜涓�涓�
+ // ========== 鍞悗鍩烘湰淇℃伅 ==========
+ afterSaleStatus?: number | null // 鍞悗鐘舵��
+ properties?: ProductPropertiesVO[] //灞炴�ф暟缁�
+}
+
+export interface ProductPropertiesVO {
+ propertyId?: number | null // 灞炴�х殑缂栧彿
+ propertyName?: string // 灞炴�х殑鍚嶇О
+ valueId?: number | null //灞炴�у�肩殑缂栧彿
+ valueName?: string // 灞炴�у�肩殑鍚嶇О
+}
+
+/** 浜ゆ槗璁㈠崟缁熻 */
+export interface TradeOrderSummaryRespVO {
+ /** 璁㈠崟鏁伴噺 */
+ orderCount?: number
+ /** 璁㈠崟閲戦 */
+ orderPayPrice?: string
+ /** 閫�娆惧崟鏁� */
+ afterSaleCount?: number
+ /** 閫�娆鹃噾棰� */
+ afterSalePrice?: string
+}
+
+// 鏌ヨ浜ゆ槗璁㈠崟鍒楄〃
+export const getOrderPage = async (params: any) => {
+ return await request.get({ url: `/trade/order/page`, params })
+}
+
+// 鏌ヨ浜ゆ槗璁㈠崟缁熻
+export const getOrderSummary = async (params: any) => {
+ return await request.get<TradeOrderSummaryRespVO>({ url: `/trade/order/summary`, params })
+}
+
+// 鏌ヨ浜ゆ槗璁㈠崟璇︽儏
+export const getOrder = async (id: number | null) => {
+ return await request.get({ url: `/trade/order/get-detail?id=` + id })
+}
+
+// 鏌ヨ浜ゆ槗璁㈠崟鐗╂祦璇︽儏
+export const getExpressTrackList = async (id: number | null) => {
+ return await request.get({ url: `/trade/order/get-express-track-list?id=` + id })
+}
+
+export interface DeliveryVO {
+ id?: number // 璁㈠崟缂栧彿
+ logisticsId: number | null // 鐗╂祦鍏徃缂栧彿
+ logisticsNo: string // 鐗╂祦缂栧彿
+}
+
+// 璁㈠崟鍙戣揣
+export const deliveryOrder = async (data: DeliveryVO) => {
+ return await request.put({ url: `/trade/order/delivery`, data })
+}
+
+// 璁㈠崟澶囨敞
+export const updateOrderRemark = async (data: any) => {
+ return await request.put({ url: `/trade/order/update-remark`, data })
+}
+
+// 璁㈠崟璋冧环
+export const updateOrderPrice = async (data: any) => {
+ return await request.put({ url: `/trade/order/update-price`, data })
+}
+
+// 淇敼璁㈠崟鍦板潃
+export const updateOrderAddress = async (data: any) => {
+ return await request.put({ url: `/trade/order/update-address`, data })
+}
+
+// 璁㈠崟鏍搁攢
+export const pickUpOrder = async (id: number) => {
+ return await request.put({ url: `/trade/order/pick-up-by-id?id=${id}` })
+}
+
+// 璁㈠崟鏍搁攢
+export const pickUpOrderByVerifyCode = async (pickUpVerifyCode: string) => {
+ return await request.put({
+ url: `/trade/order/pick-up-by-verify-code`,
+ params: { pickUpVerifyCode }
+ })
+}
+
+// 鏌ヨ鏍搁攢鐮佸搴旂殑璁㈠崟
+export const getOrderByPickUpVerifyCode = async (pickUpVerifyCode: string) => {
+ return await request.get<OrderVO>({
+ url: `/trade/order/get-by-pick-up-verify-code`,
+ params: { pickUpVerifyCode }
+ })
+}
diff --git a/src/api/member/address/index.ts b/src/api/member/address/index.ts
new file mode 100644
index 0000000..a914f97
--- /dev/null
+++ b/src/api/member/address/index.ts
@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+
+export interface AddressVO {
+ id: number
+ name: string
+ mobile: string
+ areaId: number
+ detailAddress: string
+ defaultStatus: boolean
+}
+
+// 鏌ヨ鐢ㄦ埛鏀朵欢鍦板潃鍒楄〃
+export const getAddressList = async (params) => {
+ return await request.get({ url: `/member/address/list`, params })
+}
diff --git a/src/api/member/config/index.ts b/src/api/member/config/index.ts
new file mode 100644
index 0000000..7ddca16
--- /dev/null
+++ b/src/api/member/config/index.ts
@@ -0,0 +1,19 @@
+import request from '@/config/axios'
+
+export interface ConfigVO {
+ id: number
+ pointTradeDeductEnable: number
+ pointTradeDeductUnitPrice: number
+ pointTradeDeductMaxPrice: number
+ pointTradeGivePoint: number
+}
+
+// 鏌ヨ绉垎璁剧疆璇︽儏
+export const getConfig = async () => {
+ return await request.get({ url: `/member/config/get` })
+}
+
+// 鏂板淇敼绉垎璁剧疆
+export const saveConfig = async (data: ConfigVO) => {
+ return await request.put({ url: `/member/config/save`, data })
+}
diff --git a/src/api/member/experience-record/index.ts b/src/api/member/experience-record/index.ts
new file mode 100644
index 0000000..6d40a48
--- /dev/null
+++ b/src/api/member/experience-record/index.ts
@@ -0,0 +1,22 @@
+import request from '@/config/axios'
+
+export interface ExperienceRecordVO {
+ id: number
+ userId: number
+ bizId: string
+ bizType: number
+ title: string
+ description: string
+ experience: number
+ totalExperience: number
+}
+
+// 鏌ヨ浼氬憳缁忛獙璁板綍鍒楄〃
+export const getExperienceRecordPage = async (params) => {
+ return await request.get({ url: `/member/experience-record/page`, params })
+}
+
+// 鏌ヨ浼氬憳缁忛獙璁板綍璇︽儏
+export const getExperienceRecord = async (id: number) => {
+ return await request.get({ url: `/member/experience-record/get?id=` + id })
+}
diff --git a/src/api/member/group/index.ts b/src/api/member/group/index.ts
new file mode 100644
index 0000000..df3054e
--- /dev/null
+++ b/src/api/member/group/index.ts
@@ -0,0 +1,38 @@
+import request from '@/config/axios'
+
+export interface GroupVO {
+ id: number
+ name: string
+ remark: string
+ status: number
+}
+
+// 鏌ヨ鐢ㄦ埛鍒嗙粍鍒楄〃
+export const getGroupPage = async (params: any) => {
+ return await request.get({ url: `/member/group/page`, params })
+}
+
+// 鏌ヨ鐢ㄦ埛鍒嗙粍璇︽儏
+export const getGroup = async (id: number) => {
+ return await request.get({ url: `/member/group/get?id=` + id })
+}
+
+// 鏂板鐢ㄦ埛鍒嗙粍
+export const createGroup = async (data: GroupVO) => {
+ return await request.post({ url: `/member/group/create`, data })
+}
+
+// 鏌ヨ鐢ㄦ埛鍒嗙粍 - 绮剧畝淇℃伅鍒楄〃
+export const getSimpleGroupList = async () => {
+ return await request.get({ url: `/member/group/list-all-simple` })
+}
+
+// 淇敼鐢ㄦ埛鍒嗙粍
+export const updateGroup = async (data: GroupVO) => {
+ return await request.put({ url: `/member/group/update`, data })
+}
+
+// 鍒犻櫎鐢ㄦ埛鍒嗙粍
+export const deleteGroup = async (id: number) => {
+ return await request.delete({ url: `/member/group/delete?id=` + id })
+}
diff --git a/src/api/member/level/index.ts b/src/api/member/level/index.ts
new file mode 100644
index 0000000..0ded493
--- /dev/null
+++ b/src/api/member/level/index.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+export interface LevelVO {
+ id: number
+ name: string
+ experience: number
+ value: number
+ discountPercent: number
+ icon: string
+ bgUrl: string
+ status: number
+}
+
+// 鏌ヨ浼氬憳绛夌骇鍒楄〃
+export const getLevelList = async (params) => {
+ return await request.get({ url: `/member/level/list`, params })
+}
+
+// 鏌ヨ浼氬憳绛夌骇璇︽儏
+export const getLevel = async (id: number) => {
+ return await request.get({ url: `/member/level/get?id=` + id })
+}
+
+// 鏌ヨ浼氬憳绛夌骇 - 绮剧畝淇℃伅鍒楄〃
+export const getSimpleLevelList = async () => {
+ return await request.get({ url: `/member/level/list-all-simple` })
+}
+
+// 鏂板浼氬憳绛夌骇
+export const createLevel = async (data: LevelVO) => {
+ return await request.post({ url: `/member/level/create`, data })
+}
+
+// 淇敼浼氬憳绛夌骇
+export const updateLevel = async (data: LevelVO) => {
+ return await request.put({ url: `/member/level/update`, data })
+}
+
+// 鍒犻櫎浼氬憳绛夌骇
+export const deleteLevel = async (id: number) => {
+ return await request.delete({ url: `/member/level/delete?id=` + id })
+}
diff --git a/src/api/member/point/record/index.ts b/src/api/member/point/record/index.ts
new file mode 100644
index 0000000..f47ae46
--- /dev/null
+++ b/src/api/member/point/record/index.ts
@@ -0,0 +1,18 @@
+import request from '@/config/axios'
+
+export interface RecordVO {
+ id: number
+ bizId: string
+ bizType: string
+ title: string
+ description: string
+ point: number
+ totalPoint: number
+ userId: number
+ createDate: Date
+}
+
+// 鏌ヨ鐢ㄦ埛绉垎璁板綍鍒楄〃
+export const getRecordPage = async (params) => {
+ return await request.get({ url: `/member/point/record/page`, params })
+}
diff --git a/src/api/member/signin/config/index.ts b/src/api/member/signin/config/index.ts
new file mode 100644
index 0000000..50a7d63
--- /dev/null
+++ b/src/api/member/signin/config/index.ts
@@ -0,0 +1,34 @@
+import request from '@/config/axios'
+
+export interface SignInConfigVO {
+ id?: number
+ day?: number
+ point?: number
+ experience?: number
+ status?: number
+}
+
+// 鏌ヨ绉垎绛惧埌瑙勫垯鍒楄〃
+export const getSignInConfigList = async () => {
+ return await request.get({ url: `/member/sign-in/config/list` })
+}
+
+// 鏌ヨ绉垎绛惧埌瑙勫垯璇︽儏
+export const getSignInConfig = async (id: number) => {
+ return await request.get({ url: `/member/sign-in/config/get?id=` + id })
+}
+
+// 鏂板绉垎绛惧埌瑙勫垯
+export const createSignInConfig = async (data: SignInConfigVO) => {
+ return await request.post({ url: `/member/sign-in/config/create`, data })
+}
+
+// 淇敼绉垎绛惧埌瑙勫垯
+export const updateSignInConfig = async (data: SignInConfigVO) => {
+ return await request.put({ url: `/member/sign-in/config/update`, data })
+}
+
+// 鍒犻櫎绉垎绛惧埌瑙勫垯
+export const deleteSignInConfig = async (id: number) => {
+ return await request.delete({ url: `/member/sign-in/config/delete?id=` + id })
+}
diff --git a/src/api/member/signin/record/index.ts b/src/api/member/signin/record/index.ts
new file mode 100644
index 0000000..7d13702
--- /dev/null
+++ b/src/api/member/signin/record/index.ts
@@ -0,0 +1,13 @@
+import request from '@/config/axios'
+
+export interface SignInRecordVO {
+ id: number
+ userId: number
+ day: number
+ point: number
+}
+
+// 鏌ヨ鐢ㄦ埛绛惧埌绉垎鍒楄〃
+export const getSignInRecordPage = async (params) => {
+ return await request.get({ url: `/member/sign-in/record/page`, params })
+}
diff --git a/src/api/member/tag/index.ts b/src/api/member/tag/index.ts
new file mode 100644
index 0000000..7ff6e9b
--- /dev/null
+++ b/src/api/member/tag/index.ts
@@ -0,0 +1,36 @@
+import request from '@/config/axios'
+
+export interface TagVO {
+ id: number
+ name: string
+}
+
+// 鏌ヨ浼氬憳鏍囩鍒楄〃
+export const getMemberTagPage = async (params: any) => {
+ return await request.get({ url: `/member/tag/page`, params })
+}
+
+// 鏌ヨ浼氬憳鏍囩璇︽儏
+export const getMemberTag = async (id: number) => {
+ return await request.get({ url: `/member/tag/get?id=` + id })
+}
+
+// 鏌ヨ浼氬憳鏍囩 - 绮剧畝淇℃伅鍒楄〃
+export const getSimpleTagList = async () => {
+ return await request.get({ url: `/member/tag/list-all-simple` })
+}
+
+// 鏂板浼氬憳鏍囩
+export const createMemberTag = async (data: TagVO) => {
+ return await request.post({ url: `/member/tag/create`, data })
+}
+
+// 淇敼浼氬憳鏍囩
+export const updateMemberTag = async (data: TagVO) => {
+ return await request.put({ url: `/member/tag/update`, data })
+}
+
+// 鍒犻櫎浼氬憳鏍囩
+export const deleteMemberTag = async (id: number) => {
+ return await request.delete({ url: `/member/tag/delete?id=` + id })
+}
diff --git a/src/api/member/user/index.ts b/src/api/member/user/index.ts
new file mode 100644
index 0000000..1f8acf4
--- /dev/null
+++ b/src/api/member/user/index.ts
@@ -0,0 +1,48 @@
+import request from '@/config/axios'
+
+export interface UserVO {
+ id: number
+ avatar: string | undefined
+ birthday: number | undefined
+ createTime: number | undefined
+ loginDate: number | undefined
+ loginIp: string
+ mark: string
+ mobile: string
+ name: string | undefined
+ nickname: string | undefined
+ registerIp: string
+ sex: number
+ status: number
+ areaId: number | undefined
+ areaName: string | undefined
+ levelName: string | null
+ point: number | undefined | null
+ totalPoint: number | undefined | null
+ experience: number | null | undefined
+}
+
+// 鏌ヨ浼氬憳鐢ㄦ埛鍒楄〃
+export const getUserPage = async (params) => {
+ return await request.get({ url: `/member/user/page`, params })
+}
+
+// 鏌ヨ浼氬憳鐢ㄦ埛璇︽儏
+export const getUser = async (id: number) => {
+ return await request.get({ url: `/member/user/get?id=` + id })
+}
+
+// 淇敼浼氬憳鐢ㄦ埛
+export const updateUser = async (data: UserVO) => {
+ return await request.put({ url: `/member/user/update`, data })
+}
+
+// 淇敼浼氬憳鐢ㄦ埛绛夌骇
+export const updateUserLevel = async (data: any) => {
+ return await request.put({ url: `/member/user/update-level`, data })
+}
+
+// 淇敼浼氬憳鐢ㄦ埛绉垎
+export const updateUserPoint = async (data: any) => {
+ return await request.put({ url: `/member/user/update-point`, data })
+}
diff --git a/src/api/mp/account/index.ts b/src/api/mp/account/index.ts
new file mode 100644
index 0000000..e973cda
--- /dev/null
+++ b/src/api/mp/account/index.ts
@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+export interface AccountVO {
+ id: number
+ name: string
+}
+
+// 鍒涘缓鍏紬鍙疯处鍙�
+export const createAccount = async (data) => {
+ return await request.post({ url: '/mp/account/create', data })
+}
+
+// 鏇存柊鍏紬鍙疯处鍙�
+export const updateAccount = async (data) => {
+ return request.put({ url: '/mp/account/update', data: data })
+}
+
+// 鍒犻櫎鍏紬鍙疯处鍙�
+export const deleteAccount = async (id) => {
+ return request.delete({ url: '/mp/account/delete?id=' + id, method: 'delete' })
+}
+
+// 鑾峰緱鍏紬鍙疯处鍙�
+export const getAccount = async (id) => {
+ return request.get({ url: '/mp/account/get?id=' + id })
+}
+
+// 鑾峰緱鍏紬鍙疯处鍙峰垎椤�
+export const getAccountPage = async (query) => {
+ return request.get({ url: '/mp/account/page', params: query })
+}
+
+// 鑾峰彇鍏紬鍙疯处鍙风簿绠�淇℃伅鍒楄〃
+export const getSimpleAccountList = async () => {
+ return request.get({ url: '/mp/account/list-all-simple' })
+}
+
+// 鐢熸垚鍏紬鍙蜂簩缁寸爜
+export const generateAccountQrCode = async (id) => {
+ return request.put({ url: '/mp/account/generate-qr-code?id=' + id })
+}
+
+// 娓呯┖鍏紬鍙� API 閰嶉
+export const clearAccountQuota = async (id) => {
+ return request.put({ url: '/mp/account/clear-quota?id=' + id })
+}
diff --git a/src/api/mp/autoReply/index.ts b/src/api/mp/autoReply/index.ts
new file mode 100644
index 0000000..5045e6d
--- /dev/null
+++ b/src/api/mp/autoReply/index.ts
@@ -0,0 +1,39 @@
+import request from '@/config/axios'
+
+// 鍒涘缓鍏紬鍙风殑鑷姩鍥炲
+export const createAutoReply = (data) => {
+ return request.post({
+ url: '/mp/auto-reply/create',
+ data: data
+ })
+}
+
+// 鏇存柊鍏紬鍙风殑鑷姩鍥炲
+export const updateAutoReply = (data) => {
+ return request.put({
+ url: '/mp/auto-reply/update',
+ data: data
+ })
+}
+
+// 鍒犻櫎鍏紬鍙风殑鑷姩鍥炲
+export const deleteAutoReply = (id) => {
+ return request.delete({
+ url: '/mp/auto-reply/delete?id=' + id
+ })
+}
+
+// 鑾峰緱鍏紬鍙风殑鑷姩鍥炲
+export const getAutoReply = (id) => {
+ return request.get({
+ url: '/mp/auto-reply/get?id=' + id
+ })
+}
+
+// 鑾峰緱鍏紬鍙风殑鑷姩鍥炲鍒嗛〉
+export const getAutoReplyPage = (query) => {
+ return request.get({
+ url: '/mp/auto-reply/page',
+ params: query
+ })
+}
diff --git a/src/api/mp/draft/index.ts b/src/api/mp/draft/index.ts
new file mode 100644
index 0000000..ce6a443
--- /dev/null
+++ b/src/api/mp/draft/index.ts
@@ -0,0 +1,35 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鍏紬鍙疯崏绋垮垎椤�
+export const getDraftPage = (query) => {
+ return request.get({
+ url: '/mp/draft/page',
+ params: query
+ })
+}
+
+// 鍒涘缓鍏紬鍙疯崏绋�
+export const createDraft = (accountId, articles) => {
+ return request.post({
+ url: '/mp/draft/create?accountId=' + accountId,
+ data: {
+ articles
+ }
+ })
+}
+
+// 鏇存柊鍏紬鍙疯崏绋�
+export const updateDraft = (accountId, mediaId, articles) => {
+ return request.put({
+ url: '/mp/draft/update?accountId=' + accountId + '&mediaId=' + mediaId,
+ method: 'put',
+ data: articles
+ })
+}
+
+// 鍒犻櫎鍏紬鍙疯崏绋�
+export const deleteDraft = (accountId, mediaId) => {
+ return request.delete({
+ url: '/mp/draft/delete?accountId=' + accountId + '&mediaId=' + mediaId
+ })
+}
diff --git a/src/api/mp/freePublish/index.ts b/src/api/mp/freePublish/index.ts
new file mode 100644
index 0000000..beef026
--- /dev/null
+++ b/src/api/mp/freePublish/index.ts
@@ -0,0 +1,23 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鍏紬鍙风礌鏉愬垎椤�
+export const getFreePublishPage = (query) => {
+ return request.get({
+ url: '/mp/free-publish/page',
+ params: query
+ })
+}
+
+// 鍒犻櫎鍏紬鍙风礌鏉�
+export const deleteFreePublish = (accountId, articleId) => {
+ return request.delete({
+ url: '/mp/free-publish/delete?accountId=' + accountId + '&articleId=' + articleId
+ })
+}
+
+// 鍙戝竷鍏紬鍙风礌鏉�
+export const submitFreePublish = (accountId, mediaId) => {
+ return request.post({
+ url: '/mp/free-publish/submit?accountId=' + accountId + '&mediaId=' + mediaId
+ })
+}
diff --git a/src/api/mp/material/index.ts b/src/api/mp/material/index.ts
new file mode 100644
index 0000000..fcc37ab
--- /dev/null
+++ b/src/api/mp/material/index.ts
@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鍏紬鍙风礌鏉愬垎椤�
+export const getMaterialPage = (query) => {
+ return request.get({
+ url: '/mp/material/page',
+ params: query
+ })
+}
+
+// 鍒犻櫎鍏紬鍙锋案涔呯礌鏉�
+export const deletePermanentMaterial = (id) => {
+ return request.delete({
+ url: '/mp/material/delete-permanent?id=' + id
+ })
+}
diff --git a/src/api/mp/menu/index.ts b/src/api/mp/menu/index.ts
new file mode 100644
index 0000000..cc78647
--- /dev/null
+++ b/src/api/mp/menu/index.ts
@@ -0,0 +1,26 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鍏紬鍙疯彍鍗曞垪琛�
+export const getMenuList = (accountId) => {
+ return request.get({
+ url: '/mp/menu/list?accountId=' + accountId
+ })
+}
+
+// 淇濆瓨鍏紬鍙疯彍鍗�
+export const saveMenu = (accountId, menus) => {
+ return request.post({
+ url: '/mp/menu/save',
+ data: {
+ accountId,
+ menus
+ }
+ })
+}
+
+// 鍒犻櫎鍏紬鍙疯彍鍗�
+export const deleteMenu = (accountId) => {
+ return request.delete({
+ url: '/mp/menu/delete?accountId=' + accountId
+ })
+}
diff --git a/src/api/mp/message/index.ts b/src/api/mp/message/index.ts
new file mode 100644
index 0000000..ad9b95d
--- /dev/null
+++ b/src/api/mp/message/index.ts
@@ -0,0 +1,17 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鍏紬鍙锋秷鎭垎椤�
+export const getMessagePage = (query: PageParam) => {
+ return request.get({
+ url: '/mp/message/page',
+ params: query
+ })
+}
+
+// 缁欑矇涓濆彂閫佹秷鎭�
+export const sendMessage = (data) => {
+ return request.post({
+ url: '/mp/message/send',
+ data: data
+ })
+}
diff --git a/src/api/mp/messageTemplate/index.ts b/src/api/mp/messageTemplate/index.ts
new file mode 100644
index 0000000..e0d3032
--- /dev/null
+++ b/src/api/mp/messageTemplate/index.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+// 娑堟伅妯℃澘 VO
+export interface MsgTemplateVO {
+ id: number // 妯$増涓婚敭
+ accountId: number // 鍏紬鍙疯处鍙风殑缂栧彿
+ appId: string // appId
+ templateId: string // 鍏紬鍙锋ā鏉� ID
+ title: string // 鏍囬
+ content: string // 妯℃澘鍐呭
+ example: string // 妯℃澘绀轰緥
+ primaryIndustry: string // 妯℃澘鎵�灞炶涓氱殑涓�绾ц涓�
+ deputyIndustry: string // 妯℃澘鎵�灞炶涓氱殑浜岀骇琛屼笟
+ createTime: Date // 鍒涘缓鏃堕棿
+}
+
+// 鍙戦�佹秷鎭ā鏉胯姹� VO
+export interface MsgTemplateSendVO {
+ id: number // 妯℃澘缂栧彿
+ userId: number // 鐢ㄦ埛缂栧彿
+ data?: string // 妯℃澘鏁版嵁锛圝SON 鏍煎紡瀛楃涓诧級
+ url?: string // 璺宠浆閾炬帴
+ miniProgramAppId?: string // 灏忕▼搴� appId
+ miniProgramPagePath?: string // 灏忕▼搴忛〉闈㈣矾寰�
+ miniprogram?: string // 灏忕▼搴忎俊鎭紙JSON 鏍煎紡瀛楃涓诧級
+}
+
+// 鍏紬鍙锋秷鎭ā鏉� API
+export const MessageTemplateApi = {
+ // 鏌ヨ娑堟伅妯℃澘鍒嗛〉
+ getMessageTemplateList: async (params: any) => {
+ return await request.get({ url: `/mp/message-template/list`, params })
+ },
+
+ // 鍒犻櫎娑堟伅妯℃澘
+ deleteMessageTemplate: async (id: number) => {
+ return await request.delete({ url: `/mp/message-template/delete?id=` + id })
+ },
+
+ // 鍚屾鍏紬鍙锋ā鏉�
+ syncMessageTemplate: async (accountId: number) => {
+ return await request.post({ url: `/mp/message-template/sync?accountId=` + accountId })
+ },
+
+ // 鍙戦�佹秷鎭ā鏉�
+ sendMessageTemplate: async (data: MsgTemplateSendVO) => {
+ return await request.post({ url: `/mp/message-template/send`, data })
+ }
+}
diff --git a/src/api/mp/statistics/index.ts b/src/api/mp/statistics/index.ts
new file mode 100644
index 0000000..72cae60
--- /dev/null
+++ b/src/api/mp/statistics/index.ts
@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+// 鑾峰彇娑堟伅鍙戦�佹鍐垫暟鎹�
+export const getUpstreamMessage = (query) => {
+ return request.get({
+ url: '/mp/statistics/upstream-message',
+ params: query
+ })
+}
+
+// 鐢ㄦ埛澧炲噺鏁版嵁
+export const getUserSummary = (query) => {
+ return request.get({
+ url: '/mp/statistics/user-summary',
+ params: query
+ })
+}
+
+// 鑾峰緱鐢ㄦ埛绱鏁版嵁
+export const getUserCumulate = (query) => {
+ return request.get({
+ url: '/mp/statistics/user-cumulate',
+ params: query
+ })
+}
+
+// 鑾峰緱鎺ュ彛鍒嗘瀽鏁版嵁
+export const getInterfaceSummary = (query) => {
+ return request.get({
+ url: '/mp/statistics/interface-summary',
+ params: query
+ })
+}
diff --git a/src/api/mp/tag/index.ts b/src/api/mp/tag/index.ts
new file mode 100644
index 0000000..50183a5
--- /dev/null
+++ b/src/api/mp/tag/index.ts
@@ -0,0 +1,60 @@
+import request from '@/config/axios'
+
+export interface TagVO {
+ id?: number
+ name: string
+ accountId: number
+ createTime: Date
+}
+
+// 鍒涘缓鍏紬鍙锋爣绛�
+export const createTag = (data: TagVO) => {
+ return request.post({
+ url: '/mp/tag/create',
+ data: data
+ })
+}
+
+// 鏇存柊鍏紬鍙锋爣绛�
+export const updateTag = (data: TagVO) => {
+ return request.put({
+ url: '/mp/tag/update',
+ data: data
+ })
+}
+
+// 鍒犻櫎鍏紬鍙锋爣绛�
+export const deleteTag = (id: number) => {
+ return request.delete({
+ url: '/mp/tag/delete?id=' + id
+ })
+}
+
+// 鑾峰緱鍏紬鍙锋爣绛�
+export const getTag = (id: number) => {
+ return request.get({
+ url: '/mp/tag/get?id=' + id
+ })
+}
+
+// 鑾峰緱鍏紬鍙锋爣绛惧垎椤�
+export const getTagPage = (query: PageParam) => {
+ return request.get({
+ url: '/mp/tag/page',
+ params: query
+ })
+}
+
+// 鑾峰彇鍏紬鍙锋爣绛剧簿绠�淇℃伅鍒楄〃
+export const getSimpleTagList = () => {
+ return request.get({
+ url: '/mp/tag/list-all-simple'
+ })
+}
+
+// 鍚屾鍏紬鍙锋爣绛�
+export const syncTag = (accountId: number) => {
+ return request.post({
+ url: '/mp/tag/sync?accountId=' + accountId
+ })
+}
diff --git a/src/api/mp/user/index.ts b/src/api/mp/user/index.ts
new file mode 100644
index 0000000..b89acc7
--- /dev/null
+++ b/src/api/mp/user/index.ts
@@ -0,0 +1,31 @@
+import request from '@/config/axios'
+
+// 鏇存柊鍏紬鍙风矇涓�
+export const updateUser = (data) => {
+ return request.put({
+ url: '/mp/user/update',
+ data: data
+ })
+}
+
+// 鑾峰緱鍏紬鍙风矇涓�
+export const getUser = (id) => {
+ return request.get({
+ url: '/mp/user/get?id=' + id
+ })
+}
+
+// 鑾峰緱鍏紬鍙风矇涓濆垎椤�
+export const getUserPage = (query) => {
+ return request.get({
+ url: '/mp/user/page',
+ params: query
+ })
+}
+
+// 鍚屾鍏紬鍙风矇涓�
+export const syncUser = (accountId) => {
+ return request.post({
+ url: '/mp/user/sync?accountId=' + accountId
+ })
+}
diff --git a/src/api/pay/app/index.ts b/src/api/pay/app/index.ts
new file mode 100644
index 0000000..d6fa83c
--- /dev/null
+++ b/src/api/pay/app/index.ts
@@ -0,0 +1,68 @@
+import request from '@/config/axios'
+
+export interface AppVO {
+ id: number
+ appKey: string
+ name: string
+ status: number
+ remark: string
+ payNotifyUrl: string
+ refundNotifyUrl: string
+ transferNotifyUrl: string
+ merchantId: number
+ merchantName: string
+ createTime: Date
+}
+
+export interface AppPageReqVO extends PageParam {
+ name?: string
+ status?: number
+ remark?: string
+ payNotifyUrl?: string
+ refundNotifyUrl?: string
+ transferNotifyUrl?: string
+ merchantName?: string
+ createTime?: Date[]
+}
+
+export interface AppUpdateStatusReqVO {
+ id: number
+ status: number
+}
+
+// 鏌ヨ鍒楄〃鏀粯搴旂敤
+export const getAppPage = (params: AppPageReqVO) => {
+ return request.get({ url: '/pay/app/page', params })
+}
+
+// 鏌ヨ璇︽儏鏀粯搴旂敤
+export const getApp = (id: number) => {
+ return request.get({ url: '/pay/app/get?id=' + id })
+}
+
+// 鏂板鏀粯搴旂敤
+export const createApp = (data: AppVO) => {
+ return request.post({ url: '/pay/app/create', data })
+}
+
+// 淇敼鏀粯搴旂敤
+export const updateApp = (data: AppVO) => {
+ return request.put({ url: '/pay/app/update', data })
+}
+
+// 鏀粯搴旂敤淇℃伅鐘舵�佷慨鏀�
+export const changeAppStatus = (data: AppUpdateStatusReqVO) => {
+ return request.put({ url: '/pay/app/update-status', data: data })
+}
+
+// 鍒犻櫎鏀粯搴旂敤
+export const deleteApp = (id: number) => {
+ return request.delete({ url: '/pay/app/delete?id=' + id })
+}
+
+// 鑾峰緱鏀粯搴旂敤鍒楄〃
+export const getAppList = () => {
+ return request.get({
+ url: '/pay/app/list'
+ })
+}
diff --git a/src/api/pay/channel/index.ts b/src/api/pay/channel/index.ts
new file mode 100644
index 0000000..0f4ff42
--- /dev/null
+++ b/src/api/pay/channel/index.ts
@@ -0,0 +1,46 @@
+import request from '@/config/axios'
+
+export interface ChannelVO {
+ id: number
+ code: string
+ config: string
+ status: number
+ remark: string
+ feeRate: number
+ appId: number
+ createTime: Date
+}
+
+// 鏌ヨ鍒楄〃鏀粯娓犻亾
+export const getChannelPage = (params: PageParam) => {
+ return request.get({ url: '/pay/channel/page', params })
+}
+
+// 鏌ヨ璇︽儏鏀粯娓犻亾
+export const getChannel = (appId: string, code: string) => {
+ const params = {
+ appId: appId,
+ code: code
+ }
+ return request.get({ url: '/pay/channel/get', params: params })
+}
+
+// 鏂板鏀粯娓犻亾
+export const createChannel = (data: ChannelVO) => {
+ return request.post({ url: '/pay/channel/create', data })
+}
+
+// 淇敼鏀粯娓犻亾
+export const updateChannel = (data: ChannelVO) => {
+ return request.put({ url: '/pay/channel/update', data })
+}
+
+// 鍒犻櫎鏀粯娓犻亾
+export const deleteChannel = (id: number) => {
+ return request.delete({ url: '/pay/channel/delete?id=' + id })
+}
+
+// 瀵煎嚭鏀粯娓犻亾
+export const exportChannel = (params) => {
+ return request.download({ url: '/pay/channel/export-excel', params })
+}
diff --git a/src/api/pay/demo/order/index.ts b/src/api/pay/demo/order/index.ts
new file mode 100644
index 0000000..1b29859
--- /dev/null
+++ b/src/api/pay/demo/order/index.ts
@@ -0,0 +1,29 @@
+import request from '@/config/axios'
+
+export interface DemoOrderVO {
+ spuId: number
+ createTime: Date
+}
+
+// 鍒涘缓绀轰緥璁㈠崟
+export function createDemoOrder(data: DemoOrderVO) {
+ return request.post({
+ url: '/pay/demo-order/create',
+ data: data
+ })
+}
+
+// 鑾峰緱绀轰緥璁㈠崟鍒嗛〉
+export function getDemoOrderPage(query: PageParam) {
+ return request.get({
+ url: '/pay/demo-order/page',
+ params: query
+ })
+}
+
+// 閫�娆剧ず渚嬭鍗�
+export function refundDemoOrder(id: number) {
+ return request.put({
+ url: '/pay/demo-order/refund?id=' + id
+ })
+}
diff --git a/src/api/pay/demo/withdraw/index.ts b/src/api/pay/demo/withdraw/index.ts
new file mode 100644
index 0000000..d384991
--- /dev/null
+++ b/src/api/pay/demo/withdraw/index.ts
@@ -0,0 +1,30 @@
+import request from '@/config/axios'
+
+export interface PayDemoWithdrawVO {
+ id?: number
+ subject: string
+ price: number
+ userName: string
+ userAccount: string
+ type: number
+ status?: number
+ payTransferId?: number
+ transferChannelCode?: string
+ transferTime?: Date
+ transferErrorMsg?: string
+}
+
+// 鏌ヨ绀轰緥鎻愮幇鍗曞垪琛�
+export const getDemoWithdrawPage = (params: PageParam) => {
+ return request.get({ url: '/pay/demo-withdraw/page', params })
+}
+
+// 鍒涘缓绀轰緥鎻愮幇鍗�
+export const createDemoWithdraw = (data: PayDemoWithdrawVO) => {
+ return request.post({ url: '/pay/demo-withdraw/create', data })
+}
+
+// 鍙戣捣鎻愮幇鍗曡浆璐�
+export const transferDemoWithdraw = (id: number) => {
+ return request.post({ url: '/pay/demo-withdraw/transfer', params: { id } })
+}
diff --git a/src/api/pay/notify/index.ts b/src/api/pay/notify/index.ts
new file mode 100644
index 0000000..dc8bd88
--- /dev/null
+++ b/src/api/pay/notify/index.ts
@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鏀粯閫氱煡鏄庣粏
+export const getNotifyTaskDetail = (id) => {
+ return request.get({
+ url: '/pay/notify/get-detail?id=' + id
+ })
+}
+
+// 鑾峰緱鏀粯閫氱煡鍒嗛〉
+export const getNotifyTaskPage = (query) => {
+ return request.get({
+ url: '/pay/notify/page',
+ params: query
+ })
+}
diff --git a/src/api/pay/order/index.ts b/src/api/pay/order/index.ts
new file mode 100644
index 0000000..6460c4d
--- /dev/null
+++ b/src/api/pay/order/index.ts
@@ -0,0 +1,110 @@
+import request from '@/config/axios'
+
+export interface OrderVO {
+ id: number
+ merchantId: number
+ appId: number
+ channelId: number
+ channelCode: string
+ merchantOrderId: string
+ subject: string
+ body: string
+ notifyUrl: string
+ notifyStatus: number
+ amount: number
+ channelFeeRate: number
+ channelFeeAmount: number
+ status: number
+ userIp: string
+ expireTime: Date
+ successTime: Date
+ notifyTime: Date
+ successExtensionId: number
+ refundStatus: number
+ refundTimes: number
+ refundAmount: number
+ channelUserId: string
+ channelOrderNo: string
+ createTime: Date
+}
+
+export interface OrderPageReqVO extends PageParam {
+ merchantId?: number
+ appId?: number
+ channelId?: number
+ channelCode?: string
+ merchantOrderId?: string
+ subject?: string
+ body?: string
+ notifyUrl?: string
+ notifyStatus?: number
+ amount?: number
+ channelFeeRate?: number
+ channelFeeAmount?: number
+ status?: number
+ expireTime?: Date[]
+ successTime?: Date[]
+ notifyTime?: Date[]
+ successExtensionId?: number
+ refundStatus?: number
+ refundTimes?: number
+ channelUserId?: string
+ channelOrderNo?: string
+ createTime?: Date[]
+}
+
+export interface OrderExportReqVO {
+ merchantId?: number
+ appId?: number
+ channelId?: number
+ channelCode?: string
+ merchantOrderId?: string
+ subject?: string
+ body?: string
+ notifyUrl?: string
+ notifyStatus?: number
+ amount?: number
+ channelFeeRate?: number
+ channelFeeAmount?: number
+ status?: number
+ expireTime?: Date[]
+ successTime?: Date[]
+ notifyTime?: Date[]
+ successExtensionId?: number
+ refundStatus?: number
+ refundTimes?: number
+ channelUserId?: string
+ channelOrderNo?: string
+ createTime?: Date[]
+}
+
+// 鏌ヨ鍒楄〃鏀粯璁㈠崟
+export const getOrderPage = async (params: OrderPageReqVO) => {
+ return await request.get({ url: '/pay/order/page', params })
+}
+
+// 鏌ヨ璇︽儏鏀粯璁㈠崟
+export const getOrder = async (id: number, sync?: boolean) => {
+ return await request.get({
+ url: '/pay/order/get',
+ params: {
+ id,
+ sync
+ }
+ })
+}
+
+// 鑾峰緱鏀粯璁㈠崟鐨勬槑缁�
+export const getOrderDetail = async (id: number) => {
+ return await request.get({ url: '/pay/order/get-detail?id=' + id })
+}
+
+// 鎻愪氦鏀粯璁㈠崟
+export const submitOrder = async (data: any) => {
+ return await request.post({ url: '/pay/order/submit', data })
+}
+
+// 瀵煎嚭鏀粯璁㈠崟
+export const exportOrder = async (params: OrderExportReqVO) => {
+ return await request.download({ url: '/pay/order/export-excel', params })
+}
diff --git a/src/api/pay/refund/index.ts b/src/api/pay/refund/index.ts
new file mode 100644
index 0000000..4b587f2
--- /dev/null
+++ b/src/api/pay/refund/index.ts
@@ -0,0 +1,116 @@
+import request from '@/config/axios'
+
+export interface RefundVO {
+ id: number
+ merchantId: number
+ appId: number
+ channelId: number
+ channelCode: string
+ orderId: string
+ tradeNo: string
+ merchantOrderId: string
+ merchantRefundNo: string
+ notifyUrl: string
+ notifyStatus: number
+ status: number
+ type: number
+ payAmount: number
+ refundAmount: number
+ reason: string
+ userIp: string
+ channelOrderNo: string
+ channelRefundNo: string
+ channelErrorCode: string
+ channelErrorMsg: string
+ channelExtras: string
+ expireTime: Date
+ successTime: Date
+ notifyTime: Date
+ createTime: Date
+}
+
+export interface RefundPageReqVO extends PageParam {
+ merchantId?: number
+ appId?: number
+ channelId?: number
+ channelCode?: string
+ orderId?: string
+ tradeNo?: string
+ merchantOrderId?: string
+ merchantRefundNo?: string
+ notifyUrl?: string
+ notifyStatus?: number
+ status?: number
+ type?: number
+ payAmount?: number
+ refundAmount?: number
+ reason?: string
+ userIp?: string
+ channelOrderNo?: string
+ channelRefundNo?: string
+ channelErrorCode?: string
+ channelErrorMsg?: string
+ channelExtras?: string
+ expireTime?: Date[]
+ successTime?: Date[]
+ notifyTime?: Date[]
+ createTime?: Date[]
+}
+
+export interface PayRefundExportReqVO {
+ merchantId?: number
+ appId?: number
+ channelId?: number
+ channelCode?: string
+ orderId?: string
+ tradeNo?: string
+ merchantOrderId?: string
+ merchantRefundNo?: string
+ notifyUrl?: string
+ notifyStatus?: number
+ status?: number
+ type?: number
+ payAmount?: number
+ refundAmount?: number
+ reason?: string
+ userIp?: string
+ channelOrderNo?: string
+ channelRefundNo?: string
+ channelErrorCode?: string
+ channelErrorMsg?: string
+ channelExtras?: string
+ expireTime?: Date[]
+ successTime?: Date[]
+ notifyTime?: Date[]
+ createTime?: Date[]
+}
+
+// 鏌ヨ鍒楄〃閫�娆捐鍗�
+export const getRefundPage = (params: RefundPageReqVO) => {
+ return request.get({ url: '/pay/refund/page', params })
+}
+
+// 鏌ヨ璇︽儏閫�娆捐鍗�
+export const getRefund = (id: number) => {
+ return request.get({ url: '/pay/refund/get?id=' + id })
+}
+
+// 鏂板閫�娆捐鍗�
+export const createRefund = (data: RefundVO) => {
+ return request.post({ url: '/pay/refund/create', data })
+}
+
+// 淇敼閫�娆捐鍗�
+export const updateRefund = (data: RefundVO) => {
+ return request.put({ url: '/pay/refund/update', data })
+}
+
+// 鍒犻櫎閫�娆捐鍗�
+export const deleteRefund = (id: number) => {
+ return request.delete({ url: '/pay/refund/delete?id=' + id })
+}
+
+// 瀵煎嚭閫�娆捐鍗�
+export const exportRefund = (params: PayRefundExportReqVO) => {
+ return request.download({ url: '/pay/refund/export-excel', params })
+}
diff --git a/src/api/pay/transfer/index.ts b/src/api/pay/transfer/index.ts
new file mode 100644
index 0000000..72a080a
--- /dev/null
+++ b/src/api/pay/transfer/index.ts
@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+// 鏌ヨ杞处鍗曞垪琛�
+export const getTransferPage = async (params: PageParam) => {
+ return await request.get({ url: `/pay/transfer/page`, params })
+}
+
+// 鏌ヨ杞处鍗曡鎯�
+export const getTransfer = async (id: number) => {
+ return await request.get({ url: '/pay/transfer/get?id=' + id })
+}
+
+// 瀵煎嚭杞处鍗�
+export const exportTransfer = async (params: PageParam) => {
+ return await request.download({ url: '/pay/transfer/export-excel', params })
+}
diff --git a/src/api/pay/wallet/balance/index.ts b/src/api/pay/wallet/balance/index.ts
new file mode 100644
index 0000000..d7c3edd
--- /dev/null
+++ b/src/api/pay/wallet/balance/index.ts
@@ -0,0 +1,32 @@
+import request from '@/config/axios'
+
+/** 鐢ㄦ埛閽卞寘鏌ヨ鍙傛暟 */
+export interface PayWalletUserReqVO {
+ userId: number
+}
+
+/** 閽卞寘 VO */
+export interface WalletVO {
+ id: number
+ userId: number
+ userType: number
+ balance: number
+ totalExpense: number
+ totalRecharge: number
+ freezePrice: number
+}
+
+/** 鏌ヨ鐢ㄦ埛閽卞寘璇︽儏 */
+export const getWallet = async (params: PayWalletUserReqVO) => {
+ return await request.get<WalletVO>({ url: `/pay/wallet/get`, params })
+}
+
+/** 鏌ヨ浼氬憳閽卞寘鍒楄〃 */
+export const getWalletPage = async (params: any) => {
+ return await request.get({ url: `/pay/wallet/page`, params })
+}
+
+/** 淇敼浼氬憳閽卞寘浣欓 */
+export const updateWalletBalance = async (data: any) => {
+ return await request.put({ url: `/pay/wallet/update-balance`, data })
+}
diff --git a/src/api/pay/wallet/rechargePackage/index.ts b/src/api/pay/wallet/rechargePackage/index.ts
new file mode 100644
index 0000000..c8e4cc9
--- /dev/null
+++ b/src/api/pay/wallet/rechargePackage/index.ts
@@ -0,0 +1,34 @@
+import request from '@/config/axios'
+
+export interface WalletRechargePackageVO {
+ id: number
+ name: string
+ payPrice: number
+ bonusPrice: number
+ status: number
+}
+
+// 鏌ヨ濂楅鍏呭�煎垪琛�
+export const getWalletRechargePackagePage = async (params) => {
+ return await request.get({ url: '/pay/wallet-recharge-package/page', params })
+}
+
+// 鏌ヨ濂楅鍏呭�艰鎯�
+export const getWalletRechargePackage = async (id: number) => {
+ return await request.get({ url: '/pay/wallet-recharge-package/get?id=' + id })
+}
+
+// 鏂板濂楅鍏呭��
+export const createWalletRechargePackage = async (data: WalletRechargePackageVO) => {
+ return await request.post({ url: '/pay/wallet-recharge-package/create', data })
+}
+
+// 淇敼濂楅鍏呭��
+export const updateWalletRechargePackage = async (data: WalletRechargePackageVO) => {
+ return await request.put({ url: '/pay/wallet-recharge-package/update', data })
+}
+
+// 鍒犻櫎濂楅鍏呭��
+export const deleteWalletRechargePackage = async (id: number) => {
+ return await request.delete({ url: '/pay/wallet-recharge-package/delete?id=' + id })
+}
diff --git a/src/api/pay/wallet/transaction/index.ts b/src/api/pay/wallet/transaction/index.ts
new file mode 100644
index 0000000..3377ffa
--- /dev/null
+++ b/src/api/pay/wallet/transaction/index.ts
@@ -0,0 +1,14 @@
+import request from '@/config/axios'
+
+export interface WalletTransactionVO {
+ id: number
+ walletId: number
+ title: string
+ price: number
+ balance: number
+}
+
+// 鏌ヨ浼氬憳閽卞寘娴佹按鍒楄〃
+export const getWalletTransactionPage = async (params) => {
+ return await request.get({ url: `/pay/wallet-transaction/page`, params })
+}
diff --git a/src/api/system/area/index.ts b/src/api/system/area/index.ts
new file mode 100644
index 0000000..e91a499
--- /dev/null
+++ b/src/api/system/area/index.ts
@@ -0,0 +1,11 @@
+import request from '@/config/axios'
+
+// 鑾峰緱鍦板尯鏍�
+export const getAreaTree = async () => {
+ return await request.get({ url: '/system/area/tree' })
+}
+
+// 鑾峰緱 IP 瀵瑰簲鐨勫湴鍖哄悕
+export const getAreaByIp = async (ip: string) => {
+ return await request.get({ url: '/system/area/get-by-ip?ip=' + ip })
+}
diff --git a/src/api/system/dept/index.ts b/src/api/system/dept/index.ts
new file mode 100644
index 0000000..c0959f4
--- /dev/null
+++ b/src/api/system/dept/index.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+export interface DeptVO {
+ id: number
+ name: string
+ parentId: number
+ status: number
+ sort: number
+ leaderUserId: number
+ phone: string
+ email: string
+ createTime: Date
+}
+
+// 鏌ヨ閮ㄩ棬锛堢簿绠�)鍒楄〃
+export const getSimpleDeptList = (): Promise<DeptVO[]> => {
+ return request.get({ url: '/system/dept/simple-list' })
+}
+
+// 鏌ヨ閮ㄩ棬鍒楄〃
+export const getDeptList = (params: any) => {
+ return request.get({ url: '/system/dept/list', params })
+}
+
+// 鏌ヨ閮ㄩ棬鍒嗛〉
+export const getDeptPage = async (params: PageParam) => {
+ return await request.get({ url: '/system/dept/list', params })
+}
+
+// 鏌ヨ閮ㄩ棬璇︽儏
+export const getDept = (id: number) => {
+ return request.get({ url: '/system/dept/get?id=' + id })
+}
+
+// 鏂板閮ㄩ棬
+export const createDept = (data: DeptVO) => {
+ return request.post({ url: '/system/dept/create', data })
+}
+
+// 淇敼閮ㄩ棬
+export const updateDept = (data: DeptVO) => {
+ return request.put({ url: '/system/dept/update', data })
+}
+
+// 鍒犻櫎閮ㄩ棬
+export const deleteDept = async (id: number) => {
+ return await request.delete({ url: '/system/dept/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎閮ㄩ棬
+export const deleteDeptList = async (ids: number[]) => {
+ return await request.delete({ url: '/system/dept/delete-list', params: { ids: ids.join(',') } })
+}
diff --git a/src/api/system/dict/dict.data.ts b/src/api/system/dict/dict.data.ts
new file mode 100644
index 0000000..730e194
--- /dev/null
+++ b/src/api/system/dict/dict.data.ts
@@ -0,0 +1,59 @@
+import request from '@/config/axios'
+
+export interface DictDataVO {
+ id: number
+ sort: number
+ label: string
+ value: string
+ dictType: string
+ status: number
+ colorType: string
+ cssClass: string
+ remark: string
+ createTime: Date
+}
+
+// 鏌ヨ瀛楀吀鏁版嵁锛堢簿绠�)鍒楄〃
+export const getSimpleDictDataList = () => {
+ return request.get({ url: '/system/dict-data/simple-list' })
+}
+
+// 鏌ヨ瀛楀吀鏁版嵁鍒楄〃
+export const getDictDataPage = (params: PageParam) => {
+ return request.get({ url: '/system/dict-data/page', params })
+}
+
+// 鏌ヨ瀛楀吀鏁版嵁璇︽儏
+export const getDictData = (id: number) => {
+ return request.get({ url: '/system/dict-data/get?id=' + id })
+}
+
+// 鏍规嵁瀛楀吀绫诲瀷鏌ヨ瀛楀吀鏁版嵁
+export const getDictDataByType = (dictType: string) => {
+ return request.get({ url: '/system/dict-data/type?type=' + dictType })
+}
+
+// 鏂板瀛楀吀鏁版嵁
+export const createDictData = (data: DictDataVO) => {
+ return request.post({ url: '/system/dict-data/create', data })
+}
+
+// 淇敼瀛楀吀鏁版嵁
+export const updateDictData = (data: DictDataVO) => {
+ return request.put({ url: '/system/dict-data/update', data })
+}
+
+// 鍒犻櫎瀛楀吀鏁版嵁
+export const deleteDictData = (id: number) => {
+ return request.delete({ url: '/system/dict-data/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎瀛楀吀鏁版嵁
+export const deleteDictDataList = (ids: number[]) => {
+ return request.delete({ url: '/system/dict-data/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭瀛楀吀鏁版嵁
+export const exportDictData = (params: any) => {
+ return request.download({ url: '/system/dict-data/export-excel', params })
+}
diff --git a/src/api/system/dict/dict.type.ts b/src/api/system/dict/dict.type.ts
new file mode 100644
index 0000000..af6ad5e
--- /dev/null
+++ b/src/api/system/dict/dict.type.ts
@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+export interface DictTypeVO {
+ id: number
+ name: string
+ type: string
+ status: number
+ remark: string
+ createTime: Date
+}
+
+// 鏌ヨ瀛楀吀锛堢簿绠�)鍒楄〃
+export const getSimpleDictTypeList = (): Promise<DictTypeVO[]> => {
+ return request.get({ url: '/system/dict-type/simple-list' })
+}
+
+// 鏌ヨ瀛楀吀鍒楄〃
+export const getDictTypePage = (params: PageParam) => {
+ return request.get({ url: '/system/dict-type/page', params })
+}
+
+// 鏌ヨ瀛楀吀璇︽儏
+export const getDictType = (id: number) => {
+ return request.get({ url: '/system/dict-type/get?id=' + id })
+}
+
+// 鏂板瀛楀吀
+export const createDictType = (data: DictTypeVO) => {
+ return request.post({ url: '/system/dict-type/create', data })
+}
+
+// 淇敼瀛楀吀
+export const updateDictType = (data: DictTypeVO) => {
+ return request.put({ url: '/system/dict-type/update', data })
+}
+
+// 鍒犻櫎瀛楀吀
+export const deleteDictType = (id: number) => {
+ return request.delete({ url: '/system/dict-type/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎瀛楀吀绫诲瀷
+export const deleteDictTypeList = (ids: number[]) => {
+ return request.delete({ url: '/system/dict-type/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭瀛楀吀
+export const exportDictType = (params) => {
+ return request.download({
+ url: '/system/dict-type/export-excel',
+ params
+ })
+}
diff --git a/src/api/system/loginLog/index.ts b/src/api/system/loginLog/index.ts
new file mode 100644
index 0000000..41d0367
--- /dev/null
+++ b/src/api/system/loginLog/index.ts
@@ -0,0 +1,25 @@
+import request from '@/config/axios'
+
+export interface LoginLogVO {
+ id: number
+ logType: number
+ traceId: number
+ userId: number
+ userType: number
+ username: string
+ result: number
+ status: number
+ userIp: string
+ userAgent: string
+ createTime: Date
+}
+
+// 鏌ヨ鐧诲綍鏃ュ織鍒楄〃
+export const getLoginLogPage = (params: PageParam) => {
+ return request.get({ url: '/system/login-log/page', params })
+}
+
+// 瀵煎嚭鐧诲綍鏃ュ織
+export const exportLoginLog = (params) => {
+ return request.download({ url: '/system/login-log/export-excel', params })
+}
diff --git a/src/api/system/mail/account/index.ts b/src/api/system/mail/account/index.ts
new file mode 100644
index 0000000..97b574a
--- /dev/null
+++ b/src/api/system/mail/account/index.ts
@@ -0,0 +1,47 @@
+import request from '@/config/axios'
+
+export interface MailAccountVO {
+ id: number
+ mail: string
+ username: string
+ password: string
+ host: string
+ port: number
+ sslEnable: boolean
+ starttlsEnable: boolean
+}
+
+// 鏌ヨ閭璐﹀彿鍒楄〃
+export const getMailAccountPage = async (params: PageParam) => {
+ return await request.get({ url: '/system/mail-account/page', params })
+}
+
+// 鏌ヨ閭璐﹀彿璇︽儏
+export const getMailAccount = async (id: number) => {
+ return await request.get({ url: '/system/mail-account/get?id=' + id })
+}
+
+// 鏂板閭璐﹀彿
+export const createMailAccount = async (data: MailAccountVO) => {
+ return await request.post({ url: '/system/mail-account/create', data })
+}
+
+// 淇敼閭璐﹀彿
+export const updateMailAccount = async (data: MailAccountVO) => {
+ return await request.put({ url: '/system/mail-account/update', data })
+}
+
+// 鍒犻櫎閭璐﹀彿
+export const deleteMailAccount = async (id: number) => {
+ return await request.delete({ url: '/system/mail-account/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎閭璐﹀彿
+export const deleteMailAccountList = async (ids: number[]) => {
+ return await request.delete({ url: '/system/mail-account/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 鑾峰緱閭璐﹀彿绮剧畝鍒楄〃
+export const getSimpleMailAccountList = async () => {
+ return request.get({ url: '/system/mail-account/simple-list' })
+}
diff --git a/src/api/system/mail/log/index.ts b/src/api/system/mail/log/index.ts
new file mode 100644
index 0000000..409ced6
--- /dev/null
+++ b/src/api/system/mail/log/index.ts
@@ -0,0 +1,37 @@
+import request from '@/config/axios'
+
+export interface MailLogVO {
+ id: number
+ userId: number
+ userType: number
+ toMails: string[]
+ ccMails?: string[]
+ bccMails?: string[]
+ accountId: number
+ fromMail: string
+ templateId: number
+ templateCode: string
+ templateNickname: string
+ templateTitle: string
+ templateContent: string
+ templateParams: string
+ sendStatus: number
+ sendTime: Date
+ sendMessageId: string
+ sendException: string
+}
+
+// 鏌ヨ閭欢鏃ュ織鍒楄〃
+export const getMailLogPage = async (params: PageParam) => {
+ return await request.get({ url: '/system/mail-log/page', params })
+}
+
+// 鏌ヨ閭欢鏃ュ織璇︽儏
+export const getMailLog = async (id: number) => {
+ return await request.get({ url: '/system/mail-log/get?id=' + id })
+}
+
+// 瀵煎嚭閭欢鏃ュ織
+export const exportMailLog = (params) => {
+ return request.download({ url: '/system/mail-log/export-excel', params })
+}
diff --git a/src/api/system/mail/template/index.ts b/src/api/system/mail/template/index.ts
new file mode 100644
index 0000000..d340f8a
--- /dev/null
+++ b/src/api/system/mail/template/index.ts
@@ -0,0 +1,58 @@
+import request from '@/config/axios'
+
+export interface MailTemplateVO {
+ id?: number
+ name: string
+ code: string
+ accountId: number
+ nickname: string
+ title: string
+ content: string
+ status: number
+}
+
+export interface MailSendReqVO {
+ toMails: string[]
+ ccMails?: string[]
+ bccMails?: string[]
+ templateCode: string
+ templateParams: Map<String, Object>
+}
+
+// 鏌ヨ閭欢妯$増鍒楄〃
+export const getMailTemplatePage = async (params: PageParam) => {
+ return await request.get({ url: '/system/mail-template/page', params })
+}
+
+// 鏌ヨ閭欢妯$増璇︽儏
+export const getMailTemplate = async (id: number) => {
+ return await request.get({ url: '/system/mail-template/get?id=' + id })
+}
+
+// 鏂板閭欢妯$増
+export const createMailTemplate = async (data: MailTemplateVO) => {
+ return await request.post({ url: '/system/mail-template/create', data })
+}
+
+// 淇敼閭欢妯$増
+export const updateMailTemplate = async (data: MailTemplateVO) => {
+ return await request.put({ url: '/system/mail-template/update', data })
+}
+
+// 鍒犻櫎閭欢妯$増
+export const deleteMailTemplate = async (id: number) => {
+ return await request.delete({ url: '/system/mail-template/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎閭欢妯$増
+export const deleteMailTemplateList = async (ids: number[]) => {
+ return await request.delete({
+ url: '/system/mail-template/delete-list',
+ params: { ids: ids.join(',') }
+ })
+}
+
+// 鍙戦�侀偖浠�
+export const sendMail = (data: MailSendReqVO) => {
+ return request.post({ url: '/system/mail-template/send-mail', data })
+}
diff --git a/src/api/system/menu/index.ts b/src/api/system/menu/index.ts
new file mode 100644
index 0000000..5a80668
--- /dev/null
+++ b/src/api/system/menu/index.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface MenuVO {
+ id: number
+ name: string
+ permission: string
+ type: number
+ sort: number
+ parentId: number
+ path: string
+ icon: string
+ component: string
+ componentName?: string
+ status: number
+ visible: boolean
+ keepAlive: boolean
+ alwaysShow?: boolean
+ createTime: Date
+}
+
+// 鏌ヨ鑿滃崟锛堢簿绠�锛夊垪琛�
+export const getSimpleMenusList = () => {
+ return request.get({ url: '/system/menu/simple-list' })
+}
+
+// 鏌ヨ鑿滃崟鍒楄〃
+export const getMenuList = (params) => {
+ return request.get({ url: '/system/menu/list', params })
+}
+
+// 鑾峰彇鑿滃崟璇︽儏
+export const getMenu = (id: number) => {
+ return request.get({ url: '/system/menu/get?id=' + id })
+}
+
+// 鏂板鑿滃崟
+export const createMenu = (data: MenuVO) => {
+ return request.post({ url: '/system/menu/create', data })
+}
+
+// 淇敼鑿滃崟
+export const updateMenu = (data: MenuVO) => {
+ return request.put({ url: '/system/menu/update', data })
+}
+
+// 鍒犻櫎鑿滃崟
+export const deleteMenu = (id: number) => {
+ return request.delete({ url: '/system/menu/delete?id=' + id })
+}
diff --git a/src/api/system/notice/index.ts b/src/api/system/notice/index.ts
new file mode 100644
index 0000000..c486639
--- /dev/null
+++ b/src/api/system/notice/index.ts
@@ -0,0 +1,47 @@
+import request from '@/config/axios'
+
+export interface NoticeVO {
+ id: number | undefined
+ title: string
+ type: number
+ content: string
+ status: number
+ remark: string
+ creator: string
+ createTime: Date
+}
+
+// 鏌ヨ鍏憡鍒楄〃
+export const getNoticePage = (params: PageParam) => {
+ return request.get({ url: '/system/notice/page', params })
+}
+
+// 鏌ヨ鍏憡璇︽儏
+export const getNotice = (id: number) => {
+ return request.get({ url: '/system/notice/get?id=' + id })
+}
+
+// 鏂板鍏憡
+export const createNotice = (data: NoticeVO) => {
+ return request.post({ url: '/system/notice/create', data })
+}
+
+// 淇敼鍏憡
+export const updateNotice = (data: NoticeVO) => {
+ return request.put({ url: '/system/notice/update', data })
+}
+
+// 鍒犻櫎鍏憡
+export const deleteNotice = (id: number) => {
+ return request.delete({ url: '/system/notice/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鍏憡
+export const deleteNoticeList = (ids: number[]) => {
+ return request.delete({ url: '/system/notice/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 鎺ㄩ�佸叕鍛�
+export const pushNotice = (id: number) => {
+ return request.post({ url: '/system/notice/push?id=' + id })
+}
diff --git a/src/api/system/notify/message/index.ts b/src/api/system/notify/message/index.ts
new file mode 100644
index 0000000..e407c77
--- /dev/null
+++ b/src/api/system/notify/message/index.ts
@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+import qs from 'qs'
+
+export interface NotifyMessageVO {
+ id: number
+ userId: number
+ userType: number
+ templateId: number
+ templateCode: string
+ templateNickname: string
+ templateContent: string
+ templateType: number
+ templateParams: string
+ readStatus: boolean
+ readTime: Date
+ createTime: Date
+}
+
+// 鏌ヨ绔欏唴淇℃秷鎭垪琛�
+export const getNotifyMessagePage = async (params: PageParam) => {
+ return await request.get({ url: '/system/notify-message/page', params })
+}
+
+// 鑾峰緱鎴戠殑绔欏唴淇″垎椤�
+export const getMyNotifyMessagePage = async (params: PageParam) => {
+ return await request.get({ url: '/system/notify-message/my-page', params })
+}
+
+// 鎵归噺鏍囪宸茶
+export const updateNotifyMessageRead = async (ids) => {
+ return await request.put({
+ url: '/system/notify-message/update-read?' + qs.stringify({ ids: ids }, { indices: false })
+ })
+}
+
+// 鏍囪鎵�鏈夌珯鍐呬俊涓哄凡璇�
+export const updateAllNotifyMessageRead = async () => {
+ return await request.put({ url: '/system/notify-message/update-all-read' })
+}
+
+// 鑾峰彇褰撳墠鐢ㄦ埛鐨勬渶鏂扮珯鍐呬俊鍒楄〃
+export const getUnreadNotifyMessageList = async () => {
+ return await request.get({ url: '/system/notify-message/get-unread-list' })
+}
+
+// 鑾峰緱褰撳墠鐢ㄦ埛鐨勬湭璇荤珯鍐呬俊鏁伴噺
+export const getUnreadNotifyMessageCount = async () => {
+ return await request.get({ url: '/system/notify-message/get-unread-count' })
+}
diff --git a/src/api/system/notify/template/index.ts b/src/api/system/notify/template/index.ts
new file mode 100644
index 0000000..c6bc548
--- /dev/null
+++ b/src/api/system/notify/template/index.ts
@@ -0,0 +1,54 @@
+import request from '@/config/axios'
+
+export interface NotifyTemplateVO {
+ id?: number
+ name: string
+ nickname: string
+ code: string
+ content: string
+ type?: number
+ params: string
+ status: number
+ remark: string
+}
+
+export interface NotifySendReqVO {
+ userId: number | null
+ templateCode: string
+ templateParams: Map<String, Object>
+}
+
+// 鏌ヨ绔欏唴淇℃ā鏉垮垪琛�
+export const getNotifyTemplatePage = async (params: PageParam) => {
+ return await request.get({ url: '/system/notify-template/page', params })
+}
+
+// 鏌ヨ绔欏唴淇℃ā鏉胯鎯�
+export const getNotifyTemplate = async (id: number) => {
+ return await request.get({ url: '/system/notify-template/get?id=' + id })
+}
+
+// 鏂板绔欏唴淇℃ā鏉�
+export const createNotifyTemplate = async (data: NotifyTemplateVO) => {
+ return await request.post({ url: '/system/notify-template/create', data })
+}
+
+// 淇敼绔欏唴淇℃ā鏉�
+export const updateNotifyTemplate = async (data: NotifyTemplateVO) => {
+ return await request.put({ url: '/system/notify-template/update', data })
+}
+
+// 鍒犻櫎绔欏唴淇℃ā鏉�
+export const deleteNotifyTemplate = async (id: number) => {
+ return await request.delete({ url: '/system/notify-template/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎绔欏唴淇℃ā鏉�
+export const deleteNotifyTemplateList = async (ids: number[]) => {
+ return await request.delete({ url: '/system/notify-template/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 鍙戦�佺珯鍐呬俊
+export const sendNotify = (data: NotifySendReqVO) => {
+ return request.post({ url: '/system/notify-template/send-notify', data })
+}
diff --git a/src/api/system/oauth2/client.ts b/src/api/system/oauth2/client.ts
new file mode 100644
index 0000000..348aed5
--- /dev/null
+++ b/src/api/system/oauth2/client.ts
@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+export interface OAuth2ClientVO {
+ id: number
+ clientId: string
+ secret: string
+ name: string
+ logo: string
+ description: string
+ status: number
+ accessTokenValiditySeconds: number
+ refreshTokenValiditySeconds: number
+ redirectUris: string[]
+ autoApprove: boolean
+ authorizedGrantTypes: string[]
+ scopes: string[]
+ authorities: string[]
+ resourceIds: string[]
+ additionalInformation: string
+ isAdditionalInformationJson: boolean
+ createTime: Date
+}
+
+// 鏌ヨ OAuth2 瀹㈡埛绔殑鍒楄〃
+export const getOAuth2ClientPage = (params: PageParam) => {
+ return request.get({ url: '/system/oauth2-client/page', params })
+}
+
+// 鏌ヨ OAuth2 瀹㈡埛绔殑璇︽儏
+export const getOAuth2Client = (id: number) => {
+ return request.get({ url: '/system/oauth2-client/get?id=' + id })
+}
+
+// 鏂板 OAuth2 瀹㈡埛绔�
+export const createOAuth2Client = (data: OAuth2ClientVO) => {
+ return request.post({ url: '/system/oauth2-client/create', data })
+}
+
+// 淇敼 OAuth2 瀹㈡埛绔�
+export const updateOAuth2Client = (data: OAuth2ClientVO) => {
+ return request.put({ url: '/system/oauth2-client/update', data })
+}
+
+// 鍒犻櫎 OAuth2
+export const deleteOAuth2Client = (id: number) => {
+ return request.delete({ url: '/system/oauth2-client/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎 OAuth2 瀹㈡埛绔�
+export const deleteOAuth2ClientList = (ids: number[]) => {
+ return request.delete({ url: '/system/oauth2-client/delete-list', params: { ids: ids.join(',') } })
+}
diff --git a/src/api/system/oauth2/token.ts b/src/api/system/oauth2/token.ts
new file mode 100644
index 0000000..ac89ae8
--- /dev/null
+++ b/src/api/system/oauth2/token.ts
@@ -0,0 +1,22 @@
+import request from '@/config/axios'
+
+export interface OAuth2TokenVO {
+ id: number
+ accessToken: string
+ refreshToken: string
+ userId: number
+ userType: number
+ clientId: string
+ createTime: Date
+ expiresTime: Date
+}
+
+// 鏌ヨ token鍒楄〃
+export const getAccessTokenPage = (params: PageParam) => {
+ return request.get({ url: '/system/oauth2-token/page', params })
+}
+
+// 鍒犻櫎 token
+export const deleteAccessToken = (accessToken: string) => {
+ return request.delete({ url: '/system/oauth2-token/delete?accessToken=' + accessToken })
+}
diff --git a/src/api/system/operatelog/index.ts b/src/api/system/operatelog/index.ts
new file mode 100644
index 0000000..3ab90eb
--- /dev/null
+++ b/src/api/system/operatelog/index.ts
@@ -0,0 +1,30 @@
+import request from '@/config/axios'
+
+export type OperateLogVO = {
+ id: number
+ traceId: string
+ userType: number
+ userId: number
+ userName: string
+ type: string
+ subType: string
+ bizId: number
+ action: string
+ extra: string
+ requestMethod: string
+ requestUrl: string
+ userIp: string
+ userAgent: string
+ creator: string
+ creatorName: string
+ createTime: Date
+}
+
+// 鏌ヨ鎿嶄綔鏃ュ織鍒楄〃
+export const getOperateLogPage = (params: PageParam) => {
+ return request.get({ url: '/system/operate-log/page', params })
+}
+// 瀵煎嚭鎿嶄綔鏃ュ織
+export const exportOperateLog = (params: any) => {
+ return request.download({ url: '/system/operate-log/export-excel', params })
+}
diff --git a/src/api/system/permission/index.ts b/src/api/system/permission/index.ts
new file mode 100644
index 0000000..b3c7696
--- /dev/null
+++ b/src/api/system/permission/index.ts
@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+export interface PermissionAssignUserRoleReqVO {
+ userId: number
+ roleIds: number[]
+}
+
+export interface PermissionAssignRoleMenuReqVO {
+ roleId: number
+ menuIds: number[]
+}
+
+export interface PermissionAssignRoleDataScopeReqVO {
+ roleId: number
+ dataScope: number
+ dataScopeDeptIds: number[]
+}
+
+// 鏌ヨ瑙掕壊鎷ユ湁鐨勮彍鍗曟潈闄�
+export const getRoleMenuList = async (roleId: number) => {
+ return await request.get({ url: '/system/permission/list-role-menus?roleId=' + roleId })
+}
+
+// 璧嬩簣瑙掕壊鑿滃崟鏉冮檺
+export const assignRoleMenu = async (data: PermissionAssignRoleMenuReqVO) => {
+ return await request.post({ url: '/system/permission/assign-role-menu', data })
+}
+
+// 璧嬩簣瑙掕壊鏁版嵁鏉冮檺
+export const assignRoleDataScope = async (data: PermissionAssignRoleDataScopeReqVO) => {
+ return await request.post({ url: '/system/permission/assign-role-data-scope', data })
+}
+
+// 鏌ヨ鐢ㄦ埛鎷ユ湁鐨勮鑹叉暟缁�
+export const getUserRoleList = async (userId: number) => {
+ return await request.get({ url: '/system/permission/list-user-roles?userId=' + userId })
+}
+
+// 璧嬩簣鐢ㄦ埛瑙掕壊
+export const assignUserRole = async (data: PermissionAssignUserRoleReqVO) => {
+ return await request.post({ url: '/system/permission/assign-user-role', data })
+}
diff --git a/src/api/system/post/index.ts b/src/api/system/post/index.ts
new file mode 100644
index 0000000..297f893
--- /dev/null
+++ b/src/api/system/post/index.ts
@@ -0,0 +1,51 @@
+import request from '@/config/axios'
+
+export interface PostVO {
+ id?: number
+ name: string
+ code: string
+ sort: number
+ status: number
+ remark: string
+ createTime?: Date
+}
+
+// 鏌ヨ宀椾綅鍒楄〃
+export const getPostPage = async (params: PageParam) => {
+ return await request.get({ url: '/system/post/page', params })
+}
+
+// 鑾峰彇宀椾綅绮剧畝淇℃伅鍒楄〃
+export const getSimplePostList = async (): Promise<PostVO[]> => {
+ return await request.get({ url: '/system/post/simple-list' })
+}
+
+// 鏌ヨ宀椾綅璇︽儏
+export const getPost = async (id: number) => {
+ return await request.get({ url: '/system/post/get?id=' + id })
+}
+
+// 鏂板宀椾綅
+export const createPost = async (data: PostVO) => {
+ return await request.post({ url: '/system/post/create', data })
+}
+
+// 淇敼宀椾綅
+export const updatePost = async (data: PostVO) => {
+ return await request.put({ url: '/system/post/update', data })
+}
+
+// 鍒犻櫎宀椾綅
+export const deletePost = async (id: number) => {
+ return await request.delete({ url: '/system/post/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎宀椾綅
+export const deletePostList = async (ids: number[]) => {
+ return await request.delete({ url: '/system/post/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭宀椾綅
+export const exportPost = async (params) => {
+ return await request.download({ url: '/system/post/export-excel', params })
+}
diff --git a/src/api/system/role/index.ts b/src/api/system/role/index.ts
new file mode 100644
index 0000000..b6d3bbb
--- /dev/null
+++ b/src/api/system/role/index.ts
@@ -0,0 +1,56 @@
+import request from '@/config/axios'
+
+export interface RoleVO {
+ id: number
+ name: string
+ code: string
+ sort: number
+ status: number
+ type: number
+ dataScope: number
+ dataScopeDeptIds: number[]
+ createTime: Date
+}
+
+// 鏌ヨ瑙掕壊鍒楄〃
+export const getRolePage = async (params: PageParam) => {
+ return await request.get({ url: '/system/role/page', params })
+}
+
+// 鏌ヨ瑙掕壊锛堢簿绠�)鍒楄〃
+export const getSimpleRoleList = async (): Promise<RoleVO[]> => {
+ return await request.get({ url: '/system/role/simple-list' })
+}
+
+// 鏌ヨ瑙掕壊璇︽儏
+export const getRole = async (id: number) => {
+ return await request.get({ url: '/system/role/get?id=' + id })
+}
+
+// 鏂板瑙掕壊
+export const createRole = async (data: RoleVO) => {
+ return await request.post({ url: '/system/role/create', data })
+}
+
+// 淇敼瑙掕壊
+export const updateRole = async (data: RoleVO) => {
+ return await request.put({ url: '/system/role/update', data })
+}
+
+// 鍒犻櫎瑙掕壊
+export const deleteRole = async (id: number) => {
+ return await request.delete({ url: '/system/role/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎瑙掕壊
+export const deleteRoleList = async (ids: number[]) => {
+ return await request.delete({ url: '/system/role/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭瑙掕壊
+export const exportRole = (params: any) => {
+ return request.download({
+ url: '/system/role/export-excel',
+ params
+ })
+}
diff --git a/src/api/system/sms/smsChannel/index.ts b/src/api/system/sms/smsChannel/index.ts
new file mode 100644
index 0000000..bdfadcd
--- /dev/null
+++ b/src/api/system/sms/smsChannel/index.ts
@@ -0,0 +1,48 @@
+import request from '@/config/axios'
+
+export interface SmsChannelVO {
+ id: number
+ code: string
+ status: number
+ signature: string
+ remark: string
+ apiKey: string
+ apiSecret: string
+ callbackUrl: string
+ createTime: Date
+}
+
+// 鏌ヨ鐭俊娓犻亾鍒楄〃
+export const getSmsChannelPage = (params: PageParam) => {
+ return request.get({ url: '/system/sms-channel/page', params })
+}
+
+// 鑾峰緱鐭俊娓犻亾绮剧畝鍒楄〃
+export function getSimpleSmsChannelList() {
+ return request.get({ url: '/system/sms-channel/simple-list' })
+}
+
+// 鏌ヨ鐭俊娓犻亾璇︽儏
+export const getSmsChannel = (id: number) => {
+ return request.get({ url: '/system/sms-channel/get?id=' + id })
+}
+
+// 鏂板鐭俊娓犻亾
+export const createSmsChannel = (data: SmsChannelVO) => {
+ return request.post({ url: '/system/sms-channel/create', data })
+}
+
+// 淇敼鐭俊娓犻亾
+export const updateSmsChannel = (data: SmsChannelVO) => {
+ return request.put({ url: '/system/sms-channel/update', data })
+}
+
+// 鍒犻櫎鐭俊娓犻亾
+export const deleteSmsChannel = (id: number) => {
+ return request.delete({ url: '/system/sms-channel/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鐭俊娓犻亾
+export const deleteSmsChannelList = (ids: number[]) => {
+ return request.delete({ url: '/system/sms-channel/delete-list', params: { ids: ids.join(',') } })
+}
diff --git a/src/api/system/sms/smsLog/index.ts b/src/api/system/sms/smsLog/index.ts
new file mode 100644
index 0000000..f989171
--- /dev/null
+++ b/src/api/system/sms/smsLog/index.ts
@@ -0,0 +1,37 @@
+import request from '@/config/axios'
+
+export interface SmsLogVO {
+ id: number | null
+ channelId: number | null
+ channelCode: string
+ templateId: number | null
+ templateCode: string
+ templateType: number | null
+ templateContent: string
+ templateParams: Map<string, object> | null
+ apiTemplateId: string
+ mobile: string
+ userId: number | null
+ userType: number | null
+ sendStatus: number | null
+ sendTime: Date | null
+ apiSendCode: string
+ apiSendMsg: string
+ apiRequestId: string
+ apiSerialNo: string
+ receiveStatus: number | null
+ receiveTime: Date | null
+ apiReceiveCode: string
+ apiReceiveMsg: string
+ createTime: Date | null
+}
+
+// 鏌ヨ鐭俊鏃ュ織鍒楄〃
+export const getSmsLogPage = (params: PageParam) => {
+ return request.get({ url: '/system/sms-log/page', params })
+}
+
+// 瀵煎嚭鐭俊鏃ュ織
+export const exportSmsLog = (params) => {
+ return request.download({ url: '/system/sms-log/export-excel', params })
+}
diff --git a/src/api/system/sms/smsTemplate/index.ts b/src/api/system/sms/smsTemplate/index.ts
new file mode 100644
index 0000000..2171ff6
--- /dev/null
+++ b/src/api/system/sms/smsTemplate/index.ts
@@ -0,0 +1,65 @@
+import request from '@/config/axios'
+
+export interface SmsTemplateVO {
+ id?: number
+ type?: number
+ status: number
+ code: string
+ name: string
+ content: string
+ remark: string
+ apiTemplateId: string
+ channelId?: number
+ channelCode?: string
+ params?: string[]
+ createTime?: Date
+}
+
+export interface SendSmsReqVO {
+ mobile: string
+ templateCode: string
+ templateParams: Map<String, Object>
+}
+
+// 鏌ヨ鐭俊妯℃澘鍒楄〃
+export const getSmsTemplatePage = (params: PageParam) => {
+ return request.get({ url: '/system/sms-template/page', params })
+}
+
+// 鏌ヨ鐭俊妯℃澘璇︽儏
+export const getSmsTemplate = (id: number) => {
+ return request.get({ url: '/system/sms-template/get?id=' + id })
+}
+
+// 鏂板鐭俊妯℃澘
+export const createSmsTemplate = (data: SmsTemplateVO) => {
+ return request.post({ url: '/system/sms-template/create', data })
+}
+
+// 淇敼鐭俊妯℃澘
+export const updateSmsTemplate = (data: SmsTemplateVO) => {
+ return request.put({ url: '/system/sms-template/update', data })
+}
+
+// 鍒犻櫎鐭俊妯℃澘
+export const deleteSmsTemplate = (id: number) => {
+ return request.delete({ url: '/system/sms-template/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鐭俊妯℃澘
+export const deleteSmsTemplateList = (ids: number[]) => {
+ return request.delete({ url: '/system/sms-template/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭鐭俊妯℃澘
+export const exportSmsTemplate = (params) => {
+ return request.download({
+ url: '/system/sms-template/export-excel',
+ params
+ })
+}
+
+// 鍙戦�佺煭淇�
+export const sendSms = (data: SendSmsReqVO) => {
+ return request.post({ url: '/system/sms-template/send-sms', data })
+}
diff --git a/src/api/system/social/client/index.ts b/src/api/system/social/client/index.ts
new file mode 100644
index 0000000..3239822
--- /dev/null
+++ b/src/api/system/social/client/index.ts
@@ -0,0 +1,38 @@
+import request from '@/config/axios'
+
+export interface SocialClientVO {
+ id: number
+ name: string
+ socialType: number
+ userType: number
+ clientId: string
+ clientSecret: string
+ agentId: string
+ publicKey: string
+ status: number
+}
+
+// 鏌ヨ绀句氦瀹㈡埛绔垪琛�
+export const getSocialClientPage = async (params) => {
+ return await request.get({ url: `/system/social-client/page`, params })
+}
+
+// 鏌ヨ绀句氦瀹㈡埛绔鎯�
+export const getSocialClient = async (id: number) => {
+ return await request.get({ url: `/system/social-client/get?id=` + id })
+}
+
+// 鏂板绀句氦瀹㈡埛绔�
+export const createSocialClient = async (data: SocialClientVO) => {
+ return await request.post({ url: `/system/social-client/create`, data })
+}
+
+// 淇敼绀句氦瀹㈡埛绔�
+export const updateSocialClient = async (data: SocialClientVO) => {
+ return await request.put({ url: `/system/social-client/update`, data })
+}
+
+// 鍒犻櫎绀句氦瀹㈡埛绔�
+export const deleteSocialClient = async (id: number) => {
+ return await request.delete({ url: `/system/social-client/delete?id=` + id })
+}
diff --git a/src/api/system/social/user/index.ts b/src/api/system/social/user/index.ts
new file mode 100644
index 0000000..9f1631d
--- /dev/null
+++ b/src/api/system/social/user/index.ts
@@ -0,0 +1,29 @@
+import request from '@/config/axios'
+
+export interface SocialUserVO {
+ id: number
+ type: number
+ openid: string
+ token: string
+ rawTokenInfo: string
+ nickname: string
+ avatar: string
+ rawUserInfo: string
+ code: string
+ state: string
+}
+
+// 鏌ヨ绀句氦鐢ㄦ埛鍒楄〃
+export const getSocialUserPage = async (params: any) => {
+ return await request.get({ url: `/system/social-user/page`, params })
+}
+
+// 鏌ヨ绀句氦鐢ㄦ埛璇︽儏
+export const getSocialUser = async (id: number) => {
+ return await request.get({ url: `/system/social-user/get?id=` + id })
+}
+
+// 鑾峰緱缁戝畾绀句氦鐢ㄦ埛鍒楄〃
+export const getBindSocialUserList = async () => {
+ return await request.get({ url: '/system/social-user/get-bind-list' })
+}
diff --git a/src/api/system/tenant/index.ts b/src/api/system/tenant/index.ts
new file mode 100644
index 0000000..cd6e5db
--- /dev/null
+++ b/src/api/system/tenant/index.ts
@@ -0,0 +1,73 @@
+import request from '@/config/axios'
+
+export interface TenantVO {
+ id: number
+ name: string
+ contactName: string
+ contactMobile: string
+ status: number
+ domain: string
+ packageId: number
+ username: string
+ password: string
+ expireTime: Date
+ accountCount: number
+ websites: string[]
+ createTime: Date
+}
+
+export interface TenantPageReqVO extends PageParam {
+ name?: string
+ contactName?: string
+ contactMobile?: string
+ status?: number
+ createTime?: Date[]
+}
+
+export interface TenantExportReqVO {
+ name?: string
+ contactName?: string
+ contactMobile?: string
+ status?: number
+ createTime?: Date[]
+}
+
+// 鏌ヨ绉熸埛鍒楄〃
+export const getTenantPage = (params: TenantPageReqVO) => {
+ return request.get({ url: '/system/tenant/page', params })
+}
+
+// 鏌ヨ绉熸埛璇︽儏
+export const getTenant = (id: number) => {
+ return request.get({ url: '/system/tenant/get?id=' + id })
+}
+
+// 鑾峰彇绉熸埛绮剧畝淇℃伅鍒楄〃
+export const getTenantList = () => {
+ return request.get({ url: '/system/tenant/simple-list' })
+}
+
+// 鏂板绉熸埛
+export const createTenant = (data: TenantVO) => {
+ return request.post({ url: '/system/tenant/create', data })
+}
+
+// 淇敼绉熸埛
+export const updateTenant = (data: TenantVO) => {
+ return request.put({ url: '/system/tenant/update', data })
+}
+
+// 鍒犻櫎绉熸埛
+export const deleteTenant = (id: number) => {
+ return request.delete({ url: '/system/tenant/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎绉熸埛
+export const deleteTenantList = (ids: number[]) => {
+ return request.delete({ url: '/system/tenant/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭绉熸埛
+export const exportTenant = (params: TenantExportReqVO) => {
+ return request.download({ url: '/system/tenant/export-excel', params })
+}
diff --git a/src/api/system/tenantPackage/index.ts b/src/api/system/tenantPackage/index.ts
new file mode 100644
index 0000000..49d9d40
--- /dev/null
+++ b/src/api/system/tenantPackage/index.ts
@@ -0,0 +1,48 @@
+import request from '@/config/axios'
+
+export interface TenantPackageVO {
+ id: number
+ name: string
+ status: number
+ remark: string
+ creator: string
+ updater: string
+ updateTime: string
+ menuIds: number[]
+ createTime: Date
+}
+
+// 鏌ヨ绉熸埛濂楅鍒楄〃
+export const getTenantPackagePage = (params: PageParam) => {
+ return request.get({ url: '/system/tenant-package/page', params })
+}
+
+// 鑾峰緱绉熸埛
+export const getTenantPackage = (id: number) => {
+ return request.get({ url: '/system/tenant-package/get?id=' + id })
+}
+
+// 鏂板绉熸埛濂楅
+export const createTenantPackage = (data: TenantPackageVO) => {
+ return request.post({ url: '/system/tenant-package/create', data })
+}
+
+// 淇敼绉熸埛濂楅
+export const updateTenantPackage = (data: TenantPackageVO) => {
+ return request.put({ url: '/system/tenant-package/update', data })
+}
+
+// 鍒犻櫎绉熸埛濂楅
+export const deleteTenantPackage = (id: number) => {
+ return request.delete({ url: '/system/tenant-package/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎绉熸埛濂楅
+export const deleteTenantPackageList = (ids: number[]) => {
+ return request.delete({ url: '/system/tenant-package/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 鑾峰彇绉熸埛濂楅绮剧畝淇℃伅鍒楄〃
+export const getTenantPackageList = () => {
+ return request.get({ url: '/system/tenant-package/simple-list' })
+}
diff --git a/src/api/system/user/index.ts b/src/api/system/user/index.ts
new file mode 100644
index 0000000..36776ea
--- /dev/null
+++ b/src/api/system/user/index.ts
@@ -0,0 +1,81 @@
+import request from '@/config/axios'
+
+export interface UserVO {
+ id: number
+ username: string
+ nickname: string
+ deptId: number
+ postIds: string[]
+ email: string
+ mobile: string
+ sex: number
+ avatar: string
+ loginIp: string
+ status: number
+ remark: string
+ loginDate: Date
+ createTime: Date
+}
+
+// 鏌ヨ鐢ㄦ埛绠$悊鍒楄〃
+export const getUserPage = (params: PageParam) => {
+ return request.get({ url: '/system/user/page', params })
+}
+
+// 鏌ヨ鐢ㄦ埛璇︽儏
+export const getUser = (id: number) => {
+ return request.get({ url: '/system/user/get?id=' + id })
+}
+
+// 鏂板鐢ㄦ埛
+export const createUser = (data: UserVO) => {
+ return request.post({ url: '/system/user/create', data })
+}
+
+// 淇敼鐢ㄦ埛
+export const updateUser = (data: UserVO) => {
+ return request.put({ url: '/system/user/update', data })
+}
+
+// 鍒犻櫎鐢ㄦ埛
+export const deleteUser = (id: number) => {
+ return request.delete({ url: '/system/user/delete?id=' + id })
+}
+
+// 鎵归噺鍒犻櫎鐢ㄦ埛
+export const deleteUserList = (ids: number[]) => {
+ return request.delete({ url: '/system/user/delete-list', params: { ids: ids.join(',') } })
+}
+
+// 瀵煎嚭鐢ㄦ埛
+export const exportUser = (params: any) => {
+ return request.download({ url: '/system/user/export-excel', params })
+}
+
+// 涓嬭浇鐢ㄦ埛瀵煎叆妯℃澘
+export const importUserTemplate = () => {
+ return request.download({ url: '/system/user/get-import-template' })
+}
+
+// 鐢ㄦ埛瀵嗙爜閲嶇疆
+export const resetUserPassword = (id: number, password: string) => {
+ const data = {
+ id,
+ password
+ }
+ return request.put({ url: '/system/user/update-password', data: data })
+}
+
+// 鐢ㄦ埛鐘舵�佷慨鏀�
+export const updateUserStatus = (id: number, status: number) => {
+ const data = {
+ id,
+ status
+ }
+ return request.put({ url: '/system/user/update-status', data: data })
+}
+
+// 鑾峰彇鐢ㄦ埛绮剧畝淇℃伅鍒楄〃
+export const getSimpleUserList = (): Promise<UserVO[]> => {
+ return request.get({ url: '/system/user/simple-list' })
+}
diff --git a/src/api/system/user/profile.ts b/src/api/system/user/profile.ts
new file mode 100644
index 0000000..7ac8df1
--- /dev/null
+++ b/src/api/system/user/profile.ts
@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+export interface ProfileVO {
+ id: number
+ username: string
+ nickname: string
+ dept: {
+ id: number
+ name: string
+ }
+ roles: {
+ id: number
+ name: string
+ }[]
+ posts: {
+ id: number
+ name: string
+ }[]
+ email: string
+ mobile: string
+ sex: number
+ avatar: string
+ status: number
+ remark: string
+ loginIp: string
+ loginDate: Date
+ createTime: Date
+}
+
+export interface UserProfileUpdateReqVO {
+ nickname?: string
+ email?: string
+ mobile?: string
+ sex?: number
+ avatar?: string
+}
+
+// 鏌ヨ鐢ㄦ埛涓汉淇℃伅
+export const getUserProfile = () => {
+ return request.get({ url: '/system/user/profile/get' })
+}
+
+// 淇敼鐢ㄦ埛涓汉淇℃伅
+export const updateUserProfile = (data: UserProfileUpdateReqVO) => {
+ return request.put({ url: '/system/user/profile/update', data })
+}
+
+// 鐢ㄦ埛瀵嗙爜閲嶇疆
+export const updateUserPassword = (oldPassword: string, newPassword: string) => {
+ return request.put({
+ url: '/system/user/profile/update-password',
+ data: {
+ oldPassword: oldPassword,
+ newPassword: newPassword
+ }
+ })
+}
diff --git a/src/api/system/user/socialUser.ts b/src/api/system/user/socialUser.ts
new file mode 100644
index 0000000..79f4d40
--- /dev/null
+++ b/src/api/system/user/socialUser.ts
@@ -0,0 +1,31 @@
+import request from '@/config/axios'
+
+// 绀句氦缁戝畾锛屼娇鐢� code 鎺堟潈鐮�
+export const socialBind = (type, code, state) => {
+ return request.post({
+ url: '/system/social-user/bind',
+ data: {
+ type,
+ code,
+ state
+ }
+ })
+}
+
+// 鍙栨秷绀句氦缁戝畾
+export const socialUnbind = (type, openid) => {
+ return request.delete({
+ url: '/system/social-user/unbind',
+ data: {
+ type,
+ openid
+ }
+ })
+}
+
+// 绀句氦鎺堟潈鐨勮烦杞�
+export const socialAuthRedirect = (type, redirectUri) => {
+ return request.get({
+ url: '/system/auth/social-auth-redirect?type=' + type + '&redirectUri=' + redirectUri
+ })
+}
diff --git a/src/assets/ai/copy-style2.svg b/src/assets/ai/copy-style2.svg
new file mode 100644
index 0000000..2d56a87
--- /dev/null
+++ b/src/assets/ai/copy-style2.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="1715606039621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M878.250667 981.333333H375.338667a104.661333 104.661333 0 0 1-104.661334-104.661333V375.338667a104.661333 104.661333 0 0 1 104.661334-104.661334h502.912a104.661333 104.661333 0 0 1 104.661333 104.661334v502.912C981.333333 934.485333 934.485333 981.333333 878.250667 981.333333zM375.338667 364.373333a10.666667 10.666667 0 0 0-10.922667 10.965334v502.912c0 6.229333 4.693333 10.922667 10.922667 10.922666h502.912a10.666667 10.666667 0 0 0 10.922666-10.922666V375.338667a10.666667 10.666667 0 0 0-10.922666-10.922667H375.338667z" fill="#ffffff" p-id="4257"></path><path d="M192.597333 753.322667H147.328A104.661333 104.661333 0 0 1 42.666667 648.661333V147.328A104.661333 104.661333 0 0 1 147.328 42.666667H650.24a104.661333 104.661333 0 0 1 104.618667 104.661333v49.962667c0 26.538667-20.309333 46.848-46.848 46.848a46.037333 46.037333 0 0 1-46.848-46.848V147.328a10.666667 10.666667 0 0 0-10.922667-10.965333H147.328a10.666667 10.666667 0 0 0-10.965333 10.965333V650.24c0 6.229333 4.693333 10.922667 10.965333 10.922667h45.269333c26.538667 0 46.848 20.309333 46.848 46.848 0 26.538667-21.845333 45.312-46.848 45.312z" fill="#ffffff" p-id="4258"></path></svg>
\ No newline at end of file
diff --git a/src/assets/ai/copy.svg b/src/assets/ai/copy.svg
new file mode 100644
index 0000000..f51f8d8
--- /dev/null
+++ b/src/assets/ai/copy.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="1715352878351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1499" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M624.5 786.3c92.9 0 168.2-75.3 168.2-168.2V309c0-92.4-75.3-168.2-168.2-168.2H303.6c-92.4 0-168.2 75.3-168.2 168.2v309.1c0 92.4 75.3 168.2 168.2 168.2h320.9zM178.2 618.1V309c0-69.4 56.1-125.5 125.5-125.5h320.9c69.4 0 125.5 56.1 125.5 125.5v309.1c0 69.4-56.1 125.5-125.5 125.5h-321c-69.4 0-125.4-56.1-125.4-125.5z" p-id="1500" fill="#8a8a8a"></path><path d="M849.8 295.1v361.5c0 102.7-83.6 186.3-186.3 186.3H279.1v42.7h384.4c126.3 0 229.1-102.8 229.1-229.1V295.1h-42.8zM307.9 361.8h312.3c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.9 9.6 21.4 21.4 21.4zM307.9 484.6h312.3c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.9 9.6 21.4 21.4 21.4z" p-id="1501" fill="#8a8a8a"></path><path d="M620.2 607.4c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.8 9.6 21.4 21.4 21.4h312.3z" p-id="1502" fill="#8a8a8a"></path></svg>
\ No newline at end of file
diff --git a/src/assets/ai/dall2.jpg b/src/assets/ai/dall2.jpg
new file mode 100644
index 0000000..c07374d
--- /dev/null
+++ b/src/assets/ai/dall2.jpg
Binary files differ
diff --git a/src/assets/ai/dall3.jpg b/src/assets/ai/dall3.jpg
new file mode 100644
index 0000000..7f45803
--- /dev/null
+++ b/src/assets/ai/dall3.jpg
Binary files differ
diff --git a/src/assets/ai/delete.svg b/src/assets/ai/delete.svg
new file mode 100644
index 0000000..d2ee18e
--- /dev/null
+++ b/src/assets/ai/delete.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="1715354120346" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M907.1 263.7H118.9c-9.1 0-16.4-7.3-16.4-16.4s7.3-16.4 16.4-16.4H907c9.1 0 16.4 7.3 16.4 16.4s-7.3 16.4-16.3 16.4z" fill="#8a8a8a" p-id="3257"></path><path d="M772.5 928.3H257.4c-27.7 0-50.2-22.5-50.2-50.2V247.2c0-9.1 7.3-16.4 16.4-16.4H801c12.1 0 21.9 9.8 21.9 21.9v625.2c0 27.8-22.6 50.4-50.4 50.4zM240 263.7v614.4c0 9.6 7.8 17.4 17.4 17.4h515.2c9.7 0 17.5-7.9 17.5-17.5V263.7H240zM657.4 131.1H368.6c-9.1 0-16.4-7.3-16.4-16.4s7.3-16.4 16.4-16.4h288.7c9.1 0 16.4 7.3 16.4 16.4s-7.3 16.4-16.3 16.4z" fill="#8a8a8a" p-id="3258"></path><path d="M416 754.5c-9.1 0-16.4-7.3-16.4-16.4V517.8c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4V738c0.1 9.1-7.3 16.5-16.4 16.5z" fill="#8a8a8a" p-id="3259"></path><path d="M416 465.2c-9.1 0-16.4-7.3-16.4-16.4v-59.4c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4v59.4c0.1 9.1-7.3 16.4-16.4 16.4zM604.9 754.5c-9.1 0-16.4-7.3-16.4-16.4v-67.2c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4V738c0 9.1-7.3 16.5-16.4 16.5z" fill="#8a8a8a" opacity=".4" p-id="3260"></path><path d="M604.9 619.1c-9.1 0-16.4-7.3-16.4-16.4V389.4c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4v213.3c0 9.1-7.3 16.4-16.4 16.4z" fill="#8a8a8a" p-id="3261"></path></svg>
\ No newline at end of file
diff --git a/src/assets/ai/gpt.svg b/src/assets/ai/gpt.svg
new file mode 100644
index 0000000..603e2e9
--- /dev/null
+++ b/src/assets/ai/gpt.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="1716345268026" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5622" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M956.408445 419.226665a250.670939 250.670939 0 0 0-22.425219-209.609236A263.163526 263.163526 0 0 0 652.490412 85.715535 259.784384 259.784384 0 0 0 457.728923 0.008192a261.422756 261.422756 0 0 0-249.44216 178.582564 258.453206 258.453206 0 0 0-172.848261 123.901894c-57.03583 96.868753-44.031251 219.132275 32.153053 302.279661a250.670939 250.670939 0 0 0 22.32282 209.609237 263.163526 263.163526 0 0 0 281.595213 123.901893A259.067596 259.067596 0 0 0 566.271077 1023.990784a260.60357 260.60357 0 0 0 249.339762-178.889759 258.453206 258.453206 0 0 0 172.848261-123.901893c57.445423-96.868753 44.13365-218.82508-32.050655-302.074865zM566.578272 957.124721c-45.362429 0-89.496079-15.666934-124.516283-44.543243 1.638372-0.921584 4.198329-2.150363 6.143895-3.481541l206.537289-117.757998a32.35785 32.35785 0 0 0 16.895713-29.081105V474.82892l87.243317 49.97035c1.023983 0.307195 1.638372 1.228779 1.638372 2.252762v238.075953c0 105.8798-86.936122 191.689541-193.942303 191.996736zM148.588578 781.102113a189.846373 189.846373 0 0 1-23.346803-128.612213c1.535974 1.023983 4.09593 2.559956 6.143895 3.48154L337.922959 773.729439c10.444622 6.143896 23.346803 6.143896 34.098621 0l252.30931-143.664758v99.531108c0 1.023983-0.307195 1.945567-1.331177 2.559956l-208.892449 118.986778a196.297463 196.297463 0 0 1-265.518686-70.04041zM94.112704 335.97688c22.630015-39.013737 58.367008-68.81163 101.16948-84.171369V494.591784c0 11.7758 6.45109 22.93721 16.793315 28.978707l252.30931 143.767156L377.141493 716.796006a3.174346 3.174346 0 0 1-2.867152 0.307195l-208.892448-118.986777A190.870355 190.870355 0 0 1 94.215102 335.874482z m717.607001 164.861198L559.410394 357.070922 646.653711 307.20297a3.174346 3.174346 0 0 1 2.969549-0.307195l208.892449 118.986777a190.358364 190.358364 0 0 1 70.961994 262.139544 194.556693 194.556693 0 0 1-101.16948 84.171369V529.407192a31.538664 31.538664 0 0 0-16.588518-28.671513z m87.03852-129.329002c-1.74077-1.023983-4.300727-2.559956-6.246294-3.48154l-206.639687-117.757999a34.09862 34.09862 0 0 0-33.996222 0L399.566711 393.934295v-99.531108c0-1.023983 0.307195-1.945567 1.331178-2.559956l208.892449-119.089176a195.990268 195.990268 0 0 1 265.518686 70.450003c22.732414 38.706542 31.129071 84.171369 23.346803 128.305018zM352.258716 548.862861l-87.243317-49.560757a2.457558 2.457558 0 0 1-1.638372-2.252762V258.870991c0-105.8798 87.243317-191.996736 194.556692-191.689541a194.556693 194.556693 0 0 1 124.209089 44.543243c-1.638372 0.921584-4.198329 2.252762-6.143896 3.48154l-206.639687 117.757999a31.948257 31.948257 0 0 0-16.793315 29.081105l-0.307194 286.715126z m47.307995-100.759887L512 384.001664l112.535687 63.998912v127.997824l-112.228492 63.998912-112.535687-63.998912-0.307195-127.997824z" p-id="5623" fill="#707070"></path></svg>
\ No newline at end of file
diff --git a/src/assets/ai/qingxi.jpg b/src/assets/ai/qingxi.jpg
new file mode 100644
index 0000000..d76b815
--- /dev/null
+++ b/src/assets/ai/qingxi.jpg
Binary files differ
diff --git a/src/assets/ai/ziran.jpg b/src/assets/ai/ziran.jpg
new file mode 100644
index 0000000..6290724
--- /dev/null
+++ b/src/assets/ai/ziran.jpg
Binary files differ
diff --git a/src/assets/audio/response.mp3 b/src/assets/audio/response.mp3
new file mode 100644
index 0000000..b7cb777
--- /dev/null
+++ b/src/assets/audio/response.mp3
Binary files differ
diff --git a/src/assets/imgs/avatar.gif b/src/assets/imgs/avatar.gif
new file mode 100644
index 0000000..fdbd32c
--- /dev/null
+++ b/src/assets/imgs/avatar.gif
Binary files differ
diff --git a/src/assets/imgs/avatar.jpg b/src/assets/imgs/avatar.jpg
new file mode 100644
index 0000000..d46a70a
--- /dev/null
+++ b/src/assets/imgs/avatar.jpg
Binary files differ
diff --git a/src/assets/imgs/diy/app-nav-bar-mp.png b/src/assets/imgs/diy/app-nav-bar-mp.png
new file mode 100644
index 0000000..c982804
--- /dev/null
+++ b/src/assets/imgs/diy/app-nav-bar-mp.png
Binary files differ
diff --git a/src/assets/imgs/diy/statusBar.png b/src/assets/imgs/diy/statusBar.png
new file mode 100644
index 0000000..b85562e
--- /dev/null
+++ b/src/assets/imgs/diy/statusBar.png
Binary files differ
diff --git a/src/assets/imgs/iot/device.png b/src/assets/imgs/iot/device.png
new file mode 100644
index 0000000..79339cd
--- /dev/null
+++ b/src/assets/imgs/iot/device.png
Binary files differ
diff --git a/src/assets/imgs/logo.png b/src/assets/imgs/logo.png
new file mode 100644
index 0000000..7e1043f
--- /dev/null
+++ b/src/assets/imgs/logo.png
Binary files differ
diff --git a/src/assets/imgs/profile.jpg b/src/assets/imgs/profile.jpg
new file mode 100644
index 0000000..e4bcf87
--- /dev/null
+++ b/src/assets/imgs/profile.jpg
Binary files differ
diff --git a/src/assets/imgs/wechat.png b/src/assets/imgs/wechat.png
new file mode 100644
index 0000000..6afc5e4
--- /dev/null
+++ b/src/assets/imgs/wechat.png
Binary files differ
diff --git a/src/assets/map/json/china.json b/src/assets/map/json/china.json
new file mode 100644
index 0000000..bbc0a83
--- /dev/null
+++ b/src/assets/map/json/china.json
@@ -0,0 +1,856 @@
+{
+ "type": "FeatureCollection",
+ "features": [
+ {
+ "type": "Feature",
+ "id": "710000",
+ "properties": {
+ "id": "710000",
+ "cp": [121.509062, 24.044332],
+ "name": "鍙版咕",
+ "childNum": 6
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@掳脺炉脹"],
+ [
+ "@@茮拇脮茒脡杉模潞冒蕗\\茙s脝N艑脭臍盲聹n脺皮蓨膫莯膯拇聻膜菉浓x臍漠莻坪貌茖聜聳芒脭庐漠X纽牛聘Z没脨聥茣茟G膽篓沫M贸路臋c毛茲蓧l脻漂止脜艃^脫路聫聺艣艃菋茝膹聧铆氓蓻G蓧聶驴@膬茟聨楼臉W乾脧亩艁芒"
+ ],
+ ["@@\\p|WoYG驴楼I聠j@垄"],
+ ["@@聟隆聣@聛聢V^Rq聢B聛bA聦nTXeRz陇L聻芦鲁I"],
+ ["@@脝EE聴聞kWq毛聽@聹"],
+ ["@@fced"],
+ ["@@聞炉蓽脛猫聫a矛炉脴菗I摹慕"],
+ ["@@莽没臇毛膭聳h貌艡聽"]
+ ],
+ "encodeOffsets": [
+ [[122886, 24033]],
+ [[123335, 22980]],
+ [[122375, 24193]],
+ [[122518, 24117]],
+ [[124427, 22618]],
+ [[124862, 26043]],
+ [[126259, 26318]],
+ [[127671, 26683]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "130000",
+ "properties": {
+ "id": "130000",
+ "cp": [114.502461, 38.045474],
+ "name": "娌冲寳",
+ "childNum": 3
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@o~聠Z]聜陋r聣潞c_魔虏G录s`j脦鸥n眉s脗聹艂NX_聯M`脟陆脫nUK聟臏聛膿s陇颅漏yr媒搂u模聦聫c聠J聤聸e"],
+ ["@@U`Ts驴m脗聜"],
+ [
+ "@@o潞茓脛d聳eV聨DJj拢聙J|脜dz脗聲Ft~聻K浓赂I脝v|聰聡垄r}猫聠聨onb聵}`R脦脛n掳脪d脼虏聞^庐聮ln脨猫膭l冒脫聹脳]陋脝}聬聧Li膫卤脰聫`^掳脟露p庐膽Dc聹艐`聧聳Z脭聮露锚qvF脝職聠N庐膯TH庐娄O聮戮聤Ib脨茫麓B膼散糯脝铆圈p聳膼脼XR聙路nndO聻陇聮O脌膱茠颅Qg聵碌Fo|g葤臋SWb漏osx|hYh聲g艃fm脰末n潞聙T脤聮Sp聸垄dY膜露U膱jl聮菒p盲矛毛|鲁k脹fw虏Xjz~脗qbT聤脩聞臎浓@|oM聡聮zv垄Zr脙Vw卢脓臇赂f聦掳脨T聙陋q聨s{S聻炉r聽忙脻lNd庐虏臑聽菃iG臉聜J聶录lr}~K篓鸥茞脤W枚聙聶脝聤zR職陇l锚m臑L脦聞聮聺@隆|q]SvK聙脩cwp脧脧聛聠目聫膰猫n莫W聬l膭kT}聢J聰陇~聝脠T聛聞d聞聶pdd示默聤聰聨BVt聞E脌垄么P膸茥猫@~聜k聳眉\\r脢臄脰忙W_搂录F聵聠麓漏貌D貌j聬聮聢Y脠rb臑膩酶艀G{苺|娄冒rb|脌H`p蕿kv聜GpuARh脼脝嵌g臉聤T聫羌乒S拢篓隆霉鲁艠脥]驴脗y聧聶么EP聽xX露聫鹿脺聡O隆聯g脷隆Iw脙茅聭娄脜B聡脧|脟掳聟N芦煤mH炉聺聥芒聼D霉聨y艤聻挪I脛u膼篓D聛聻聲赂d蓚聡聬聜F聼聝聲聸Oh聡膽漏O聼聸i脙`ww^聝脤k聼聭脩H芦茋扦艞暮tFu聟{Z}脰@U聡麓聟蕷Lg庐炉O谋掳脙w聼聽^聵聴聙Vb脡s聡聢mA聟锚]]w聞搂聸RRl拢聡拳碌u炉b{脥D臎茂每颧聨uT拢摹聝臎艞苾臐聯Q篓fV聠茓聲茀n颅a@聭鲁@職膹聞y脙陆聫I墓脢K職怒f膵虐贸聦xV@t聧聢漂聦J聰]e聝R戮fe|rHA聵|h~臇茘l搂脧聤lT铆b聽脴o聢脜bbx鲁^z聬脙亩職露Sj庐A聰y脗h冒k`職芦P聙聰脣聢碌EF聠脹卢Y篓幕r玫qi录聣Wi掳搂聬聮脨卤麓掳^[聢脌|臓O@脝xO\\t聨a\\t臅t没{摹聦颧X媒莫脫j霉脦Rb聸職^脦聸fK[脻聫d臎Yf铆脵Ty聨uUSy艑艔暖@Oi陆聮茅艆颅aVc艡搂ax鹿X呕谩c聡聻WU拢么茫潞Q篓梅脩ws楼qEH聣脵|聣聸拧YQo艜脟y谩膫拢M脙掳o钮聫脢聣P隆m聫職WO隆聙v聠{么v卯膿脺ISp脤hp篓聽聭j聠de艛Q脰j聵X鲁脿聶膱[n`Yp@U聻聳cM`聮RKh聦Eb聹聰p艦lNut庐Etq聜ns脕聤gA聺聥i煤聛聥oH聡qCX聡聰hfgu聯~脧聥WP陆垄G^}炉脜墨GC聼聺脩^茫ziM谩募MT脙茦rMc|O_聫聻炉艓麓|聡morDkO\\m膯Jfl@c蘑卢垄a摩tR谋脪聶戮霉苺^ju懦艙K颅聝UFy聶聴茲聟聸墨脹梅膮V脳q匹V驴a葔d鲁聺B聸qPBm聸a脣膽呕模m聯脜庐V聤鹿d^K聡Ko聼nYg聯炉Xhqa聰Ldu楼聲脥p菂隆K膮脜聝k臐臋臎hq聡}Hy脙聯]鹿千拢聟脥梅驴q谩碌搂職聶g聭陇o^谩戮ZE聡陇i`某{n聲聝Ol聫禄聼W脻臄寞hg聧聸F[驴隆聴脽kO眉拧_聣聙奴聥i聞潜脿Ut臈Gyl聝}聧聦脫M}聙jpEC~隆FtoQi聭職Hk聺k{聬脙m茂聜"
+ ]
+ ],
+ "encodeOffsets": [[[119712, 40641]], [[121616, 39981]], [[116462, 37237]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "140000",
+ "properties": {
+ "id": "140000",
+ "cp": [111.849248, 36.857014],
+ "name": "灞辫タ",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@脼末脪聝S聣ra}脕聙yWix卤脺e麓l猫聯脽脫菑ok聭膰i碌VZ模隆co聹聭TS脣鹿莫mn脮艅e聳hZg{gtw陋pXa臍Th葢p{露Eh聴庐R膰茟P驴拢聭P聺mc赂mQ脻W聲膹去o脜卯伞懦A膹盲鲁a脧聣J聭陆楼PG颅膮SM颅聶聟E脜ru碌茅聙聭Y脫聨聲艑_d聸膾Co颅脠碌]炉聬_虏脮j膩聨K~聫漏脜脴^脭聸k茂莽膬m脧聭k]颅卤聝c脻炉脩脙mQ脥~_a聴pm聟聧~莽隆q聯聢u{J脜脓路聺Ls}聳Ey脕脝cI{陇Ii聧CfUc聲茖脙p搂]臎聸芦vD@隆S脌聭碌M聜脜wu聨YY聡隆Db脩c隆h聝脳]nkoQdaM莽~eD聲脹tT聣漏卤@楼霉@脡隆聣ZcW|WqOJm末l芦魔艧vO脫芦Iq聧膬V聴楼聼D[mI~脫垄cehi脥]茡~磨qX聤路e品聹n卤聯}v聲[臎膹聨艜]_聭艙聲`聣鹿聝搂脮艒I聶o漏b颅s^}脡t聧卤奴芦鲁p拢每路W碌|隆楼膬F脧s脳聦楼艆x聼脢d脪{潞v拇脦锚脤蓨虏露聙眉篓|脼聘碌炔聭LL煤脡茙陇蠆臋臄V`聞_b陋聥S^|聼d聤zY|dz楼p聠Zb脝拢露脪K}t摩脭艈茽聜PYzn聙脥vX露臍n聽臓聛脭聞z媒娄陋聵梅聻脩母脵聨U葘赂聜d貌脺J冒麓聮矛煤NM卢聦XZ麓聭陇艎歉_tldI職{娄苺冒臓趣楼NehXn聛YG聜聡R掳聽片Dj卢赂|C臑聞Kq聜潞f茞i暮漏陋~膯OQ陋聽陇@矛铅蓪虏忙B聦脢聰T聹鸥聵蕚艒臇聮職拇艦聳葊聹脝每葎l扭膾枚聞t聰脦陆卯录抹Xh聦聭聵|陋M陇脨z"
+ ],
+ "encodeOffsets": [[116874, 41716]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "150000",
+ "properties": {
+ "id": "150000",
+ "cp": [111.670801, 41.818311],
+ "name": "鍐呰挋鍙�",
+ "childNum": 2
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ [
+ "@@炉聧Pq聝FB聟聣|S聲鲁C|k帽聲H聥聫d聭i脛楼聧s聢艍脜聭聟P贸脩脩E^聭脜Ppy_YtS聶聧hQ路aHwsOn艍脙職s漏iqj聸聣聙USi潞]茂W職聣芦gW聛隆A聳聛R毛楼_聨sg脕nUI芦m聣聟聞聥]j聡vV录聧euhwqA聞聫aW聵聝_碌j聟禄莽jioQR鹿膿脙脽t@r鲁[脹l膰脣^脥脡谩G聯聸OU脹聴OB卤聲X聼k脜聡鹿拢k|e]ol聶聼kV脥录脮qta脧玫jg脕拢搂U^聦聰RL聢脣nX掳脟聮Bz聧聠^~wfv聢ypV聽炉聞偏膲谁全茥欧煽每目茟藘臐每脙莾脽脣艖贸漏菒葝艗聺臇M脳脥Eyx聥镁p]脡v茂猫聭v聺苺n脗拇脰@聜聣聠V~膱聛v娄w臇t聴膿jy脛DX脛xGQuv_聸i娄aB莽w聭藳wD聶漏{聼t膩mQ聙{EJ聫搂KP艣茦瓶楼聧@聣sCT聲脡}蓛w聢茋y卤聼g聬脩聯}T[梅k脨莽娄芦聟S脪楼赂毛BX陆聣H谩脜碌脌臒tS聬脻脗a[疲掳炉聛娄聛P茂隆聛]拢摹聯聳聯脪k庐G虏聞猫Q掳贸Mq}E聤贸茞脟\\聛聝聡@谩眉gQ脥聥u楼F聝聯T脮聸驴J没聡]|mv膩脦Yua聛^Wo脌a路颅z膮脪ot脳露CL茥i炉陇m茙H菉陇卯矛删艎矛Td氓wsR聧脰g膾懦煤聛脥摹盲脮}Q露聴聢驴A聲聠聥[隆聦{d脳uQA聝聸M聲xV聥vMOm膬l芦ct[w潞_職脟脢聤聨聼jb脗拢摩S_茅聯QZ聯_lwgOi媒e`YYLq搂I脕聢浅拢脵脣[脮陋u茝鲁脥T聴s路b聛脕慕盲臈[聸b[聢艞f茫cn楼卯C驴梅碌[艔脌Q颅艒職膲m驴脕^拢mJVm聡聴聫L[{脧_聺拢聸F楼脰聧{殴A}聟脳Wu漏脜a懦某瞥hB{路TQq脵I姆脣聭Z膽漏Yc|M隆聟L聲聺eVU贸K_聺聺QWk聮_磨聭驴茫Z聫聲聛禄X\\拇uU聝猫聡lG庐臎艂T臓臒D艃聰Or脥d聜脝脥z]聥卤聟怒漏聼脜聮]聦脜脨}U脣楼漏T膵聶茂xgckfWgi\\脧膾楼Hk碌E聵毛{禄脧etcG卤ahUi帽iWs蓙聢路c聺聳C聜脮k]w葢|膰a}w聟Va臍谩聽聻聦G掳霉nM卢炉聠{脠聢脨脝A聮楼脛锚Jx脵垄聰hP垄脹聢潞聙碌聬wWO聼贸F聨職脕z^脌艞脦煤麓搂垄T陇腔坪S聬臈聣堑h脻脜QgvBHou聛蕽l_o驴Ga{茂q{楼|趴目H膫梅a臐脟q聡Z聫聭帽i帽C鲁陋聴聟禄E`篓氓X膿脮q脡没[l聲}聛莽@膷茦贸O驴隆聝FUs聧A聣聯式墨cc職oc聝聜聝脟S}聞聯拢聡IS~膬l聫k末X莽m聬膱聟艀脨聜o脨dx脪uL^T{r@垄聭聻脥聝臐K茅n拢kQ聶聣y職脜玫脣X欧茝L搂~}kq職禄聛IH臈菂j臐聼禄脩脼o聼氓掳qT聺t|r聧漏脧S聥炉路e浓臅x芦脠[eM聢驴yu聢聭pN~鹿脧yN拢{漏聮聴g聥魔W铆禄脥戮s聯蓹拧菂_脙膧蓷卤膮聶某膲聫蕧艑欧聴S聸脡聯A聥卤氓钎蓩@毛聼拢R漏膮P漏}墓陋茝j鹿er聛聝LD臐路{i芦偏C拢碌sKC職聟GS|煤镁X聰gp聸{脕X驴聼膰{票葟帽Z谩臄yo脕hA聫聶}艆膯fd艍聞_鹿聞Y掳臈签脩隆H炉露oMQq冒隆脣聶|聭脩`骗艁X陆路贸脹聯聧x臒寞脜cQ聡聢聯聝s芦t葖菂F聯聼聛聺霉^i聧聭t芦膶炉[聸h聛聛Ai漏谩楼脟臍脳l|鹿y炉Y鹊茡聥帽聬菣碌茂聜膵聶幕|D聹聶眉拳露隆聵聸o沤盲脮G\\脛聫T驴脪玫r炉聹聼Lgu脧Y臋R譬職煞艑O\\陌脨垄忙^艎聽牟榷葐b脺G聨臐卢驴臍V膸g陋聧^铆u陆j每臅臋j谋k@慕聝]臈l楼脣聡沫没脕聞聝臈茅V漏卤膰n漏颅葒聻脥q炉陆聲Y脙脭艍聯脡N脩聺脜脻y鹿Nq谩蕝D恰脣帽颅苼Y脜y瘫os搂葖碌式菢菑片杀脿聭瓢N垄茢脢u木媒木蠋泉坪蓚募聻x聹Z膱}脤艍弄聵暮艙聨沫F袥慕虆龋徒脪诺矛譬脟蠇每犬恰艔莽茟暖臅~脟聧聸录瘸脨Uf聠dIx每\\G聽聫聢z芒蓮脵O潞路pqy拢聠@聬聦聤q镁@菫私IB盲疲zs脗Z聠脕脿幕d帽掳艜z茅脴疟z葯C矛D葠拇暮f庐聨脌木瓢酶@蓽脰脼K膴艊苿搂聜蛻t臎茂汀VA摹脩脩禄d鲁枚菎脻X膲臅脰{镁膲u赂脣蕝臒U處茅h晒茊虠坍葮菉芝啷溹“牛嗒久裁な甒卢庐覍e专奴葼k涩苫碳茫眉f茽S爪嫂蟼氓葓H蠚脦K浅筒O冒脧葐茦录C蠚菤啖毸夹っ斅偮て屄災灺澨猀胜麓录m葼J藔聼撇脌蔂m菒n菙膸葐脼菭N~聙盛臏聜露茖膯臉藕蕟痊霜臍膾赂臑G葨拼苺j`蘑莽亩膩脿艃潞膿蘑聝臇膰職Y聦脌艓眉么Q脨脗艓艦菃艦锚茤職聵o藛D膜脮潞脩菢脹摔鲁蛝g艅聛茦臄脌^聻陋苽`陋t戮盲茪锚摩膧录脨聙臄菐篓葦禄蜖^水脢圈皮酶xRr艤H陇赂脗xD脛聺聦|酶藗藴飘聬脨卢蓺w刹Fj臄虏脛w掳菃d脌脡聻_母d卯脿艓j脢聹锚T臑陋艑聡艤W脠|tq蘑UB~麓掳脦FC聲聨U录p膧膿苿N娄聧戮O露聤艂K膴Oj聯臍聰j麓臏Yp聵{娄聞聢S臍脥\\T職脳陋V聳梅艩铆篓脜DK掳脽t艊臄K職篓堑脗c木蹋臍牵葎慕F聡l摹U牡聹艊聥龋F蕢蓙聝M臒寞蕪贫煞脴怒O墙芦平聬奴鹿票艖虧权搂葹蕵臇i蓽啥师}篓知酄溙�茋乾鹿迁E甩磨陋脭锚F聨x煤Q聬聞Er麓W聞rh陇茞聽\\tal膱DJ聵脺|[Pll虤赂茙G煤麓P聻卢W娄聠^娄聳H]prR聯n|聬or戮聬wLVn脟Iujkmon拢cX^聫Bh`楼V聰聞娄U陇赂}聙xRj聳[^xN[~陋聤xQ聞聜[`陋H脝脗Exx^w職N露脢聵|篓矛聠聵聙Mr聹dYp聜聬oRzNy聵脌Ds~聙bcf脤`L聳戮n聥|戮T聜掳c篓脠垄a聜r陇聳`[|貌D艦臄枚xEl脰dH聞脌I`聞膸\\脌矛聛~脝聨R录tf聲娄^垄姆露e聰脨脷M聦ptgj聳聞伞膶脜y摹L没聶艊V庐聤脛聫脠苺聠膸掳P|陋VV聠陋j聳卢臍脪锚p卢聳E|努脗c|脌t茞K聽f聢{臉F膾聹茖X撇膮o陆臉聭\\楼聳o}聸脹u拢莽颅kX聭{u末芦膩铆脫U艆脽泞q聙聫扭楼ly艌[聙oi{娄聥L聡艅聡冒F泉葨聰聺膾L聞驴脤聥聢f聦拢K拢屎聶oqN聼聝w臒c`ue聴tOj脳掳KJ卤q聝脝摹m聣臍艞os卢聟qehqsu聹聝H{赂kH隆聤聟脢R仟脟茖b葐垄麓盲脺聧垄N矛脡蕱娄芒漏臓u娄枚膶^芒拢膫h聳職臇M脠脛w聜\\f纽掳W聽垄戮lu鸥D聞w聤\\虁蕢脤脹M聟膧[b脫聻En}露Vc聟锚聯s聝"
+ ]
+ ],
+ "encodeOffsets": [[[129102, 52189]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "210000",
+ "properties": {
+ "id": "210000",
+ "cp": [123.429096, 41.796767],
+ "name": "杈藉畞",
+ "childNum": 16
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@L聳聨@@s聶a"],
+ ["@@MnNm"],
+ ["@@聛d聜c"],
+ ["@@e脌聜C@b聜聯聺聣"],
+ ["@@f聡聟Xwkbr聳脛`qg"],
+ ["@@^jtW聭Q"],
+ ["@@聛~聽Y]c"],
+ ["@@G`臄N^_驴聛Z聜脙M"],
+ ["@@iX露B聥Y"],
+ ["@@聞Y聝Z"],
+ ["@@L_{Epf"],
+ ["@@^WqCT\\"],
+ ["@@\\[聯聥搂t|聰陇_"],
+ ["@@m`n_"],
+ ["@@脧x菍{q_脳^Giip"],
+ [
+ "@@@聹茅^B聠聡nt聢a脢U聴聵聼]x聽炉脛P牟颅掳h聙蕶K鲁聠V聢脮@Y~聠|Ev墓s脟聞聧娄颅L^p脙聜虏鸥脪G聽聮脣l]聞xx脛_聵fT陇膸陇c聨聹P聞聳C篓赂TVjbgH虏sd脦dH聬t`B聢聴虏卢GJj臋露[脨hjeXdlwh職冒S膶娄陋V脢脧聙聥脝聭Z聵脝哦庐虏聠^聦脦y聛脜脦cPq艅聯臍DM魔臏艁H颅聢k聞莽vV[某录W聳聜Y聬脌盲摩聮聭`Xl聻R`聻么LUV聻fK聳垄聠{NZd膾陋聮Y母脤脷JRr赂SA|拼g糯拇脝bv陋脴X~聠藕B聨|娄脮聹E聻陇脨`\\|聬K聢聵UnnI]陇脌脗膴n艓聶R庐艕驴露\\脌酶铆Dm娄脦b浓ab聣聹a臉\\木茫聜脗赂a聵t脦S茞麓漏v\\脰脷脤谴陇脗聡篓JKr聙Z_Z聙fj镁hPkx聙`Y聰聮RI聦jJcVf~sCN陇聽聢E聜聹h忙聫m聣聳sHy篓S冒脩脤\\\\聼膼RZk掳IS搂fq艗脽媒谩臑聧脵脡脰[^炉扦挪聞锚麓\\娄卢膯PM炉拢聼聢禄u茂p霉zEx聙聻an碌yoluqe娄W^拢脢L}帽rkqW艌没P聶聣UP隆么J聤oo路聦U}拢聦聞[路篓@X聦母聼聯聥聥DXm颅脹脻聫潞聡聸GU聥C脕陋陆{铆膫^聬cj聡k聯露脙[q陇聯L脡枚鲁cux芦zZf聝虏BW脟庐Y脽陆ve卤脙C聲媒拢W{脷^聮q^s脩路篓聥脥O聛t聯鹿路C聛楼聡GD聫聸r铆@w聧脮K牛脙聺聥聵聼芦V路i}x脣脥梅聭i漏臐聡蓾恰]聝聢{c聶卤OW聥鲁Ya卤聼聣_莽漏聜H聻臅o偏聙艊q聝r聺鲁聣Lys[聞帽鲁炉OS聳膹OMisZ聠卤脜FC楼Pq{聫聜脙[Pg}\\聴驴gh膰O聟聲k^模脕聧F谋膲磨M颅oEqqZ没臎艍鲁F聭娄o牡聴h聼脮P{聫炉~T聺脥l陋聣N聣脽Y聯脨{Ps{脙VU聶聶e膸wk卤艍V脫陆沤J茫聛脟脟禄Jm掳dhc脌ff聭dF~聢聙膧e臇聙d`sx虏聽職聝庐E偶膧dQ聧聥脗d^~膬脭H聢娄\\聸LKp膭Vez陇NP聽枪脫聴R聶脝膮JSh颅a[娄麓脗ghwm聙B聬脨篓藕hI|聻VV聨聴聨|p]聽脗录猫N盲露脺B脰录聯L`聜录b脴忙聦KV聰聼po聹煤NZ脼脪Kxpw|脢EMnzEQ職聨IZ聰聨Z聡NB聢膷脷F脺莽m末聜W莫帽t聭脼牡脟帽Z芦uD聜卤|茝l某楼茫n路卤Pm脥a聣聳d聫a聡聽CL聡菓k霉贸隆鲁脧芦Qa膵脧聭O脙楼脮膽Q去膵骗y聥鲁脙A"
+ ]
+ ],
+ "encodeOffsets": [
+ [[123686, 41445]],
+ [[126019, 40435]],
+ [[124393, 40128]],
+ [[126117, 39963]],
+ [[125322, 40140]],
+ [[126686, 40700]],
+ [[126041, 40374]],
+ [[125584, 40168]],
+ [[125453, 40165]],
+ [[125362, 40214]],
+ [[125280, 40291]],
+ [[125774, 39997]],
+ [[125976, 40496]],
+ [[125822, 39993]],
+ [[125509, 40217]],
+ [[122731, 40949]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "220000",
+ "properties": { "id": "220000", "cp": [125.3245, 43.886841], "name": "鍚夋灄", "childNum": 1 },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@聭p盲聰鲁PCl聝Fbb脥z職聙wBG聮沫聙Z聞脜i聯禄聝lY颅膵虏Sg聨k脟拢聴^S聣聯qd炉聲聥R聟漏茅聨拢炉S聠\\cZ鹿i疟茝Cu茘脫X聡oR}聯M^o聲拢聟R}o陋U颅F聟uuXHl聫E脜聲聡聙脧漏陇脹mT聨镁陇D聳虏脛uf脿脌颅XX脠卤Ae聞yYw卢dv玫麓K脢拢聰\\r碌脛l聰i聢聫d膩]|卯漏戮D脗V聦聹H鹿聢脼聬庐脺Wn聦C聰聦姆聽W聥搂@\\赂聥聝~陇聥Vp赂聣p贸IO垄聤VO職艊眉rXql~貌脡K]陇楼Xrfkvzpm露聺bwyFo煤v冒聡录陇聽N掳膮O楼芦鲁[聝茅恰疟_掳脮\\脷脢臐聨镁芒艖脿erR篓颅JYl膹Q[聽脧Y毛脨搂TGz聲tn聤脽聬隆gF聛聫kM聼膩G脕陇ia聧聽脡聣聶脠鹿`\\xs聙卢d膯kNnuNU聤聳u聻P@聜vRY戮聲聳\\垄聟聦G陋贸膭~R茫脰脦蘑霉聜膽糯脮hQ聨xtc忙毛S山艍铆毛菈拢茘G拢nj掳K茦碌Ds脴脩py膯赂庐驴bXp聜]vb脥Zu膫{n聢^I眉聹脌S脰聞聰娄E聦vR脦没h@芒聞聢[聜茝脠聣么~FNr炉么莽R卤聝颅H脩l聲聮蘑聳^陇垄聜O冒聼聦忙vxs艗]脼脕T聫臓s露驴芒脝聤GW戮矛A娄路T脩卢聠猫楼聙脧脨J篓录聫脪脰录聝痞蓜x脢~S聳tD@聤膫录糯隆jl潞W聻v脨聣聢z痞Z脨聨虏CH聴聽聞A聺xiukd聥聦Ggetqmc聻脹拢Ozy楼cE}|聟戮cZ聟聧k聜聣驴u艕茫[oxGikfe盲T@聟職SUwpi脷FM漏聮拢猫^脷聼聜`@v露e艌聠f聛聽h聵eP露聬聻t聯盲Ol脙聰Ug聝脼z鸥U`l聹}脭脝Uv脴_艑卢脰i^膲i搂虏脙聤B~隆膱聶脷Egc|DC_圈m虏rBx录M脭娄女d抹脙芒Yx聭茦DV脟暮目g聧驴cw脜\\鹿聵楼Y沫l聛聹陇聻Ov聠職LjM_a聽W`z募M啪路\\swq脻SA聡職聴q聣艢某炉聤聭掳k聬聤R膿掳wx^膼k莻脪聯聞聹聻聯聹聨聞聥\\]聵nr膫}虏膴挪脪酶茫h路M{yMzys臎n聺膾摹V路掳聯G鲁录X脌聯聯聶陇鹿聧i麓o陇艃職聼脠`脤聝遣脛U臑d\\i脰職聦聢m脠B膜脺刹DEh聽LG戮苺脛戮{Wa聦聧Y脥脠聫蘑臉脭R卯膼j聥}脟聻聯ccj聡oUb陆職聧{聯h搂蔷{K聥茤碌脦梅聻G膧脰艩氓瓢脦s颅l聸聲yi膿芦聥`氓搂聺H楼Ae聺^搂聞GK聛}i茫\\c]v漏模Z聯m脙|聯[M}模T蔁牡脗聭脗`脌聳莽m聣聭FK楼脷铆脕bX職鲁脤Q脪聭Ho聛f{聣]e聙pt路G艐臏Y眉n膸懦VY^聮聵yd玫k脜ZW聞芦WUa~U路Sb聲wG莽菓聜聯iW^q聥F聜聯聸uN臐聧聴路Ew聞聥UtW路脻膹忙漏PuqEzwAV聲聴XR聣茫Q`颅漏G聦M聡ehc聸c聰聺膹脧聺d聡漏脩W_脧聴Y茀聦禄聟茅\\聧聝晒~菣G鲁m脴漏B拧聬uT搂膜陆垄脙_脙陆聭L隆聧聭媒聼qT^rme聶\\Pp聲ZZb聝y聼聮uy聛bQ聴ef碌]Uh目DCm没va職脵NSk聺Cwn聣c膰fv~聟Y聥聞脟G"
+ ],
+ "encodeOffsets": [[130196, 42528]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "230000",
+ "properties": {
+ "id": "230000",
+ "cp": [128.642464, 46.756967],
+ "name": "榛戦緳姹�",
+ "childNum": 2
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ [
+ "@@U聝碌N每楼墨猫莽聛聥聲聺H脥酶茣露L聦墙|g篓|聰聶聨a戮pVi聢dd聺聰~脠i聦铆膹脫Q摹臈脟聬Z脦聥聨Xb陆|趴脙H陆聼KFg杀C模脹脟A聡n聶聥j脮c[V臐潜脙脣聞脟_聶聽拢艅鲁p聨j拢潞聰職驴聰禄WH麓炉聰U赂膽蘑m聻t臏yzzNN|g赂梅盲疟脩卤膲膩~mq^聴聦[聧聝聰聸聸聺聰聝莵脩膹lw]炉xQ臄聫聣炉l聣聮聙掳艡拇r聤聶聵B聢脼Txr[t沤赂幕N_y聼X`biN聶Ku聟聺P聸拢k聜Z漠聴娄[潞x脝脌dh聨墓艀U脠茥Cw聮谩Z魔脛怒c脫楼禄NAw卤q去nD`{Chd脵F膰職}垄聣A卤脛j篓]膴脮j艐芦脳`Vu脫脜聸~_k欧V脻yh聞聯Vk脛茫Ps聰聫聺O碌聴f聼ge聜艊聟碌f@u聛_脵聽脵c聼陋N陋脵EojVx聶T@聠茫SefjlwH\\p艔盲脌v聛聤聨lY聠陆d{聠F~娄dyz陇P聺脺ndsrhf聥Hc聦vlwjF聹拢聛G聵卤D脧聧匹Y聡y脧聤聺u鹿Xik目娄脧q茥莯O艤篓LI聬|FR膫n聽s陋|C職藴zx聬A猫楼b聹fudTrFW脕鹿Am|聵臄臅s姆脝F聡麓N職聣}膰聟U聛聤脮@脕某趴mu聻莽聮u冒^脢媒ow聦Fz脴脦臅N艖聻菑葞么陋脤艗莿脿膧脛藙臑艀茠蕗膧茦鸥水痊片膴掳聝U聼zou聡xe]}聨聟Ay脠聭W炉脤mK聡聯Q]聥莫潞if赂脛X|sZt|陆脷U脦聽lk職^p{f陇l聢潞l脝W聽聳聙A虏聵PV脺聹PH聰脢芒]脦膱脤脺k麓\\@q脿s臄脛Q潞pRij录猫i聠`露聴聞bX聝聬rBgxfv禄聨uUi聢聦^v聧~聰J卢mVp麓拢聦麓VWrnP陆矛垄BX聜卢h聶聤冒X鹿^TjV聹聤ri陋j聶t艎脛m聙tPGx赂bgR職聨sT`Zoz脝O]聮脪F么脪聠O脝聮聡艎聦v脜聻聰p聮cG聦锚聤sx麓DR聳聦{A聠聞EOr掳聦聲聻x|铆聹b聢鲁Wm~聺DVj聧潞茅NN聠脣脺藳啥颅G聝x欧CSt聼}]没艒聲Smtu脟脙臅N聲聶膩g禄職铆T芦u}莽陆B牡脼剩楼毛脢隆M脹聨鲁茫葏隆茓a签脠脡Q聣聠G垄路lG|聸聞tvgrrf芦聠pt臋艠n聤脜蘑r聞I虏炉Li脴sPf聵_v臓d聞xM聽pr使職L陇聥陇聡e脣聦聝脌膽K聯聻茂脵VY搂]I聡贸谩磨]姆聠K聢楼聦j|p艊\\kz牛娄聬拧n艈盲脭V膫卯莫卢|vW聮庐l陇猫脴r聜聵聲xm露膬~l脛漂膭蛣枚葎E脭陇脴Q膭聳膭禄脾j圈O呛篓矛S艝脝片y聰聧Q聹v`聳cw聝ZS脤庐眉卤莿]艀莽卢B卢漏艅z坪欧蓜e聬eO聛抹S聮聺聦fm聽膴聜苺P處膿z漏膴聜脛脮脢mg聼脟sJ楼茢聢艎艣忙聮脦聛脩qv驴铆UO碌陋聣脗n摩脕_陆盲@锚铆聟拢P}臓[@g摹}g聯蓨脳聯没脧WX谩垄u啪苹脤sN脥陆聺茙脕搂膷艕聸A聫膿eL鲁脿ydl聸娄臉V莽艁p艣菃慕暮趴脢聝Q铆脺莽脹摹脭聫s臅卢聴歉炉Y脽膵摹H碌聽隆e氓`聺募聝r膲艠贸脾F矛聯膸W酶x脢k聠聰茍d片聫v|聳I|路漏Nq艅R艀聝陇茅聰e艎聹艀聸聢脿艀U虏艜苺B聜Q拢膸}L鹿脦k@漏膱u前懦迁聰脷搂茍nT脣脟茅茻脢cf膷扭^Xm聡聴H聧膴臅脣芦W路膵毛x鲁菙姆脨膵J膩聜w陌_母聵葊^么Wr颅掳o煤卢摩聟浓K~聰劝C膼麓嵌拢聮fN脦猫芒w垄Xn女e脗脝亩聨聬戮戮x盲L拇聛臉l募O陇脪抹A垄脢蓺篓庐聜脴C脭聽努G茽聰痞Y臏聡臉脺片DJ聴g_庭艙@膷艆幕A聯露炉@w脦qC陆膱禄N聼膬毛K聶膹脥Q聯脵偏[芦脙铆聲g脽脭脟O脻谩W聭帽聫uZ聯炉磨聙聼聧艜膩隆脩姆Ju陇E聽聼氓炉掳WK脡卤_d_}}聧vy聼玫u卢茂鹿脫U卤陆@g脧驴r脙陆D聣聠g聟Cd聣碌聴掳MF聛聫Yxw驴CG拢聥R茮陆脮{]L搂{qq膮職驴B脟苹臒毛職墉菉脣|c虏}F碌}聸脵Rs脫p聫g卤聤QNq谦艐Rw艜n茅脩脡K聼聠芦SeYR聟艐聥@{陇SJ}職D聽脹菛謲聼]g聺r隆碌欧jqW脹ham鲁~S芦聯聞聸脼]"
+ ]
+ ],
+ "encodeOffsets": [[[134456, 44547]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "320000",
+ "properties": {
+ "id": "320000",
+ "cp": [119.767413, 33.041544],
+ "name": "姹熻嫃",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@c镁脜Pi聤`Z聼Ru楼脡\\]~掳聨Y`碌聠脫聝^ph脕bn脌艧煤聨貌a聳默潞T脰艗b聜聵e娄娄聙{赂Z芒膰Np聦漏聻Hr|^聢mjh聤SEb\\afv`sz^lk聨lj聥脛tg聥陇D聵颅戮X職驴脌聮|膼聰iZ聞聺葊氓B路卯}GL垄玫c脽ja聼yBF碌脧C^沫聲c脵t驴s臒H]j{s漏HM垄聝QnD脌漏Da脺脼聦路jg脿iDbPufjDk`dPO卯聝hw隆磨聡楼職G聵聼P虏膼ob潞rY聠聞卯露aH泞麓聽]麓聜r谋l聺w鲁r_{拢DB_脹d氓uk|聢浓炉F聽C潞yr{XFy聶e鲁脼膵聡驴脗聶k沫B驴聞Mv脹pm`r脷茫聰@臉鹿h氓g脣脰瓶xnl膷露脜矛陆Ot戮dJl聤VJ膫聤聹莯聹艦qvnO聤^聼J聰Z聭偶路Q}锚脥聨脜m碌脪]聨茘娄Dq}卢R^猫膫麓艀幕膴I脭聮t聻牟yQ艕臓MNt聹R庐貌Lh聫聣聸臍s漏禄聹}O脫聦GZz露A\\j抹F聢盲O膜聵HY職聠Jv聬脼HNi脺a膸職脡聳nFQl職NM陇聢B麓膭N枚蓚tp聳努df氓聟聢聥qm驴Q没聤霉艦聡脷b陇u艃J糯u禄鹿膭聲l聛葨魔糯w虒诺虏枪菭蜎h沫艂茣r莽眉卤Y聶xci聡t臒庐聺j疟垄KO姆聲Coy`氓庐VTa颅_膧]艕脻蔀茂虏石脢^]afY歉脙膯膿莫龋J膽蛵么茓脛聺脛脥聨墨聣莽脹蓤钎拢颅脹mY`贸拢Z聧芦搂掳脫鲁QafusN谋菂_k}垄m[脻贸D碌聴隆RL膷iXy聡脜N茂膬隆赂i臄脧聭N脤艜o膿d艒卯氓扭没Hcs}~脹wb霉鹿拢娄脫C聛t聥OPr聝E^脪o聤g聶膲I碌聻脹脜使K聟陇陆phM聤眉`o忙聙聠艀"
+ ],
+ "encodeOffsets": [[121740, 32276]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "330000",
+ "properties": {
+ "id": "330000",
+ "cp": [120.153576, 29.287459],
+ "name": "娴欐睙",
+ "childNum": 45
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@E^dQ]K"],
+ ["@@聛jX^j聡"],
+ ["@@sf聤bU聡"],
+ ["@@qP\\xz[ck"],
+ ["@@聭R聝垄聜FX}掳[s_"],
+ ["@@Cb聹\\聴}"],
+ ["@@e|v\\la{u"],
+ ["@@v~u}"],
+ ["@@Qx脗F炉}"],
+ ["@@鹿n聦v脼s炉o"],
+ ["@@rSkUEj"],
+ ["@@bi颅Z聦P"],
+ ["@@p[}INf"],
+ ["@@脌驴聙"],
+ ["@@鹿dnb聦聟"],
+ ["@@rS聼BnR"],
+ ["@@g~h}"],
+ ["@@FlEk"],
+ ["@@OdPc"],
+ ["@@v[u\\"],
+ ["@@Fj芒L~wyoo~聸s碌L聳\\"],
+ ["@@卢e鹿aN聢"],
+ ["@@\\n脭隆q]L鲁毛\\每庐聦Q脰聨"],
+ ["@@脢A颅漏[卢"],
+ ["@@Kx聦v颅"],
+ ["@@@hlIk]"],
+ ["@@pW{聧o||j"],
+ ["@@Md|_mC"],
+ ["@@垄聟X拢脧聧ylD录X聢tH"],
+ ["@@hl脺[LykAvy聫聛fw聧^E聻聸陇"],
+ ["@@fp陇Mus聯R"],
+ ["@@庐_ma~聫聲L脕卢職Z"],
+ ["@@i聺M聞xZ"],
+ ["@@ZcYd"],
+ ["@@Z~dOSo|A驴qZv"],
+ ["@@@`聰EN聫隆v"],
+ ["@@|聳TY聫{"],
+ ["@@@n@m"],
+ ["@@XWkCT\\"],
+ ["@@潞w職ZRk臅WO垄"],
+ ["@@聶X庐卤Gr脝陋\\脭谩Xq{聥"],
+ ["@@暖TG掳膭LHm掳UC聥"],
+ [
+ "@@陇聨聙a脺x~}dt眉G忙牛艓铆臄c艝pM脣脨聮j膿垄路冒膭脝Mz聢jWK膸垄Q露聵脌_锚聮聰_B谋聙i芦pZ聙gf聙陇Nrq]搂膫N庐芦H卤聡y瞥铆戮脳鸥墨脿L艂膷糯菨膫铆脌B艝脮陋聢聤脕艝H艞艍氓q没玫聬i篓h脺路聝帽t禄鹿媒v_[芦赂m聣YL炉聣Q陋聟m膲脜dM聢聲g脟jc潞芦聲臋聹卢颅K颅麓聝B芦脗膮co膵\\xKd隆g臎脓芦庐谩聮[~谋xu路脜聰Ks脣脡聫c垄脵\\沫茮毛bf鹿聺颅模S聝臏聺k谩茐脭颅膱ZB{聤aM聭碌聣fz艍f氓脗脓寞茓菨脢臅聛摹膰拢g鲁ne颅膮禄@颅娄S庐聜\\脽冒C職h聶iq陋沫iAu聡A聺颅碌聰聧_W楼疲O\\l膵蘑ttC篓拢t`聢聶PZ盲uX脽B聧s聡幕yek聙聺O膽摹牡HuXB職碌]脳聦聡颅颅\\聸掳庐卢F垄聫戮p聬碌录k艠贸卢W盲t聮赂|@聻聲L篓赂碌r聯潞霉鲁脵~搂WI聥聼ZW聨庐聮卤脨篓脪脡x聙`聣虏p臏聲rO貌gt脕Z}镁脵]聞聮隆聦聼FK聜wsPlU[}娄Rv聧n`hq卢\\聰聬nQ麓臉RWb聰聜_聽rt膶FI脰聤k聤聤摩PJ露脰脌脰J膱膭T臍貌聻C聽虏@P煤聟脴z聹漏P卯垄拢聹C脠脷聹膾卤聞h艝聡l卢芒~nm篓f漏聳i募芦m聡nt聳u聠脰Z脺脛j聯聤L聨庐E脤聹F陋虏i脢x脴篓聻I脠hhst"
+ ],
+ ["@@o\\V聮zRZ}y聛"],
+ ["@@聠@掳隆m脹聸G臅篓搂Ian谩[媒皮jf忙聡脴L聳聲盲Gr聶"]
+ ],
+ "encodeOffsets": [
+ [[125592, 31553]],
+ [[125785, 31436]],
+ [[125729, 31431]],
+ [[125513, 31380]],
+ [[125223, 30438]],
+ [[125115, 30114]],
+ [[124815, 29155]],
+ [[124419, 28746]],
+ [[124095, 28635]],
+ [[124005, 28609]],
+ [[125000, 30713]],
+ [[125111, 30698]],
+ [[125078, 30682]],
+ [[125150, 30684]],
+ [[124014, 28103]],
+ [[125008, 31331]],
+ [[125411, 31468]],
+ [[125329, 31479]],
+ [[125626, 30916]],
+ [[125417, 30956]],
+ [[125254, 30976]],
+ [[125199, 30997]],
+ [[125095, 31058]],
+ [[125083, 30915]],
+ [[124885, 31015]],
+ [[125218, 30798]],
+ [[124867, 30838]],
+ [[124755, 30788]],
+ [[124802, 30809]],
+ [[125267, 30657]],
+ [[125218, 30578]],
+ [[125200, 30562]],
+ [[124968, 30474]],
+ [[125167, 30396]],
+ [[124955, 29879]],
+ [[124714, 29781]],
+ [[124762, 29462]],
+ [[124325, 28754]],
+ [[123990, 28459]],
+ [[125366, 31477]],
+ [[125115, 30363]],
+ [[125369, 31139]],
+ [[122495, 31878]],
+ [[125329, 30690]],
+ [[125192, 30787]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "340000",
+ "properties": { "id": "340000", "cp": [117.283042, 31.26119], "name": "瀹夊窘", "childNum": 3 },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@^iuLX^"],
+ ["@@聜e漏Ehl"],
+ [
+ "@@掳Z脝毛膸碌mk莯w脤脮忙h潞gB臐芒聧q脵膴聫z聸脰g艈t脌脕膫聤脝谩聮hEz|WzqD鹿聙聼掳E聡脓l{忙v脺cA`陇C`|麓q聻x牟k聛q^鲁鲁聼G拧碌b聝铆Z聟鹿qpa卤膹聽OH聴娄聶摩聢x垄聞gP铆cOl_iCveaOjCh脽赂i脻聥b脹陋CC驴聙m聞RV搂垄A|t^i臓G脌t脷s聳d]漠脨DE露zAb聽脿i枚dK隆~H赂铆忙A聻强Y聝聯j{膹驴聭聶脌陆W聴庐拢Ch聦脙si聦聧kkly]_teu[bFa聣Tig聡n{]Gq陋聺o聥膱MY谩|路楼f楼聛聴艖aS脮臈聶N碌聧帽臑芦Im聦_m驴脗a]u臏p聽聟Z_搂{C聝盲g陇掳r[_Yj聣脝Od媒聯[聨I[谩路楼聯Q_n聡霉gL戮mv聶藠B脺脝露聺膴Jh職p聯c鹿聵O]i艩]聹楼聽jtsggJ脟搂聺w脳j脡漏卤聸EF脣聧颅聣Ki聰脹脙脮Yv聟s聲聢m卢nj幕陋聲搂emn谩}k芦艜聢聝g膽虏脵聸D脟陇聸铆隆陋Oy聸聠脳O霉卤@D聼帽聺S臋膰膬脮I脮驴I碌磨O聣聣聫jN脮脣T隆聧驴tN忙艊脿氓y姆r臅q搂脛末sW脝脽聨F露聺聻X庐驴聣m聦聶w聟RI脼聯f脽oG聭鲁戮漏uyH聭寞{苼魔炉AFnuP聫聟脥脭z職聦V聴d脿么潞^脨忙d麓聙聡oG陇{S聣卢膰x茫}聸脓脳K钎末芦聻脮OE脨路脰d脰s茦脩篓[聮脹^Xr垄录聵搂xv脛聸脝碌`K聰搂聽t脪麓Cvlo赂fz浓冒戮NY麓谋~脡臄膿聟脽煤聺L脙脙聫聳_脠脧|]脗脧Fl聰g`b職e聻聻聙n戮垄pU聜h~拼臇露_聜r聽s膭~c聻聰茍]|r聽c~`录{脌{葤iJjz聫`卯脌聧T楼脹鲁聟]聮u}聸f聟茂Ql{skl聯oNd聼j聼盲脣zDv膷oQ聤膹HI娄rb聯tH臄~BmlR職聴V_聞魔TLn帽H卤聮D聻聹L聭录L聵陋l聬搂扭a赂聦臍lK虏聙\\R貌vDc脦Jbt[陇聙D@庐hh~kt掳蔷z聫脰@戮聧陋db聞Yh眉贸Z聽艌露vHr木\\脢聴JuxAT|dm脌O聞聥[脙脭聥G路臍膮膼l弄脷pSJ篓母聢Lv脼cP忙姆浓聨庐m脨聬聢谩l聼wKh茂gA垄懦脝漏脼聳陇O脠聹m聮掳聦K麓聬"
+ ]
+ ],
+ "encodeOffsets": [[[121722, 32278]], [[119475, 30423]], [[119168, 35472]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "350000",
+ "properties": {
+ "id": "350000",
+ "cp": [118.306239, 26.075302],
+ "name": "绂忓缓",
+ "childNum": 18
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@聯zht麓聫聡]"],
+ ["@@aj^~膯G聴聫漏O"],
+ ["@@ed篓聞C}}i"],
+ ["@@@v聢PGsQ"],
+ ["@@聣sBz聜ddW]Q"],
+ ["@@S聨篓Q聯{"],
+ ["@@N聨VucW"],
+ ["@@qptBAq"],
+ ["@@聣聮赂[mu"],
+ ["@@Q\\pD]_"],
+ ["@@jSwUadpF"],
+ ["@@eX陋~聝聲"],
+ ["@@AjvFso"],
+ ["@@fT聳聸_脟铆\\聼聶聴v|ba娄jZ脝y聙掳"],
+ ["@@IjJi"],
+ ["@@wJI聙聢x職芦录AoNe{M颅聬"],
+ ["@@K聣卤隆脫聢聰膶盲eZ聛"],
+ [
+ "@@k隆鹿Eh~c庐wBk聥Upl脌隆I聲~M膩聧e拢bN篓gZ媒隆a卤脰cp漏Ph聻I聰聼垄Qq聟脟Gj聺聺聥|楼U聶聽g[Ky卢聧聧艔聧聳v@Op聢t脡E聼聬F聞聬\\@聽氓A卢聢V{X模聣膼聺By聟cp聛臎聟录鲁膫p聫路陇聝楼o聯hqq脷隆艆Ls聛聝^脙隆聴聻搂ql聼脌hH聛篓MCe禄氓脟GD楼zPO拢膷脵kJA录脽聳臈聫u聸臅e没脪聧i脕脓聬SW楼聵Q聤没艞陆霉臎c脻搂S霉末膮SW贸芦铆臋AC碌聸eR聴氓聛莾RC脪脟Z脥垄聥藕卤^dls聦tjD赂聲聜Zpu聻脭芒脙聮H戮oLU锚脙脭jj膿貌麓膭W聜茮聧聟^脩楼聥摩聼@脟貌聳聤m聦聝Ow隆玫yJ聠yD}垄膹聫脩脠摹f聧聤Zd聳a漏潞虏z拢職N聳聝jD掳脰tj露卢ZS脦~戮c掳露脨m聵x聜O赂垄Pl麓聻SL|楼聻A聠泉臇M聮艈牟g庐谩IJ膶膾眉`聽聨QF聡卢h|膫聯J@z碌聽|锚鲁脠聽赂U脰努努脌Ett母r聜]聙聵冒聨M陇亩牟Ht脧聽A聮聠聻默kvsq聡^a脦bv聦d聳聶f脢貌SD聙麓Z^聮xPs膫聻rv聥茷艀聵聺jJd脳艠脡聽庐A聳脦娄膜d聙x膯qA聦聠ZR聰脌M藕聦n膴禄聦陌脨Z聴聽YX聳忙J聤y膴虏聢路露q搂路聳K@聺路{s聭X茫么芦l艞露禄o聫聬陆E隆颅芦垄卤篓Y聢庐脴聥露^A聶vW亩G膾蘑聻Plzf聢募聨t脿AvWY茫職O_聡陇sD搂ss膶摹[k皮PX娄聨`露聯聻庐聢BBv莫jv漏職jx[L楼脿茂聛[F聟录脥脣禄臒V`芦聲Ip聶}cc脜磨ZE聥茫oP聟麓B@聤D聴赂m卤聯z芦瞥聴驴氓鲁BR脴露聢聹Wl芒镁盲膮`聯]Z聫拢T聛c聴聽墓G碌露H聶m@_漏聴k聦聣戮x抹聡么葔冒X芦陆膽CI聫b膰qK鲁脕聥脛拧卢OA聧w茫禄aL艍聡脣磨W[聯脗GI聴脗Nx某陇D垄聨聫卯膸脦B搂掳_J聹Gs聝楼E@聟陇u膰聟P聭氓聠c聬uMuw垄BI驴聡]zG鹿gu漠ck\\_"
+ ]
+ ],
+ "encodeOffsets": [
+ [[123250, 27563]],
+ [[122541, 27268]],
+ [[123020, 27189]],
+ [[122916, 27125]],
+ [[122887, 26845]],
+ [[122808, 26762]],
+ [[122568, 25912]],
+ [[122778, 26197]],
+ [[122515, 26757]],
+ [[122816, 26587]],
+ [[123388, 27005]],
+ [[122450, 26243]],
+ [[122578, 25962]],
+ [[121255, 25103]],
+ [[120987, 24903]],
+ [[122339, 25802]],
+ [[121042, 25093]],
+ [[122439, 26024]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "360000",
+ "properties": {
+ "id": "360000",
+ "cp": [115.592151, 27.676493],
+ "name": "姹熻タ",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@聫蘑抹茞g募聢录脗MD~艈陋e^\\^搂聞媒聺漏j脳聧cZ聠脴篓zd脪a聬露聢l脪聧J聦矛玫`oz梅@聺陇u艦赂麓聠么臋枚Y录聣H膶贫ajl脼譬楼茅Z[聰|h}^U聽聦聽楼p聞膭啪痞O聽lt赂脝聽聙Q\\聙聤a脝|Cn脗Ojt颅臍膜d聮脠聦F`聮露聞@脨毛聰聽娄艒脪聻篓S锚v聠H蘑没XD庐聟Qg脛聴Wi脴P脼矛潞r陇菃聙N臓垄l聳聲膭tZo聹C茷脭潞Cxrp臓V庐脢{f_Y`_聝eq聮聮庐Aot`@o聜DXfkp篓|聤s卢\\D聭脛Sf猫漏Hn卢聟^Dh脝y酶Jh聯脴x蘑膧L脢聢聞茽P偶膵膭w葼臍娄G庐聫菕膜盲T聬艩脝~摩w聤芦|TF隆聤n聙c鲁脧氓鹿]膲膽xe{聫脦脫聬聠vOEm掳B苽抹陌|G聮vz陆陋麓聙H聮脿p聰eJ脻聠Q職xn聥脌聤W颅聻聺E碌脿聧X脜莫t篓脙臇r脛w脌F脦|艌脫M氓录ib碌炉禄氓DT卤聫m[聯r芦_g聨mQu~楼V\\OkxtL聛聽E垄聥聝聭脷^~媒锚聥P贸聳qo臎聤卤_脢w搂脩陋氓茥膩录聥m膲殴聥驴NQ聯聟YB聥膮rw模c脥楼B聲聼颅艞脢c脴iI聴聻茲目u聦聺qt膩wO]聭鲁YC帽Te脡聲職聥caub脥聢]trlu聙墨聟聺B聭脨聼Gs牡谋N拢茂聧聴聛^姆qss驴F奴奴V脮聼路麓脟{茅膱媒聣每聸OE聢聧R_聼膽没I膵芒Jh颅艆谋N聭醛臅B聟娄聺K{聺Tk鲁隆OP路w聛n聴碌脧d炉}陆T脥芦Yi碌脮sC炉聞iM聲陇聶颅聲娄聺炉P|每聧UHv聯he楼oFTu聣玫\\聫聨OSs聥M貌膽茋ia潞膰X聼膴牡脿聺路莽h苾梅脟聹{聭铆gu^聸膽g聮m[脳zkKN聺聛聫聭露脮聺禄l膷脫{XS脝聣v漏_聺脠毛JbVk聞臄V脌陇P戮潞脠M脰xl貌~陋脷脿G膫垄B聞卤聮脤聦K聵y聮谩V聡录脙~颅聺聟`g聸聼s脵fI聸茓l臋鹿e|聳~udj聢uTlX碌f`聬驴Jd聤[\\聵聞L聜聭虏"
+ ],
+ "encodeOffsets": [[116689, 26234]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "370000",
+ "properties": {
+ "id": "370000",
+ "cp": [118.000923, 36.275807],
+ "name": "灞变笢",
+ "childNum": 13
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@Xjd]{K"],
+ ["@@itbFHy"],
+ ["@@HlGk"],
+ ["@@T聜聦G聼y"],
+ ["@@K卢聵聲聥U"],
+ ["@@WdXc"],
+ ["@@PtOs"],
+ ["@@聲LnXhc"],
+ ["@@ppV聝u]Or"],
+ ["@@cdzAUa"],
+ ["@@udRhnCI聡"],
+ ["@@聢oI聝聫pR聞"],
+ [
+ "@@慕膷{fz皮卯聮K職聳脦M漠]聠聴ZF聢陆Y]芒拢ph聮聶職露篓r芒酶脌聠脦迁陇^潞脛聰Gz聢~gr臍臏l臑脝聞L膯菃垄脦o娄聳cv聯Kb聙gr掳Wh聰mZp聽聢L]L潞cU聣脝颅n聰偶膜脤膾聹bAnrOA聹麓聻葕c脌b痞U脴r膯U脺酶聹默茷聠職聵Ez聞VL庐枚脴Bk艝脻膼臇鹿脓虅卤脌b脦脡聹nb虏摩h艈B臇聸聻寞摩氓X膰矛@L炉麓yw聛茣C茅脙碌臈聽瓶赂聭l碌戮Z|聠ZWyFY聼篓Mf~C驴`聙脿_R脟zw茖fQnny麓INo片聢猫么潞|sT聞JU職聸聜L聞卯Vj聞菐戮膾脴聧聜Dz虏XPn卤聬糯P猫赂艛L茢脺坪_T聭眉脙膜BB膵脠聣枚A麓fa聞聵M篓{芦M`聡露聛d隆么聣脰掳職m劝B脭jj聦麓PM|聰c^d陇u聲聝陇脹麓聦盲芦脾fPk露M么l聢]Lb聞}su聫^聫ke{lC聭聟M聲rD聤脟颅]N聛脩Fsmo玫木H聣yG膬{{莽rn聺脫E聣聥茣ZG陋鹿Fj垄茂聫W聟u酶C欠毛聫隆膮uh脹隆^Kx聲C`聫C\\b脜x矛虏臐脻驴_N聣墨C冉目氓B楼垄路I艝脮y聧\\聡鹿kx聡脙拢聫膶脳GDy脙聲陇脕莽FQ隆聞Kt诺茓]Cg脧A聺聫霉Sed聡c脷藕聴聤uYf聝yMmh聛UWp聫S聧yG聫wMPq艀聴聸脕录zK聸露聠G聲颅Y搂脣聝@聳麓艣脟碌茣聛Bm聹聛@Io聜g聴聴Z聫炉u聥TMx聛}C聭聣VK聜茂{茅频P聴聶_K芦聶p脹脵q膵tkk霉]g聨聥T臒wo聲蓙sM玫鲁膬聡AN拢聶MRkmE脢聲膷聶脹bMj脻Gu聫聟IZ聶聴GP模聡茫魔E[i碌BEu聼DP脭聸~陋录臋t聤聹]聦没G搂聙隆QMs臒NP艔寞zs聺拢Ug{膽J目募膩鲁]莽芦Qr~楼C聧茙脩^n露聧脝茅脦R~呕赂Y聮I聯]聽P聣um艥r瓶聸聣聸I膩聥[x聣e脟鲁聫聥L聭炉聫v炉s聺卢脕Y聟~}聟钮u艁聦g聸茓p脻膭_艈墨露脧SR麓脕P~聻驴Cy聻膵聛脽dwk聺麓Ss聲X|t聣`脛聽聧脠冒聙A陋矛脦T掳娄Dd聳聙a^l膸D亩脷Y掳聨`莫糯菕聢聰脿艩v\\聬eb聦ZH聞艝R卢泞票霉臋O聲聫脩M颅聺鲁F脹聝Wp[聝"
+ ]
+ ],
+ "encodeOffsets": [
+ [[123806, 39303]],
+ [[123821, 39266]],
+ [[123742, 39256]],
+ [[123702, 39203]],
+ [[123649, 39066]],
+ [[123847, 38933]],
+ [[123580, 38839]],
+ [[123894, 37288]],
+ [[123043, 36624]],
+ [[123344, 38676]],
+ [[123522, 38857]],
+ [[123628, 38858]],
+ [[118260, 36742]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "410000",
+ "properties": {
+ "id": "410000",
+ "cp": [113.665412, 33.757975],
+ "name": "娌冲崡",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@聲媒L聶霉碌P鲁swI脫xc泞臑冒聠麓E庐聻脷Pt聠拇X脴x脗露聵@芦艜艜QG聝聥Yfa[聫艧u聯脽签聶膽拧聧_X鲁某脮膷C]kbc聲楼CS炉毛脥B漏梅聥聳鲁颅聛Si聢_}m聵YTt聻鲁xl脿c膶聜z聺脌D}聺脗OQ鲁脨T抹炉聠茥貌脣艝[h聹艂聥纽v~聠聠}脗Z聻芦陇lP脟聲拢陋脻糯脜R搂脴nhc聦t芒k聫聡n脧聧颅木殴U脫脻dKu姆聡I搂oT农脵膹k臋膯H赂脫聦\\脛聝聦驴PcnS{wBIv脡聵慕[Gq碌u聼艊么Yg没聝Zca聨漏@陆聬脮墙ys炉}lgg@颅C\\拢as聙Id脥uCQ帽[L卤臋k路聥牛b聫篓漏kK聴聮禄聸KC聺虏聭貌GKm抹S`聝聵UQ聶nk}AG膿聰sqaJ楼膼GR聣膸pCu脤y聽茫聽iMc聰plk|tRk聠冒聹ev~^聭麓聠娄脺聨S铆驴聬_iyjI|葢|驴_禄d}q聼^{聯茋d聺膬}聼t聛q碌`瞥臅g}V聛隆om陆聺f聺a聶脟o鲁TTj楼聞t臓聴Ry聰聫K{霉脫ju碌{t}u脣R聭i聼vG聤莽JFj碌聤脥yq脦聵脿Q脗FewixGw陆Y欧p碌煤鲁X聺U聸陆摹y聶艂氓聣k脷wZX聢路l聞垄脕垄K聰zO聞脦聸脦聙jc录htoDHr聟|颅J聯陆}JZ_炉iPq{t臋陆臅娄Zp牡酶芦kQ聟墓陇聝]M聧脹f聛aQp臎卤墙戮]u颅Fu聥梅n聝聶膷脛炉ADp}AjmcE脟聮a陋鲁o鲁脝脥S茋膱脵DIz脣聭膷木聼^聢KL聹聴i聴脼帽聙[聹聝aA虏zz聣脤梅D聹|[職铆脛鲁gf聜脮脼d庐|`聝膯~聞o臓茟么鲁艎聭D脳掳炉Cs聤酶脌芦矛聣UMhT潞篓赂恰卯S聳脭聞Dru脗脟Z聲脰E聨聮v聧PZ聞聻W聰~脴聥脨t膭E垄娄脨y赂b聤么麓o努卢聨虏脢s~聙聙]庐t陋a職p艓J篓脰潞聞_聤艛聳`聮艝^膼聧\\臏u聳聰~m虏聘聸赂fW聣摩r茢}脦^gjdf脭隆J}\\n聽C聵娄镁Wx聬陋JR脭艩u卢抹抹mF聠dM{\\d\\聤Y脢垄煤@@娄陋虏S聤脺sC聳}fN猫cbpRml脴^g聞d垄a脪垄CZ聢聧Zxv聛脝露N驴聮垄T@聙uC聹卢^膴冒聛脛n|聫聻lGl聮聶Rjsp垄ED}聙Fio~脭N聨聥聞~zk臉HVs遣脽j聝努聦聤泞`P没脿l垄聵\\脌聹Eh聨陌g脼膿聽X聬录Pk聳聞|m"
+ ],
+ "encodeOffsets": [[118256, 37017]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "420000",
+ "properties": {
+ "id": "420000",
+ "cp": [113.298572, 30.684355],
+ "name": "婀栧寳",
+ "childNum": 3
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@A聛B聜"],
+ ["@@lskt"],
+ [
+ "@@戮芦}{ra庐p卯脙\\聶聸{酶C聤脣yyB卤聞b\\聸貌聵脻聵jK聸聡L聽]膸慕脤聮Jy脷C茍膰脦T麓脜麓pb漏脠聭dFin~BCo掳B膸脙聞職酶mv聦庐E^v蔷陆臏虏Ro聜b脺eN聨聞^暮拢R聠卢l亩梅Yo臇楼臍戮|sOr掳jY`~I聰戮庐I聠{GqpCgyl{聡拢聹脥聝脥yPL聯脗隆聝隆赂kW聡xYl脵忙聤職艁蘑z聹戮聻V麓W露霉鸥o戮ZHxjwfx聞GN脕聲鲁X茅忙l露聣Ei猫IH聣聽u聮j脤Q~v|sv露脭i|煤垄Fh聧聵Qs臒娄聝Si艩Bg聶脨E^脕脨{聳膷nO脗脠聻U脦贸臄聠脢膿牟}Z鲁陆M脓茂eyp路uk鲁Ds脩篓聼L聯露_聹脜u脙篓w禄聴聙隆Wq脺]\\聭脪搂t茥c脮赂脮F脧菨膲膬x呕膶茻O聡聝K脡摹每脳wg聰梅I脜zCg聠]m芦陋Ge莽脙TC聮芦[聣t搂{loWe聛C@ps_Bp聭颅r聭聞f_``Z|ei隆聴o膵Mqow聙鹿D茲脫聸DYp没s聲聳聥Yk谋莾}s楼莽鲁[搂聼cY聤搂HK聞芦Qy聣]垄聯ww枚聙赂茂x录艈戮Xv庐脟脌碌R臓脨聥聻聛HM聻卤c脧d聞茠菎农葏确卤DSy聛煤臐拢聧扭膧脿t脰每茂[卯b\\}p沫脡I卤脩y聟驴鲁x炉N聣o聣|鹿H聶脧脹m聥聺j煤脣~T職聺聲u聵臋jC枚Aw臎卢R聮膽l炉聽脩b颅聣艊T聠目_[聦聭I膷膭士nM娄臒\\脡[T路聶k鹿聹漏o臅@A聫戮w聲ya楼聬Y\\楼脗az炉茫脕隆k楼ne拢脹w聠聺E漏脢艒露藫u聫oj_U聝隆cF鹿颅[Wv聯P漏w聴hu脮yBF聯聝`R聥qJUw\\i隆{j聼聼EP茂每陆f膰聟Q脩脌聛Q{聻聜掳聡fL脭聛~wXg聴墨聛t锚脻戮聳暮聭Hd聢鲁fJd]聥聧HJ虏聟E聙聝oU楼聠HhwQs茞禄Xmg卤莽ve聸]Dm脥聜P聢oCc戮聥_h聰聳h酶Yr艎U露eD掳膶_N~酶墓臍路`z聮]脛镁p录聟盲脤Q聦v\\rC聦茅戮Tnk啪艕脷聙脺a聡聯录脻茊蘑露脹o聟d聟臄艌脨垄Jq聮Pb聽戮|J聦戮fX聤茞卯抹_Z炉脌}煤撇聥N_膾脛聤^聞聭膱a艕yp禄C脟脛聲K聤職帽L鲁聤摹M聦虏wrI脪怒xjb[聹聻n芦酶聹聵聴忙聢脿聝聽^虏颅h炉脷聙艕陋脼赂聙Y虏膾V酶}膧^陌聶麓聜L聤脷m聬聞楼脌J脼{JV聦懦脼艃x脳sxx茍膿聽模M艡聳脷冒貌If聳膴聯艗\\飘卤艗d脢搂臉D聠v膶_脌忙~D聦膵麓A庐碌聠篓脴LV娄锚H脪陇"
+ ]
+ ],
+ "encodeOffsets": [[[113712, 34000]], [[115612, 30507]], [[113649, 34054]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "430000",
+ "properties": { "id": "430000", "cp": [111.782279, 28.09409], "name": "婀栧崡", "childNum": 3 },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@聴n聞FTs"],
+ ["@@脽脜脝谩聣陆脭Xr聴聠CO聶聬聯聟脣R聭茂每聧末颅TooQy職脫[聥艆BE卢聳脦脫Xa聞寞搂脙赂G聽掳ITx聧p聣煤x脷某楼脧職聳蘑戮聤ed聻脛漏母聫G聟聹脿Gh聜聙M陇聳脗_U}膴}垄pczf聤镁g陇聙聰脟貌AV聭聥M"],
+ [
+ "@@聬漏K聛聴聝A路鲁CQ卤脕芦鲁BU聤茟鹿A聤聧t膰Ow聬聶D]聦聺Ji脴Sm炉b拢聭yl聝聸X聟聧H脣脩卤H聲芦聛聳聭C^玫木A聳聺脜搂陇脡聫楼聞茂yu菣uA垄聧^{脤C麓颅娄欧J拢^[聠聫聺聯陋驴聡臅~聲茋聟聲N聟聽sk贸膩聧聡鹿驴聙茂]膬~梅O搂颅@聴聺Vm隆聥Q膽娄垄膜{潞j脭聫聨聦陋楼nf麓聲~脮o聼聻脳脹膮聥M膮谋uZ聹mZc脪聽牟莫虏S脢莿哦篓茪聝聮C脰艓陋Q脴录r怒聨颅芦}N脧眉r脢卢聦mjr聙@臉rTW聽颅SsdHz茡^脟脗yUi炉D脜Yl殴u{hT聹}m膲聳鹿楼聺臎聣D每毛漏谋脫[O潞拢聻聯楼贸t聙艂鹿M脮聞聻篇聝`P職聟Di聳脹U聤戮脜芒聙聦矛聢U聮帽B聯脠拢媒he聣dy隆o膵聙`pfmjP~聜kZa聟聧Zs脨d掳wj搂聝@聙拇庐w~^聜k脌脜KvNmX\\篓聛a聯聰艃聛qv铆贸驴F聞陇隆@农脩Vw}S@j聺}聛戮芦p膫r聳陋g聽脿脌虏NJ露露D聬么聟K聜|^陋聠聨聛掳LX戮糯盲P莫卤聹拢EXd聸聰^露聸牟脼脺聯~聭u赂菙聵聨聸MRhsR聟e聠`脛ofI脭\\脴聽聽i聰膰ymn煤篓cj聽垄禄聳G膶矛茒每脨篓Xe膱膧戮O冒聽Fi聽垄|[jVxrIQ聦聞_E聰zAN娄zLU`聹c陋x聰OTu聽RL脛垄dV聞i`聬p藬v艓碌陋脡聻F~聝脴聙d垄潞g陌脿w赂脕b[娄聫Zb娄聳z陆xB聬臇@陋p潞聸職lS赂脰\\臄[N楼藔m膸膬聮J\\聥艀`聙聟艌S脷聤臇脕膼iO聯臏芦BxD玫臍iv聴聻聳S聶脤}i霉聦聻脺n職脨潞G聤{聤p掳M麓w聠脌脪zJ虏貌篓聽oT莽眉枚o脹每帽聨艖臑陇聜霉Tz虏C葐雀菐弄聝茟脨c掳dP聺脦聼臒脣露[脠陆u炉陆WM隆颅脡聻聯聮B路r铆聻nZ聼脪聽`聡篓GA聛戮\\p膿聵Xh脙聠RC颅眉WG摹u聟T茅聺搂艓脩聺漏貌鲁I卤鲁}_聭聥E聧脙魔g庐臋is脕PDm脜{聣b[R脜聼s路聙kP聼沤匹聝贸Ro聰O聥聼V聼~]{g\\聯锚Y篇娄k脻bi膵频聤GZ禄臍玫聟贸路鲁v艥聻拢酶@py枚_聥毛聨Ik脩碌聡b聫c脩搂y聟脳dY脴聨陋i镁聻篓聝[]f]艆漏C}脕N聡禄h幕魔茝聮末"
+ ]
+ ],
+ "encodeOffsets": [[[115640, 30489]], [[112543, 27312]], [[116690, 26230]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "440000",
+ "properties": {
+ "id": "440000",
+ "cp": [113.280637, 23.125178],
+ "name": "骞夸笢",
+ "childNum": 24
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@Qd聢Aua"],
+ ["@@聝lxDLo"],
+ ["@@sbhNLo"],
+ ["@@膫聽膩聼"],
+ ["@@WltO[["],
+ ["@@Kr聹]聫S"],
+ ["@@e聞聞I]y"],
+ ["@@I|聞Mym"],
+ ["@@聝脹鲁LS聦聬聻录Y"],
+ ["@@nv潞B聳毛ui漏`聺戮"],
+ ["@@zd職脹聸Jw庐"],
+ ["@@聠掳聟炉"],
+ ["@@a聽yA陋赂脣JIx脴聦@聙膧HAm脙聼V隆o聲fu聲o"],
+ ["@@職s聣艞脙脭臈A苼聸Z職脛聽~掳膶P聜聥盲h"],
+ ["@@聥露脻聮脤聜vm臑h颅谋聡Q"],
+ ["@@H聹聤dSj膾垄D}war聟聯u芦ZqadYM"],
+ ["@@el聦\\LqqU"],
+ ["@@~rM聧o\\"],
+ ["@@f聞^聝C"],
+ ["@@酶P陋oj梅脥脻魔X膶x聰掳Q篓谋XNv"],
+ ["@@g脟瞥聢聨聢聰o聢聤聢[~tly"],
+ ["@@E聳脝C驴聭"],
+ ["@@O聨P聧"],
+ [
+ "@@w聥聠膽贸g聣聶臐聴[鲁聥聛隆V脵忙脜枚M脤鲁鹿p脕a脣媒媒漏D漏脺聯J殴茣模G膮陇{脵奴聟脟聵O虏芦B票茅A聴脪聣磨聡隆聫芦Bhlmt脙P碌yU炉uc聯d路w_b艥c墨铆mGO聨|KP聮葟聡殴茫艥I艜怒艜@脫oo驴膿聥卤脽}聨聟怒聜聼牟W脠C艖芒U芒菣I聸臒艍漏I聸聧某E脳聟脕聰鲁A贸聸wXJ镁卤脤聦脺脫聰抹拢L]膱脵坪Z蔷膯臇M母膜f聦脦牡l聲浓n脠聢聭膼tF聰聤聳F膜聳聜锚k露聹^k掳f露g聤聨聹}庐Fa聵f`聧vX挪聧xl聵聞娄聳脭脕虏卢脨聼娄pq脢脤虏聢i聙X聼脴RD脦}聠脛@Z臓聮s聞x庐AR~庐ETt膭Z聠聳聬茍f艩艩H芒脪脨A聠碌\\S赂聞^w臇kRz聤al聨艤|E篓脠N膧艌ZT聦聮pBh拢\\聦膸苺uX臇tKL聳露G|聨禄暮E募臑~脺蘑脹膴r聢O聵脵卯vd]聬n聢卢V聹脢臏掳R脰聼pM聠聠聳聜苽陋Fbw聻E脌聢聵漏聦聻\\聟陇]鸥I庐楼D鲁|脣聨]C聺枚A扭娄聟忙聮麓楼赂Lv录聙聲垄慕Ba么聳F~聴職庐虏G脤脪聬EY聞聞聹zk陇聮掳ahlV脮聻I^聥職Cx聫膱P聨sB聣茠潞V聣赂@戮陋R虏抹N]聧麓_eavSi聡vc聲}p}膼录茖kJ聹脷e聽th聹聠_赂聽潞x卤貌_x聧N聸脣聥虏聭@聝膬隆脽H漏脵帽}wkN脮鹿脟O陆驴拢臅]ly_W矛I聻脟陋`聤uT脜xY膾脰录k脰聻聮碌聜聬M聻jJ脷wn\\h聭聹膾v]卯h|聮脠聸苿酶猫g聻赂亩脽聽膲膱Wb鹿苺d茅臉聦NTt聧P[聬聤枚SvrCZ聻聻aGu聹bo麓艝聛脪脟膼聬~隆zCI聟枚zx垄聞Pn聥聲聣脠帽聽@聦磨脪娄聠]茷聤V}鲁膬臄帽ii脛脫V茅pKG陆脛聭脫谩v聺Yo聳聛C路sit聥ia脌y聞脓脦隆脠YD脩暖m}聣媒|m[w臋玫膲Z脜xUO}梅N鹿鲁膲o_qt膬聯qw碌艁Y脵聞菨艜鹿t茂聺脹U脙炉mRC潞聟聢沫|碌聛聸脮脢K聶陆R聭膿聽贸]聭聳G陋臋Ax聳禄HO拢聫|膩m聡隆di膹脳Y茂聧YW陋艍Oe脷t膼芦z膽鹿T聟膩聡煤E聶谩虏\\聥姆脥}jY脿脵脝趴驴脟d臒路霉T脽脟牛蕜隆XgW脌菄臒路驴脙聢Oj聬聽Y脟梅Q臎聥i"
+ ]
+ ],
+ "encodeOffsets": [
+ [[117381, 22988]],
+ [[116552, 22934]],
+ [[116790, 22617]],
+ [[116973, 22545]],
+ [[116444, 22536]],
+ [[116931, 22515]],
+ [[116496, 22490]],
+ [[116453, 22449]],
+ [[113301, 21439]],
+ [[118726, 21604]],
+ [[118709, 21486]],
+ [[113210, 20816]],
+ [[115482, 22082]],
+ [[113171, 21585]],
+ [[113199, 21590]],
+ [[115232, 22102]],
+ [[115739, 22373]],
+ [[115134, 22184]],
+ [[113056, 21175]],
+ [[119573, 21271]],
+ [[119957, 24020]],
+ [[115859, 22356]],
+ [[116561, 22649]],
+ [[116285, 22746]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "450000",
+ "properties": { "id": "450000", "cp": [108.320004, 22.82402], "name": "骞胯タ", "childNum": 2 },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@H聳聽TQ搂聲A"],
+ [
+ "@@抹脢陋聝L聝茒D脦墓膼C铅臈赂z脷Gn拢戮聸r陋艀脺t卢@脰聸脷聢Sx~酶O艗聵哦脨脗忙葼\\聞脠脺Ob臇w^o脼聞Lf卢掳bI聽lT脴B脤聢F拢膯鹿g帽膜aY聯t驴陇VS帽聹K赂陇nM聠录聜JE卤聞陆赂職聤帽o聥脺C茊忙莫^聤臍Q脰娄^聡聢聢f麓聛Q聠眉脺脢聺z炉職lzU暮拧@矛聡聙p露n]sxtx露@聛聞~脪膫Jb漏gk聜{掳聜~c掳`脭聶卢rV\\聯la录陇么谩`炉鹿聺LC聠脝b聦xEr忙O聬聜v[H颅聵聞[~|aB拢脰s潞dA膼zN脗冒s聨脼脝聰聟膜陋b聝聳ab`ho隆鲁F芦猫Vlo聨陇聶脭Rzpp庐S聨莫潞篓脰聝潞N聟某聞d`聮a聰娄陇聺F鲁潞D脦艅膧矛聤C聻臏潞娄膴聲~nS聸|g藕vZkC脝j掳zV脠脕茢]L聧脢FZg聟膷P颅kini芦聥q脟聙cz脥聰Y庐卢女聧禄qR脳艒漏D脮聞聭搂茩莾诺T脡末卤聼谋d脩nYY聸牟vN膯膯聦脴脺聽聺脰p聳}e鲁娄m聥漏聬聧i脫|鹿聼魔艈聸|陋娄QF垄脗卢蕱ovg驴em聡^聫uc脿梅g脮聨u聦铆脵膰臐}F幕录墓{聧碌HK聲sLS膽苾r聥膷陇[Ag聭oS聥艊YM每搂脟{F聧艣bky聣lQx臅聝]T路露[B聟脩脧G谩艧艧茋e聙聫聟聲膬YSs颅FQ}颅B聝w聭tY臒脙@~聟C脥聙Q聽脳W聡j脣卤r脡楼o脧聽卤芦脫脗楼聲聝聙k聴聨wW疟聦mcih聫鲁K聸~聣聧碌h炉e]l碌聸茅l聲聛E模聣聲E聯膹s聡聮m脟聳脓膿`茫貌gK_脹sU蕽聯膰聫臒露h聦枚聦O陇菦n鲁聨聺c聭`隆y聥娄C聭聛ez聙Y聤wa聶聳聭[膹牡疟M臋搂]X聵脦_聜铆聸聵聧脹]茅聮脹U膰陌脮B疲卤聟d聝y鹿T^d聛聻没脜脩纽路聡P幕镁脵`K聙娄聵聟垄脥e聛聹磨R驴聦鲁拢[~聦盲u录dl聣t聜聠W赂oRM垄膹\\z聹}脝zdv艌聳{脦XF露掳脗_聞脪脗脧L漏脰聲Tmu聼录茫l聣聸墨kiq茅fA聞路脢碌\\艖Dc楼脻F聯y聸脭膰聵c聙疟H_hL脺聥锚暮膼篓c}rn`陆聞脤@赂露陋VL聦聤h艗聥\\聲泞暮k~聨臓i冒掳|g聦tT沫母^x聭vK聵聫VGr茅A聛茅聭bUu聸MJ聬聣V脙O隆聟q膫X脣S聣模茫l媒脿聼_ju聡Y脹脪B聠聹G^聵茅脰聤露搂聨聝EG聰脜z臎聝漂陇Ek聡N[kd氓uc茅卢dnYp聫Ay膶{`]镁炉T聮b聬脺脠k聜隆臓聲v聦脿h聞脗苿垄J卯露虏"
+ ]
+ ],
+ "encodeOffsets": [[[111707, 21520]], [[107619, 25527]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "460000",
+ "properties": { "id": "460000", "cp": [109.83119, 19.031971], "name": "娴峰崡", "childNum": 1 },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@職娄艤il垄聰X痞聭茷貌聳茂猫搂艦C锚蓵r脓暖脟膮幕玫聶路膲鲁艙聛虆k脟m@膵颧聝脓磨聣慕蕢聧颅茀趴聯葥脪脣娄艥E}潞茟[脥臏葖聽g脦f菒脧膜篓锚聺坪\\茊赂臓膸v蕜葊聹脨戮jN冒膧脪R聦職Z菃聶z脨艠脦掳H篓聧脾b虏_臓聽"
+ ],
+ "encodeOffsets": [[112750, 20508]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "510000",
+ "properties": {
+ "id": "510000",
+ "cp": [104.065735, 30.659462],
+ "name": "鍥涘窛",
+ "childNum": 2
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@LqKr"],
+ [
+ "@@聤[幕茅V拢聻_牛摹帽pG聽聲r茅脧路~膮Sfy脳脥聜路潞聫趴平i脥谋疲谋幕mHH}siaX@i脟掳脕脙脳t芦聝颅T聝陇J聳JJ聦yJ聲脠聤`Oh脽娄隆u脣hIyCjm每w聟ZG聟聟Ti聥S聢sO聣聻B虏聼fNmsPa聢{M{聤玫E聭^Hj}gYpaeu聻炉聭o谩wHj脕陆M隆pM聯聳u氓聡mni{fk聰\\o聝脦qCw聠EZ聛录K聸臐聤聝Ay{m梅L聡wO脳SimRI聧炉rK聶玫BS芦sFe聡]f碌垄贸Y_脝PRcue掳Cb聛o脳聦bd拢艑IHgtrnyPt娄foaX膹x聸lBowz聥_{脢茅Wi锚E聞聛Gh脺赂潞uF聺膱Ixf庐聨聲Y陆膧菣]聛陇聺Ey聼F聧虏膵聮w赂驴@g垄搂RGv禄聳聛谩聼W`脙牡Jwi]t楼wO颅陆a[脳聢]`脙i颅眉聫L聙娄LabbT脌氓聮c}脥h聶脝h聢聥庐BH聙卯|卯聝潞脡k颅陇S聠聺y拢聞ia漏ta寞路茐`艒楼Uh聯O聟聝臐Lk}漏Fos聣麓聸Jm聞碌l艁u聺聴聟酶聳n脩JW脦陋聳Y脌茂AetT聻艆聜脫聧G聶脣芦bo聣{谋wod茻陆聝聻聛O摹脺聭脗碌x脿N脰戮P虏搂HKv戮聳]|聲B聡脝氓oZ`隆脴`脌m潞臓~脤脨搂n脟聟驴陇]w臒@s聝聣r臒u聣~聭Io聰[茅卤鹿聽驴聻趴膽脫聣@q聥g聢鹿z票艡a铆掳Kt脟陇V禄脙[末黔茟^脟脫@谩钮聴s聸Z脧聲聥聹脜沫聙茓聲臎pwD贸脰谩聢呕neQ脣聦聺q路聲GC艙媒S]x聼路媒聥q鲁聲O脮聹聦露Qz脽ti{艡聣谩脥脟W艥怒帽z脟W聥p莽驴J聦聶聜X聹末猫陆c聧聦F聳脗LiVjx}\\N聠艊臇楼Ge聳聯JA录脛Hf脠u~赂脝芦dE鲁脡MA|聛b聵脪聟聵膰hG卢CM聜玫聤聞皮膮Av聝眉V聙茅艀聣_V脤鲁膼wQj麓路Ze脠脕篓X麓聧脝隆Qu路禄聼聯聵脮Z鲁摹qDo聣y`L卢gdp掳艧聤p娄臈矛脜漠Z聨掳I盲聰h聜聭聢z聤牡聹f虏氓聽聸臍脩聙Kp聥IN|聥聞脩z]艅聟聟路FU脳茅禄R鲁聶M聝脡禄GM芦聙聫ki聙聶茅r聶}脙`鹿膬脼m脠聺n脕卯R莯鲁臏o陌z艛w嵌V脷拢脌]蓽禄膯l苽虏臓聟镁T潞路脿U葹脧师露聠I聮芦d慕蘑d默驴聳禄臄脳聤h\\c卢聠盲虏G锚毛膜艂楼脌强偶脙脝M潞}B脮蘑yFVvw聳聢xB猫幕膾漏膱聯tC蘑山艩龋娄膩忙路H慕卯聯么N脭聯~^陇茒聹u聞聹^s录{TA录酶掳垄陌陋D猫戮艊露脻J聭庐Z麓臒~Sn|陋W脷漏貌zPO聬雀聜b冒垄|聥酶臑聤聦聹艗Q矛脹脨@臑聶菐RS陇脕搂d聟i聯麓ez脻煤聫脴茫聫]Hq聞kI聼镁脣Q脟娄脙s脟陇[E聺卢脡弄脥xX茠路脰苼陌l茷鹿陋鹿|X脢wn聭脝苿m脌锚Er膾tD庐膵忙cQ聝聰E庐鲁^沫楼漏l}盲Qto聵艝脺q脝聨k碌聳聞陋聫脭幕拇隆@膴掳B虏脠w^^Rs潞T膧拢艢忙聹QP聭Jv脛z聞聬^膼鹿脝炉fL脿麓GC虏聭dt聵颅膧Rt录陇摩O冒臒f脭冒D浓艁臑茦茂聻P脠聠庐芒bM眉脌XZ聽赂拢@脜職聸禄禄Q脡颅聶]d聯s脰脳_脥聳_脤锚女Pr臄膼脮G膫eZ脺卯臉qBhtO聽陇tE[h|Y聥脭聜Z艣脦s麓x潞卤U聦聮帽聢t|O聮末臓潞Nbg镁聤Jy^d脗Y聽漠聞]艠z娄gC聜鲁聙R`膧聤z聮垄Aj聦赂CL聞陇R脝禄@颅艓k\\脟麓拢YW}z@Z}聣脙露聯o没露]麓^N聡脪}猫N聜陋聳P聵脥y鹿`S掳麓聠ATe聙VamdU膼w蕜v漠脮\\聝u聥脝艞篓Yp鹿脿Z脗m聶Wh{谩聞}W脴菎聲脡眉w聶ga搂谩CN臋脦[膧脮莫g脰脡陋X聵聧酶x卢陆女娄娄[聙聴聞N脦聠L聙脺U脰麓貌r脵艩xR^聳聠J聵k聞某nDX{U聝~ET{募潞娄PZc聰jF虏臇@聨p聵g聙聢篓聯B{聝u篓聺纽yho脷D庐炉垄聵聽W貌聬脿F脦陇篓GD聫盲z娄k女P聹摹q脣職楼脌]聙聼聵e聨芒脷麓陋Kx墨聞P聢聴脰|忙[x脙陇J脼磨聜s聮N脰陆聻聙I聠卢n抹Y麓庐脨聴茞聤聙mD聶艥u盲膽膽Eb聛聟e聮聛e_聶v隆}矛臋菉膿}q聰脡氓聼聛T炉碌Rs隆M@}暖a聠聫a颅炉wv聺茐氓Zw聻\\Z{氓没^聸"
+ ]
+ ],
+ "encodeOffsets": [[[108815, 30935]], [[110617, 31811]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "520000",
+ "properties": {
+ "id": "520000",
+ "cp": [106.713478, 26.578343],
+ "name": "璐靛窞",
+ "childNum": 3
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@聠G\\聠lY拢聭in"],
+ ["@@q聜|聢聜mc炉t脧聡VS脦"],
+ [
+ "@@h脩拢Is聡Ng聛脽H聠聸聬H陋姆聺脙h_鹿聝隆臐脛搂艅娄u脵聤霉聨gS炉JH聼|s脻脜t聧脕茂yMD膷禄e脮tA陇{b\\}聴聝G庐u\\聫聛氓PFq聥w脜aD聟聻K掳潞芒_拢霉b碌聰m脕聥脹聹墓M[q聛|hla陋膩I}脩聜聝碌@swtwm^o碌聢D茅慕艩yV聶ky掳脡聻没脹R聟鲁聜聡e聢聡楼]R脮聥臎魔[茀氓脹Dp聦聛聰J聞iV聶聶聣脗F虏聛I聟禄mN路拢聸Lb脪Yb聴Ws脌b聨聶pki聶TZ膭膬露H聦q`聟聟磨_J聼炉ae芦聝Kp脻x]a臅脹P聝脟葻[脕氓诺脧艖聴梅Pw}聡T聹脵@脮s芦目脹q漏陆聹m陇脵H路y钎臉膲B碌抹脮n膽]K聞漏聞艙谩聥聼G莽艧聧搂脮脽g聡聧菞摩T猫皮坪{露脡H脦d戮艢脢路O脨jXWr茫Lyz脡AL戮臋垄b亩臈y_qM臄膮ro录h膴聻w露酶V陇w聰虏膱]脢職Kx|`藕娄脗脠dr聞c脠聛be赂聸`I录膷TF麓聺录脫媒葍r鹿脥J漏k_葯l鲁麓_聬p膼聺`o脪h聨露pa聜^聬脫臄}D禄聺^Xy聹`d聵[Kv聟JPh猫hCr膫臍脗^脢茖聽w聢ZL颅臓拢職脕brzOIl聮MM聰莫艕啪脣r脳脦e纽聨tw|聦垄mK聛jS菢艌膫St聧脦纽EtqFT聠戮聠E矛卢卢么x脤O垄聼聽K聤鲁艀潞盲Y聠聞聰PVg艓娄艎m艦录VZwVl聦聧聞z陇聟聻拢Tl庐ct慕脷贸{G颅A聡聦脟ge職~脦聭d驴忙aSba楼KK没聬j庐_脛聡^\\脴戮bP庐娄x^sxj亩I_脛聽X聜芒录聲Hu篓Qh隆脌@聬脣么}聨卤聻GN矛膸lT赂聢聟`V~R掳tb脮膴`赂煤脹t脧聙FDu聙[聝Mf聛qGH路楼yA聣zt聬MFe|R聜_Gk聠ChZe脷掳to聵v`x聥b聞聦Dn脨{E}職Z聵猫聙x聴聠NE脼聤REn聵[Pv@{~r膯AB搂聜EO驴|UZ~矛聞Uf篓J虏膫脻脝聙聜s陋聳B`聞s露聹fv聛枚娄聤脮~d脭q篓赂潞禄u霉聬[[搂麓sb陇垄z镁F聹垄脝聟脌h聢聶聬脗聢W\\谋聨脣I脻聤o卤沫艩拢镁聢脢s}隆R聺聫]聦臎聝D聺聜g麓VG垄聜j卤庐猫聠潞脙m聧pU[脕聸聭聦毛潞掳r聸脺bNu赂}聨潞录聡`ni聰潞脭X膭陇录脭da碌聙脕_脙聙聟聠ftQQg聹R聴聭路菗聮v聰}脻脳聹牡]碌聹聯Wc陇F虏聸O末懦茫W陆炉K聧聜漏聟]聙聛{聠L聫贸碌CI碌卤M脽驴h聼聲漏膩q卢o聜陆聻~@i~TUx弄脪垄@聝拢脌E卯么ru艅聜聰聯聜b[搂nWuM脝Ll驴]聛x}某颅聙陆"
+ ]
+ ],
+ "encodeOffsets": [[[112158, 27383]], [[112105, 27474]], [[112095, 27476]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "530000",
+ "properties": {
+ "id": "530000",
+ "cp": [101.512251, 24.740609],
+ "name": "浜戝崡",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@聧聛[聞霉x陆}脩RH聭聛Y墨暮没s脥n聭iEo茫陆Ya聛虏臈{c卢臐g聲膫sA聲脴脜w膹聜玫zFjw}聴芦Dx驴聛}U农l聼锚聶@聲聫H聫脜颅F聣篓脟oJ麓脫n农u膮隆脙垄p脪脜聦聯脴聽TF虏聜xa虏脣X聙聜c脢聥lH卯A脽脣艁k呕茟欧脡漏h聶W颅忙脽U聡聯脣s隆娄}聲te猫脝露St脟聙脟}Fd拢j聥膱Z膯聬脝聥陇T聜膷\\D聝}O梅職拢聛U聢搂~艃G聶聜氓聛艃D臐赂聹Tsd露露B陋職陇u垄艑膸o~t戮脥哦脪tD娄脷聞i么枚聣聙z聸脴X虏gh寞h陆聬脹卤炉聙每m路zR娄茻`陋艎脙h垄rO脭聧麓拢Ym录猫锚聧f炉弄慕n聞聠c脷b聦w\\zlvW聻陋芒聢聽娄g聳聫m目B墓聼拢垄乒艡b磨k谦脽eeZk脵IKueT聛聺禄sVesb聭聧a聬臅聽聽露庐dN聹膭脛p陋y聨聬录聴聞鲁BE聵庐l聡聨G聹怒C聹嵌w锚偶臄脗e聞p脥脌Q茷pC聞聳录挪脠颅聬A脦么露R聞盲聮Q^脴u卢掳職_脠么c麓鹿貌篓P脦垄hl膸娄麓摩聯脝麓s芒脟聞聫挪Pn脢D^炉掳聮Upv聠}庐聛BP脤陋聳j乾x聳S枚wlf貌陋v聙q母|`H聙颅vi募聙nd臏颅膯h艌聲聜em路Fy脼聛q贸聻S寞炉聭鲁X_臑莽锚tryvL陇搂z聞娄c娄楼jn艦k聵聢lD陇酶z陆臏脿聻膫脓M脜|谩茊脿脢c冒脗F脺聨聜谩泞楼\\\\潞聶陌酶脪脨J拇聡聞卯D娄聬zK虏菑脦Eh~聮CD聬颅hMn^脤枚脛漏膶Z脌聻a眉聞f森y艙p寞麓臎F疟k]脭臎垄ql脜膯脵a露~脛q職職锚聙lj聬N卢录H聞脢職NQ麓锚录V脴赂E聠聠^艃脪y聦聝M{聦JLo脪聹臋忙聼e卤亩聸y聣聮聡g聛茫聯炉JY聧脝沫臉毛o楼艩聣o炉hcK芦z_p聤rC麓蘑脰Y聰聴录聽v赂垄R聨脜W鲁脗搂f脟赂Yi鲁xR麓膹U脣聤`锚目U聞没聙u膯B聝疲枚聣N聛聙DH芦膱g聠聴聴脩聜aB{脢NF麓卢c路脜v}e脟脙GB禄聰If聲娄H艌臅M聟~[iwjU脕KE聲聨聥戮d莫莽W聸職聛I聥猫脌聦o脠X貌y艦女脠X芒脦艢聤j|脿sRy聥碌脰聸聳Pr麓镁聦聽赂^w镁TD艛聳Hr赂聥聻R脤mf聡偶脮芒C么ox聳臏茖脝漠聦聸脨聳聹Y聵t芒纽脭@]脠钱茠\\莫录脛拢Us脠炉Lb卯撇艢潞yh聡r聦聤@膾脭聺苺聼脌虏潞\\锚p聯聮J聤}臓v聤qt聞臓@^x脌拢脠聠篓m脣脧臒}n鹿_驴垄脳Y_忙p聢脜聳A^{陆聲Lu篓GO卤脮陆脽M露w聮脕蘑脹聜P聜聸脾录pc牟x聤|ap脤卢H職脨聦艎S聺fs冒BZ驴漏聯X脧脪K聲k聠梅E没驴聣S聟rEFs脮奴k聰贸V钎艍iTL聜隆n{聥ux牛聧脧h聶么艥卢臒艒N聯聭NJkyPaq聶脗臒陇K庐聡Y聼x脡茓脕]膩臋Dq莽gOg聠ILu聴\\_gz聴]W录聻~C脭膿]聫b聛碌og聧p脩聻_o膹`聫麓鲁葰kl`I陋潞脦葎q脭镁聻禄E鲁膸SJ禄聹_f路聜ad脟q聝脟c楼脕_殴w{聶L^脡卤膰x聯U拢碌梅聺xg膲p禄膯qN膿`r臉za牡臍隆K陆脢Bzy盲KXqiWP脧脡赂陆艡脥c脢G|碌茣疲G脣聸梅聼k掳_^媒|_z膵聬BZocm酶炉hhc忙\\l聢MFl瓢拢臏聞脝yH聯聞F篓聧聣碌锚脮]聴聸HA聟脿脫聞^it聽`镁脽盲k聤膜脦T~Wl每篓聞脭PzUC聫聳NVv聽[j芒么D么膹[}聻聣z驴聳msSh聥炉{j茂聧臒l}拧墓[聳艖聦聣gK聥漏U路碌脣@戮聝m_~q隆f鹿聟脜脣^禄聭f鲁酶}Q聲聞隆脰脣鲁g脥卤^脟聛聟\\毛脙聝A_聴驴bW聸脧[露茮茅聫聺拢F{墨Zgm@|kH黔苼膰娄U臄钮聝脳毛}菨聝e膹潞取葮脧铆B脡聶拢膩臉P陋某露聯艍每聡y漏n聣膹拢G鹿隆I聸聤卤L脡暮脩d膲脺聡W楼聵聣}g聵脕聠{aq脙楼a聤聺谋臋聺脧Z聴茂`"
+ ],
+ "encodeOffsets": [[104636, 22969]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "540000",
+ "properties": { "id": "540000", "cp": [89.132212, 30.860361], "name": "瑗胯棌", "childNum": 1 },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@脗聺h聻木x聨聺艝聣x聝脪V聨聠潞脜芒A莫脻葐碌臋炉艊a卤r_w~uS脮艌聭qOj]蓜Q聟拢Z聟聟UD没oY聮禄漏M[聥L聧录q茫脣{V脥聲莽WV聫i聫聨]毛漏脛梅脿y聛茮h聸脷U掳聦聦a聰d聞cQ聝~Mx楼聶cc隆脵aSyF聴脰聛k颅聦uR媒q驴脭碌聺聲Q慕鲁aG{驴F碌毛陋茅臏每陋@卢路聳K聣路脿ar聧i臅膧芦V禄哦聶拇奴聵g猫L谴艊贫af聥t聦猫B艢拢^聤芒聠菒脻庐聳職M娄脕菫每卢Lh聼聨J戮贸凭脝潞cxw聧聥f]Y聟麓聝娄|聹QLn掳a聹d膴聟聹\\篓o聮聹莯脥艓聹麓末膧d`t脢Q艦艜|聜篓C^漏聹膱娄聞娄脦J膴{聨毛膸j陋聫虏r脨聣職l`录膭[t|娄St猫戮聣P聦脺K赂聙d聵苿谋]s陇聴卯_v鹿脦V貌聺纽j聵拢茝sc聴卢_臑麓|艁聵聺娄Av聨娄w`膬聫a聬聧脻aa颅垄e陇谋虏漏陋S陋職脠M膭w聻脡脴艛矛@T聭陇聴臉聶\\玫陋@聰镁o麓颅xA聽聬聬s聰脗t艓Kz贸麓脟膴碌垄r聻^n膴颅脝卢脳眉G聻垄聜鲁聽{芒膴]職聶G聜~b脌gVj聛zlh嵌聧f聙聻O聛職fd聤聣陋B]pj聞聲TO聳t膴聜n陇}庐娄聝膶楼d垄录禄dd職聰Y录聨t聴垄e趣J陇}蔷隆掳搂陇A脨聯lc@臐聰s陋膰募膽A莽聡wx聲UuzE脰摹~AN鹿脛脜葊呕娄驴模艁茅矛卤H聟茫d芦g[脴聣录膿脌聲c墨木摹卢cJ聭碌聟脨圣V葷赂脽S鹿聠媒卤臒k苼录膮^蓻陇脹每聣聺b[}卢艒玫脙]脣Nm庐g@聧聲Bg}脥F卤菒yL楼聫铆C聢聝I某聙脧梅脩職寞[鹿娄[芒拧聧聛E脹茂脕脡d茀脽{芒N脝膩浓脽聺戮臎梅yC拢聡k颅麓脫H@脗鹿聠TZ楼垄寞聝路脤A脨搂庐聴Z聫c聟v陆聼Z颅聧鹿|脜聲WZqgW聯|ieZ脜YV脫聛qdq聫聲bc虏R@聛聠c聡楼R茫禄Ge聠聼e苾墨Q聲}J[脪聯K聟卢茝|o聮臈j摹臓脩N隆冒炉EB膷nw么蓫聫臈陋聝虏聲C位殴摹菨蕝寞沫茫蹋奴裙]螕艇g拧sg冉贸惜碌菦聠臋g趴露覎膰`臉膮艑J脼職盲陇r脜艌楼脰脕U臎t臋u暖脼i膴脛脌\\脝s娄脫Rb|脗^艡脤k脛欧露陆梅聡f卤iM脻聭聸聣@磨掳G卢脙M楼n拢脴膮聜臒炉脽聰搂a毛b茅眉脩O膷聹k拢{\\聭e碌陋脳M聭職脡fm芦茟{脜脳聝G艔签茫y鲁漏W脩膬没聜路路聭Q聴貌谋}炉茫聣I聲茅脮脗Z篓墨猫s露Z脠s聨忙臄T艠v聨g脤sN@卯谩戮贸@聣聵脵wU卤脡T氓禄拢T膽聼Wx聫q鹿Zo聭b聥s[脳聦炉c末v聡聦臈脓鲁BM|鹿k聣陋魔聴楼TzNYn脻聧脽p聧聛臋r帽臓膲RS~陆聤臎VV聤碌聜玫聡芦聦M拢拢碌B聲膲楼谩潞ae聛~鲁Au膼h`聫脺鲁莽聺@B脹聵茂目a漏|z虏脻录D聰拢脿膷虏聥鸥聝I聝没聸聛I聽膩聙贸K聧楼}r脻_脕麓茅Ma艌篓聙~陋S膱陆聨陆K脵贸目e苾脝B聨路卢毛n脳W聧|U潞}LJr瞥聵l艗碌`b脭`Q聢聢脨脫@s卢帽I聦脥@没ws隆氓Q脩脽脕`艐拇{莫聯T聲脷脜TS脛鲁聜聥Yo|脟[脟戮碌MW垄沫i脮脴驴@聵職Mh聟p脮]j聠茅貌驴O茋膯茋p聙锚膲芒l脴w聳臎s聫聢签聜牡赂c聟聺bU鹿艡篓WavquSM聺zeo_^gs脧路楼脫@~炉聺驴Ri墨B聶聤\\聰qTG陋脟臏莽Po聤每f帽貌膮娄贸Q墨脠谩P聲聹膩b脽{聝Z艞母I忙脜聞hnsz脕C脣矛帽職脧路膮臍脻Um庐贸颅L路膬U聸脠铆o霉麓聛脢j掳艁扭_u碌^聭掳聦矛脟聳@t亩膾隆脝聡M鲁蘑芦聵陌抹脜庐臒聠R聨膩冒聯gghe脝垄z聜脢漏脭\\掳脻膸z~藕陇Pn聳M莫脰B拢聬聼k聶n茅聞搂偶膰聤聵膯K聞膾掳聬录L露猫聣芒z篓u娄楼LD臉z卢媒脦m臉d戮脽聰Fz聯hg虏聶Fy娄臐陇膵艈b脦聸@y聜膭忙m掳N漠聛ZR脰铆聨J虏枚L母脪篓Y庐茖脨V聣脿聵tt_脷聙脗y臓z聻]泞h聙z膸{脗聠蘑X聰聢c|職脨q聨職fO垄陇枚g聜脤HN聨聞PK艝聹聨聵U煤麓xx[x聢v膼C没膧聤矛脰T卢赂^}脤s貌d麓_聨聡Kg啪L拇聟脌Bon|H@聳脢x聵聴娄Bp聺虐聢艑驴f碌茖A戮z菆Rx聤露F聰聹k膭藕聬Rz艀聢~露[聰麓Hn陋聳V茷u膾颅脠篓茙c聬平脤m赂脕脠M娄x蛫毛脌x菃B聮職煤^麓W聠拢聳d聞k删默p聹w聜藗脴搔募默I艢聹脢聲n聸艛a赂聶~J掳卯聰l蓪x膜脢脠冒h脤庐聜g聵T麓酶聨脿C聢聨脌^陋err茦d聻垄陌P|聧臇聽鸥W聹陋摩^露麓脗L聞aT卤眉W茰聵莯R脗職哦U艅職臇[QhlL眉A聠聥脺\\聠qR聸膭漏"
+ ],
+ "encodeOffsets": [[90849, 37210]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "610000",
+ "properties": {
+ "id": "610000",
+ "cp": [108.948024, 34.263161],
+ "name": "闄曡タ",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@聵p垄聴犬碌職没G聶摩}摩職冒聛菤露貌聫苿聙j蓚z掳{潞脴k脠臋芒娄j陋聜Bg聜\\聹膵掳s卢聨聮]j聻煤聽聜E聰葘菃卢s聞t聡聰R聢脝d臓陌聨w脺聰赂么W戮飘艂脪_{聮脤職没录聞j潞鹿垄G仟脪炉臉聝Z`潞艎聝ec艈膮職~B聬脗gzp芒膿貌Y菭劝脤T脦篓脗W聹|fc聼膬搂uF聴聦@N聼垄XL聝聤RM潞[臒龋趴茂|楼J聶kc`s艍欠聮聺Y鹿聥W@碌梅K聟茫茂鲁脹Ic帽路聛V葖脷聧脪姆酶聺漏聴镁楼聝y聜脫聼臒臋mW碌脦umZy聛O艆茻磨脫~s脩L陇碌a脜聟Y娄ocyZ{聣y聽c]{聦Ta漏聝`U_臍膿拢蠅脢茘K霉聮K露缺脻品搂{没禄脜脕裙脥茅u某|鹿c脩d聭聤矛UY聝聨O聭uF聧聳脮脠Yv脕Cq脫聝T聲洽铆搂路S鹿Ng聤V卢毛梅脕t聡掳D脴炉聮C麓艍茠贸p模}聞膵聛cE脣聟F聼聼茅GU楼脳K聟搂颅露鲁B聥膶}C驴氓膵`聧w摹B路陇艖c骗虏艖[脜^axwQO聟每E脣脽艢聲膜N臄聼w茋聢聛脛聤艅w莫颅聤o[聞_K脫陋鲁聯聫脵nK聣脟聝臎聹每]膹聙膬_d漏路漏脻艔掳脵庐g]卤聞聼聡脽聵氓聸聴卢梅聺m\\聸ia菓k臎X{垄|ZKl莽hLt聛聙艊卯诺聙艙猫[聙脡@茐膭E聹聡t茋聫脧聵鲁颅魔Z芦mJ聟聸脳戮聭M聛t脻摩拢Iw脛氓\\脮{聡聵聝Ow默漏L脵鲁聛脵gB茣艀r脤聸蘑怒O楼l茫yC聬搂H脥拢脽E帽聫聼X隆聴颅掳脵Cgp钮z聭聢b`wI聞vA|搂聰聡聴h聧o臅@E卤聯iYd楼O幕鹿S|}F@戮oAO虏{tf聻脺聴垄F莻脪聢W虏掳B膜h^Wx{@聞卢聜颅F赂隆聞姆n拢聬P|聼陋拇@^臓膱忙b聳脭c露l聵Yi聟聳^Mi聵c膸掳脗[盲聙v茂露gv@聬脌聯默路lJ赂sn|录u~聧a]聮脝脠t艑潞Jp聮聝镁拢KKf~聤娄Uby盲I職暮茫n聡脭驴^颅聻诺MT聳h臓脺陇ko录艓矛膮菧h`[t聦Rd虏牟_聹XPr刹聣l聭聜X聻iL搂脿聝聳鹿聨H聵掳圈q聬潞庐QC聴bA聠聞艑J赂聬臅脷鲁暮搂聽`d篓Yj聻iZvR暮卤枚VKkjG葕脛聬eP臑聻Zm募K脌聙聜[聤聨`枚s矛h聠茂脦o默dtK脼{卢猫脪脪B聦脭p牟脟默J艎聛娄卤J芦聢Y搂聥@路pH聙碌脿氓VKe聸pW聠ftsA聛脜qC路卢ko芦pH脝uK@o聼H聛膯脹聞姆h聧x聯e聭n聸S鲁脿菎rq贫Rbzy聙赂脣脨聬l聸录E潞p膜录聦x录陆~臑聮聛聰脿@聠脷眉dK^聢m聬脤Sj"
+ ],
+ "encodeOffsets": [[110234, 38774]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "620000",
+ "properties": {
+ "id": "620000",
+ "cp": [103.823557, 36.058039],
+ "name": "鐢樿們",
+ "childNum": 2
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@VuUv"],
+ [
+ "@@农聥E聧臓tt~nkh`Q聣娄脜脛脺dw聵Ab脳臓膮J聢陇D眉猫g暮qBq聹j掳lI隆抹脪陇煤SHb職聡聤j脦聭B聤掳aZ聢垄KJ聨聮O[聬|A拢聻Dx}N膫卢聲HUnrk聞聽kp聙录Y聽kMJn[aG聜谩脷脧[陆rc聠}aQxOgsPMnUs聡nc聫聥Z聟聻聳sK煤vA聸t聞脼摹聮拢庐膧YKdnFw職垄JE掳聰Latf`录h卢we|聙脝聡職bj}GA聙路~W聨聰聴`聠垄MC陇tL漏牟掳qdf聰O聜聯b脼默鹿ttu`^Z煤聺E`聦[@聞脝s卯z庐隆聮C聞瞥茰G虏聯R聭垄R聮m聰f聨w母g脺聝聜膮聽G@pzJM陆聬m聤hVy赂u脠脭O卤篓{Lf忙U露脽G膫q\\陋卢聡虏I聜楼I艍脠墨o谋聥脫脩A莽脩|芦L脻csp墨冒脥g聟t毛_玫聣聧聛\\膲帽LYn臐聺g聮聼R恰脕iHLl玫U墓虏uQjYi聺搂Z_c篓聼麓墓臇脵路聫艐I聟聝aBD聵颅R聫鹿去r聴炉聧G聲潞脽聞K篓聺j聛Wk聮杀聤Oq聸W某\\a颅聥Q\\sg_膯菦艒毛p禄拢l臒脹聙gS聲哦N庐聺脌]聢脫盲m聶墓茫Jaz楼V}聣Le陇L聞媒o聭鹿Is艐脜脟^聭聨bz聟鲁tmE脕麓a聤鹿c膷ec脟N聲膴茫脕\\膷炉聴dNj聲]j聠聴Z碌k脫da聲膰氓]臒某@聽漏O{陇聫母m垄聝E路聛庐聝芦|@Xwg]A模卤炉聡X莵脩浅陋c聸wQ脷艥帽s脮鲁脹V_聫媒聝聵楼\\暖楼漏戮梅w聴聨漏W脮脢末h每脰脕Ro赂V卢芒Db篓職h没x聳脢脳菍~Z芒聝g|職X脕n脽Yo潞搂Z脜艠v聦[聞沫脰蕛u膹xcVbnUSf聟B炉鲁_Tz潞聴脦聲O聛漏莽M脩~M聢鲁]碌^p眉碌聰聤脛Y~y@X~陇Z鲁聙[脠艒l@庐脜录拢QK聝路Di聥隆By聭每聣Q_麓D聛楼h艞y聝^聼沫脕Z]cIz媒聣ah鹿M莫臒P聭s{貌聡聥聭虏Vw鹿t鲁艤脣聛[聨脩}X\\gsF聼拢sPAg臎p脳毛fYH膩膹脰q膿怒O脧毛聯dL眉聲\\i聦聰t^c庐職R脢潞露聴垄H掳m聢聭rY聼拢B聼鹿膷Io木u露uI]v模SQ{聝U呕聰脜}Q脗|脤聥掳茀陇末弄U聽臋膭聻脤Z脪聻\\v聵虏P臄禄脾NH聝膫yAm苽wVm聻`聰]脠聫b聲聰H`聣脤垄虏ILv臏聴H庐陇Dlt_聞垄JJ脛盲m聬猫脭D毛镁g潞偏聬聶聰a蕩脤r锚Yi~聽脦陌陇Np脌A戮臄录b聟冒梅聮聨聢聡庐聜聰眉s聰zMz脰臇Qd权媒聠v搂T猫|聺陋H聮脙戮a赂|職脨聽茠wK蘑x娄ivr^每聽赂l聽枚忙f茻拇路PJv}n\\h鹿露v聠路脌|\\苼臍N麓臏聙莽猫脕z]聬摹陇虏篓Q脪浓TIl聡陋钮脴}录藯痞v脛霉脴E脗聥聮芦F茂脣聸Iq聰艒聦Tv膩脺艔聜铆脹脽聹脹V聴j鲁芒wG膬脗铆NO聤聢聤P矛yV鲁艍臇媒Zso搂H脩聳聧iY聬w[脽聠\\X娄楼c]脭譬脺路芦j聡脨qv脕娄m^膵卤R聶娄螊茍钮臍g脌禄I茂抹蕳飘聨掳茲聵幕镁脥A茐趴卤t脥E脮脼膩NU脥聴隆\\聛趴膷氓脪驶臉m聽骗脤殴枚圣聮毛Q陇碌颅脟c茣陋oI媒聢聣I脡聬_mkl鲁膬聣茡娄j聴隆Yz聲艊i聳}Ms脽玫聳墨蕥聽聴}聝脕Vm聼_[n}e谋聬颅U磨录聭陋聲I{脦搂D脫聹苹臈oj聭qYh墓T漏o奴亩拢]膹x末聥菓M臐聣q`B麓苾撕效聴莽~聶虏艈j@聰楼@膽麓委}磨tP艅脟戮V卢uf脫聝脡C聥t脫袒聣聟鹿拢G鲁聙]茤凭艓莫弄臉號篓蕡蘑苽l蓸郦眉潞艌U冒菧娶脾偶虒圈羌聜膜艎刹臇脗颅Kq麓茂娄聴潞膾遣艈删陋莯脼膱膫D聠陆膭膸脤艞臑r么帽n聨聹N录芒戮蕜木詥|莿聨枝啷浫椙壧樚浩吤猤V虓蕟臓路脤膴v|媒臇脮W膴菐脼麓玫录c脪脪B蘑廷U臏冒蛼s篓聛艌苾L膲脮脻@蓻漂梅驴慕颅聧墓e葟某毛C葰D挪y锚脳艝y貌炉募c脗脽Y聟t脕皮yA茫司J@菨r媒聥聣@聫陇聟rz赂oP鹿蓯脷y谩聬聡H聼膧[Jw聟cVe却脧聹禄脠聨臇}茠虐艕猫拳洽蠈膧篇脠哦毛途脩虇趣袦木漠E艛聧聴墓艎农~脣U膬{聼幕聺乒蓙蠉醛镁慕v慕茡脡@膿聞慕刹脽菒偏示菞膾p盲W脨xns脌^茊wW漏娄c脜隆聛Ji搂v聫煤F露聨篓c~c录墨聦eX聺菤聥\\膽戮J聨w脌膹ks茫A聧聥f脮娄聧L}wa聜o聰Z聮聥D陆聠Ml芦]e脪脜a脡虏谩o聺陆F玫脹]幕脪隆wYR拢垄rv脫庐y庐LF聥Lz膱聞么e]聛gx}聲|KK}xklL]c娄拢fRt铆v娄聠P膜oH{tK"
+ ]
+ ],
+ "encodeOffsets": [[[108619, 36299]], [[108589, 36341]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "630000",
+ "properties": { "id": "630000", "cp": [96.778916, 35.623178], "name": "闈掓捣", "childNum": 2 },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@InJm"],
+ [
+ "@@C聝脝陆O艃摩s伟~膾鲁娄@@聯艆i職卤猫}楔聵苿斯A鲁r_臑聤菕N莫聦膼w陇^努牡陋p暮SZg聮rpi萍臉脭聸篓C|脥聳J聮漏摩禄庐V牟聫~f\\m聽`Un聞脗聵~蕦聼聲默脿枚Nt聲~艌jy聳垄Zi聵茢楼膭聤k麓nl聫`J脢聡聤J镁漏pd茤庐脠拢露矛R师聭藕玫飘脣n聼始臈忙脩苺膸[聜聵垄V脦膫M脰脻脦F虏s茒苺脦B募媒茷聴聧炉蕵骗冒魔录Jh驴纽臋螌茋職楼虏Q]膶楼nu脗脧ri聢赂卢篇脹^脫娄d聙楼[W脿聟x\\Z聫聨j脪聲篓Gtp镁Y艎臅麓聙zUO毛聡聣P聣卯M膭脕xH麓谩聵i脺U脿聸卯脺艕聛膫脹Su艓聥r聯聹J冒脤卢E聦聭F脕煤脳u脙脦kr聯膾{V}陌芦O_脤脣默漏聨脫脓SR脩卤搂蘑拢^聫脗y猫莽臎M鲁苽臋{[赂驴u聟潞碌[gt拢赂O皮目茅Y聼玫路k膧聼q]juw楼D末茘聙玫脟P茅脛陆G聭聻漏茫聡陇G聟u颧镁Rc脮臅Ny聯y没t聛聯聢颅聧聡酶聭聠茂禄a陆膿驴BMo寞拢聼脥j}茅Z脣qb蕧職聯片聫h鹿矛每脫A聫莽茫nI脙隆I`聝ks拢CG颅臎聵Uy脳Cy聲聟聮聼@聛露省脢Bn膩zG聞啤M膿录卤O梅玫J聫聺脣臍膬V聼莫农茊拢聦炉{脣L陆脤z偶聯聞VR|臓TbuvJv碌h幕臇H聰聬A毛谩a聟颅O脟冒聺帽臋Nw聡聟艙木路L聸mI卤铆臓末P脡脳庐每s聴聮cB鲁卤JK脽膴芦`聟a膽禄路聫QAmO聮聭V牛茅每陇鹿SQt]]脟x聙卤炉A@膲某垄脫募漏聬聲聝l露脜聧脹r聴艜sp茫Rk~娄陋]漠颅麓聯FR聞氓d颅膶sCq膽茅Fn驴脜苾m聮脡x{W漏潞茲潞寞k脮苽茟赂wW奴脨漏脠F聻拢\\t脠楼脛R脠媒脤J聽聝lGr聬^脳盲霉聬y脼鲁fj聰c聠聙篓拢脗Z|菗M臐職脧@毛脺艖R聥聸臐聣聦梅隆{a茂确聛P聫u掳脣X脵{漏Tm臓}Y鲁聮颅脼I艌碌莽陆漏C隆寞梅炉B禄|St禄聸]v聝懦聝s禄聰}M脫聽每湿茻黔A隆fs聵禄聧聺PY聫录c隆禄娄c聞膵颅楼拢~聛ms膲P聲聳Si聝^o漏A聣聤ec聜聶聺Pe堑聨kg聜yUi驴h}aH聛聶職膲^|谩麓聼隆聛H聺脴没脜芦膲庐]m聺聙隆q膲露鲁脠y么艒L脕st聯聫聫B聼庐wn卤聺膬楼HS貌臈職拢聵S聮毛@脳艙聺脢膬x脟N漏聶漏T卤陋拢牟隆fb庐脼b聫聨聬b_膭楼xu聧楼B聴聻{艂臐鲁芦`d聵茞t聴陇钮i帽聻脥Uu潞铆`拢聵^t苾牟c聴路脹LO聥陆聤s莽楼Ts{膬\\_禄聶k脧聤卤q漏聬膷i聧矛膲|脥I聝楼膰楼聸聙]陋搂D{聛艥艝脡R_s每c鲁莫艒聸瓶脦聭聸搂p聸[膲聠聸c炉bKm聸R楼{鲁聞Z聠e^聨聦wx鹿d平脜陆么聧Ig聽搂M臅聽乒拇驴聴牵脺脥聝]聥脻聳聛]sn氓A{聥e聦骗`腔艎目\\某努疟聰Y脗每卢j臇q聨脽b聤赂聲L芦赂漏@臎膧漏锚露矛脌EH|麓bR木聻聳脫露r脌Q镁聥vl庐脮聜E聵Tz脺db聽聵hw陇{LR聞聝d聯c聥b炉聥脵Vg聹聜茰脽z脙么矛庐聧^j聬U聬猫X脦聳|U盲脤禄rK聨\\聦陋N聭录pZC眉聠VY聠聠陇蓛Ri^rP艊聮T脰}|br掳q艌b臍掳陋i贫GQ戮虏聞x娄P聹ml艤聭[聛膜隆螢s摩聼脭脧芒\\陋脷艗U\\f聟垄N虏搂x|陇搂聞x臄sZP貌蕸虏S脨qF`陋聞V聝脼艤亩屁VZ聦脤L`聢垄d艕Iqr\\聬o盲玫聳F脦路陇禄哦脳h鹿]Cl脵聙\\娄膹脤寞卢艡tT雍茩gQ脟脫H牛膾聰麓脙bE脛lb蕯C聰|C聢女聬聢聬k聞飘[始卢艌聹麓K女脠伟脤莫露贫l冒聰募A聠TUvdT聤G聠潞碳聤脭聙聦s脢D脭聞veOg"
+ ]
+ ],
+ "encodeOffsets": [[[105308, 37219]], [[95370, 40081]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "640000",
+ "properties": { "id": "640000", "cp": [106.278179, 37.26637], "name": "瀹佸", "childNum": 2 },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ [
+ "@@K毛脌臋臑芦O臋瓤葧聼谋]艍隆氓寞脮脭芦谴玫篇聶臍Q脨Zhv聽K掳聸枚q脌脩聬S[脙脰H茤膷聫脣聡nL]没c聟脵脽@聜聯臐聭戮}w禄禄聥o模F鹿聹禄聫k脤脧路{zP聝搂B颅垄铆y脜t@聝聺@谩職]Yv_ss模录i脽聛聰幕L戮摹sKD拢隆N_聟聯聵X聧赂}B~Hai聢聶脜f{芦x禄ge_bs聯KF炉隆Ix聶mELc每Z陇颅蘑聭聝脻聹suBL霉聲t聠聺聦Yd聬聢mVtNmtOPhRw~bd聟戮q脨\\芒脵H聛\\bImlNZ聼禄lo聝聼qlVm聳G膩搂~QCw陇聶{A\\聫聭PK聫聼NY聡炉bF聡kC楼聮sk聥聤s_脙\\膬芦垄魔kJi炉r聸rAh墓没莽拢CU聡臅膴_脭聴Bix脜脵聫膭n陋脩aM~魔p聛Ou楼s卯eQ楼陇^dkKwlL~{L~聳hw^聜贸f膰聝KyE聦颅K颅zu脭隆qQ陇xZ脩垄^募枚脺戮Ep聻卤芒b脢脩脝^fk卢聟NC戮聭聦聯Y聛pxbK~楼聨e脰聨聦盲Blt驴膼聧x陆I[膾菣聦W聻聥f禄默}d搂d碌聧霉Eu聬j篓聜I脝垄楼dX陋茀x驴]mt脧w脽R亩聦X垄蛶v脝z苽Z貌庐洽脤蕟Cr芒潞M脼z聻脝M脪聰脢脫艎Z脛戮聳r掳聺脦庐葓m陋虏膱U陋臍卯聛聢酶潞聢漠娄脤臉k聞^F艂默h臍i膧臇戮i陌bj脮"
+ ],
+ ["@@mfw臎wMr泞陋v@G聣"]
+ ],
+ "encodeOffsets": [[[109366, 40242]], [[108600, 36303]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "650000",
+ "properties": { "id": "650000", "cp": [85.617733, 40.792818], "name": "鏂扮枂", "childNum": 1 },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@Q脴臄虏X篓聰~菢B潞j蕫脽脴vK聰茢X篓v膴O聻脙聝路垄i@聛~c聴聡臐e_芦聰E職聯}Qxg瑟毛脧脙@s脜yXo艝{么芦鸥uX聟锚聲脦f`聧聹C聜聛鹿脗每脨G漠脮臑X弄艒鸥M藕脠坪Q猫慕么e|驴聘JR陇臉EjcU贸潞炉抹_艠脕聫M陋梅脨楼O茅脠聡聧驴脰臒扦欠脗F脪聡z脡x[]颅膜臐聣艙娄EP}没匹茅驴陌品T臈偏艙艜茀聶票B禄膼卤聮膿O聟娄E聳聲}聭`c群r摩谩艝u脪聻陋芦牟聡蟺d坪脧脴Z拼w蕜陇臇G膼聶莻Z亩聝猫H露}脷Z爪圣莫茂|脟摩M艛禄陌臐菆聥矛楼螔聹ba颅炉楼菚菤k膯诺摩蓱暮漂x奴袛痰n啤蕛慕谩陆M禄聧聸貌mq贸艠臐膷脣戮膬C聟膰膩瓶脻山漏潜艆鹿膽楼聵鲁冒Lr脕庐杀臅模艍腔聛虌去啤呕菦取V茂鹿艊郓没k蓷摹苼搂蕠臈虝末农平艒^茣聤Uv拢苼Q茂聯频k艔陆螇脙怒脟鲁L艊聸驶芦骗\\l聝聡聫怒聫D聡聯{蕮DkaF脙脛a聯鲁扭膽脭GR脠茪hS庸艢s陌芦膼脣[楼脷Dk潞^脴g录诺赂拢E脥枚聲聙暖艍T隆c_聡脣KY聥僻U艣牡聞脻聝U_漏rET脧蕼卤O帽tYw膿篓聝{拢篓uM鲁x陆艧L漏脵谩[脫脨磨聽螡t模垄\\聜艣聮nkO聸w楼卤聝T禄品F莎脿末脼谩B鹿脝聧聟脩Uw聞艜聤聧聻慕w[聯mG陆脠氓~聡脝梅Qy聤臎CFm沫Z墨聴诺V脕聶瓶Q茮聴没XS虏聣b陆K脧陆膲S聸漏欧X臅聼{聨臅K路楼茥cqq漏f驴]聡聧脽D玫聧U鲁h聴颅聛g脣脟茂模脡蓩w聯k炉铆}I路職艙bm聹脡聳艡聸墨J丧幕藖脳x聛o聸晒墨聡l聲c聟陇鲁X霉]聭聶菂A驴w蛪矛楼w脟N路脗脣n聫凭茘d脟搂膽庐茲v聲Um漏鲁G\\聯}碌目聡Qy殴聫l膬聯聸碌Ew聣菄Q陆y茓Be露艐脌暖聡o聻楼A聴聵脡w@聲{Gpm驴A某聠沤KLh聧聢鲁`帽c脣tW聜卤禄脮S聣毛眉每膹D聡u\\www霉鲁聴V聸聧L艜聝OM聧脣Gh聛拢玫P隆聶er聧聶脧d{聯聡摹W脕聟膷|y拧g聺^臒y脕z脵s`聴s|脡氓陋脟}m垄艃篓`x楼聮霉^聲}聝脤楼H芦聣Y陋茀聰A脨鹿n~藕炉職f陇谩脌z聞g聤脟DI脭聺麓A艌膧脪聞露没EYosp玫D[{霉掳]u聸Jq聧聲U聲|So膵x牛[玫脭磨k艐脼怒Z脣潞贸Y脣眉膵rw聽聙脼kr钮脣驴XG聫脡b艡aD眉路膾梅A脙陋[脛盲聙I脗庐B脮膼聵脼_垄膩臓p聤脹脛葔臇摹DK聺wb聺m聡脛N么聡聤f聹偏V脡vi聠浅聴H聭聥Q碌芒F職霉颅脗艙鲁聧娄{YG聻聝d垄臍脺O聽聞聙{脰娄脼脥脌P聦^b聳凭聤l聨[聞vt脳膱脥E脣篓隆膼~麓卯赂霉脦h聙u猫`赂聼H脮艛V潞w臓芒芒W貌聡@{聹脵N脻麓蓹虏葧n{驴楼{l聴梅e茅^e聮膹聢Xj漏卯\\陋脩貌聵脺矛c\\眉q聢脮[聛膶隆xo脗膵陋b脴颅聦酶|聙露却Zd脝脗職o艅茅聦G職\\聰录聬C掳脤脝聛n麓nx職脢O抹聮弄聛拼母垄赂貌Tx脢仟M墨臑聵脰挪脙蓭Ov聢师脾~F聨聡R臎貌聴驴摹~氓艎聹煤聣N職聻職赂q聨聮臉[臄露脗膰n聬脪P膾脺v煤膧脢b脰{脛卯赂~艛眉np陇脗H戮聹膭Y脪漏脢f潞m脭聢臉cDo默M努聮聵S陇聞s虏聜聰蕵脷聠啪葌V纽聽聳聨猫W掳陋B|牟X艛镁脠J摩脝忙F臍锚聤Y膫陋膫]酶陋艝N脼眉A聙聮f扫J聙聵炉脦rDD職膜聙`聙mz\\聞搂~D卢{vJ聬脗聵芦l碌膫b聳陇p聙艑虐N膭篓膴XW|懦聽驴戮蓜摩茞MT聰聡貌P聵梅f脴亩K垄葷藬S么鹿貌E冒颅聰`茤陆菕脗艌脳盲谋聳搂膜茲搂C~隆聜hl氓聜呛纽艦k芒聮~}聨F酶脿牟a臑聜f聬茽楼聨聞艛d聻聵庐U赂聢藕X聹v垄a茊煤弄t艩懦茽jd聲坪聤坪脜矛nrh\\暮炉盲蓾摩]猫p膭娄麓L茷默聤麓皮乾思膾筛陇r潞羌虏篓z脤P冒艀b镁鹿募D垄鹿聹\\臏脩艢聼露Z苿鲁脿j抹o芒聤却L脢聣犬聦膼颅臍膬聨脌锚Z菤艕陇q葌\\L垄艑陌f脝s|z潞e陋脵忙搂微{膧麓茞脷卢篓拇脿虏艂h屎K脼潞脰T聤i脾戮陋矛掳`枚酶u庐脢戮茫脴"
+ ],
+ "encodeOffsets": [[88824, 50096]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "110000",
+ "properties": {
+ "id": "110000",
+ "cp": [116.405285, 39.904989],
+ "name": "鍖椾含",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@慕O脕聸没t欧mi脥t_H禄抹卤d`聤鹿颅{bw聟Yr聯鲁S]搂搂o鹿聙qGtm_S脓聙聯oa聸聥FLg聭QN_聲dV聺聙@Zom_膰\\脽職c脗卤x炉o艙Rcfe聟拢聮o搂脣gTo脹J铆臄贸u聟|wP陇聶Xn聬O垄脡聢纽聬炉rN脛膩陇z芒艝脠Rp泞Z聤聹脷{G聤rFt娄脪x搂酶鹿R贸盲V陇聺Xd聢偶芒潞Wbw艢篓Ud庐b锚艈戮聭jn艓G艃哦聤nz聧脷Se卯臏Z聬cz卯戮i]脥聹聶Qa煤脥脭i镁末权W蘑聥眉|臇u[q聧b[swP@脜臒P驴{\\聡楼A篓脧聭脩篓聧j炉聤X\\炉聹MK聭pA鲁[H聟墨u}}"
+ ],
+ "encodeOffsets": [[120023, 41045]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "120000",
+ "properties": {
+ "id": "120000",
+ "cp": [117.190182, 39.125596],
+ "name": "澶╂触",
+ "childNum": 1
+ },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": [
+ "@@努gX搂脺芦E聟露F脤聡聯卢O_聶茂l脕g聯z卤AXe聶碌脛牡{聫露]gitg職Ij路聸楼卯akS聙聣篓脨茙k}臅{gB聴qGf{驴a聠U^fI聯瓢聥聬鲁玫{Y聝谋毛N目聻k漏茂脣Z艔聭R搂貌oY脳脫gc聟磨s隆b摹芦@dek膮I[nlPqCnp{聢艒鲁聺掳`聬{PNd茥qS脛幕NN芒yj]盲聻脪D聽默H掳脝]~隆HO戮聦X}脨x聦gp聯gW聢rDG聢聦p霉聜聤^L聜聫聢rzWx聢Z^篓麓T\\|~@I聣z聝聳b膜聥聹je膴陋z拢庐臄v臎聙L聠mV戮脭_脠聰NW~zb默vG聠虏ZmDM~聰~"
+ ],
+ "encodeOffsets": [[120237, 41215]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "310000",
+ "properties": {
+ "id": "310000",
+ "cp": [121.472644, 31.231706],
+ "name": "涓婃捣",
+ "childNum": 6
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@骚瓢卢Ep聛聘脕x聺c聡"],
+ ["@@漏聞陋聝"],
+ ["@@聰MA聥聭職"],
+ ["@@Qp陌聛E搂脡C聧戮"],
+ ["@@b艥脮聲脮E龋脷匹锚聫Im聛蓢铅猫脺臓聦脷聫聻脙茖脙蛶贸"],
+ ["@@菧没痊蓩聤怒聶脳^聣sY聫聦蓫D艐聭沤膮帽CG虏芦陋膷@h聳_p炉A{聡oloY聙卢j@牟聧`聲gQ脷聸hr|莯^M牟vtbe麓R炉脭卢篓Y聨么陇r]矛聠片寞"]
+ ],
+ "encodeOffsets": [
+ [[124702, 32062]],
+ [[124547, 32200]],
+ [[124808, 31991]],
+ [[124726, 32110]],
+ [[124903, 32376]],
+ [[124438, 32149]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "500000",
+ "properties": {
+ "id": "500000",
+ "cp": [107.304962, 29.533155],
+ "name": "閲嶅簡",
+ "childNum": 2
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ [
+ "@@vjG~nG艠努亩葌苺凭鹿聬聛赂脴脦ez膯T赂}锚脨聳qH聼冒q臇盲聮聤楼^C脝聮Ij聳虏p聟\\_聽忙眉Y|聺[Yx茒忙u聻掳xb庐聟聬虐b@~垄NQt掳露聜S忙聽聯脢~r菈臄毛臍垄~職uf`聭聜聠fa聜臄J氓膴聞n脰]聞j茙膰脢@聤拢戮a庐拢虐{哦臅F聥猫gLk{Y|隆臏W茢t片J脩xq聥卤蘑N麓聣貌K聣聶聳L脠脙录D|s`艐聮膰]聝脙聣`膽聦M没票陆~Y掳魔`茝铆W聣陆eI聥陆{a聼聭OIr脧隆臅艊a聠p聠碌脺茀摹聭聹^脰脹b脵沤艔ml陆S聥锚qDu[R聥茫脣禄聠每w`禄y聭赂_暮臋}梅`M炉膵fCV碌q艍梅Z聲gg聯聦`d陆pDO聡脦聛Cn聹^uf虏猫nh录Wt茝xRGg娄聟pV聞聠FI卤聨G^聦Ic麓ec聡聮G聲墓脼陆s毛默聞h聵xW聜}K脫聢e颅Xsbk聰F娄聸L聭脴gTk茂频N茂露}Gy聯w\\o帽隆nm膱zj聺聼聲@聶脫c拢禄W膬鹿脫j聯_m禄聢聧鹿路~Mv脹aq聹禄颅聣锚聹聮\\脗oVn聨脫脴脥聶虏芦聧聥bq驴e聛fE聽聞聙聥臏聬^Q聻~聽脡v媒聡艧陇虏漠聣pE陌}zc暮聝L聥陆聡職驴g脜聠聸隆媒E隆ya拢鲁t\\篓聫\\v煤禄录搂路脩r聫_o脪媒楼u聜聲_n禄_聝聲At漏聛脼脜卤膩搂IVe毛聝Y}{VP脌聛FA篓膮B}q@|Ou聴\\Fm聣QF脻聟Mw聵氓}]聲聙聺|Fm脧聥Ca聝w聦u_p聴炉sf脵gY聟DHl聛`{QEf聫NysB聤娄zG赂rHe聜聞N\\CvEs脨霉脺_路脰膲saQ炉聙}_U聡聠x脙膽聤q聸聛NH卢聲脛d^脻虐R卢茫掳we膰JE聻路v脻路Hg聝聜茅FXj脡锚`|y聦pxkAw聹W膼pb楼eOsmzwqCh贸UQl楼F^laf聥an貌sr聸EvfQd脕UVf聴脦v脺^ef聢tET卢么A\\聹垄sJ聨nQTjP脴聢x酶K|nBz聣聞聹臑禄LY聜聟FDx脫聞vr聯[eh木職聲vN聰垄o戮Ni脗xGp芒卢聬z聸bfZo~hGi聮]枚F|聣|Nb聡tOMn聽eA卤聤聺tPT聡LjpYQ|聠SH聠聠Y膧xinzDJ聙脤g垄v脿楼Pg聣_聳脟zII聥聙II聲聞拢庐S卢聞脴s脦录聬拢聦N"
+ ],
+ ["@@ifjN@s"]
+ ],
+ "encodeOffsets": [[[109628, 30765]], [[111725, 31320]]]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "810000",
+ "properties": {
+ "id": "810000",
+ "cp": [114.173355, 22.320048],
+ "name": "棣欐腐",
+ "childNum": 5
+ },
+ "geometry": {
+ "type": "MultiPolygon",
+ "coordinates": [
+ ["@@AlBk"],
+ ["@@m聨n聧"],
+ ["@@EpFo"],
+ ["@@ea垄pl聫赂E玫鹿聡hj[聝]脭C脦聳@聫lj聵隆uBX聼聟聛聲麓聥AI鹿聬聟[聥yDU聢]W`莽wZkmc聳聟M聸聻p聙脜v聸}I聥oJl聧ca聝f艃聭K聨掳盲卢XJm脨聽膽hI庐忙脭tSHn聙E聢聞脪r脠c"],
+ ["@@rMUw聡AS庐聙e"]
+ ],
+ "encodeOffsets": [
+ [[117111, 23002]],
+ [[117072, 22876]],
+ [[117045, 22887]],
+ [[116975, 23082]],
+ [[116882, 22747]]
+ ]
+ }
+ },
+ {
+ "type": "Feature",
+ "id": "820000",
+ "properties": { "id": "820000", "cp": [113.54909, 22.198951], "name": "婢抽棬", "childNum": 1 },
+ "geometry": {
+ "type": "Polygon",
+ "coordinates": ["@@k脢d聬掳氓搂s"],
+ "encodeOffsets": [[116279, 22639]]
+ }
+ }
+ ],
+ "UTF8Encoding": true
+}
diff --git a/src/assets/svgs/403.svg b/src/assets/svgs/403.svg
new file mode 100644
index 0000000..4500596
--- /dev/null
+++ b/src/assets/svgs/403.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" enable-background="new 0 0 800 800"><style>.st26{fill:#fff}</style><g id="鍥惧眰_11"><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="401.773" y1="162.104" x2="401.773" y2="717.596"><stop offset="0" stop-color="#F4F2FB"/><stop offset="1" stop-color="#E1EEF5"/></linearGradient><path d="M485.03 203.46c-38.37 30.29-120.74 33.81-181.17-2.22s-172-31.38-202.22 34.87 37.19 131.33 12.78 178.98S8.66 530.13 64.45 611.49s126.6 60.62 169.22 52.45c84.17-16.13 189.79 115.67 308.62 16.13 68.47-57.35 170.44 42.09 210.17-81.36 32.78-101.86-85.67-139.5-49.97-208.03 37.96-72.88 30.67-159.24-10.46-201.06-38.31-38.96-140.75-38.46-207 13.84z" style="fill:url(#SVGID_1_)"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="494.782" y1="599.604" x2="494.782" y2="428.659"><stop offset=".34" stop-color="#B0B9E1"/><stop offset=".866" stop-color="#EAF0F8"/></linearGradient><path d="M406.65 428.66h216.44l-22.53 49.03s59.19 57.87-14.13 121.91c-134.28-44.17-221.74-37.1-219.98-38.87 1.77-1.76 40.2-132.07 40.2-132.07z" style="fill:url(#SVGID_2_)"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="116.855" y1="542.49" x2="116.855" y2="405.316"><stop offset=".227" stop-color="#B7ACE0"/><stop offset=".789" stop-color="#E8E7FA"/></linearGradient><path d="M117.64 405.56s-.22-.57-.52.04c-2.7 5.49-27.15 64.96-29.09 110.86 0 0-4.08 26.37 30.11 26.02 28.54-.29 27.78-24.6 27.68-32.79-.39-33.22-28.18-104.13-28.18-104.13z" style="fill:url(#SVGID_3_)"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="116.857" y1="420.547" x2="116.857" y2="571.681"><stop offset="0" stop-color="#ECF1FB"/><stop offset=".818" stop-color="#AFB0E7"/></linearGradient><path d="M116.86 571.68c-.55 0-1-.45-1-1V421.55c0-.55.45-1 1-1s1 .45 1 1v149.13c0 .55-.45 1-1 1z" style="fill:url(#SVGID_4_)"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="617.984" y1="450.968" x2="617.984" y2="362.644"><stop offset=".227" stop-color="#CCD4F4"/><stop offset=".789" stop-color="#ECF1FB"/></linearGradient><path d="M618.49 362.8s-.14-.37-.33.03c-1.74 3.53-17.48 41.83-18.73 71.38 0 0-2.63 16.98 19.39 16.76 18.38-.18 17.89-15.84 17.82-21.11-.25-21.4-18.15-67.06-18.15-67.06z" style="fill:url(#SVGID_5_)"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="617.985" y1="372.451" x2="617.985" y2="469.764"><stop offset="0" stop-color="#ECF1FB"/><stop offset="1" stop-color="#A6A8E2"/></linearGradient><path d="M617.99 469.76c-.36 0-.64-.29-.64-.64V373.1c0-.36.29-.64.64-.64s.64.29.64.64v96.02c0 .36-.29.64-.64.64z" style="fill:url(#SVGID_6_)"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="463.902" y1="88.362" x2="429.148" y2="148.558"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><circle cx="446.52" cy="118.46" r="34.75" style="fill:url(#SVGID_7_)"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="421.565" y1="118.828" x2="421.565" y2="176.282"><stop offset="0" stop-color="#F9FAFE"/><stop offset="1" stop-color="#E5EDF7"/></linearGradient><path d="M466.3 137.41h-34.57c-2.23-10.61-11.65-18.58-22.93-18.58s-20.69 7.97-22.93 18.58h-9.05c-10.73 0-19.44 8.7-19.44 19.44 0 10.73 8.7 19.44 19.44 19.44h89.47c10.73 0 19.44-8.7 19.44-19.44.01-10.74-8.69-19.44-19.43-19.44z" style="fill:url(#SVGID_8_)"/><g><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="688.586" y1="540.208" x2="688.586" y2="512.38"><stop offset=".227" stop-color="#AFB0E7"/><stop offset="1" stop-color="#ECF1FB"/></linearGradient><circle cx="688.59" cy="526.29" r="13.91" style="fill:url(#SVGID_9_)"/><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="688.635" y1="515.894" x2="688.635" y2="560.69"><stop offset="0" stop-color="#DDE1F6"/><stop offset=".818" stop-color="#A6A8E2"/></linearGradient><path d="M688.64 560.69c-.24 0-.43-.19-.43-.43v-43.94c0-.24.19-.43.43-.43s.43.19.43.43v43.94a.44.44 0 01-.43.43z" style="fill:url(#SVGID_10_)"/></g><g><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="2622.045" y1="266.481" x2="2451.058" y2="562.64" gradientTransform="matrix(-1 0 0 1 2941.346 0)"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M248.82 393.99c0-24.52-.03-49.03.01-73.54.02-14.37 4.24-18.36 17.97-20.53 41.87-6.61 82.03-18.72 117.91-42.29 10.38-6.82 18.3-7.59 29.06-.47 34.85 23.06 73.26 37.11 114.55 42.8 13.12 1.81 16.84 5.88 16.85 19.25.04 45.72-.4 91.44.18 137.15.34 26.77-8.17 49.99-24.02 70.73-31.46 41.17-74.88 63.76-122.21 80.03-2.5.86-5.83.67-8.36-.23-38.47-13.74-74.58-31.84-104.15-61.09-22.97-22.73-37.84-49.56-37.79-83.22.03-22.87.01-45.73 0-68.59z" style="fill:url(#SVGID_11_)"/><linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="2625.25" y1="279.944" x2="2462.749" y2="561.403" gradientTransform="matrix(-1 0 0 1 2941.346 0)"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M247.94 401.44c0-23.21-.03-46.42.01-69.63.02-13.61 4.06-17.38 17.23-19.43 40.15-6.26 78.67-17.72 113.07-40.04 9.95-6.46 17.55-7.18 27.86-.44 33.42 21.83 70.25 35.14 109.84 40.52 12.58 1.71 16.14 5.56 16.15 18.22.03 43.28-.38 86.57.18 129.84.33 25.34-7.83 47.33-23.03 66.96-30.17 38.98-71.81 60.36-117.19 75.77-2.4.81-5.59.64-8.01-.22-36.89-13.01-71.52-30.14-99.87-57.84-22.03-21.52-36.28-46.91-36.23-78.78.02-21.65-.01-43.29-.01-64.93z" style="fill:url(#SVGID_12_)"/><linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="361.421" y1="346.477" x2="449.513" y2="499.057"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M411.59 435.75c23.18-5.61 40.41-26.11 40.41-50.49 0-28.68-23.85-52.01-53.17-52.01s-53.17 23.33-53.17 52.01c0 24.38 17.24 44.88 40.41 50.49v85.2h25.52v-36.38h32.67v-24.96h-32.67v-23.86zm-40.41-50.49c0-14.91 12.41-27.05 27.65-27.05s27.65 12.14 27.65 27.05-12.41 27.05-27.65 27.05-27.65-12.14-27.65-27.05z" style="fill:url(#SVGID_13_)"/><path class="st26" d="M407.67 439.03c21.8-5.39 38.01-25.1 38.01-48.54 0-27.58-22.43-50.01-50.01-50.01s-50.01 22.43-50.01 50.01c0 23.44 16.21 43.15 38.01 48.54v81.92h24v-34.98h30.73v-24h-30.73v-22.94zm-38.01-48.55c0-14.34 11.67-26.01 26.01-26.01s26.01 11.67 26.01 26.01-11.67 26.01-26.01 26.01-26.01-11.67-26.01-26.01z"/><linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="484.836" y1="475.674" x2="565.754" y2="615.828"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><circle cx="525.3" cy="545.75" r="80.9" style="fill:url(#SVGID_14_)"/><linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="482.787" y1="483.323" x2="559.605" y2="616.376"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#C6D5F4"/></linearGradient><circle cx="521.2" cy="549.85" r="76.81" style="fill:url(#SVGID_15_)"/><path class="st26" d="M538.5 547.62l23.01-23.01c4.44-4.44 4.44-11.63 0-16.06-4.44-4.44-11.63-4.44-16.06 0l-23.01 23.01-23.01-23.01c-4.44-4.44-11.63-4.44-16.06 0-4.44 4.44-4.44 11.63 0 16.06l23.01 23.01-23.01 23.01c-4.44 4.44-4.44 11.63 0 16.06 2.22 2.22 5.13 3.33 8.03 3.33 2.91 0 5.81-1.11 8.03-3.33l23.01-23.01 23.01 23.01c2.22 2.22 5.13 3.33 8.03 3.33s5.81-1.11 8.03-3.33c4.44-4.44 4.44-11.63 0-16.06l-23.01-23.01z"/></g><g><linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="232.569" y1="558.709" x2="232.569" y2="484.191"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M224.88 484.54s-18.08-2.5-23.95 5.81-8.02 29.58-8.02 29.58l13.61-.72-1.15 24.78 25.11 14.72 35.77-19.24-5.44-22.45 11.43-2.98s-3.4-32.58-19.31-27.77c-8.17.87-10.74.73-10.74.73s-2.15 6.85-9.53 6.27c-7.38-.59-7.78-8.73-7.78-8.73z" style="fill:url(#SVGID_16_)"/><linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="233.602" y1="471.483" x2="233.602" y2="495.089"><stop offset="0" stop-color="#F4AE98"/><stop offset="1" stop-color="#FAD1BB"/></linearGradient><path d="M226.69 474.3l-3.76 16.76c-.18.79.23 1.59.98 1.89 1.94.79 5.83 2.13 9.82 2.13 4.15 0 8.06-2.27 9.86-3.48.62-.42.88-1.19.64-1.9l-5.75-17.09a1.643 1.643 0 00-1.86-1.1l-8.61 1.53c-.65.11-1.18.61-1.32 1.26z" style="fill:url(#SVGID_17_)"/><linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="-816.068" y1="920.854" x2="-804.529" y2="839.612" gradientTransform="rotate(-8.082 -2795.015 -6505.71)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M204.24 487.44c5.26-1.75 12.4-.58 12.69 11.22s-11.28 30.62-7.13 37.16c4.2 6.63 13.17 16.05 18.89 21.41-1.33 6.3-4.91 11.61-4.91 11.61s-21.05-9.71-30.21-19.44c-9.17-9.73-4.54-32.03-.3-47.9 3.19-11.95 10.97-14.06 10.97-14.06z" style="fill:url(#SVGID_18_)"/><linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="-6575.898" y1="102.823" x2="-6564.359" y2="21.581" gradientTransform="scale(-1 1) rotate(-8.082 -118.103 -44396.273)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M259.39 487.44c-5.26-1.75-12.4-.58-12.69 11.22s11.28 30.62 7.13 37.16c-4.2 6.63-13.17 16.05-18.89 21.41 1.33 6.3 4.91 11.61 4.91 11.61s21.05-9.71 30.21-19.44c9.17-9.73 4.54-32.03.3-47.9-3.19-11.95-10.97-14.06-10.97-14.06z" style="fill:url(#SVGID_19_)"/><linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="232.569" y1="531.798" x2="232.569" y2="579.152"><stop offset="0" stop-color="#275C89"/><stop offset="1" stop-color="#013F7C"/></linearGradient><path d="M206.79 579.15h51.1c2.31 0 4.38-1.75 5.19-4.4l10.3-33.89c1.34-4.4-1.33-9.07-5.19-9.07h-71.23c-3.82 0-6.48 4.6-5.21 8.98l9.84 33.89c.77 2.69 2.86 4.49 5.2 4.49z" style="fill:url(#SVGID_20_)"/><path class="st26" d="M204.75 594.74s-.79-1.74-1.4-1.93c-.61-.19-9.35-.54-12.53-1.36-3.19-.83-12.38-2.14-16.32 1.59-3.43 3.25-4.56 10.84.66 15.2 1.96 1.7 3.89 2.2 11.14 1.86 7.26-.34 17.78-.26 20.09-3.63-.07-5.55-1.64-11.73-1.64-11.73z"/><linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="-5720.751" y1="599.589" x2="-5703.986" y2="599.589" gradientTransform="matrix(-1 0 0 1 -5504.059 0)"><stop offset="0" stop-color="#F4B9A4"/><stop offset=".652" stop-color="#FAD1BB"/></linearGradient><path d="M212.86 592.81s-8.44 1.9-11.45 1.62-.49 11.87-.49 11.87 8.05.56 15.18-1.51c2.4-9.3-3.24-11.98-3.24-11.98z" style="fill:url(#SVGID_21_)"/><linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="209.839" y1="581.112" x2="296.322" y2="581.112"><stop offset="0" stop-color="#18264B"/><stop offset=".652" stop-color="#2D3C65"/></linearGradient><path d="M209.84 592.37l4.39 13.64s94.25-12.41 80.78-43c-11.27-25.57-85.17 29.36-85.17 29.36z" style="fill:url(#SVGID_22_)"/><linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="190.339" y1="591.445" x2="190.339" y2="609.24"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><path d="M203.66 593.42s3.45 1.35 3.89 6.17c.44 4.82-.99 8.05-8.33 8.94s-9.21.56-13.81.67-11.29.56-12.27-8.2c-.99-8.75 7.96-10.98 17.24-8.75 2.92.56 13.28 1.17 13.28 1.17z" style="fill:url(#SVGID_23_)"/><g><path class="st26" d="M263.56 594.74s.79-1.74 1.4-1.93c.61-.19 9.35-.54 12.53-1.36 3.19-.83 11.75-2.2 16.08 1.49 4.01 3.42 4.27 11-.29 15.18-1.96 1.7-4.02 2.32-11.28 1.98-7.26-.34-17.78-.26-20.09-3.63.09-5.55 1.65-11.73 1.65-11.73z"/><linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="251.623" y1="599.589" x2="268.387" y2="599.589"><stop offset="0" stop-color="#F4B9A4"/><stop offset=".652" stop-color="#FAD1BB"/></linearGradient><path d="M255.45 592.81s8.44 1.9 11.45 1.62.49 11.87.49 11.87-8.05.56-15.18-1.51c-2.4-9.3 3.24-11.98 3.24-11.98z" style="fill:url(#SVGID_24_)"/><linearGradient id="SVGID_25_" gradientUnits="userSpaceOnUse" x1="171.993" y1="581.112" x2="258.476" y2="581.112"><stop offset="0" stop-color="#445677"/><stop offset="1" stop-color="#293861"/></linearGradient><path d="M258.48 592.37L254.09 606s-94.25-12.41-80.78-43c11.26-25.56 85.17 29.37 85.17 29.37z" style="fill:url(#SVGID_25_)"/><linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="277.976" y1="591.445" x2="277.976" y2="609.24"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><path d="M264.66 593.42s-3.45 1.35-3.89 6.17.99 8.05 8.33 8.94c7.34.89 9.21.56 13.81.67s11.29.56 12.27-8.2c.99-8.75-7.96-10.98-17.24-8.75-2.92.56-13.28 1.17-13.28 1.17z" style="fill:url(#SVGID_26_)"/></g><linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="249.053" y1="466.067" x2="218.202" y2="466.067"><stop offset="0" stop-color="#F4B9A4"/><stop offset=".652" stop-color="#FAD1BB"/></linearGradient><path d="M248.39 467.6c.56-.8.91-2.84.46-3.44-.83-.67-1.61-.28-2.21.3.14-4.88-.31-8.94-.41-9.97-.3-2.99-3.35-8.48-13.3-8.48-9.95 0-11.88 7.18-11.88 7.18s-.65 5.08-.46 11.24c-.59-.57-1.37-.93-2.18-.27-.46.6-.1 2.64.46 3.44.56.8.91 2.69 1.02 3.74.1.99-.62 3.65 2 3.31 1.56 6.25 7.89 11.47 11.82 11.47 4.3 0 10.01-5.26 11.63-11.48 2.68.37 1.95-2.31 2.04-3.31.09-1.04.45-2.93 1.01-3.73z" style="fill:url(#SVGID_27_)"/><linearGradient id="SVGID_28_" gradientUnits="userSpaceOnUse" x1="213.957" y1="454.142" x2="249.774" y2="454.142"><stop offset="0" stop-color="#4F5C7C"/><stop offset="1" stop-color="#274168"/></linearGradient><path d="M240.1 443.88s-1.94-6.12-9.39-4.65c-7.44 1.46-7.95 4.98-10.87 5.12-4.99.23-8.97 6.45-2.58 13.03 2.85 2.93.44 4.19 1.79 6.78s1.34 5.12 1.34 5.12 2.38-7.6.81-10.84c-.81-1.67 2.77-2.13 7.24-1.73s11.51-1.08 12.06-4.12c1.32 6.23 2.64 6.88 4.31 7.83 1.68.95 1.78 8.48 1.78 8.48s.3-5.53 1.47-6.78c.96-2.04 2.85-10.07.72-12.02s-.32-8.19-8.68-6.22z" style="fill:url(#SVGID_28_)"/></g></g></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/404.svg b/src/assets/svgs/404.svg
new file mode 100644
index 0000000..5244d8d
--- /dev/null
+++ b/src/assets/svgs/404.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" enable-background="new 0 0 800 800"><style>.st49{fill:#d4e4fe}</style><g id="鍥惧眰_5"><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="401.193" y1="159.763" x2="401.193" y2="715.254"><stop offset="0" stop-color="#F4F2FB"/><stop offset="1" stop-color="#E1EEF5"/></linearGradient><path d="M484.45 201.12c-38.37 30.29-120.74 33.81-181.17-2.22s-172-31.38-202.22 34.87 37.19 131.33 12.78 178.98S8.08 527.79 63.87 609.15s126.6 60.62 169.22 52.45c84.17-16.13 189.79 115.67 308.62 16.13 68.47-57.35 170.44 42.09 210.17-81.36 32.78-101.86-85.67-139.5-49.97-208.03 37.96-72.88 30.67-159.24-10.46-201.06-38.31-38.96-140.75-38.46-207 13.84z" style="fill:url(#SVGID_1_)"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="484.537" y1="604.68" x2="484.537" y2="493.367"><stop offset=".34" stop-color="#B0B9E1"/><stop offset=".866" stop-color="#EAF0F8"/></linearGradient><path d="M285.1 583.44c1.77-1.63 77.74-90.07 77.74-90.07h321.13l-99.5 111.31-299.37-21.24z" style="fill:url(#SVGID_2_)"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="616.023" y1="627.266" x2="657.332" y2="555.716"><stop offset="0" stop-color="#B0B9E1"/><stop offset=".866" stop-color="#EAF0F8"/></linearGradient><path d="M604.49 620.61L659.43 556.93 633.22 624.12z" style="fill:url(#SVGID_3_)"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="116.275" y1="540.149" x2="116.275" y2="402.974"><stop offset=".003" stop-color="#9A9ADB"/><stop offset=".789" stop-color="#CECDF1"/></linearGradient><path d="M117.06 403.22s-.22-.57-.52.04c-2.7 5.49-27.15 64.96-29.09 110.86 0 0-4.08 26.37 30.11 26.02 28.54-.29 27.78-24.6 27.68-32.79-.39-33.22-28.18-104.13-28.18-104.13z" style="fill:url(#SVGID_4_)"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="116.277" y1="418.206" x2="116.277" y2="569.34"><stop offset="0" stop-color="#ECF1FB"/><stop offset=".818" stop-color="#AFB0E7"/></linearGradient><path d="M116.28 569.34c-.55 0-1-.45-1-1V419.21c0-.55.45-1 1-1s1 .45 1 1v149.13c0 .55-.45 1-1 1z" style="fill:url(#SVGID_5_)"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="617.404" y1="448.627" x2="617.404" y2="360.303"><stop offset=".227" stop-color="#CCD4F4"/><stop offset=".789" stop-color="#ECF1FB"/></linearGradient><path d="M617.91 360.46s-.14-.37-.33.03c-1.74 3.53-17.48 41.83-18.73 71.38 0 0-2.63 16.98 19.39 16.76 18.38-.18 17.89-15.84 17.82-21.11-.25-21.4-18.15-67.06-18.15-67.06z" style="fill:url(#SVGID_6_)"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="617.405" y1="370.11" x2="617.405" y2="467.422"><stop offset="0" stop-color="#ECF1FB"/><stop offset="1" stop-color="#A6A8E2"/></linearGradient><path d="M617.41 467.42c-.36 0-.64-.29-.64-.64v-96.02c0-.36.29-.64.64-.64.36 0 .64.29.64.64v96.02c0 .35-.29.64-.64.64z" style="fill:url(#SVGID_7_)"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="463.322" y1="86.02" x2="428.568" y2="146.217"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><circle cx="445.95" cy="116.12" r="34.75" style="fill:url(#SVGID_8_)"/><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="420.985" y1="116.487" x2="420.985" y2="173.941"><stop offset="0" stop-color="#F9FAFE"/><stop offset="1" stop-color="#E5EDF7"/></linearGradient><path d="M465.72 135.07h-34.57c-2.23-10.61-11.65-18.58-22.93-18.58s-20.69 7.97-22.93 18.58h-9.05c-10.73 0-19.44 8.7-19.44 19.44 0 10.73 8.7 19.44 19.44 19.44h89.47c10.73 0 19.44-8.7 19.44-19.44.01-10.74-8.69-19.44-19.43-19.44z" style="fill:url(#SVGID_9_)"/><g><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="688.006" y1="537.867" x2="688.006" y2="510.039"><stop offset=".227" stop-color="#AFB0E7"/><stop offset="1" stop-color="#ECF1FB"/></linearGradient><circle cx="688.01" cy="523.95" r="13.91" style="fill:url(#SVGID_10_)"/><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="688.056" y1="513.553" x2="688.056" y2="558.349"><stop offset="0" stop-color="#DDE1F6"/><stop offset=".818" stop-color="#A6A8E2"/></linearGradient><path d="M688.06 558.35c-.24 0-.43-.19-.43-.43v-43.94c0-.24.19-.43.43-.43s.43.19.43.43v43.94a.44.44 0 01-.43.43z" style="fill:url(#SVGID_11_)"/></g><g><linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="2879.853" y1="308.382" x2="2737.462" y2="450.774" gradientTransform="matrix(-1 0 0 1 3207.18 0)"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M270.73 392.79l91.4-73.3c7.43 11.92 20.65 19.87 35.7 19.87 16.43 0 30.69-9.48 37.6-23.26l92.11 76.85 10.83-12.98-98.5-82.19c0-.16.01-.31.01-.47 0-23.18-18.86-42.04-42.05-42.04-23.18 0-42.04 18.86-42.04 42.04 0 1.8.13 3.58.35 5.32l-95.98 76.97 10.57 13.19zm101.96-95.48c0-13.86 11.28-25.14 25.14-25.14s25.14 11.28 25.14 25.14-11.28 25.14-25.14 25.14-25.14-11.27-25.14-25.14z" style="fill:url(#SVGID_12_)"/><linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="2814.247" y1="259.815" x2="2814.247" y2="392.836" gradientTransform="matrix(-1 0 0 1 3207.18 0)"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#C6D5F4"/></linearGradient><path d="M268.75 392.68l88.31-70.82c7.18 11.51 19.95 19.2 34.49 19.2 15.88 0 29.65-9.16 36.33-22.47l88.99 74.25 10.46-12.54-95.17-79.41c0-.15.01-.3.01-.46 0-22.4-18.22-40.62-40.62-40.62s-40.62 18.22-40.62 40.62c0 1.74.12 3.46.34 5.14l-92.73 74.37 10.21 12.74zm98.51-92.24c0-13.4 10.9-24.29 24.29-24.29 13.4 0 24.29 10.9 24.29 24.29 0 13.4-10.9 24.29-24.29 24.29-13.4 0-24.29-10.9-24.29-24.29z" style="fill:url(#SVGID_13_)"/><linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="2966.463" y1="329.794" x2="2654.707" y2="641.55" gradientTransform="matrix(-1 0 0 1 3203.43 0)"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M230.6 619.91h326.35c17.89 0 32.39-14.5 32.39-32.39V388.31c0-21.39-17.34-38.72-38.72-38.72H230.6c-17.89 0-32.39 14.5-32.39 32.39v205.54c-.01 17.88 14.5 32.39 32.39 32.39z" style="fill:url(#SVGID_14_)"/><linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="2716.773" y1="319.563" x2="2914.293" y2="661.678" gradientTransform="matrix(-1 0 0 1 3203.43 0)"><stop offset="0" stop-color="#EBF2FA"/><stop offset=".525" stop-color="#FDFEFF"/></linearGradient><path d="M223.6 619.91h328.59c14.03 0 25.4-11.37 25.4-25.4V386.73c0-14.03-11.37-25.4-25.4-25.4H223.6c-14.03 0-25.4 11.37-25.4 25.4v207.78c0 14.03 11.38 25.4 25.4 25.4z" style="fill:url(#SVGID_15_)"/><linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="2815.495" y1="361.334" x2="2815.495" y2="425.526" gradientTransform="matrix(-1 0 0 1 3203.43 0)"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M198.24 425.53h379.39v-38.79c0-14.03-11.37-25.4-25.4-25.4H223.64c-14.03 0-25.4 11.37-25.4 25.4v38.79z" style="fill:url(#SVGID_16_)"/><linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="276.445" y1="488.742" x2="350.685" y2="531.604"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M328.82 457.46H307.7c-1.27 0-2.46.59-3.24 1.59L261.91 514c-.56.72-.86 1.6-.86 2.51v23.15c0 2.26 1.83 4.09 4.09 4.09h41.34c2.26 0 4.09 1.83 4.09 4.09v13.46c0 2.26 1.83 4.09 4.09 4.09h14.14c2.26 0 4.09-1.83 4.09-4.09v-13.46c0-2.26 1.83-4.09 4.09-4.09s4.09-1.83 4.09-4.09V525.5c0-2.26-1.83-4.09-4.09-4.09s-4.09-1.83-4.09-4.09v-55.77a4.059 4.059 0 00-4.07-4.09zm-39.3 57.35l13.74-17.74c2.39-3.08 7.33-1.4 7.33 2.51v17.74c0 2.26-1.83 4.09-4.09 4.09h-13.74c-3.41 0-5.33-3.91-3.24-6.6z" style="fill:url(#SVGID_17_)"/><linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="455.095" y1="488.742" x2="529.335" y2="531.604"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M511.56 517.32v-55.77c0-2.26-1.83-4.09-4.09-4.09h-21.12c-1.27 0-2.46.59-3.24 1.59L440.56 514c-.56.72-.86 1.6-.86 2.51v23.15c0 2.26 1.83 4.09 4.09 4.09h41.34c2.26 0 4.09 1.83 4.09 4.09v13.46c0 2.26 1.83 4.09 4.09 4.09h14.14c2.26 0 4.09-1.83 4.09-4.09v-13.46c0-2.26 1.83-4.09 4.09-4.09s4.09-1.83 4.09-4.09V525.5c0-2.26-1.83-4.09-4.09-4.09-2.24 0-4.07-1.83-4.07-4.09zm-43.39-2.51l13.74-17.74c2.39-3.08 7.33-1.4 7.33 2.51v17.74c0 2.26-1.83 4.09-4.09 4.09H471.4c-3.4 0-5.32-3.91-3.23-6.6z" style="fill:url(#SVGID_18_)"/><linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="339.488" y1="482.174" x2="441.31" y2="540.961"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M356.4 566.16h68c2.26 0 4.09-1.83 4.09-4.09v-101c0-2.26-1.83-4.09-4.09-4.09h-68c-2.26 0-4.09 1.83-4.09 4.09v101c0 2.26 1.83 4.09 4.09 4.09zm49.76-82.76v56.34c0 2.26-1.83 4.09-4.09 4.09h-23.34c-2.26 0-4.09-1.83-4.09-4.09V483.4c0-2.26 1.83-4.09 4.09-4.09h23.34c2.26 0 4.09 1.83 4.09 4.09z" style="fill:url(#SVGID_19_)"/></g><g><linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="871.514" y1="4485.232" x2="872.065" y2="4498.77" gradientTransform="rotate(2.333 95904.663 -3670.234)"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><path d="M605.95 610.6s3.25 4.88 10.55 1.06c3.91 2.72 8.92 4.97 12.39 5.88 3.47.91 3.68 5.4 3.12 6.61-4.66-.47-18.14.64-27.3-2.94.72-7.53 1.24-10.61 1.24-10.61z" style="fill:url(#SVGID_20_)"/><path class="st49" d="M604.06 623.84l.43-3.23s10.54 2.63 28.38 1.03c.17 1.66.35 2.48.35 2.48s-13.56 2.02-29.16-.28z"/><linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="-1427.263" y1="-235.579" x2="-1409.896" y2="-215.318" gradientTransform="rotate(40.6 -1575.457 2818.52)"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><path d="M520.47 596.12s-.05 5.81 7.27 7.94c1.95 5-3.73 11.79 5.37 12.42 3.34.23 1.75 5.12.73 5.63-10.95 4.01-14.63-10.12-19.62-18.98 4.32-5.09 6.25-7.01 6.25-7.01z" style="fill:url(#SVGID_21_)"/><linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="-3772.01" y1="604.486" x2="-3772.01" y2="502.198" gradientTransform="matrix(-1 0 0 1 -3222.68 0)"><stop offset="0" stop-color="#445677"/><stop offset="1" stop-color="#293861"/></linearGradient><path d="M569.3 502.2s-14.44-.26-17.67 18.85c-3.23 19.11 1.57 23.66-5.38 37.29-3.62 7.1-27.15 41.12-27.15 41.12l6.83 5.03s37.94-34.72 43.52-48.71 9.83-28.83 10.13-41.46c.28-12.62-10.28-12.12-10.28-12.12z" style="fill:url(#SVGID_22_)"/><linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="-3839.642" y1="559.801" x2="-3786.238" y2="559.801" gradientTransform="matrix(-1 0 0 1 -3222.68 0)"><stop offset="0" stop-color="#445677"/><stop offset="1" stop-color="#293861"/></linearGradient><path d="M572.72 506.19s14.87 3.53 15.75 3.98c.44.23 2.89 7.07 5.24 13.95 5.04 6.87 23.02 32.28 23.21 45.51.29 20.13-.96 43.67-.96 43.67l-9.24.11s-3.5-38.9-5.85-42.31c-.42-.61-1.29-1.95-2.42-3.74-5.14-6.22-16.5-16.65-28.16-27.07-16.45-14.66 2.43-34.1 2.43-34.1z" style="fill:url(#SVGID_23_)"/><linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="5317.908" y1="132.095" x2="5317.908" y2="56.817" gradientTransform="rotate(26.086 2112.504 -9908.036)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M603.14 448.91s-10.69-8.37-16.99-4.36c-6.3 4-14.27 18.91-14.27 18.91l8.85 4.38-23.8 39.67 40.69 21.83 14.6-42.28 11.79.69s7.96-25.24-3.62-27.43c-5.45-2.3-7.04-3.34-7.04-3.34s-3.49 4.27-7.99 1.18-2.22-9.25-2.22-9.25z" style="fill:url(#SVGID_24_)"/><linearGradient id="SVGID_25_" gradientUnits="userSpaceOnUse" x1="5161.945" y1="1134.369" x2="5171.26" y2="1068.78" gradientTransform="rotate(18.006 4848.87 -13687.47)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M589.15 443.6c3.88.61 8.04 4.05 4.56 12.85-3.48 8.8-16.66 18.5-16.06 24.82.6 6.4 3.37 16.58 5.33 22.6-2.8 4.17-6.72 6.78-6.72 6.78s-10.33-14.75-13.12-25.23 7.07-25.25 14.69-35.41c5.73-7.67 11.32-6.41 11.32-6.41z" style="fill:url(#SVGID_25_)"/><linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="-8924.659" y1="-865.525" x2="-8915.544" y2="-929.706" gradientTransform="scale(-1 1) rotate(-34.172 -2504.53 -13720.806)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M624.12 463.5c-2.79-3.19-7.68-4.9-11.53 3.69s-2.35 26.64-7.02 29.97c-4.72 3.37-13.34 7.07-18.62 8.96-1.12 5.12-.49 10.33-.49 10.33s16.36.44 25.19-3.42c8.83-3.86 12.82-21.97 15.06-35.2 1.69-9.97-2.59-14.33-2.59-14.33z" style="fill:url(#SVGID_26_)"/><linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="-3813.896" y1="480.898" x2="-3841.811" y2="423.883" gradientTransform="matrix(-1 0 0 1 -3222.68 0)"><stop offset="0" stop-color="#4F5C7C"/><stop offset="1" stop-color="#274168"/></linearGradient><path d="M590.9 439.68c.43-4.69 4.5-7.9 9.3-7.17.4-1.31 4.44-2.98 5.38-4.6 3.5-6.03 9.26-7 14-3.56 9.79 2.79 8.01 12.2 4.75 21.55 2.8 5.61 1.52 12.41-.06 15.18 4.75 5.07 2.09 11.58-1.39 16.52-.4.56-.82 1.06-1.25 1.52-.21 5.85-8.34 7.86-11.32 4.89-3.17-3.16-3.57-4.49-9.32-1.76-5.75 2.73-11.24-1.54-11.3-7.34-.06-5.8-4.28-4.1-6.12-5.63-3.33-2.77-1.15-5.93-1.15-5.93s-4.85-.26-6.01-7.38c-1.33-16.99 11.95-17.08 14.49-16.29z" style="fill:url(#SVGID_27_)"/><path class="st49" d="M515.38 601.24s4.92 12.03 5.91 13.61 5.9 9.27 14.26 5.05c-.04 1.49-.11 2.43-.11 2.43s-9.42 6.26-15.33-4.62c-5.91-10.88-6.75-14.63-6.75-14.63l2.02-1.84z"/></g></g></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/500.svg b/src/assets/svgs/500.svg
new file mode 100644
index 0000000..9c02092
--- /dev/null
+++ b/src/assets/svgs/500.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" enable-background="new 0 0 800 800"><style>.st26{fill:#fff}</style><g id="鍥惧眰_16"><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="402.832" y1="159.843" x2="402.832" y2="715.335"><stop offset="0" stop-color="#F4F2FB"/><stop offset="1" stop-color="#E1EEF5"/></linearGradient><path d="M486.09 201.2c-38.37 30.29-120.74 33.81-181.17-2.22s-172-31.38-202.22 34.87 37.19 131.33 12.78 178.98S9.72 527.87 65.5 609.23s126.6 60.62 169.22 52.45c84.17-16.13 189.79 115.67 308.62 16.13 68.47-57.35 170.44 42.09 210.17-81.36 32.78-101.86-85.67-139.5-49.97-208.03 37.96-72.88 30.67-159.24-10.46-201.06-38.3-38.96-140.75-38.46-206.99 13.84z" style="fill:url(#SVGID_1_)"/><linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="117.913" y1="540.229" x2="117.913" y2="403.055"><stop offset=".227" stop-color="#B7ACE0"/><stop offset=".789" stop-color="#E8E7FA"/></linearGradient><path d="M118.7 403.3s-.22-.57-.52.04c-2.7 5.49-27.15 64.96-29.09 110.86 0 0-4.08 26.37 30.11 26.02 28.54-.29 27.78-24.6 27.68-32.79-.39-33.22-28.18-104.13-28.18-104.13z" style="fill:url(#SVGID_2_)"/><linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="117.915" y1="418.287" x2="117.915" y2="569.42"><stop offset="0" stop-color="#ECF1FB"/><stop offset=".818" stop-color="#AFB0E7"/></linearGradient><path d="M117.92 569.42c-.55 0-1-.45-1-1V419.29c0-.55.45-1 1-1s1 .45 1 1v149.13c0 .55-.45 1-1 1z" style="fill:url(#SVGID_3_)"/><linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="619.042" y1="448.707" x2="619.042" y2="360.383"><stop offset=".227" stop-color="#CCD4F4"/><stop offset=".789" stop-color="#ECF1FB"/></linearGradient><path d="M619.55 360.54s-.14-.37-.33.03c-1.74 3.53-17.48 41.83-18.73 71.38 0 0-2.63 16.98 19.39 16.76 18.38-.18 17.89-15.84 17.82-21.11-.26-21.4-18.15-67.06-18.15-67.06z" style="fill:url(#SVGID_4_)"/><linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="619.043" y1="370.19" x2="619.043" y2="467.503"><stop offset="0" stop-color="#ECF1FB"/><stop offset="1" stop-color="#A6A8E2"/></linearGradient><path d="M619.04 467.5c-.36 0-.64-.29-.64-.64v-96.02c0-.36.29-.64.64-.64s.64.29.64.64v96.02c.01.35-.28.64-.64.64z" style="fill:url(#SVGID_5_)"/><linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="464.96" y1="86.101" x2="430.206" y2="146.297"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><circle cx="447.58" cy="116.2" r="34.75" style="fill:url(#SVGID_6_)"/><linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="422.623" y1="116.567" x2="422.623" y2="174.021"><stop offset="0" stop-color="#F9FAFE"/><stop offset="1" stop-color="#E5EDF7"/></linearGradient><path d="M467.36 135.15h-34.57c-2.23-10.61-11.65-18.58-22.93-18.58s-20.69 7.97-22.93 18.58h-9.05c-10.73 0-19.44 8.7-19.44 19.44 0 10.73 8.7 19.44 19.44 19.44h89.47c10.73 0 19.44-8.7 19.44-19.44.01-10.74-8.7-19.44-19.43-19.44z" style="fill:url(#SVGID_7_)"/><linearGradient id="SVGID_8_" gradientUnits="userSpaceOnUse" x1="689.644" y1="537.948" x2="689.644" y2="510.119"><stop offset=".227" stop-color="#AFB0E7"/><stop offset="1" stop-color="#ECF1FB"/></linearGradient><circle cx="689.64" cy="524.03" r="13.91" style="fill:url(#SVGID_8_)"/><linearGradient id="SVGID_9_" gradientUnits="userSpaceOnUse" x1="689.694" y1="513.633" x2="689.694" y2="558.429"><stop offset="0" stop-color="#DDE1F6"/><stop offset=".818" stop-color="#A6A8E2"/></linearGradient><path d="M689.69 558.43c-.24 0-.43-.19-.43-.43v-43.94c0-.24.19-.43.43-.43s.43.19.43.43V558c0 .24-.19.43-.43.43z" style="fill:url(#SVGID_9_)"/><linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="289.384" y1="477.19" x2="289.384" y2="411.226"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M202.07 451.28L270.1 411.23 376.7 411.23 315.15 477.19 237.41 476.01z" style="fill:url(#SVGID_10_)"/><linearGradient id="SVGID_11_" gradientUnits="userSpaceOnUse" x1="454.145" y1="502.809" x2="454.145" y2="420.65"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M386.71 479.55L431.76 420.65 521.58 420.65 423.81 502.81 394.37 495.15z" style="fill:url(#SVGID_11_)"/><linearGradient id="SVGID_12_" gradientUnits="userSpaceOnUse" x1="589.016" y1="472.132" x2="589.016" y2="397.68"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M501.26 458.64l64.79-60.96h110.72l-48.99 66.61a19.243 19.243 0 01-17.85 7.7l-108.67-13.35z" style="fill:url(#SVGID_12_)"/><linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="314.267" y1="607.349" x2="314.267" y2="497.361"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M212.23 592.77L303.67 497.36 416.3 497.36 297.04 607.35 247.57 604.7z" style="fill:url(#SVGID_13_)"/><g><linearGradient id="SVGID_14_" gradientUnits="userSpaceOnUse" x1="515.604" y1="312.867" x2="613.092" y2="481.721"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M564.35 296.53c-41.79 0-75.67 33.6-75.67 75.05v51.43c0 41.45 33.88 75.05 75.67 75.05s75.67-33.6 75.67-75.05v-51.43c-.01-41.45-33.88-75.05-75.67-75.05zm23.82 137.83c0 13.05-10.67 23.63-23.82 23.63-13.16 0-23.82-10.58-23.82-23.63v-74.13c0-13.05 10.67-23.63 23.82-23.63 13.16 0 23.82 10.58 23.82 23.63v74.13z" style="fill:url(#SVGID_14_)"/><linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="513.839" y1="321.619" x2="606.64" y2="482.355"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M560.24 305.91c-39.52 0-71.56 32.04-71.56 71.56v49.03c0 39.52 32.04 71.56 71.56 71.56s71.56-32.04 71.56-71.56v-49.03c0-39.52-32.04-71.56-71.56-71.56zm22.53 131.41c0 12.44-10.09 22.53-22.53 22.53-12.44 0-22.53-10.09-22.53-22.53v-70.67c0-12.44 10.09-22.53 22.53-22.53 12.44 0 22.53 10.09 22.53 22.53v70.67z" style="fill:url(#SVGID_15_)"/><linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="217.031" y1="307.363" x2="316.583" y2="479.793"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M333.72 412.6c-5.55-58.15-65.99-54.01-90.14-49.98l2.26-15.28 71.49 5.88 8.98-5.88V307.2h-109l-9.09 7.47-14.81 92.41h43.6c22.73-19.99 38.77-11.37 45.38 0 6.34 10.92 7.27 43.26-19.71 43.87-23.34.53-23.13-19.92-23.13-19.92l-41.55.58-8.06 7.52s6.18 59.41 69.73 59.41 77.3-50.09 74.05-85.94z" style="fill:url(#SVGID_16_)"/><linearGradient id="SVGID_17_" gradientUnits="userSpaceOnUse" x1="212.735" y1="311.982" x2="309.699" y2="479.928"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M324.26 415.94c-5.19-55.89-61.65-51.92-84.21-48.04l2.11-14.69h75.17v-38.58H208.14l-14.95 96h40.73c21.23-19.21 36.22-10.93 42.39 0 5.92 10.49 6.79 46.38-18.41 46.97-21.8.51-24.41-19.14-24.41-19.14l-43.54.66s5.78 59.41 65.14 59.41 72.2-48.14 69.17-82.59z" style="fill:url(#SVGID_17_)"/><linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="368.459" y1="304.731" x2="452.448" y2="450.205"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M387.26 461.26s-54.09-36.72-56.49-83.83c-2.29-45.03 25.47-81.27 76.27-81.27 55.29 0 78.12 47.95 78.12 73.99 0 26.04-10.63 63.25-55.73 93.35-23.53 0-42.17-2.24-42.17-2.24z" style="fill:url(#SVGID_18_)"/><linearGradient id="SVGID_19_" gradientUnits="userSpaceOnUse" x1="366.623" y1="312.428" x2="445.175" y2="448.483"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M384.76 461.29s-51.7-34.94-53.99-79.77c-2.19-42.85 24.35-77.34 72.9-77.34 52.85 0 73.47 45.54 73.47 70.32 0 24.78-12.03 58.72-55.14 87.36-22.49.01-37.24-.57-37.24-.57z" style="fill:url(#SVGID_19_)"/><linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="400.418" y1="454.748" x2="417.994" y2="485.191"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M414.59 486.78h-16.64c-.85 0-1.64-.44-2.08-1.17l-11.39-18.8c-.7-1.15-.33-2.64.82-3.34 1.15-.69 2.64-.33 3.34.82l10.68 17.62h13.84l10.6-19.05c.65-1.17 2.13-1.6 3.31-.94 1.17.65 1.6 2.13.94 3.31l-11.29 20.3c-.44.77-1.25 1.25-2.13 1.25z" style="fill:url(#SVGID_20_)"/><linearGradient id="SVGID_21_" gradientUnits="userSpaceOnUse" x1="397.841" y1="454.748" x2="415.417" y2="485.191"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M412.01 486.78h-16.64c-.85 0-1.64-.44-2.08-1.17l-11.39-18.8c-.7-1.15-.33-2.64.82-3.34 1.15-.69 2.64-.33 3.34.82l10.68 17.62h13.84l10.6-19.05c.65-1.17 2.13-1.6 3.31-.94 1.17.65 1.6 2.13.94 3.31l-11.29 20.3c-.43.77-1.25 1.25-2.13 1.25z" style="fill:url(#SVGID_21_)"/><linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="395.626" y1="441.888" x2="415.816" y2="476.856"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M429.22 468.35h-47.66c-2.76 0-5-2.24-5-5V452.9h57.65v10.45c0 2.76-2.23 5-4.99 5z" style="fill:url(#SVGID_22_)"/><linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="395.022" y1="445.756" x2="412.776" y2="476.507"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M425.57 468.35h-44.01c-2.76 0-5-2.24-5-5v-6.93h54.01v6.93c0 2.76-2.24 5-5 5z" style="fill:url(#SVGID_23_)"/><linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="396.171" y1="472.261" x2="416.697" y2="507.813"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M418.79 505.46h-25.7c-4.09 0-7.4-3.31-7.4-7.4v-19.75h40.5v19.75c0 4.09-3.31 7.4-7.4 7.4z" style="fill:url(#SVGID_24_)"/><linearGradient id="SVGID_25_" gradientUnits="userSpaceOnUse" x1="395.099" y1="476.159" x2="413.018" y2="507.195"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><path d="M414.04 505.46h-20.95c-4.09 0-7.4-3.31-7.4-7.4v-16.47h35.75v16.47c0 4.09-3.31 7.4-7.4 7.4z" style="fill:url(#SVGID_25_)"/><linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="370.752" y1="345.042" x2="439.366" y2="413.656"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M404.4 311.4s-17.23 79.51 1.33 135.9c47.84-62.43-1.33-135.9-1.33-135.9z" style="fill:url(#SVGID_26_)"/><linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="352.936" y1="350.49" x2="415.513" y2="413.067"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M386.43 316.99s-15.24 26.94-16.34 62.72c-.75 24.43 11.93 66.85 11.93 66.85s-20.76-36.07-20.76-70.23 25.17-59.34 25.17-59.34z" style="fill:url(#SVGID_27_)"/><linearGradient id="SVGID_28_" gradientUnits="userSpaceOnUse" x1="389.798" y1="347.846" x2="456.792" y2="414.84"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><path d="M420.65 316.99s34.1 22.12 34.1 60.99-29.68 68.58-29.68 68.58 23.5-42.18 23.5-70.9c0-14.24-13.98-48.76-27.92-58.67z" style="fill:url(#SVGID_28_)"/><path class="st26" d="M386.43 316.99s-62.13 47.12-4.42 129.57c-7.06-15.6-36.21-73.62 4.42-129.57zM420.65 316.99s62.13 47.12 4.42 129.57c7.07-15.6 36.22-73.62-4.42-129.57zM404.4 311.4s-35.48 79.66 1.33 135.9c32.24-57.5-1.33-135.9-1.33-135.9z"/></g><g><linearGradient id="SVGID_29_" gradientUnits="userSpaceOnUse" x1="234.692" y1="561.708" x2="234.692" y2="486.088"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M226.89 486.45s-18.35-2.54-24.31 5.89c-5.96 8.43-8.14 30.01-8.14 30.01l13.81-.73-1.16 25.14 25.48 14.94 36.3-19.52-5.52-22.78 11.6-3.03s-3.46-33.06-19.59-28.18c-8.29.89-10.9.74-10.9.74s-2.18 6.95-9.67 6.36c-7.49-.58-7.9-8.84-7.9-8.84z" style="fill:url(#SVGID_29_)"/><linearGradient id="SVGID_30_" gradientUnits="userSpaceOnUse" x1="235.741" y1="473.191" x2="235.741" y2="497.147"><stop offset="0" stop-color="#F4AE98"/><stop offset="1" stop-color="#FAD1BB"/></linearGradient><path d="M228.72 476.05l-3.81 17.01c-.18.8.24 1.61 1 1.92 1.97.8 5.91 2.17 9.97 2.17 4.21 0 8.18-2.3 10-3.53.63-.42.89-1.21.65-1.93l-5.83-17.35a1.681 1.681 0 00-1.89-1.12l-8.74 1.55c-.67.11-1.2.62-1.35 1.28z" style="fill:url(#SVGID_30_)"/><linearGradient id="SVGID_31_" gradientUnits="userSpaceOnUse" x1="-1535.437" y1="750.954" x2="-1523.728" y2="668.51" gradientTransform="rotate(-8.082 -1929.216 -11692.611)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M205.94 489.39c5.34-1.77 12.58-.59 12.88 11.39.29 11.98-11.45 31.07-7.24 37.71 4.26 6.73 13.37 16.29 19.17 21.73-1.35 6.4-4.99 11.78-4.99 11.78s-21.36-9.86-30.66-19.73c-9.3-9.87-4.61-32.5-.3-48.61 3.24-12.13 11.14-14.27 11.14-14.27z" style="fill:url(#SVGID_31_)"/><linearGradient id="SVGID_32_" gradientUnits="userSpaceOnUse" x1="-5585.118" y1="175.804" x2="-5573.409" y2="93.36" gradientTransform="scale(-1 1) rotate(-8.082 -118.041 -37329.02)"><stop offset="0" stop-color="#C3D5FD"/><stop offset="1" stop-color="#1A90FC"/></linearGradient><path d="M261.91 489.39c-5.34-1.77-12.58-.59-12.88 11.39-.29 11.98 11.45 31.07 7.24 37.71-4.26 6.73-13.37 16.29-19.17 21.73 1.35 6.4 4.99 11.78 4.99 11.78s21.36-9.86 30.66-19.73c9.3-9.87 4.61-32.5.3-48.61-3.24-12.13-11.14-14.27-11.14-14.27z" style="fill:url(#SVGID_32_)"/><linearGradient id="SVGID_33_" gradientUnits="userSpaceOnUse" x1="234.692" y1="534.399" x2="234.692" y2="582.454"><stop offset="0" stop-color="#275C89"/><stop offset="1" stop-color="#013F7C"/></linearGradient><path d="M208.53 582.45h51.85c2.35 0 4.45-1.78 5.26-4.46l10.45-34.39c1.36-4.46-1.35-9.21-5.26-9.21h-72.29c-3.87 0-6.58 4.67-5.29 9.11l9.98 34.39c.8 2.74 2.92 4.56 5.3 4.56z" style="fill:url(#SVGID_33_)"/><path class="st26" d="M206.46 598.27s-.8-1.76-1.42-1.95c-.62-.19-9.49-.54-12.72-1.38s-12.56-2.17-16.56 1.61c-3.48 3.3-4.63 11 .67 15.43 1.99 1.73 3.94 2.23 11.31 1.89s18.04-.27 20.38-3.68c-.07-5.65-1.66-11.92-1.66-11.92z"/><linearGradient id="SVGID_34_" gradientUnits="userSpaceOnUse" x1="-3991.106" y1="603.193" x2="-3974.093" y2="603.193" gradientTransform="matrix(-1 0 0 1 -3772.525 0)"><stop offset="0" stop-color="#F4B9A4"/><stop offset=".652" stop-color="#FAD1BB"/></linearGradient><path d="M214.69 596.31s-8.56 1.92-11.62 1.64c-3.06-.28-.5 12.05-.5 12.05s8.17.57 15.4-1.53c2.45-9.44-3.28-12.16-3.28-12.16z" style="fill:url(#SVGID_34_)"/><linearGradient id="SVGID_35_" gradientUnits="userSpaceOnUse" x1="211.625" y1="584.443" x2="299.388" y2="584.443"><stop offset="0" stop-color="#18264B"/><stop offset=".652" stop-color="#2D3C65"/></linearGradient><path d="M211.63 595.87l4.45 13.84s95.64-12.6 81.97-43.63c-11.43-25.96-86.42 29.79-86.42 29.79z" style="fill:url(#SVGID_35_)"/><linearGradient id="SVGID_36_" gradientUnits="userSpaceOnUse" x1="191.837" y1="594.929" x2="191.837" y2="612.987"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><path d="M205.35 596.94s3.5 1.37 3.95 6.26c.44 4.89-1 8.17-8.45 9.07-7.45.91-9.34.57-14.01.68-4.67.11-11.45.57-12.46-8.32-1-8.88 8.08-11.15 17.5-8.88 2.96.56 13.47 1.19 13.47 1.19z" style="fill:url(#SVGID_36_)"/><g><path class="st26" d="M266.14 598.27s.8-1.76 1.42-1.95c.62-.19 9.49-.54 12.72-1.38 3.23-.84 11.93-2.24 16.32 1.51 4.07 3.48 4.34 11.16-.3 15.4-1.99 1.73-4.08 2.35-11.44 2.01s-18.04-.27-20.38-3.68c.08-5.64 1.66-11.91 1.66-11.91z"/><linearGradient id="SVGID_37_" gradientUnits="userSpaceOnUse" x1="254.028" y1="603.193" x2="271.04" y2="603.193"><stop offset="0" stop-color="#F4B9A4"/><stop offset=".652" stop-color="#FAD1BB"/></linearGradient><path d="M257.92 596.31s8.56 1.92 11.62 1.64c3.06-.28.5 12.05.5 12.05s-8.17.57-15.4-1.53c-2.45-9.44 3.28-12.16 3.28-12.16z" style="fill:url(#SVGID_37_)"/><linearGradient id="SVGID_38_" gradientUnits="userSpaceOnUse" x1="173.22" y1="584.443" x2="260.983" y2="584.443"><stop offset="0" stop-color="#445677"/><stop offset="1" stop-color="#293861"/></linearGradient><path d="M260.98 595.87l-4.45 13.84s-95.64-12.6-81.97-43.63c11.43-25.96 86.42 29.79 86.42 29.79z" style="fill:url(#SVGID_38_)"/><linearGradient id="SVGID_39_" gradientUnits="userSpaceOnUse" x1="280.771" y1="594.929" x2="280.771" y2="612.987"><stop offset="0" stop-color="#FFDB80"/><stop offset="1" stop-color="#FFBB24"/></linearGradient><path d="M267.26 596.94s-3.5 1.37-3.95 6.26 1 8.17 8.45 9.07 9.34.57 14.01.68 11.45.57 12.46-8.32c1-8.88-8.08-11.15-17.5-8.88-2.96.56-13.47 1.19-13.47 1.19z" style="fill:url(#SVGID_39_)"/></g><linearGradient id="SVGID_40_" gradientUnits="userSpaceOnUse" x1="251.42" y1="467.696" x2="220.113" y2="467.696"><stop offset="0" stop-color="#F4B9A4"/><stop offset=".652" stop-color="#FAD1BB"/></linearGradient><path d="M250.74 469.25c.57-.81.93-2.88.46-3.49-.84-.68-1.63-.29-2.24.3.14-4.96-.31-9.07-.42-10.12-.31-3.04-3.4-8.6-13.5-8.6s-12.05 7.29-12.05 7.29-.66 5.15-.46 11.41c-.6-.58-1.39-.95-2.22-.28-.46.61-.1 2.68.46 3.49.57.81.93 2.73 1.03 3.79.1 1.01-.63 3.7 2.03 3.36 1.59 6.35 8.01 11.64 11.99 11.64 4.36 0 10.16-5.33 11.8-11.65 2.71.37 1.98-2.34 2.07-3.35.13-1.06.49-2.98 1.05-3.79z" style="fill:url(#SVGID_40_)"/><linearGradient id="SVGID_41_" gradientUnits="userSpaceOnUse" x1="215.804" y1="455.594" x2="252.152" y2="455.594"><stop offset="0" stop-color="#4F5C7C"/><stop offset="1" stop-color="#274168"/></linearGradient><path d="M242.34 445.19s-1.97-6.21-9.53-4.72c-7.55 1.48-8.06 5.06-11.03 5.19-5.06.24-9.11 6.54-2.61 13.22 2.89 2.97.45 4.25 1.82 6.88s1.36 5.19 1.36 5.19 2.41-7.71.82-11c-.82-1.7 2.82-2.16 7.35-1.75s11.68-1.1 12.24-4.18c1.34 6.32 2.68 6.98 4.38 7.94 1.7.96 1.8 8.6 1.8 8.6s.3-5.62 1.49-6.88c.98-2.07 2.89-10.22.73-12.19s-.34-8.31-8.82-6.3z" style="fill:url(#SVGID_41_)"/></g><linearGradient id="SVGID_42_" gradientUnits="userSpaceOnUse" x1="509.948" y1="612.061" x2="509.948" y2="547.57"><stop offset="0" stop-color="#B0B9E1"/><stop offset="1" stop-color="#E7EFF7"/></linearGradient><path d="M452.67 596.16L498.32 547.57 567.22 547.57 506.27 612.06z" style="fill:url(#SVGID_42_)"/><linearGradient id="SVGID_43_" gradientUnits="userSpaceOnUse" x1="461.835" y1="563.724" x2="495.632" y2="622.263"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><circle cx="478.73" cy="592.99" r="33.79" style="fill:url(#SVGID_43_)"/><linearGradient id="SVGID_44_" gradientUnits="userSpaceOnUse" x1="455.798" y1="564.313" x2="489.595" y2="622.851"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><circle cx="472.7" cy="593.58" r="33.79" style="fill:url(#SVGID_44_)"/><linearGradient id="SVGID_45_" gradientUnits="userSpaceOnUse" x1="479.001" y1="231.35" x2="503.267" y2="273.38"><stop offset="0" stop-color="#C8CBF2"/><stop offset="1" stop-color="#AFB0E7"/></linearGradient><circle cx="491.13" cy="252.36" r="24.26" style="fill:url(#SVGID_45_)"/><linearGradient id="SVGID_46_" gradientUnits="userSpaceOnUse" x1="474.666" y1="231.772" x2="498.933" y2="273.803"><stop offset=".116" stop-color="#DEE4FF"/><stop offset=".847" stop-color="#BACBEE"/></linearGradient><circle cx="486.8" cy="252.79" r="24.26" style="fill:url(#SVGID_46_)"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/add-user.svg b/src/assets/svgs/bpm/add-user.svg
new file mode 100644
index 0000000..bc7bdbf
--- /dev/null
+++ b/src/assets/svgs/bpm/add-user.svg
@@ -0,0 +1 @@
+<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/approve.svg b/src/assets/svgs/bpm/approve.svg
new file mode 100644
index 0000000..06aa09d
--- /dev/null
+++ b/src/assets/svgs/bpm/approve.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="1724316565416" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1339" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#13C463" p-id="1340"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#13C463" p-id="1341"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#13C463" p-id="1342"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#13C463" p-id="1343"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#13C463" p-id="1344"></path><path d="M356.770909 569.250909l22.341818 38.749091-15.476363 8.727273L349.090909 592.64l-153.483636 88.785455 14.778182 25.483636-15.476364 8.96-23.272727-39.912727L256 627.2c-6.283636-4.887273-11.636364-8.843636-16.174545-11.636364L256 602.647273c3.956364 3.141818 9.774545 8.261818 17.338182 15.127272z m-17.338182 199.447273l-49.221818 28.392727 7.563636 13.149091-15.476363 8.96-62.138182-107.52 64.814545-37.469091-12.8-22.574545 15.941819-9.192728 12.8 22.109091 65.396363-37.701818 61.672728 106.821818-15.476364 8.96-7.214546-12.450909-49.92 28.858182 26.065455 45.032727-16.058182 9.192728z m-46.545454-79.825455L244.363636 717.265455l14.778182 25.6 49.221818-28.509091zM267.636364 756.945455l14.778181 25.6 49.221819-28.509091-14.778182-25.483637z m106.938181-80.523637l-14.778181-25.483636-49.92 28.741818 14.778181 25.483636zM346.996364 744.727273l49.803636-28.741818-14.661818-25.483637-49.92 28.741818zM505.832727 609.978182c-4.654545 6.283636-10.123636 13.265455-16.523636 21.061818l35.84 62.021818a18.967273 18.967273 0 0 1-6.749091 29.672727l-19.316364 11.636364-12.450909-13.847273a170.123636 170.123636 0 0 0 17.803637-8.727272 8.494545 8.494545 0 0 0 2.909091-13.614546L477.090909 645.352727l-9.890909 10.472728-10.007273 10.24-12.683636-13.149091c9.309091-8.261818 17.221818-15.941818 23.272727-23.272728l-31.301818-54.341818-25.018182 14.545455-8.843636-15.36 25.018182-14.429091-23.272728-41.076364 15.476364-8.96 23.272727 41.076364L465.454545 538.763636l8.843637 15.36-22.109091 12.567273 28.509091 49.221818c5.469091-6.516364 10.938182-13.498182 16.407273-21.061818z m9.076364-45.730909L572.043636 663.272727a207.825455 207.825455 0 0 0 23.272728-27.461818l11.636363 13.149091a365.381818 365.381818 0 0 1-41.774545 45.498182l-12.567273-12.567273a11.636364 11.636364 0 0 0 1.745455-13.963636L453.818182 493.963636l15.709091-9.076363 36.887272 63.883636 31.301819-18.152727 8.96 15.592727z m129.745454 83.316363a20.596364 20.596364 0 0 1-31.418181-9.774545l-103.098182-178.618182 15.709091-9.192727 38.632727 67.025454a200.261818 200.261818 0 0 0 28.043636-41.076363l16.872728 7.68a303.243636 303.243636 0 0 1-35.723637 49.338182l53.410909 93.090909a9.192727 9.192727 0 0 0 13.963637 4.072727l10.821818-6.283636a14.312727 14.312727 0 0 0 8.029091-11.636364 103.447273 103.447273 0 0 0-15.243637-39.098182l17.338182-3.84c12.567273 25.134545 18.036364 41.658182 16.290909 49.803636A28.392727 28.392727 0 0 1 663.272727 636.741818zM860.276364 521.774545c-7.563636 4.421818-20.829091 11.636364-39.912728 22.574546a179.432727 179.432727 0 0 1-37.352727 16.174545 58.181818 58.181818 0 0 1-33.047273-1.978181 14.312727 14.312727 0 0 0-11.636363-0.581819c-5.352727 3.025455-8.261818 18.385455-8.727273 45.847273l-18.269091-3.956364c1.047273-25.483636 5.003636-42.821818 11.636364-52.014545l-38.865455-67.374545-31.534545 18.152727-8.378182-14.661818 46.545454-26.647273 47.825455 82.850909a55.505455 55.505455 0 0 1 8.494545 1.861818 59.694545 59.694545 0 0 0 25.367273 4.072727 101.701818 101.701818 0 0 0 33.512727-11.636363L849.454545 508.509091l31.418182-18.734546c11.636364-7.214545 19.898182-12.334545 24.087273-15.127272l5.469091 18.152727zM676.072727 413.207273L671.185455 430.545455a279.272727 279.272727 0 0 0-58.181819-13.265455l4.887273-16.64a307.781818 307.781818 0 0 1 58.181818 12.567273zM754.967273 372.363636a261.818182 261.818182 0 0 0 20.247272-38.516363l-98.443636 56.785454-7.796364-13.498182 119.97091-69.46909 6.632727 11.636363a281.134545 281.134545 0 0 1-25.949091 54.807273l5.236364 0.930909L818.734545 349.090909l57.25091 99.025455a18.385455 18.385455 0 0 1-8.843637 27.927272l-18.385454 10.589091-11.636364-11.636363 17.92-9.425455a7.796364 7.796364 0 0 0 3.607273-11.636364L849.454545 437.410909l-37.236363 21.527273 21.992727 38.050909-14.894545 8.610909-21.992728-38.167273L760.203636 488.727273l22.458182 38.749091-15.127273 8.727272L699.461818 418.909091l55.389091-32a306.269091 306.269091 0 0 0-39.330909-1.047273l4.305455-15.127273c13.265455-0.232727 24.901818 0.465455 35.141818 1.629091z m15.825454 49.454546l-11.636363-20.014546-37.003637 21.410909 11.636364 20.014546z m-29.44 34.909091l11.636364 19.549091 37.003636-21.410909-11.636363-19.549091z m81.454546-64.814546l-11.636364-19.898182-37.236364 21.527273 11.636364 19.898182z m-29.556364 34.909091l11.636364 19.432727 37.236363-21.527272-11.636363-19.432728zM1086.370909 391.214545l-19.898182 11.636364-10.589091 6.167273-10.938181 6.050909a186.181818 186.181818 0 0 1-38.749091 16.989091 60.16 60.16 0 0 1-33.978182-1.978182 14.312727 14.312727 0 0 0-11.636364 0c-5.585455 3.258182-8.610909 18.734545-8.96 46.545455l-18.036363-3.723637c0.814545-26.181818 4.770909-43.752727 11.636363-52.945454l-38.865454-67.141819-31.883637 18.385455-8.727272-15.010909 47.243636-27.345455 47.941818 83.2h4.189091a32.465455 32.465455 0 0 1 4.538182 1.163637 71.68 71.68 0 0 0 26.298182 3.490909 112.872727 112.872727 0 0 0 34.210909-13.265455c16.523636-9.192727 31.767273-17.803636 46.545454-25.949091l14.545455-8.727272 14.196363-8.727273c11.636364-6.865455 18.618182-11.636364 22.574546-14.196364l5.352727 18.385455zM896 286.021818l-4.770909 18.385455a296.378182 296.378182 0 0 0-58.181818-14.661818l4.770909-16.872728a311.156364 311.156364 0 0 1 58.181818 13.149091zM1031.098182 384l-12.334546-13.149091c11.636364-5.934545 21.76-11.636364 30.138182-15.941818a9.658182 9.658182 0 0 0 4.189091-14.661818l-54.341818-94.138182-83.781818 48.290909-9.076364-15.709091 83.781818-48.407273-20.712727-35.84 16.174545-9.425454 20.712728 36.072727 32.814545-18.967273 8.610909 15.243637-32.349091 18.850909 56.552728 97.978182a20.247273 20.247273 0 0 1-8.843637 31.185454z m-23.272727-59.345455L1000.727273 340.48a405.876364 405.876364 0 0 0-58.181818-25.6l7.796363-15.127273a393.890909 393.890909 0 0 1 57.716364 24.436364z" fill="#13C463" p-id="1345"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/auditor.svg b/src/assets/svgs/bpm/auditor.svg
new file mode 100644
index 0000000..66d2c2c
--- /dev/null
+++ b/src/assets/svgs/bpm/auditor.svg
@@ -0,0 +1 @@
+<svg t="1729561718271" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8640" width="200" height="200"><path d="M908.5952 920.4224H164.7616a31.0784 31.0784 0 0 1-30.976-30.976c0-17.0496 13.9264-30.976 30.976-30.976h743.8336c17.0496 0 31.0272 13.9264 31.0272 30.976a31.0784 31.0784 0 0 1-31.0272 30.976z m0-123.9552H164.7616a31.0784 31.0784 0 0 1-30.976-30.976v-154.9824c0-51.1488 41.8304-92.9792 92.9792-92.9792h198.3488c-6.1952-37.1712-24.7808-72.8064-51.1488-103.8336a216.576 216.576 0 0 1-54.2208-144.128c0-58.88 23.2448-114.688 66.6112-156.4672C429.7728 71.168 485.5296 51.0976 545.9968 52.6848c111.5648 4.608 206.08 100.6592 207.616 212.2752 1.536 55.808-20.1216 110.0288-57.344 151.8592-26.3168 27.904-41.8304 61.952-48.0256 100.7104h198.3488c51.2 0 93.0304 41.8304 93.0304 92.9792v154.9824a31.0784 31.0784 0 0 1-31.0272 30.976z m-712.8064-61.952H877.568v-124.0064a31.0784 31.0784 0 0 0-31.0272-30.976h-232.448a31.0784 31.0784 0 0 1-30.976-31.0272c0-65.024 23.2448-127.0784 66.6624-173.568 27.8528-29.3888 41.8304-68.1472 41.8304-108.4416-1.536-80.5888-68.1984-148.7872-148.7872-151.8592a150.528 150.528 0 0 0-113.152 43.3664 153.6 153.6 0 0 0-48.0256 111.616c0 37.1712 13.9776 74.3424 38.7584 102.2464 44.9536 51.1488 69.7344 113.152 69.7344 176.64a31.0784 31.0784 0 0 1-30.976 31.0272h-232.448a31.0784 31.0784 0 0 0-30.976 30.976v123.9552z" fill="#fff" p-id="8641"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/cancel.svg b/src/assets/svgs/bpm/cancel.svg
new file mode 100644
index 0000000..ab9b155
--- /dev/null
+++ b/src/assets/svgs/bpm/cancel.svg
@@ -0,0 +1 @@
+<svg t="1729178183592" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4332" width="200" height="200"><path d="M784.074702 99.196443l10.927871 18.473304-21.302843-2.56935-14.180213 16.066571-4.130475-21.042655-19.676671-8.521137 18.733492-10.440019 2.016452-21.335366 15.708814 14.603017 20.945085-4.683373-9.041512 19.449008zM1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512z" fill="#8a8a8a" p-id="4333"></path><path d="M1067.22363 642.402668l-18.440781 10.92787 2.56935-21.302842-16.099094-14.180213 21.042655-4.130475 8.521137-19.676671 10.440019 18.733492 21.367889 2.016452-14.603017 15.708814 4.683373 20.945085-19.481531-9.041512zM571.924408 100.009528l-17.400031-12.488994 20.52228-6.211974 6.504685-20.457234 12.261331 17.595172 21.432936-0.09757-12.944323 17.074798 6.732349 20.359663-20.262093-7.02506-17.269938 12.716659 0.422804-21.46546zM991.444053 784.43246l-21.172749 3.480006 10.114785-18.928632-9.822074-19.026203 21.107702 3.772717 15.090868-15.253486 2.927109 21.237796 19.156296 9.626933-19.318914 9.366746-3.219819 21.205273-14.863204-15.48115zM428.008258 156.795426l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.936097 19.936859-7.870669-5.88674 20.619851 13.692361 16.521899-21.432936 0.813086-11.513292 18.083024-7.382817-20.132zM854.260251 896.475655l-20.749945-5.333841 16.879657-13.237034-1.365983-21.400413 17.822836 11.96862 19.936859-7.903192-5.854217 20.619851 13.659838 16.554423-21.432936 0.780562-11.513292 18.115547-7.382817-20.164523zM562.460092 923.665237l10.895347 18.440782-21.302843-2.569351-14.180212 16.099095-4.130475-21.042655-19.676672-8.521137 18.733493-10.440019 2.016452-21.36789 15.708814 14.603018 20.945085-4.683373-9.008989 19.48153zM242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988z" fill="#8a8a8a" p-id="4334"></path><path d="M242.787359 420.788058l-18.473305 10.895347 2.569351-21.302843-16.066572-14.180213 21.042656-4.130474 8.521137-19.676672 10.440019 18.733492 21.335366 2.016453-14.603018 15.708813 4.683374 20.945085-19.449008-9.008988zM700.814737 943.959854l-17.400032-12.521518 20.522281-6.211974 6.504685-20.42471 12.26133 17.595172 21.432937-0.130094-12.944323 17.107321 6.732349 20.359663-20.262093-7.025059-17.269938 12.684135 0.422804-21.432936zM303.541115 278.823313l-21.140226 3.480006 10.114785-18.928633-9.854597-19.058726 21.107702 3.772717 15.090868-15.220962 2.927109 21.237796 19.156296 9.626933-19.28639 9.366746-3.252342 21.172749-14.863205-15.448626z" fill="#8a8a8a" p-id="4335"></path><path d="M407.648595 90.642782a486.713038 486.713038 0 0 1 504.568397 11.578339l25.010513-14.407877A512.081309 512.081309 0 0 0 139.850723 547.401747l24.977989-14.407877a486.778085 486.778085 0 0 1 242.819883-442.351088zM893.28836 933.422265a486.810608 486.810608 0 0 1-504.568398-11.610863l-25.010513 14.407877a512.081309 512.081309 0 0 0 797.5394-459.621026l-24.97799 14.505447a486.843132 486.843132 0 0 1-242.982499 442.318565z" fill="#8a8a8a" p-id="4336"></path><path d="M814.061299 795.880705a326.665269 326.665269 0 0 1-258.170939 29.563792l-29.791456 17.172368a353.236906 353.236906 0 0 0 472.793013-272.448721l-29.693886 17.172367a326.762839 326.762839 0 0 1-155.136732 208.540194zM486.875655 228.119295a326.795363 326.795363 0 0 1 258.170939-29.563792l29.791456-17.172368a353.236906 353.236906 0 0 0-472.793013 272.448721l29.82398-17.172367a326.762839 326.762839 0 0 1 155.006638-208.540194zM1288.350389 374.73489a53.923837 53.923837 0 0 1-14.34283 12.001143L229.420232 988.712085A53.793743 53.793743 0 0 1 156.112434 968.937843l-148.924757-258.235985a53.76122 53.76122 0 0 1 19.741718-73.437891L1071.516722 35.352962A53.826266 53.826266 0 0 1 1144.82452 55.062157l148.827187 258.268508a53.793743 53.793743 0 0 1-5.398888 61.404225zM32.19819 665.754486a28.360426 28.360426 0 0 0-5.626553 10.73273 28.067715 28.067715 0 0 0 2.699444 21.432936L178.195839 956.188661a28.165285 28.165285 0 0 0 38.442687 10.342449l1044.587328-601.976052a28.132762 28.132762 0 0 0 10.440019-38.442687l-148.924758-258.268509a28.197808 28.197808 0 0 0-38.442687-10.342449L39.711101 659.444942a28.230332 28.230332 0 0 0-7.512911 6.309544z" fill="#8a8a8a" p-id="4337"></path><path d="M498.941845 597.390249l-138.322121 79.877529 38.637827 66.933207q8.000762 13.854979 21.595554 5.98431l114.254788-65.957504a21.172749 21.172749 0 0 0 9.952167-11.123011q2.634397-9.757027-16.91218-47.321582l18.440781-4.130474q20.489757 43.22363 18.148071 56.167953a36.166047 36.166047 0 0 1-16.261712 19.514054l-123.068636 71.031158q-25.17313 14.603017-40.394092-11.77348L317.103383 639.020232l16.066571-9.269176 18.570875 32.133143 122.027886-70.47826-33.596697-58.249452-150.160648 86.707448-9.041511-15.611243 166.454883-96.106718zM691.903319 563.663459c-3.935334 3.837764-9.757027 9.399269-17.497602 16.619469l23.319295 40.394093-15.611244 9.008988-21.237795-36.816516q-31.027346 27.709957-64.754137 54.314118l-12.814229-13.39965 9.171605-7.382818 9.236653-7.122629-79.714912-138.126982-17.627696 10.179832-8.781324-15.155915L601.683341 414.836271l6.960013 12.06619 86.34969-49.858408 8.488614 14.733111q28.197808 65.82741 30.506972 123.39387a274.660314 274.660314 0 0 0 69.339939 27.612387l-3.642623 18.440781a322.177037 322.177037 0 0 1-65.534699-26.40902 220.899095 220.899095 0 0 1-15.38358 72.819946l-18.14807-6.179451a215.272542 215.272542 0 0 0 15.448626-77.340702 312.940384 312.940384 0 0 1-89.374369-86.739971l-8.748801 5.138701-7.2202-12.488995-17.172368 9.919644 71.876767 124.499667q10.570113-10.017215 17.465079-16.61947z m-134.32174-56.948515l40.166428-23.189202-19.969382-34.702493-40.166429 23.189201z m28.067714 48.785135l40.166429-23.189201-19.514055-33.921931-40.166428 23.189201z m48.557472-8.813847l-40.166428 23.189201 21.888264 37.922312q13.334604-10.92787 35.775766-30.767159z m7.2202-117.832365A289.848753 289.848753 0 0 0 715.515325 503.365031a330.437986 330.437986 0 0 0-26.441544-101.92841zM812.760362 400.460918l-4.813467 17.95293a280.482007 280.482007 0 0 0-56.167953-12.781706l5.073654-17.530125a291.637542 291.637542 0 0 1 55.907766 12.358901z m24.360045 28.78323a925.063745 925.063745 0 0 1 10.017214 101.895887l-18.440781 2.016452a812.792886 812.792886 0 0 0-8.878895-101.375512z m-45.923075-86.25212l-4.813467 18.017977a290.922026 290.922026 0 0 0-58.542163-11.513292l5.073655-17.497602a308.972527 308.972527 0 0 1 58.281975 10.992917z m48.459902-17.562649l-9.334223 13.724885A298.792695 298.792695 0 0 0 783.814515 315.477211l9.757027-14.180212a437.635191 437.635191 0 0 1 46.085692 24.13238zM834.355916 269.944418l16.521899-9.529363 35.157821 60.916373 48.199714-27.840051L1003.282579 413.047483q12.716659 22.115928-8.228426 34.214642l-26.018739 15.058345-13.237034-13.009369 25.238177-13.952549c6.992536-4.065428 8.45609-9.561887 4.423186-16.554423l-12.716659-22.018358-80.527997 46.475973L919.762427 491.1037l-16.066572 9.269176-81.926505-141.899698 47.744387-27.579864z m107.750103 73.763125l-14.830682-25.660981-80.56052 46.508496 14.830681 25.726028z m-72.592282 60.330952l14.700587 25.433317 80.560521-46.508496-14.700587-25.433318z m45.532793-166.064603a222.720407 222.720407 0 0 1-2.406733 56.13543l-16.456853 0.878132a242.722312 242.722312 0 0 0 2.081499-55.647578z" fill="#8a8a8a" p-id="4338"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/child-process.svg b/src/assets/svgs/bpm/child-process.svg
new file mode 100644
index 0000000..249723f
--- /dev/null
+++ b/src/assets/svgs/bpm/child-process.svg
@@ -0,0 +1 @@
+<svg t="1740116949537" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1153" width="200" height="200"><path d="M440.32 296.96h283.30496v145.92h66.56V230.4H440.32V17.92H17.92v424.96H440.32V296.96zM373.76 376.32H84.48v-291.84H373.76v291.84zM586.24 588.8v143.36512H298.66496V586.24h-66.56v212.48512H586.24V1013.76H1008.64v-424.96h-422.4z m355.84 358.4h-289.28v-291.84H942.08v291.84z" p-id="1154" fill="#ffffff"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/condition.svg b/src/assets/svgs/bpm/condition.svg
new file mode 100644
index 0000000..41ea85d
--- /dev/null
+++ b/src/assets/svgs/bpm/condition.svg
@@ -0,0 +1 @@
+<svg t="1729585232424" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1602" width="200" height="200"><path d="M925.5 898.9H804.9c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V572.2c0-19-15.4-34.4-34.5-34.4H529.2V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H443.1c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4h34.5V537.8H219.1c-19 0-34.5 15.4-34.5 34.4V727h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4H98.5c-19 0-34.5-15.4-34.5-34.4V761.3c0-19 15.4-34.4 34.5-34.4H133V555c0-38 30.9-68.8 68.9-68.8h275.7V297.1h-34.5c-19 0-34.5-15.4-34.5-34.4V159.5c0-19 15.4-34.4 34.5-34.4h120.6c19 0 34.5 15.4 34.5 34.4v103.2c0 19-15.4 34.4-34.5 34.4h-34.5v189.2h292.9c38.1 0 68.9 30.8 68.9 68.8v172h34.5c19 0 34.5 15.4 34.5 34.4v103.2c0 18.8-15.4 34.2-34.5 34.2z m0 0" p-id="1603" fill="#fff"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/copy.svg b/src/assets/svgs/bpm/copy.svg
new file mode 100644
index 0000000..8ff3bba
--- /dev/null
+++ b/src/assets/svgs/bpm/copy.svg
@@ -0,0 +1 @@
+<svg t="1729649333541" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1644" width="200" height="200"><path d="M647.888 893.84L491.904 571.52l393.888-393.888-237.904 716.208zM872.32 123.232L459.872 535.68 134.96 380.88 872.32 123.232z m90.72-68.32a23.968 23.968 0 0 0-24.784-5.568L64.08 354.816a23.984 23.984 0 0 0-2.4 44.32l381.392 181.728 187.36 387.088a24.048 24.048 0 0 0 23.152 13.504 24.032 24.032 0 0 0 21.232-16.4L968.96 79.552c2.88-8.672 0.592-18.24-5.92-24.64z" fill="#fff" p-id="1645"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/delay.svg b/src/assets/svgs/bpm/delay.svg
new file mode 100644
index 0000000..cbc31df
--- /dev/null
+++ b/src/assets/svgs/bpm/delay.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="1735905505218" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4277" width="200" height="200" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M561.778 454.929h198.117c0.549 0 0.994 0.444 0.994 1.001v97.553a0.998 0.998 0 0 1-0.994 1.001H463.224a1.005 1.005 0 0 1-1.002-1V207.04c0-0.552 0.444-1 1.002-1h97.552c0.553 0 1.002 0.455 1.002 1v247.89zM512 952.706c-247.424 0-448-200.576-448-448 0-247.423 200.576-448 448-448s448 200.577 448 448c0 247.424-200.576 448-448 448z m0-99.555c192.44 0 348.444-156.004 348.444-348.445 0-192.44-156.003-348.444-348.444-348.444-192.44 0-348.444 156.004-348.444 348.444 0 192.441 156.003 348.445 348.444 348.445z" fill="#3296FA" p-id="4278"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/finish.svg b/src/assets/svgs/bpm/finish.svg
new file mode 100644
index 0000000..674c6df
--- /dev/null
+++ b/src/assets/svgs/bpm/finish.svg
@@ -0,0 +1 @@
+<svg t="1730189225011" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2651" id="mx_n_1730189225011" width="200" height="200"><path d="M793.889347 200.380242c27.648573 20.615681 42.196018 32.710677 63.781037 56.119312 25.313864 27.453234 43.242957 48.52047 64.502857 86.507991 44.537416 79.580127 53.527718 136.949077 53.517684 212.063821 0 64.933675-15.452562 130.459388-40.138263 187.311893-22.076044 50.841799-61.545336 104.359483-101.886297 138.933914-45.506755 39.001681-81.214423 60.462941-137.605337 81.826531-55.699867 21.102023-114.070267 28.641326-181.379458 27.791064-68.274516-0.862973-129.364283-11.040029-180.533878-31.80489-46.159002-18.731189-98.338744-46.827973-141.596418-87.541551-43.946046-41.361142-70.369064-75.958317-93.88139-127.198155-26.157437-57.004361-40.094111-129.065922-39.680686-191.781288 0-36.980719 4.033895-70.902234 12.252873-105.241856 8.532726-35.651474 20.069131-69.572989 38.13135-102.35257 18.856956-34.221214 36.754607-62.067803 58.869452-88.973149 23.248751-28.285434 39.2104-46.417894 64.295476-63.475987 18.297696-12.442861 36.879036-9.295353 47.199252-2.306612 4.403836 2.982273 8.919391 6.577992 12.933218 12.933217 9.572307 15.156208-0.334486 29.769212-6.69038 38.465836-7.148625 9.781026-23.130343 26.023643-38.738775 43.218205-38.192895 42.075603-55.133918 65.965228-74.986303 106.965794-30.772668 63.552249-37.495827 115.718611-38.131349 166.573791-0.668971 53.517684 9.995096 99.647251 27.427813 140.483919 33.916163 80.572211 94.807915 144.44289 175.270414 178.615938 41.108271 17.845472 113.812713 37.319888 181.960793 38.13135 56.193568 0.668971 125.919751-11.321666 166.574459-28.096784 45.935566-18.954626 97.223569-56.862539 127.10383-94.324918 23.013273-28.852721 52.179742-70.910931 64.413884-105.694749 14.863868-42.260239 24.806784-87.661297 24.559934-132.458943 0-54.414105-11.53373-108.417461-36.918505-156.856317-20.16747-38.483228-46.480777-74.607665-84.66899-108.048189-13.377414-11.714352-23.822728-20.067124-38.808348-31.619586-10.191774-7.857065-36.059546-25.027545-28.923632-47.326356 4.970455-15.53217 18.303717-25.294464 31.887843-27.205046 19.456354-2.736092 28.565733 2.427027 43.705885 12.041479l6.179955 4.322891zM510.755379 531.65738c-8.696624-0.668971-10.034566-0.446204-20.738102-6.689711-11.031333-6.434832-17.839451-21.183637-16.514219-35.175166V92.220334c0-18.178619 0.386665-22.815926 8.988295-31.685813 5.351768-5.519011 10.963097-11.381873 26.08987-11.539751 16.055305-0.167243 21.407073 3.846584 27.929542 9.700081 9.70677 8.711341 10.703537 17.56049 10.377078 33.525483v397.5715c-0.509756 15.273947 0.326458 22.967114-11.380535 33.502739-3.884046 3.495374-8.027653 7.693167-20.96087 8.362138l-3.791059 0.000669z m4.453341 0.573308" p-id="2652" fill="#ffffff"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/parallel.svg b/src/assets/svgs/bpm/parallel.svg
new file mode 100644
index 0000000..ba0ac67
--- /dev/null
+++ b/src/assets/svgs/bpm/parallel.svg
@@ -0,0 +1 @@
+<svg t="1729585239190" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1755" width="200" height="200"><path d="M901.489435 536.822664v-0.931601l-1.001722-198.240726c-0.100172-19.162936-9.21584-37.474409-25.043042-50.246361-14.024104-11.349507-32.265456-17.60025-51.348255-17.610268l-618.062295-0.18031c-19.142902 0-37.424323 6.280795-51.478478 17.690405-15.827203 12.842072-24.902802 31.2437-24.892785 50.486775v196.798247A114.987635 114.987635 0 1 0 195.295664 536.922836V338.782282c1.15198-1.252152 4.808264-3.596181 10.768509-3.596181l276.725622 0.090155v199.753326a114.987635 114.987635 0 1 0 65.612772 1.412428V335.326342l275.693849 0.080138c6.01033 0 9.626546 2.344029 10.768508 3.596181l1.001722 195.70637a114.987635 114.987635 0 1 0 65.592737 2.113633zM214.979496 645.910158a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m354.689623 0a56.437001 56.437001 0 1 1-56.437001-56.437001 56.507122 56.507122 0 0 1 56.437001 56.437001z m295.507904 56.437001a56.437001 56.437001 0 1 1 56.437001-56.437001 56.507122 56.507122 0 0 1-56.457035 56.437001z" p-id="1756" fill='#fff'></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/reject.svg b/src/assets/svgs/bpm/reject.svg
new file mode 100644
index 0000000..21fd5f6
--- /dev/null
+++ b/src/assets/svgs/bpm/reject.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="1724316570161" class="icon" viewBox="0 0 1185 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1505" xmlns:xlink="http://www.w3.org/1999/xlink" width="231.4453125" height="200"><path d="M414.276535 230.004913l-2.443086-31.647244 26.446614 17.351559 29.437984-11.852598-8.143622 30.31685 20.423559 24.221229-31.623055 1.475527-16.722646 26.801386-11.239811-29.760504-30.663559-7.522772zM581.664252 176.902047l13.884472-28.542992 14.206993 28.220473 31.42148 4.321763-22.350614 22.092599 5.684409 31.123149-28.180157-14.513385-27.897953 14.819779 5.28126-31.066709-22.76989-21.689448zM896.507969 672.735748l17.754708 26.398236-31.494047-2.064126-19.560819 24.705008-7.95011-30.502299-29.575055-11.02211 26.744945-16.771024 1.104629-31.526299 24.414741 20.197795 30.268472-8.619338zM777.030551 801.961323l2.112504 31.647244-26.446614-17.682142-29.413795 11.546205 8.466141-30.308787-20.092976-24.221229 31.606929-1.153008 17.045166-26.793323 10.86085 29.704063 30.647433 7.837229zM609.312252 853.451591l-14.198929 28.518803-14.110236-28.542992-31.405355-4.636221 22.673134-22.084535-5.36189-31.12315 27.833449 14.835906 28.188221-14.803654-5.28126 31.066709 22.76989 22.060346zM298.435528 354.828094l-17.448315-26.390173 31.485984 2.394709 19.875275-24.753386 7.611465 30.865134 29.583118 11.288189-27.011024 16.779087-1.419086 31.526299-24.084158-20.504189-30.518425 8.280693zM962.56 91.53915a43.636913 43.636913 0 0 1 59.375874 15.601889l138.627024 236.753638c12.175118 20.447748 5.12 47.208819-15.609953 59.375874L229.13411 938.185575a43.636913 43.636913 0 0 1-59.375874-15.60189L31.12315 685.773606a43.636913 43.636913 0 0 1 15.601889-59.319433z m25.672567 24.108346a13.594205 13.594205 0 0 0-10.441575 1.548095L61.625449 652.054173a13.586142 13.586142 0 0 0-4.853921 18.83515l138.643149 236.793953a13.586142 13.586142 0 0 0 18.843213 4.837795l915.818834-534.915024a13.957039 13.957039 0 0 0 5.160315-18.778708l-138.602834-236.78589a13.594205 13.594205 0 0 0-8.401638-6.393953z" fill="#F5222D" p-id="1506"></path><path d="M395.981606 172.338394c123.670173-72.349228 271.11811-69.462677 388.394331-5.12l29.623433-17.335433a414.574866 414.574866 0 0 0-112.107842-47.071748 429.991307 429.991307 0 0 0-162.009701-10.498016 412.792945 412.792945 0 0 0-158.80063 54.707401 417.856504 417.856504 0 0 0-125.363402 111.922394A426.282331 426.282331 0 0 0 185.206929 405.004094a417.348535 417.348535 0 0 0-13.529701 120.977134l29.623433-17.335433c1.386835-133.958551 70.688252-263.958173 194.672882-336.307401z m397.666772 679.484472c-123.670173 72.365354-271.110047 69.462677-388.394331 5.128063l-29.623433 17.335433a414.679685 414.679685 0 0 0 112.075591 47.087874 429.991307 429.991307 0 0 0 162.009701 10.498016 412.744567 412.744567 0 0 0 158.808692-54.707402 423.145827 423.145827 0 0 0 209.105638-378.976756l-29.623433 17.335434c-1.072378 133.974677-70.712441 263.95011-194.350362 336.307401h-0.008063z" fill="#F5222D" p-id="1507"></path><path d="M478.377323 313.110173a226.271748 226.271748 0 0 1 109.979212-31.219905l45.668788-26.761071c-58.634079-9.13537-118.316346 2.314079-170.612914 32.735748a258.693039 258.693039 0 0 0-111.91433 132.71685l45.67685-26.761071a230.359685 230.359685 0 0 1 81.097575-80.589606l0.104819-0.120945z m232.568945 397.674835a226.328189 226.328189 0 0 1-109.979213 31.227968l-45.668787 26.753008c58.634079 9.13537 118.316346-2.314079 170.612913-32.735748a258.709165 258.709165 0 0 0 111.914331-132.71685l-45.676851 26.761071a225.215496 225.215496 0 0 1-81.097574 80.597669l-0.104819 0.112882zM188.57726 706.938961l-10.062614-17.424126 109.938897-63.471874 9.578835 16.585574 17.093543-9.869102-18.770645-32.509984-63.689575 36.767244c-4.047622-3.918614-7.804976-7.337323-11.272063-10.24l-16.859717 13.747401c3.249386 2.144756 6.595528 4.458835 9.869103 7.038993l-62.173733 35.896441 19.254426 33.348535 17.093543-9.869102zM317.44 781.142677l-19.060913-33.017953 32.679307-18.867401 4.741039 8.216189 17.093543-9.869103-48.474708-83.959937-49.772851 28.736504-7.933984-13.747401-17.432189 10.062614 7.933984 13.747402-49.264882 28.446236 48.764977 84.459842 17.093543-9.869102-5.031307-8.708032 32.171339-18.585196 19.060913 33.017952 17.432189-10.062614z m-12.505701-97.126803l-32.679307 18.867402-8.321008-14.41663 32.679307-18.867402 8.321008 14.41663z m-50.111496 28.930016l-32.171338 18.577134-8.321008-14.41663 32.171338-18.577134 8.321008 14.41663z m16.932284 29.325102l-32.171339 18.577134-8.127496-14.077984 32.171339-18.577134 8.127496 14.077984z m50.111496-28.930016l-32.679307 18.867402-8.127496-14.077984 32.679307-18.867402 8.127496 14.077984z m95.828661 7.684032c11.062425-6.38589 13.368441-15.537386 6.692284-27.099717l-25.05978-43.411149c3.55578-4.289512 7.014803-8.740283 10.48189-13.199118l-9.482079-16.424315c-3.467087 4.458835-6.92611 8.917669-10.48189 13.199118l-17.803086-30.832882 14.755275-8.51452-9.780409-16.932283-14.747213 8.522582-16.738771-28.994519-17.093544 9.869102 16.738772 28.99452-16.924221 9.772346 9.772347 16.924221 16.932283-9.772347 20.891213 36.202835a299.927181 299.927181 0 0 1-16.690394 15.214866l13.868347 14.344063a572.617575 572.617575 0 0 0 12.497638-12.804031l19.157669 33.179212c2.322142 4.031496 1.475528 7.200252-2.20926 9.328882-3.85411 2.225386-8.167811 4.039559-12.578268 5.692472l13.55389 14.964914 14.247307-8.224252z m111.390236-65.205417c6.369764-3.676724 10.15937-8.329071 11.151118-13.586142 1.225575-5.619906-3.201008-18.706142-13.182992-39.089386l-18.827086 4.160504c7.627591 14.368252 11.368819 23.164976 11.570393 26.615937 0.112882 3.289701-0.959496 5.692472-3.467086 7.143811l-6.539087 3.77348c-3.354205 1.935118-6.095622 1.064315-8.224252-2.628535l-38.702362-67.027654c8.933795-10.07874 17.762772-21.874898 26.390173-35.573921l-18.383622-8.603213a168.443969 168.443969 0 0 1-17.972409 26.914268l-26.801386-46.426709-17.254803 9.965859 77.686929 134.571338c6.966425 12.070299 16.077606 15.077795 27.478677 8.498394l15.077795-8.708031z m-78.501291 45.547842c13.626457-12.779843 25.285543-25.100094 34.783748-37.291339l-12.473449-14.247307a157.808882 157.808882 0 0 1-14.706897 17.875654l-38.412095-66.535811 20.617071-11.900976-9.869102-17.093544-20.617071 11.900977-27.18841-47.087874-17.254803 9.965858 72.94589 126.363212c2.999433 5.192567 2.418898 9.99811-1.564221 14.311811l13.739339 13.739339z m201.663496-113.978457l-65.21348-112.946393c0.137071-7.901732-0.16126-15.771213-0.886929-23.624567l53.78822-31.050583-9.869102-17.093543-144.795213 83.597102 9.869102 17.093543 71.05915-41.024504c1.894803 37.331654-9.45789 76.517795-33.848441 117.856756l20.367118 8.570961c14.860094-26.898142 25.05978-53.344756 30.445859-79.243087l50.990362 88.313953 18.093354-10.449638z m28.728441-76.017889l5.716661-21.850709c-21.157291-7.224441-45.330142-12.707276-72.349228-16.54526l-5.603779 19.318929c29.163843 4.837795 53.385071 11.191433 72.244409 19.07704z m18.738394-105.33493l5.265134-19.13348c-12.739528-4.25726-27.414173-7.627591-43.612725-10.127118l-5.410268 18.101417c17.674079 2.74948 32.380976 6.555213 43.757859 11.159181z m88.934803 67.74526l-15.76315-27.317417 21.786205-12.578268 15.674457 27.148095 16.085669-9.288567-15.674457-27.148095 22.455433-12.965291 4.063748 7.038992c2.031874 3.523528 1.249764 6.426205-2.435023 8.554835l-11.852599 6.176252 12.175118 12.183181 12.570205-7.256693c11.393008-6.579402 13.997354-15.230992 7.998488-25.616126l-42.862866-74.244032-33.848441 19.544693-0.532157-0.145133a202.445606 202.445606 0 0 0 18.738393-38.750741L790.173228 306.87748l-92.676031 53.506016 8.321008 14.41663 31.679496-18.286866-3.85411 13.836094c8.401638-0.16126 16.125984 0.08063 23.261732 0.427339l-37.202646 21.479811 52.538457 90.998929 16.424315-9.482079z m-25.35811-117.856756c-6.724535-0.806299-14.126362-1.233638-21.947465-1.628724l33.517858-19.351181c-3.305827 7.047055-7.143811 13.948976-11.570393 20.979905z m47.571653 16.996788l-22.455433 12.965291-6.095622-10.56252 22.455433-12.965291 6.095622 10.56252z m-38.541102 22.253858l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m-24.253481 137.570772c-0.330583-19.915591 1.112693-30.582929 4.458835-32.518048 1.846425-1.064315 4.628157-0.886929 8.627402 0.604725 8.304882 2.797858 16.400126 3.265512 24.269606 1.402961 8.006551-2.386646 17.464441-6.506835 28.462362-12.626646 10.812472-6.031118 20.96378-11.66715 30.187843-16.988725l38.379842-22.157102-5.781165-19.673701c-4.329827 2.942992-10.675402 7.055118-19.028662 12.320252-8.708031 5.031307-16.996787 10.038425-25.374236 14.876221-13.07011 7.546961-24.398614 13.868346-34.211275 19.302803-10.07874 5.378016-18.230425 8.296819-24.543748 8.587087-5.28126 0.145134-11.070488-0.983685-17.440252-3.120378l-2.902678-0.774048-36.767244-63.681511-38.379842 22.157102 9.288567 16.085669 22.116787-12.771779 26.511118 45.91874c-4.571717 7.555024-7.014803 20.359055-7.651779 38.605606l19.778519 4.450772z m38.476599-112.938331l-21.786205 12.578268-6.095622-10.56252 21.786205-12.578268 6.095622 10.56252z m38.541102-22.253858l-22.455433 12.965291-6.095622-10.56252 22.455433-12.957228 6.095622 10.56252z m172.241638-43.798173c12.062236-6.966425 14.610142-16.488819 7.740472-28.381733l-39.863433-69.051464 23.302048-13.449071-9.869103-17.093543-23.302047 13.44907-14.513386-25.132346-17.424126 10.062614 14.513386 25.132347-62.681701 36.186708 9.869103 17.093544 62.6817-36.186709 37.34778 64.689386c2.515654 4.354016 1.523906 8.062992-2.838173 10.578645-6.692283 3.870236-14.190866 7.522772-21.955528 11.110804l13.529701 14.537574 23.463307-13.545826z m-130.942992-43.725607l5.386079-20.092976c-12.900787-4.168567-27.389984-7.200252-43.65304-9.433701l-5.321575 18.27074c17.682142 2.74948 32.219717 6.643906 43.596599 11.255937z m80.702488 27.148095l8.466142-17.851465c-10.756031-5.853732-24.825953-12.038047-41.846929-18.302992l-8.740284 16.22274c16.883906 6.789039 30.808693 13.497449 42.121071 19.931717z m-31.219905 99.577952c-0.354772-20.350992 1.064315-31.445669 4.418519-33.380787 2.007685-1.161071 5.128063-1.177197 9.119244 0.32252a42.951559 42.951559 0 0 0 24.938835 1.007874c8.175874-2.483402 18.141732-6.893858 29.639559-13.303937 11.320441-6.321386 21.810394-12.150929 31.365039-17.657953l35.525544-20.520315-5.966614-20.012346c-3.999244 2.74948-10.006173 6.668094-17.859528 11.651023-7.95011 4.805543-15.722835 9.522394-23.439118 13.973166a2406.72252 2406.72252 0 0 1-35.719055 20.181669c-10.586709 5.66022-19.165732 8.603213-25.712882 9.256315-5.28126 0.145134-11.401071-0.790173-17.940158-2.822047l-3.080063-0.685355-36.767244-63.681512-39.041008 22.544126 9.482079 16.424315 22.455433-12.965291 26.511118 45.91874c-4.57978 7.555024-7.256693 20.72189-7.700157 39.299024l19.770457 4.450771z" fill="#F5222D" p-id="1508"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/running.svg b/src/assets/svgs/bpm/running.svg
new file mode 100644
index 0000000..5908c13
--- /dev/null
+++ b/src/assets/svgs/bpm/running.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="1724304256588" class="icon" viewBox="0 0 1300 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1272" xmlns:xlink="http://www.w3.org/1999/xlink" width="253.90625" height="200"><path d="M784.058182 99.258182l10.938182 18.385454-21.294546-2.56-14.196363 16.058182-4.072728-21.061818-19.781818-8.494545 18.734546-10.472728 2.094545-21.294545 15.709091 14.545454 20.945454-4.654545-9.076363 19.549091zM1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96z" fill="#2196F3" p-id="1273"></path><path d="M1067.287273 642.443636l-18.501818 10.821819 2.56-21.294546-16.058182-14.196364 21.061818-4.072727 8.494545-19.665454 10.472728 18.734545 21.294545 1.978182-14.661818 15.709091 4.770909 20.945454-19.432727-8.96zM571.927273 100.072727l-17.454546-12.567272 20.596364-6.167273 6.516364-20.48 12.218181 17.570909 21.410909-0.116364-12.916363 17.105455 6.749091 20.363636-20.247273-6.981818-17.338182 12.683636 0.465455-21.410909zM991.418182 784.407273l-21.178182 3.490909 10.123636-18.967273-9.774545-18.967273 21.061818 3.723637 15.127273-15.243637 2.909091 21.294546 19.2 9.658182-19.316364 9.309091-3.258182 21.178181-14.894545-15.476363zM427.985455 156.741818L407.272727 151.505455l16.872728-13.265455-1.396364-21.410909 17.803636 11.985454 20.014546-7.912727-5.934546 20.596364 13.730909 16.523636-21.410909 0.814546-11.52 18.152727-7.447272-20.247273zM854.225455 896.465455l-20.712728-5.352728 16.872728-13.265454-1.396364-21.294546 17.803636 11.869091 20.014546-7.912727-5.934546 20.712727 13.730909 16.523637-21.527272 0.814545-11.403637 18.036364-7.447272-20.130909zM562.501818 923.694545l10.821818 18.385455-21.294545-2.56-14.196364 16.058182-4.072727-21.061818-19.665455-8.494546 18.734546-10.356363 1.978182-21.41091 15.709091 14.661819 20.945454-4.770909-8.96 19.54909zM242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364z" fill="#2196F3" p-id="1274"></path><path d="M242.734545 420.770909l-18.385454 10.938182 2.56-21.294546-16.058182-14.196363 21.061818-4.189091 8.494546-19.665455 10.356363 18.734546 21.410909 2.094545-14.545454 15.709091 4.654545 20.945455-19.549091-9.076364zM700.858182 943.941818l-17.454546-12.450909 20.48-6.283636 6.516364-20.48 12.334545 17.687272 21.41091-0.116363-12.916364 17.105454 6.632727 20.363637-20.247273-7.098182-17.221818 12.683636 0.465455-21.410909zM303.592727 278.807273l-21.178182 3.490909 10.123637-18.967273-9.890909-18.967273 21.178182 3.723637 15.010909-15.243637 2.909091 21.294546 19.2 9.541818-19.316364 9.425455-3.258182 21.178181-14.778182-15.476363z" fill="#2196F3" p-id="1275"></path><path d="M407.272727 90.647273a486.632727 486.632727 0 0 1 504.552728 11.636363l25.018181-14.429091A512 512 0 0 0 139.636364 546.909091l25.018181-14.429091A486.981818 486.981818 0 0 1 407.272727 90.647273zM893.323636 933.352727a486.749091 486.749091 0 0 1-504.669091-11.636363l-24.901818 14.429091A512 512 0 0 0 1161.192727 477.090909l-24.901818 13.963636a486.981818 486.981818 0 0 1-242.967273 442.298182z" fill="#2196F3" p-id="1276"></path><path d="M814.545455 795.927273a327.447273 327.447273 0 0 1-258.21091 29.556363l-29.78909 17.105455A353.163636 353.163636 0 0 0 998.865455 570.181818l-29.789091 17.105455A326.865455 326.865455 0 0 1 814.545455 795.927273zM486.865455 228.072727A327.447273 327.447273 0 0 1 744.727273 198.516364l29.789091-17.105455A353.163636 353.163636 0 0 0 302.545455 453.818182l29.78909-17.105455A326.865455 326.865455 0 0 1 486.865455 228.072727zM1288.378182 374.690909a53.294545 53.294545 0 0 1-14.429091 11.636364L229.469091 989.090909a53.876364 53.876364 0 0 1-73.425455-19.665454L7.214545 710.632727a53.527273 53.527273 0 0 1 19.781819-73.309091L1071.476364 34.909091a53.876364 53.876364 0 0 1 73.425454 19.665454l148.829091 258.327273a53.061818 53.061818 0 0 1 5.352727 40.727273 55.272727 55.272727 0 0 1-10.705454 21.061818zM32.232727 665.716364A28.043636 28.043636 0 0 0 29.323636 698.181818l148.829091 257.978182a28.392727 28.392727 0 0 0 38.516364 10.356364l1044.48-601.949091a28.16 28.16 0 0 0 10.356364-38.516364L1122.676364 67.84a28.276364 28.276364 0 0 0-38.4-10.356364L39.68 659.432727a27.810909 27.810909 0 0 0-7.447273 6.283637z" fill="#2196F3" p-id="1277"></path><path d="M477.090909 500.945455l22.109091 38.283636-15.36 8.843636-13.963636-24.436363-151.272728 87.621818 14.545455 25.134545-15.243636 8.843637-23.272728-39.330909L377.949091 558.545455c-6.050909-4.887273-11.636364-8.843636-15.825455-11.636364l14.894546-12.450909c3.956364 3.141818 9.658182 8.145455 17.105454 14.894545zM459.869091 698.181818l-48.407273 28.043637 7.447273 12.334545-15.36 8.843636-61.207273-106.007272L406.225455 605.090909l-12.683637-21.876364 15.709091-9.076363 12.683636 21.876363L486.4 558.545455l60.509091 104.727272-15.36 8.843637-7.098182-12.218182-49.105454 28.392727L501.294545 733.090909l-15.70909 9.076364z m-45.381818-78.661818l-48.523637 27.461818 14.545455 25.134546 48.523636-28.043637zM388.538182 686.545455l14.545454 25.134545 48.523637-28.043636-14.545455-25.134546z m105.425454-79.476364L479.418182 581.818182 430.545455 609.861818l14.545454 25.134546z m-26.647272 67.490909l49.221818-28.392727-14.545455-25.134546-49.105454 28.392728zM624.058182 541.090909c-4.654545 6.167273-10.123636 13.149091-16.290909 20.829091l34.909091 61.207273a18.734545 18.734545 0 0 1-6.632728 29.207272l-18.734545 10.938182-11.636364-13.614545a174.545455 174.545455 0 0 0 17.454546-8.610909 8.378182 8.378182 0 0 0 2.327272-12.683637l-30.021818-52.363636-9.774545 10.24-9.890909 10.123636-12.450909-12.916363c9.076364-8.145455 16.872727-15.709091 23.272727-22.574546l-30.836364-53.527272-24.785454 14.196363-8.727273-15.010909L546.909091 492.218182l-23.272727-40.378182 15.36-8.843636 23.272727 40.378181 21.643636-12.450909 8.727273 15.127273-21.643636 12.450909L599.156364 546.909091c5.352727-6.4 10.821818-13.381818 16.290909-20.712727z m8.843636-45.032727L689.221818 593.454545a193.745455 193.745455 0 0 0 22.574546-27.112727l11.636363 13.032727a363.985455 363.985455 0 0 1-41.192727 44.8l-12.334545-12.450909a10.821818 10.821818 0 0 0 1.62909-13.730909l-98.90909-171.403636 15.476363-8.96 36.305455 62.952727 30.836363-17.803636 8.029091 15.476363z m128 81.454545a20.130909 20.130909 0 0 1-30.836363-9.541818L628.363636 392.378182l15.36-8.378182 38.050909 66.094545A206.08 206.08 0 0 0 709.818182 409.018182l16.64 7.563636a297.890909 297.890909 0 0 1-34.909091 48.64l52.712727 91.112727a8.843636 8.843636 0 0 0 13.614546 4.072728l10.821818-6.167273a14.429091 14.429091 0 0 0 7.912727-11.636364 102.981818 102.981818 0 0 0-15.010909-38.516363l17.105455-3.723637c12.334545 24.669091 17.687273 41.076364 16.058181 48.989091a28.16 28.16 0 0 1-15.127272 18.152728zM805.236364 288.116364l16.174545-9.309091 23.272727 39.330909 78.429091-45.265455 59.345455 102.749091-16.64 9.076364-7.912727-13.847273L896 407.272727l42.938182 74.472728-16.174546 9.30909-42.938181-74.472727-62.603637 36.072727 8.029091 13.73091-15.825454 9.192727L749.730909 372.363636l78.196364-45.265454z m2.676363 149.061818l62.603637-36.072727-33.745455-58.181819-62.487273 36.072728z m78.778182-45.381818l62.72-36.189091-33.745454-58.181818-62.72 36.072727z" fill="#2196F3" p-id="1278"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/simple-process-bg.svg b/src/assets/svgs/bpm/simple-process-bg.svg
new file mode 100644
index 0000000..eb23ab5
--- /dev/null
+++ b/src/assets/svgs/bpm/simple-process-bg.svg
@@ -0,0 +1 @@
+<svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/starter.svg b/src/assets/svgs/bpm/starter.svg
new file mode 100644
index 0000000..c12c712
--- /dev/null
+++ b/src/assets/svgs/bpm/starter.svg
@@ -0,0 +1 @@
+<svg t="1729561814171" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1359" width="200" height="200"><path d="M674.496 603.456c120.256 0 218.176 90.752 221.44 203.84l0.064 5.888v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352a21.568 21.568 0 0 1-22.144-20.928v-125.888c0-67.712-56.512-123.264-128-125.76l-4.928-0.064H349.568c-71.488 0-130.176 53.504-132.864 121.152l-0.064 4.672v125.888c0 11.52-9.92 20.928-22.144 20.928h-44.352A21.568 21.568 0 0 1 128 939.072v-125.888c0-113.92 95.872-206.528 215.36-209.664l6.208-0.064h324.928zM497.216 128c122.368 0 221.568 93.888 221.568 209.728s-99.2 209.792-221.568 209.792c-122.304 0-221.44-93.952-221.44-209.728C275.712 221.952 374.848 128 497.152 128z m0 83.904c-73.408 0-132.864 56.32-132.864 125.888 0 69.504 59.52 125.824 132.864 125.824 73.408 0 132.928-56.32 132.928-125.824 0-69.504-59.52-125.888-132.928-125.888z" fill="#fff" p-id="1360"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/bpm/transactor.svg b/src/assets/svgs/bpm/transactor.svg
new file mode 100644
index 0000000..a9547a7
--- /dev/null
+++ b/src/assets/svgs/bpm/transactor.svg
@@ -0,0 +1 @@
+<svg t="1739406626368" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1300" width="200" height="200"><path d="M803.221 925.573H224.356c-68.568 0-124.352-55.784-124.352-124.353V222.356c0-68.568 55.784-124.352 124.352-124.352h355.311v64H224.356c-33.278 0-60.352 27.074-60.352 60.352V801.22c0 33.278 27.074 60.353 60.352 60.353H803.22c33.278 0 60.353-27.074 60.353-60.353V448.208h64V801.22c0 68.569-55.784 124.353-124.352 124.353z" fill="#ffffff" p-id="1301"></path><path d="M300.357 756.916l35.024-195.867L770.117 84.404c10.05-11.02 25.015-18.052 41.058-19.293 16.017-1.247 31.987 3.379 43.841 12.667l83.662 65.549c21.643 16.956 24.254 45.964 5.942 66.038l-437.613 479.8-206.65 67.751z m104.994-170.751l-13.14 73.487 69.671-22.842 415.465-455.517-59.909-46.939-412.087 451.811z" fill="#ffffff" p-id="1302"></path><path d="M732.25 220.897l41.144-49.023 81.151 68.11-41.145 49.023z" fill="#ffffff" p-id="1303"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/icon.svg b/src/assets/svgs/icon.svg
new file mode 100644
index 0000000..7024bec
--- /dev/null
+++ b/src/assets/svgs/icon.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 014.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 012.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/iot/card-fill.svg b/src/assets/svgs/iot/card-fill.svg
new file mode 100644
index 0000000..4c74ecd
--- /dev/null
+++ b/src/assets/svgs/iot/card-fill.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" class="design-iconfont" viewBox="0 0 12 12"><path fill="url(#a)" fill-rule="evenodd" d="M1 0a1 1 0 0 0-1 1v3.538a1 1 0 0 0 1 1h3.538a1 1 0 0 0 1-1V1a1 1 0 0 0-1-1H1Zm0 6.462a1 1 0 0 0-1 1V11a1 1 0 0 0 1 1h3.538a1 1 0 0 0 1-1V7.462a1 1 0 0 0-1-1H1ZM6.462 1a1 1 0 0 1 1-1H11a1 1 0 0 1 1 1v3.538a1 1 0 0 1-1 1H7.462a1 1 0 0 1-1-1V1Zm1 5.462a1 1 0 0 0-1 1V11a1 1 0 0 0 1 1H11a1 1 0 0 0 1-1V7.462a1 1 0 0 0-1-1H7.462Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="0" x2="12" y1="0" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient></defs></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/iot/cube.svg b/src/assets/svgs/iot/cube.svg
new file mode 100644
index 0000000..200ac1b
--- /dev/null
+++ b/src/assets/svgs/iot/cube.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="none" viewBox="0 0 12 12"><g clip-path="url(#a)"><path fill="url(#b)" fill-rule="evenodd" d="M6.958.42C6.444.216 5.61.216 5.098.42L1.15 1.975c-.77.304-.77.797 0 1.1l3.947 1.558c.514.202 1.347.202 1.86 0l3.948-1.557c.77-.304.77-.797 0-1.1L6.958.418ZM4.715 11.788a.857.857 0 0 0 .3.056c.383 0 .671-.295.671-.7V6.404c0-.49-.364-1.007-.817-1.177L1.09 3.805a.808.808 0 0 0-.284-.056c-.353 0-.581.275-.581.7V9.19c0 .508.33 1.014.763 1.177l3.726 1.422Zm2.229-.024h-.02l.073.003c.074.004.154.009.227-.019L11 10.367c.45-.168.83-.686.83-1.177V4.45c0-.413-.29-.7-.673-.7a.965.965 0 0 0-.317.055l-3.72 1.422c-.44.165-.75.67-.75 1.177v4.74c0 .42.218.621.575.621Z" clip-rule="evenodd"/></g><defs><linearGradient id="b" x1=".226" x2="11.803" y1=".267" y2="11.871" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3149"/><stop offset="1" stop-color="#717D8A"/></linearGradient><clipPath id="a"><path fill="#fff" d="M0 0h12v12H0z"/></clipPath></defs></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/login-bg.svg b/src/assets/svgs/login-bg.svg
new file mode 100644
index 0000000..bbe06c1
--- /dev/null
+++ b/src/assets/svgs/login-bg.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="5760" height="3040"><image width="5760" height="3040" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAFoAAAAvgAQMAAAC1QKagAAAABGdBTUEAALGPC/xhBQAAACBjSFJN AAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEUsNEr///91v/yPAAAA AWJLR0QB/wIt3gAAAAd0SU1FB+YBBQYyN1c3BnEAAAhjSURBVHja7cExAQAAAMKg9U9tDB+gAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAACAtwFzzwABY3VrRQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMi0wMS0wNVQwNjo1 MDo1MyswMDowMCfNlVoAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjItMDEtMDVUMDY6NTA6NTQrMDA6 MDCTNxNoAAAAAElFTkSuQmCC"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/login-box-bg.svg b/src/assets/svgs/login-box-bg.svg
new file mode 100644
index 0000000..ab10040
--- /dev/null
+++ b/src/assets/svgs/login-box-bg.svg
@@ -0,0 +1 @@
+<svg version="1.1" id="鍥惧眰_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 700 700" xml:space="preserve" enable-background="new 0 0 700 700"><style>.st0{fill:#e5e6eb}.st1{fill:#fff}.st2{fill:#84a9ff}.st3{fill:#050f64}.st4{fill:#155bcd}.st5{fill:#ffbd00}.st6{fill:#ff654f}.st9{fill:#f5bdc8}.st10{fill:#ea8096}.st11{opacity:0}.st13{fill:#dca000}</style><path class="st0" d="M101.8 176.7c21.4-19.8 48.8-33.2 77.8-37.2 92.4-12.6 158.2 78.1 240.3 104.9 40.8 13.3 85.4 12.6 125.4 28 68.5 26.2 131.4 117.8 101 191.6-23.7 57.5-79.6 71.8-134.6 54-33.5-10.9-64.1-29.4-97.6-40.5-38.1-12.6-78.7-15.1-118.9-16.7s-80.6-2.4-119.6-12-77-28.9-101.2-60.9C40.8 343.4 48 260.8 73.1 213.7c7.4-13.9 17.2-26.3 28.7-37z"/><path class="st1" d="M82 257.1c5.7-23.2 18.9-44.7 37.3-60.4l1.7-1.5 1.8-1.4 1.8-1.4 1.8-1.3c.6-.4 1.2-.9 1.8-1.3l1.9-1.3c.6-.4 1.2-.9 1.9-1.3l1.9-1.2c5.1-3.2 10.5-6 16.1-8.4 11.1-4.7 23-7.8 35.1-9 12.1-1.1 24.3-.5 36.1 1.5 5.9 1 11.8 2.4 17.6 4 .7.2 1.5.4 2.2.6l2.2.7 2.2.7 2.1.7 2.1.7 2.1.8 2.1.8 2.1.8c5.6 2.2 11.1 4.6 16.5 7.2 5.4 2.6 10.7 5.4 15.9 8.3 10.4 5.9 20.6 12.2 30.5 18.8-10.4-5.9-20.7-11.8-31.4-17.2-5.3-2.7-10.7-5.3-16.1-7.7-5.4-2.4-10.9-4.7-16.5-6.7l-2.1-.8-2.1-.7-2.1-.7-2.1-.7-2.1-.7-2.1-.6-2.1-.6-2.1-.6c-5.7-1.5-11.5-2.8-17.3-3.7-11.6-1.9-23.5-2.5-35.2-1.3-11.7 1.1-23.2 4-34.1 8.5-5.4 2.2-10.7 4.9-15.8 7.9l-1.9 1.1c-.6.4-1.2.8-1.9 1.2l-1.8 1.2c-.6.4-1.2.8-1.8 1.3l-1.8 1.3-1.8 1.3-1.8 1.3-1.7 1.4c-18.2 15.2-32 35.7-39.1 58.4z"/><path class="st2" d="M183.1 543.2c-.3 1.2-.5 1.8-.5 1.8-.7-.5-1.4-.9-2.1-1.4-120.8-82.8-72.6-232.2-72.6-232.2 115.7 67.3 80.1 213.8 75.2 231.8z"/><path class="st3" d="M183.1 543.2c-.3 1.2-.5 1.8-.5 1.8-.7-.5-1.4-.9-2.1-1.4-10.1-29.9-20.1-59.8-29.8-89.8-5-15.5-10-31.1-14.8-46.7l-3.6-11.7-3.5-11.7c-1.2-3.9-2.2-7.8-3.4-11.8-.6-2-1.1-3.9-1.6-5.9l-1.6-5.9 1.6 5.9c.5 2 1.1 3.9 1.7 5.9 1.2 3.9 2.3 7.8 3.5 11.7l3.6 11.7 3.7 11.7c5 15.5 10.2 31 15.4 46.5 10.4 30 20.8 59.9 31.4 89.7zM137.9 384.9c-.1 0-.2 0-.4-.1-.3-.1-.4-.5-.2-.8 3.7-7.2 6-15.3 6.7-23.4 0-.3.3-.5.6-.5s.5.3.5.6c-.7 8.2-3.1 16.5-6.9 23.8 0 .3-.2.4-.3.4zM154 430.5h-.3c-.3-.1-.4-.5-.3-.7 3.4-8.3 7.6-16.4 12.3-24.1.2-.3.5-.3.8-.2.3.2.3.5.2.8-4.7 7.6-8.8 15.6-12.2 23.9-.1.1-.3.3-.5.3zM137.4 440.3h-.3c-9.5-3.9-18.3-9.3-26.1-16.1-.2-.2-.3-.6-.1-.8.2-.2.6-.3.8-.1 7.7 6.7 16.3 12 25.7 15.9.3.1.4.5.3.7 0 .2-.1.3-.3.4zM125.9 390.5c-.2.1-.4.1-.6-.1l-19.2-15c-.2-.2-.3-.6-.1-.8.2-.2.6-.3.8-.1l19.2 15c.2.2.3.6.1.8 0 .1-.1.2-.2.2zM170.7 478.4h-.3c-.3-.1-.4-.5-.3-.7l10.1-23.5c.1-.3.5-.4.7-.3.3.1.4.5.3.7l-10.1 23.5c0 .1-.2.3-.4.3zM151.6 481.6h-.3l-24.3-10c-.3-.1-.4-.5-.3-.7.1-.3.5-.4.7-.3l24.3 10c.3.1.4.5.3.7-.1.1-.3.3-.4.3z"/><path class="st4" d="M182.3 543.2c.3 1.2.4 1.9.4 1.9-.8-.1-1.7-.2-2.5-.3C35 525 11 369.8 11 369.8c133.5 8.2 167.5 155.1 171.3 173.4z"/><path class="st1" d="M182.3 543.2c.3 1.2.4 1.9.4 1.9-.8-.1-1.7-.2-2.5-.3-22.5-22.1-44.8-44.4-66.9-66.8-11.5-11.6-22.9-23.3-34.2-35.1l-8.5-8.8-8.4-8.9c-2.8-3-5.5-6-8.3-9-1.4-1.5-2.7-3-4.1-4.6l-4-4.6 4.1 4.5c1.4 1.5 2.7 3 4.1 4.5 2.8 3 5.6 6 8.4 8.9l8.5 8.8 8.6 8.7c11.5 11.6 23 23.1 34.7 34.6 22.5 22.2 45.2 44.3 68.1 66.2zM70.7 422.1c-.1.1-.2.1-.3.1-.3 0-.6-.3-.6-.6.1-8.1-1.5-16.4-4.5-23.9-.1-.3 0-.6.3-.7.3-.1.6 0 .7.3 3 7.7 4.6 16.1 4.6 24.4 0 .1-.1.3-.2.4zM105.6 455.5c-.1.1-.2.1-.3.1-.3 0-.6-.2-.6-.5-.7-9-.6-18.1.2-27 0-.3.3-.5.6-.5s.5.3.5.6c-.8 8.9-.9 17.9-.2 26.8.1.2 0 .4-.2.5zM95.2 471.7c-.1.1-.2.1-.3.1-10.3.8-20.5-.1-30.5-2.7-.3-.1-.5-.4-.4-.7.1-.3.4-.5.7-.4 9.9 2.5 20 3.4 30.1 2.6.3 0 .6.2.6.5 0 .4-.1.5-.2.6zM62.6 432.4c-.1.1-.3.2-.5.2l-23.9-4.8c-.3-.1-.5-.4-.4-.7.1-.3.4-.5.7-.4l23.9 4.8c.3.1.5.4.4.7-.1.1-.1.2-.2.2zM142.1 490.8c-.1.1-.2.1-.3.1-.3 0-.6-.2-.6-.5l-1.5-25.5c0-.3.2-.6.5-.6s.6.2.6.5l1.5 25.5c0 .2-.1.4-.2.5zM126.4 502.3c-.1.1-.2.1-.3.1l-26.2 2c-.3 0-.6-.2-.6-.5s.2-.6.5-.6l26.2-2c.3 0 .6.2.6.5 0 .2-.1.4-.2.5z"/><g><path class="st5" d="M259.6 503.3c1.2.5 1.8.7 1.8.7-.5.7-1.1 1.3-1.7 1.9C164 616.8 20.9 552.3 20.9 552.3c79.7-107.4 221.4-55.9 238.7-49z"/><path class="st1" d="M259.6 503.3c1.2.5 1.8.7 1.8.7-.5.7-1.1 1.3-1.7 1.9-30.8 6.8-61.6 13.3-92.5 19.7-16 3.3-32 6.5-48 9.6l-12 2.3-12 2.2c-4 .7-8 1.4-12.1 2-2 .4-4 .6-6 .9l-6.1.9 6-1c2-.3 4-.6 6-1 4-.7 8-1.4 12-2.2l12-2.3 12-2.4c16-3.3 31.9-6.7 47.9-10.2 31-6.9 61.9-13.9 92.7-21.1zM97.3 530.8c0 .1 0 .2-.1.3-.2.3-.5.3-.8.2-6.8-4.5-14.6-7.7-22.5-9.3-.3-.1-.5-.4-.4-.7.1-.3.4-.5.7-.4 8.1 1.6 16 4.9 22.9 9.5.1 0 .1.2.2.4zM144.3 519.7c0 .1 0 .2-.1.3-.2.3-.5.4-.8.2-7.9-4.3-15.5-9.4-22.5-14.9-.2-.2-.3-.6-.1-.8.2-.2.6-.3.8-.1 7 5.5 14.6 10.5 22.4 14.8.2.1.3.3.3.5zM152.2 537.3c0 .1 0 .2-.1.3-4.9 9-11.3 17.2-18.8 24.1-.2.2-.6.2-.8 0-.2-.2-.2-.6 0-.8 7.5-6.9 13.7-14.9 18.6-23.8.2-.3.5-.4.8-.2.2.1.3.3.3.4zM101.5 543.2c.1.2 0 .4-.1.6l-17 17.5c-.2.2-.6.2-.8 0-.2-.2-.2-.6 0-.8l17-17.5c.2-.2.6-.2.8 0 .1 0 .1.1.1.2zM193.8 508.4c0 .1 0 .2-.1.3-.2.3-.5.4-.8.2l-22.2-12.7c-.3-.2-.4-.5-.2-.8.2-.3.5-.4.8-.2l22.2 12.7c.2.2.3.3.3.5zM194.9 527.8c0 .1 0 .2-.1.3l-12.7 23.1c-.2.3-.5.4-.8.2-.3-.2-.4-.5-.2-.8l12.7-23.1c.2-.3.5-.4.8-.2.1.2.3.3.3.5z"/></g><g><path class="st2" d="M608.8 430.3c-1 .2-2.4-.3-4.4-1.4-3.2-1.9-8.3-4.9-10.2-6.1 3 6.3 5.8 12.7 8.3 19.2 4.5-1 7.9-.1 10.1 1.4 2.2 1.5 3.3 3.6 3.3 4.6-.1 2-1.8 2.4-4.9.3-1.6-1.1-3.7-2.6-5.5-3.9-1.3-.9-2.3-1.7-2.8-2 .8 2 1.5 4 2.2 6h.2c1.3.2 3.1 3.1 3.9 4.1 1.7 2.3 3 4.9 3.2 7.8.1 1.2-.1 2.6-1.2 3.2-1.2.6-2.6-.3-3.5-1.3-2.5-2.8-4-6.5-4.1-10.2 0-1-.1-3.3 1.2-3.5-.8-2-1.5-3.9-2.3-5.9-.1.6-.4 1.9-.7 3.4-.5 2.1-1.1 4.7-1.7 6.4-1.1 3.5-2.7 4.1-4 2.8-.7-.7-1.1-2.7-.3-5.2.8-2.4 2.6-5.3 6.6-7.7-2.7-6.4-5.6-12.7-8.8-18.9-.1.8-.3 2.2-.5 3.7-.3 2.6-.9 5.7-1.4 7.8-.5 2.1-1.2 3.4-2 4-.8.6-1.7.4-2.5-.3-.9-.7-1.6-3.1-.9-6.2.6-2.9 2.6-6.5 7-9.6-3.5-6.6-7.2-13.1-11.2-19.4v.3c0 1 0 2.5-.1 4.1-.1 1.6-.2 3.4-.3 5-.1 1.7-.4 3.3-.5 4.6-.8 5.3-3 6.6-5.2 5-1.2-.8-2.1-3.7-1.7-7.4.2-1.9.9-4 2.2-6.2 1.1-2 2.8-4.2 5.2-6.3-3.8-5.8-7.8-11.5-12-17 .1 1.2.2 2.8.2 4.6.1 1.8.1 3.9.1 5.8v2.8c0 .9-.1 1.8-.1 2.5-.4 6.1-2.8 7.8-5.5 6.2-.7-.4-1.4-1.4-1.9-2.8s-.8-3.3-.7-5.4c.1-2.2.7-4.6 1.9-7.3 1.1-2.4 2.8-5 5.2-7.6-4.2-5.4-8.5-10.5-13.1-15.5l2-1.8c4.5 5.2 8.8 10.5 12.9 16 3.1-1.6 6.1-2.5 8.8-2.7 3-.3 5.6.1 7.8.9s4 1.9 5.3 3.1c1.2 1.2 2 2.4 2.2 3.3.7 3.5-2 4.7-8 2.5-3.1-1.2-7.3-2.8-10.7-4.2-1.7-.6-3.3-1.2-4.4-1.6 4.1 5.6 8 11.5 11.6 17.4 2.9-1.2 5.6-1.7 8-1.8 2.6 0 4.8.5 6.7 1.4 3.8 1.7 5.8 4.5 6 6 .3 3.1-2 4-7.1 1.6-2.6-1.3-6.1-3-9-4.4-1.4-.7-2.8-1.3-3.7-1.8-.1 0-.1-.1-.2-.1 3.9 6.4 7.5 13 10.8 19.8 5.1-1.6 9.2-.9 12 .7 2.8 1.6 4.3 4 4.4 5.2-.7 1.1-1.2 1.8-2.2 2z"/></g><g><path class="st2" d="M552.1 373.7c-.5 1.1-.8 1.7-.8 1.7l-1.8-1.8c-105.3-101.8-32.8-241.1-32.8-241.1 102.7 85.7 43.2 224.2 35.4 241.2z"/><path class="st1" d="M552.1 373.7c-.5 1.1-.8 1.7-.8 1.7l-1.8-1.8c-5-31.1-9.8-62.3-14.4-93.5-2.4-16.1-4.7-32.3-6.8-48.5l-1.6-12.1-1.5-12.2c-.5-4.1-.9-8.1-1.4-12.2-.2-2-.4-4.1-.6-6.1l-.5-6.1.6 6.1c.2 2 .4 4.1.7 6.1.5 4 1 8.1 1.5 12.1l1.6 12.1 1.7 12.1c2.4 16.1 4.9 32.3 7.5 48.4 5.1 31.4 10.3 62.7 15.8 93.9zM533.9 210c-.1 0-.2 0-.3-.1-.3-.2-.3-.5-.1-.8 4.9-6.5 8.5-14.1 10.6-21.9.1-.3.4-.5.7-.4.3.1.5.4.4.7-2.1 8-5.8 15.7-10.7 22.3-.3.1-.5.2-.6.2zM542.2 257.6c-.1 0-.2 0-.3-.1-.3-.2-.3-.5-.2-.8 4.8-7.6 10.2-14.9 16.2-21.7.2-.2.6-.3.8-.1.2.2.3.6.1.8-5.9 6.7-11.3 13.9-16.1 21.5-.1.3-.3.4-.5.4zM524.2 264.5c-.1 0-.2 0-.3-.1-8.7-5.4-16.5-12.2-23-20.2-.2-.2-.2-.6.1-.8.2-.2.6-.2.8.1 6.4 7.9 14.1 14.6 22.7 19.9.3.2.3.5.2.8-.2.2-.4.3-.5.3zM521.2 213.5c-.2 0-.4 0-.5-.2l-16.5-18c-.2-.2-.2-.6 0-.8.2-.2.6-.2.8 0l16.5 18c.2.2.2.6 0 .8-.1.2-.2.2-.3.2zM550.7 307.7c-.1 0-.2 0-.3-.1-.3-.2-.3-.5-.2-.8l13.9-21.5c.2-.3.5-.3.8-.2.3.2.3.5.2.8l-13.9 21.5c-.1.2-.3.3-.5.3zM531.2 307.6c-.1 0-.2 0-.3-.1l-22.3-13.9c-.3-.2-.3-.5-.2-.8.2-.3.5-.3.8-.2l22.3 13.9c.3.2.3.5.2.8-.1.2-.3.3-.5.3z"/><g><path class="st4" d="M526.6 382.8c-1 .7-1.6 1-1.6 1-.2-.8-.4-1.6-.6-2.5-35-142.2 100.5-221.5 100.5-221.5 41.5 127.2-82.7 212.8-98.3 223z"/><path class="st3" d="M526.6 382.8c-1 .7-1.6 1-1.6 1-.2-.8-.4-1.6-.6-2.5 12.3-29 24.8-58 37.5-86.8 6.6-14.9 13.3-29.8 20-44.7l5.1-11.1 5.2-11.1c1.7-3.7 3.6-7.3 5.3-11 .9-1.8 1.8-3.6 2.7-5.5l2.8-5.4-2.7 5.5c-.9 1.8-1.8 3.6-2.7 5.5-1.7 3.7-3.5 7.4-5.2 11.1l-5.1 11.1-5 11.2c-6.6 14.9-13 29.9-19.4 44.9-12.2 29.2-24.3 58.5-36.3 87.8zM598.2 234.5c-.1-.1-.2-.2-.2-.3-.1-.3 0-.6.3-.7 7.6-2.9 14.7-7.4 20.6-13 .2-.2.6-.2.8 0 .2.2.2.6 0 .8-6 5.7-13.3 10.2-21 13.2-.1.1-.3.1-.5 0zM580 279.3c-.1-.1-.2-.1-.2-.2-.1-.3 0-.6.3-.8 8.1-3.9 16.6-7.2 25.2-9.7.3-.1.6.1.7.4.1.3-.1.6-.4.7-8.6 2.5-17 5.8-25 9.7-.3 0-.5 0-.6-.1zM561 275.5c-.1-.1-.2-.1-.2-.3-4.5-9.3-7.4-19.1-8.7-29.3 0-.3.2-.6.5-.6s.6.2.6.5c1.3 10.1 4.2 19.8 8.6 29 .1.3 0 .6-.3.8-.1 0-.3 0-.5-.1zM585.6 230.8c-.2-.1-.3-.2-.4-.4l-4.4-24c-.1-.3.1-.6.5-.7.3-.1.6.1.7.5l4.4 24c.1.3-.1.6-.5.7-.1-.1-.2-.1-.3-.1zM560.5 326.2c-.1-.1-.2-.1-.2-.2-.1-.3 0-.6.3-.8l23.2-10.8c.3-.1.6 0 .8.3.1.3 0 .6-.3.8l-23.2 10.8c-.2 0-.4 0-.6-.1zM544.1 315.8c-.1-.1-.2-.1-.2-.2l-11.5-23.7c-.1-.3 0-.6.3-.8.3-.1.6 0 .8.3l11.5 23.7c.1.3 0 .6-.3.8-.2 0-.5 0-.6-.1z"/></g><g><path class="st5" d="M482.2 415.1c-1.2 0-1.9-.1-1.9-.1l.9-2.4C532.4 275.4 689 286.2 689 286.2c-37.4 128.5-188.2 129.3-206.8 128.9z"/><path class="st1" d="M482.2 415.1c-1.2 0-1.9-.1-1.9-.1l.9-2.4c26.5-17 53.2-33.9 79.9-50.6 13.9-8.6 27.8-17.2 41.7-25.6l10.5-6.3 10.5-6.2c3.5-2.1 7.1-4.1 10.6-6.1 1.8-1 3.6-2 5.3-3l5.4-2.9-5.3 3c-1.8 1-3.6 2-5.3 3-3.5 2.1-7 4.1-10.5 6.2l-10.5 6.4-10.4 6.4c-13.8 8.6-27.6 17.4-41.4 26.2-26.6 17.2-53.1 34.5-79.5 52zM624.9 333c0-.1-.1-.2 0-.4.1-.3.4-.5.7-.4 7.9 1.8 16.3 2.2 24.3.9.3 0 .6.2.7.5 0 .3-.2.6-.5.7-8.2 1.3-16.7 1-24.8-.9l-.4-.4zM584.6 359.7c0-.1-.1-.2 0-.3 0-.3.3-.5.6-.5 8.9 1.3 17.8 3.4 26.3 6.2.3.1.5.4.4.7-.1.3-.4.5-.7.4-8.5-2.7-17.3-4.8-26.1-6.1-.3-.1-.4-.3-.5-.4zM571.1 345.9c-.1-.1-.1-.2-.1-.3 1.5-10.2 4.6-20 9.3-29.2.1-.3.5-.4.8-.2.3.1.4.5.2.8-4.6 9.1-7.7 18.7-9.2 28.8 0 .3-.3.5-.6.5-.2-.2-.4-.3-.4-.4zM616.6 322.8c-.1-.2-.1-.4-.1-.6l9.9-22.3c.1-.3.5-.4.8-.3.3.1.4.5.3.8l-9.9 22.3c-.1.3-.5.4-.8.3l-.2-.2zM542.1 387.4c0-.1-.1-.2 0-.3.1-.3.3-.5.7-.5l25.2 4.2c.3.1.5.3.5.7-.1.3-.3.5-.7.5l-25.2-4.2c-.3-.1-.4-.2-.5-.4zM534.4 369.6c0-.1-.1-.2 0-.3l3.9-26c0-.3.3-.5.6-.5s.5.3.5.6l-3.9 26c0 .3-.3.5-.6.5s-.4-.2-.5-.3z"/></g></g><g><path class="st2" d="M445 229c-.1 1 .4 2.4 1.6 4.3 2.1 3.1 5.4 8 6.6 9.9-6.4-2.7-13-5.1-19.6-7.3.8-4.5-.3-7.9-1.9-10-1.6-2.2-3.7-3.1-4.8-3-2 .2-2.3 1.9 0 4.9 1.1 1.5 2.8 3.5 4.2 5.3 1 1.2 1.8 2.2 2.2 2.7-2-.7-4.1-1.3-6.1-1.9v-.2c-.3-1.3-3.3-3-4.3-3.7-2.4-1.6-5.1-2.7-7.9-2.8-1.2 0-2.6.3-3.1 1.4-.5 1.2.5 2.6 1.5 3.4 2.9 2.4 6.7 3.6 10.4 3.5 1 0 3.3 0 3.5-1.4 2 .7 4 1.3 6 2-.6.2-1.9.5-3.4.9-2.1.6-4.6 1.4-6.3 2-3.4 1.3-4 2.9-2.6 4.2.7.6 2.8 1 5.2 0 2.4-.9 5.2-2.9 7.3-7 6.6 2.3 13 4.9 19.3 7.7-.8.1-2.2.4-3.7.7-2.5.5-5.6 1.2-7.7 1.8-2.1.6-3.3 1.4-3.9 2.2-.5.8-.4 1.7.4 2.5s3.2 1.4 6.2.5c2.9-.8 6.3-2.9 9.3-7.5 6.8 3.1 13.5 6.5 20 10.2h-.3c-1 .1-2.5.2-4.1.4-1.6.2-3.3.4-5 .6-1.7.2-3.3.5-4.6.8-5.2 1.1-6.4 3.3-4.7 5.5.9 1.1 3.8 1.9 7.5 1.3 1.9-.3 4-1.1 6.1-2.5 2-1.3 4-3 6-5.5 6 3.4 11.9 7.1 17.7 11.1h-4.6c-1.8 0-3.9.1-5.8.2-1 .1-1.9.1-2.8.2-.9.1-1.7.2-2.5.3-6.1.8-7.6 3.2-5.9 5.8.4.7 1.5 1.3 2.9 1.7 1.5.4 3.3.7 5.5.4 2.2-.3 4.6-.9 7.2-2.3 2.3-1.2 4.8-3 7.3-5.6 5.6 3.9 11 8 16.2 12.3l1.7-2.1c-5.4-4.2-11-8.2-16.7-12 1.4-3.2 2.1-6.2 2.2-8.9.1-3-.4-5.6-1.3-7.7-.9-2.1-2.1-3.9-3.4-5.1-1.3-1.2-2.5-1.9-3.4-2-3.5-.5-4.6 2.2-2 8.1 1.4 3 3.2 7.1 4.8 10.5.7 1.7 1.4 3.2 1.9 4.3-5.9-3.8-11.9-7.4-18-10.7 1-3 1.4-5.7 1.3-8.1-.1-2.6-.8-4.8-1.7-6.6-1.9-3.7-4.8-5.5-6.3-5.6-3.1-.2-3.9 2.3-1.2 7.2 1.4 2.5 3.3 5.9 4.9 8.8.8 1.4 1.5 2.7 2 3.6 0 .1.1.1.1.2-6.6-3.5-13.4-6.8-20.3-9.7 1.3-5.2.4-9.2-1.3-11.9-1.8-2.8-4.2-4.1-5.4-4.1-1.6.3-2.3.8-2.4 1.8z"/></g><g><path class="st2" d="M100.2 255.8c1-.1 2.4.5 4.3 1.8 3 2.2 7.8 5.7 9.6 7-2.4-6.5-4.6-13.2-6.4-19.9-4.6.6-7.9-.6-9.9-2.3-2.1-1.7-3-3.9-2.8-4.9.3-2 2-2.2 4.9.2 1.5 1.2 3.4 3 5.1 4.4 1.2 1 2.2 1.9 2.6 2.3-.6-2.1-1.1-4.1-1.6-6.2h-.2c-1.3-.3-2.8-3.4-3.5-4.5-1.5-2.4-2.5-5.2-2.5-8 0-1.2.4-2.6 1.5-3 1.3-.5 2.6.6 3.4 1.6 2.3 3 3.3 6.8 3.1 10.5-.1 1-.2 3.3-1.5 3.4.6 2 1.2 4.1 1.8 6.1.2-.6.6-1.9 1-3.4.7-2.1 1.6-4.5 2.3-6.2 1.4-3.4 3.1-3.9 4.3-2.4.6.7.8 2.8-.2 5.2-1 2.4-3.1 5-7.3 7 2 6.6 4.4 13.2 6.9 19.6.2-.8.5-2.2.8-3.7.6-2.5 1.4-5.6 2.1-7.6.7-2.1 1.5-3.3 2.4-3.8.9-.5 1.7-.3 2.5.5s1.2 3.3.3 6.2c-.9 2.8-3.2 6.2-7.8 8.9 2.8 6.9 5.9 13.8 9.3 20.4v-.3c.1-1 .3-2.4.5-4s.5-3.3.8-5c.3-1.7.7-3.3 1-4.6 1.3-5.2 3.6-6.3 5.7-4.5 1.1.9 1.8 3.9 1 7.5-.4 1.8-1.3 3.9-2.8 6-1.3 1.9-3.2 3.9-5.8 5.8 3.2 6.2 6.6 12.2 10.3 18.1 0-1.2.1-2.8.2-4.6.1-1.8.3-3.9.5-5.8.1-1 .2-1.9.3-2.8.1-.9.3-1.7.4-2.5 1-6 3.5-7.5 6.1-5.6.6.5 1.2 1.5 1.6 3 .3 1.5.5 3.3.2 5.5s-1.1 4.5-2.6 7.1c-1.3 2.3-3.2 4.7-5.9 7 3.6 5.7 7.5 11.3 11.6 16.7l-2.2 1.6c-4-5.6-7.8-11.3-11.3-17.2-3.3 1.3-6.3 1.9-9 1.9-3 0-5.6-.6-7.7-1.6-2.1-1-3.8-2.3-4.9-3.6-1.1-1.3-1.8-2.6-1.8-3.4-.3-3.5 2.4-4.5 8.2-1.7 2.9 1.5 7 3.5 10.3 5.2 1.7.8 3.1 1.5 4.2 2-3.6-6-6.9-12.2-9.9-18.5-3 .9-5.7 1.2-8.1 1-2.6-.2-4.7-1-6.5-2-3.6-2-5.3-5-5.4-6.6 0-3.1 2.4-3.8 7.2-.9 2.4 1.5 5.8 3.5 8.6 5.2 1.4.9 2.6 1.6 3.5 2.1.1 0 .1.1.2.1-3.3-6.8-6.2-13.7-8.8-20.7-5.3 1.1-9.2 0-11.9-1.8-2.7-1.9-3.9-4.4-3.8-5.6-.1-.9.5-1.6 1.5-1.7z"/></g><g><path class="st4" d="M106.8 558.3c0 13.1 8.1 23.7 18.2 23.7h455c10.1 0 18.2-10.6 18.2-23.7H106.8z"/><path class="st2" d="M155.4 290.9H549.6V538.5H155.4z"/><path class="st3" d="M556.6 264.8h-408c-7.6 0-13.8 6.2-13.8 13.8V540c0 7.6 6.2 13.8 13.8 13.8h408c7.6 0 13.8-6.2 13.8-13.8V278.6c0-7.7-6.2-13.8-13.8-13.8z"/><path class="st1" d="M155.4 285.5H549.6V533.1H155.4z"/><path class="st0" d="M295.7 558.3L196.6 558.3 196.9 553.9 197.3 548.4 294.9 548.4zM508.6 558.3L409.4 558.3 409.8 553.9 410.2 548.4 507.8 548.4z"/></g><g><path class="st0" d="M188 451.7H222.4V455.59999999999997H188zM235 451.7H269.4V455.59999999999997H235zM328.6 451.7H353.5V455.59999999999997H328.6zM374.8 451.7H413.5V455.59999999999997H374.8zM342.3 465.1H359.90000000000003V469H342.3zM342.3 475.3H359.90000000000003V479.2H342.3zM342.3 485.6H359.90000000000003V489.5H342.3zM342.3 495.8H359.90000000000003V499.7H342.3z"/><path class="st6" d="M209.7 465.1H222.39999999999998V469H209.7z"/><path class="st2" d="M209.7 475.3H222.39999999999998V479.2H209.7z"/><path class="st4" d="M209.7 485.6H222.39999999999998V489.5H209.7z"/><path class="st5" d="M209.7 495.8H222.39999999999998V499.7H209.7z"/><path class="st0" d="M399.7 465.1H417.3V469H399.7zM399.7 475.3H417.3V479.2H399.7zM399.7 485.6H417.3V489.5H399.7zM399.7 495.8H417.3V499.7H399.7zM234.6 465.1H252.2V469H234.6zM234.6 475.3H260.7V479.2H234.6zM234.6 485.6H267.5V489.5H234.6zM234.6 495.8H249.7V499.7H234.6zM180.4 314.6H306V321.5H180.4z"/><path class="st4" d="M180.4 340.4H198.20000000000002V347.9H180.4zM216.1 340.4H233.9V347.9H216.1zM251.8 340.4H269.6V347.9H251.8zM287.5 340.4H305.3V347.9H287.5zM323.3 340.4H341.1V347.9H323.3zM359 340.4H376.8V347.9H359zM394.7 340.4H412.5V347.9H394.7z"/><g><path class="st0" d="M180.4 355.7H430.20000000000005V358H180.4z"/></g><g><path class="st0" d="M427.7 446.2H181v-90.4h-2v92.5h250.7v-92.5h-2v90.4z"/><path class="st0" d="M405.1 355.7H407.1V447.2H405.1zM382.4 355.7H384.5V447.2H382.4zM359.8 355.7H361.8V447.2H359.8zM337.2 355.7H339.2V447.2H337.2zM314.6 355.7H316.6V447.2H314.6zM292 355.7H294V447.2H292zM269.4 355.7H271.4V447.2H269.4zM246.8 355.7H248.8V447.2H246.8zM224.2 355.7H226.2V447.2H224.2zM201.6 355.7H203.7V447.2H201.6z"/><path class="st0" d="M179 355.7H429.7V357.7H179zM180 378.4H428.7V380.4H180zM180 401H428.7V403H180zM180 423.6H428.7V425.6H180z"/><g><path class="st2" d="M203.6 396.2H219.79999999999998V446.3H203.6zM248.8 385.8H265V446.3H248.8zM294.1 410.5H310.3V446.3H294.1zM339.3 373.7H355.5V446.29999999999995H339.3zM384.5 393.3H400.7V446.3H384.5z"/></g><g><path class="st6" d="M201.6 396.2H217.79999999999998V446.3H201.6zM246.8 385.8H263V446.3H246.8zM292 410.5H308.2V446.3H292zM337.2 373.7H353.4V446.29999999999995H337.2zM382.5 393.3H398.7V446.3H382.5z"/></g></g><g><path class="st0" d="M179 471.1H429.7V473.20000000000005H179z"/></g><g><path class="st0" d="M179 481.3H429.7V483.40000000000003H179z"/></g><g><path class="st0" d="M179 491.6H429.7V493.70000000000005H179z"/></g><g><path class="st0" d="M179 501.8H429.7V503.90000000000003H179z"/></g><g><path class="st6" d="M473.5 352.4c.9-5.5 5.4-9.8 10.9-10.6l-.2-5.1-.5-12.6c-14.7 1.2-26.4 12.7-27.9 27.2l12.6.8 5.1.3z"/><path class="st5" d="M491.1 366.7c-1.5.6-3.1.9-4.8.9-2.9 0-5.6-.9-7.7-2.5l-3.5 3.8-8.5 9.2c5.3 4.5 12.2 7.2 19.7 7.2 4.7 0 9.1-1 13-2.9l-5.9-11.1-2.3-4.6zM516.3 361.3l-12.4-2.1c-1.2 4.6-4 8.4-7.9 10.9l5.9 11.1 2.7 5.2c8.8-5.1 15.3-13.8 17.5-24.1l-5.8-1z"/><path class="st6" d="M468.2 354.9l-12.6-.8-5.9-.4v.9c0 10.1 4.1 19.3 10.7 25.9l4-4.3 8.5-9.2c-2.8-3.2-4.6-7.4-4.7-12.1z"/><path class="st4" d="M495.9 339.3l-2.4 4.6c3.5 2.3 5.7 6.3 5.7 10.8v.8l5.1.9 12.4 2.1c.2-1.3.2-2.5.2-3.8 0-11.3-6.1-21.2-15.2-26.5l-5.8 11.1zM487 336.6c2.3.1 4.4.6 6.4 1.4l5.8-11.2 2.7-5.2c-4.7-2.3-10-3.5-15.7-3.5l.2 5.9.6 12.6z"/></g><g><path class="st0" d="M446.7 407.2H525.1V411.09999999999997H446.7zM446.7 441.5H450.59999999999997V445.4H446.7zM454.4 441.5H525.1V445.4H454.4z"/><path class="st4" d="M446.7 456.1H450.59999999999997V460H446.7z"/><path class="st0" d="M454.4 456.1H525.1V460H454.4z"/><path d="M446.7 470.8H450.59999999999997V474.7H446.7z" style="fill:#6292ff"/><path class="st0" d="M454.4 470.8H525.1V474.7H454.4z"/><path d="M446.7 485.4H450.59999999999997V489.29999999999995H446.7z" style="fill:#da5544"/><path class="st0" d="M454.4 485.4H525.1V489.29999999999995H454.4z"/><path class="st5" d="M446.7 500H450.59999999999997V503.9H446.7z"/><path class="st0" d="M454.4 500H525.1V503.9H454.4zM446.7 417.7H525.1V430.7H446.7z"/></g></g><g><path class="st3" d="M522.8 556.7c.3-.3.7-.5 1.1-.6.4-.1.8-.1 1.3-.1 1-.1 2-.3 2.9-.8.5-.3.9-.6 1.4-.8l2.9.1c.4.4.7 1 .8 1.6.1.5.1 1.1.1 1.6v.6h-10.8v-.6c0-.4 0-.8.3-1z"/><path class="st9" d="M532.7 551.2L532.4 554.5 529.4 554.4 529.2 551.4z"/><path class="st3" d="M494 555.5c.3-.3.7-.4 1.1-.5.4 0 .8 0 1.3.1 1 .1 2.1-.1 3-.5.5-.2 1-.5 1.5-.6l2.9.4c.4.5.5 1.1.6 1.7.1.5 0 1.1-.1 1.6l-.1.6-10.7-1.2.1-.6c0-.4.1-.8.4-1z"/><path class="st4" d="M535.3 503.7c.6-11.4.5-27.5-2.6-36.6 0-.2-23.9 2-23.9 2l-5.6 22.9c-2 8.1-2.9 16.3-2.8 24.6l.3 34.4 4 .3 7.5-45.5c2.8-5.4 5.8-11.6 8.1-17.7l8.7 63.4 4-.2c0-.1 2.3-47.6 2.3-47.6z"/><path class="st9" d="M504.5 551.2L503.8 554.4 500.9 554 501 551z"/><path class="st10" d="M481.6 394.3c.7-.3 1.6 0 1.9.7 2 4 4.2 7.8 6.6 11.5 2.4 3.7 5 7.2 7.8 10.5s5.8 6.4 9.1 9.1c1.6 1.4 3.3 2.7 5 3.9.4.3.9.6 1.3.9l1.3.9c.9.6 1.8 1.1 2.7 1.7.3.2.5.4.8.6.2.2.4.5.6.7.3.5.6 1.1.7 1.8.3 1.3.1 2.7-.7 4-.8 1.3-2 2.1-3.3 2.3-.7.1-1.4.1-2.1 0-.3-.1-.7-.2-1-.3l-.9-.6c-.9-.7-1.8-1.5-2.7-2.3l-1.3-1.2c-.4-.4-.9-.8-1.3-1.2-1.7-1.6-3.4-3.4-4.9-5.1-3.1-3.5-6-7.3-8.5-11.2-2.5-3.9-4.7-8-6.6-12.2-1.9-4.2-3.6-8.4-5.1-12.7-.5-.7-.1-1.5.6-1.8z"/><path class="st2" d="M500.2 434.6l9.4 7.3c2.8 2.2 6.8 1.9 9-.9s1.8-7.2-1.1-9.4l-9.4-7.3-7.9 10.3z"/><path class="st2" d="M521.8 428.5c-9-.1-16 7.9-14.8 16.8l1.8 23.7c10 3.6 17.5 1.6 23.9-2l1.1-25.2c.7-7.1-4.9-13.2-12-13.3z"/><path class="st1" d="M531.8 433.5l-.2.2c1 1.4 1.7 3 2 4.7h.3c-.3-1.7-1-3.4-2.1-4.9zm-9.9 37.3v.3c2.2-.2 4.4-.8 6.6-1.7l-.1-.2c-2.1.8-4.2 1.3-6.5 1.6zm5.1-41.3c-1.6-.8-3.4-1.2-5.2-1.2h-.2c-4.3 0-8.5 1.9-11.3 5.2-1.7 1.9-2.8 4.2-3.3 6.6l.3.1c1.5-6.6 7.4-11.6 14.5-11.5 1.9 0 3.6.5 5.2 1.2v-.4zM508.6 466c-.1 0-.2.1-.3.1l.2 3 .2.1c2.2.8 4.5 1.4 6.6 1.7v-.3c-2.1-.3-4.2-.8-6.5-1.7l-.2-2.9zm-1.8-20.6l.9 12h.3l-.9-12h-.3z"/><path class="st3" d="M524 412.1s6.2 1.5 4.7 8.4c-1 4.6-4.4 7-9.2 7.8l4.5-16.2z"/><path class="st9" d="M517.5 423.7l.5 7.1c2 1.2 4 1.1 5.9-.3l-.5-7.1-5.9.3z"/><path class="st10" d="M517.6 424.6l.1 2.2c.9.5 1.9.7 3 .7h.2c1-.1 2-.5 2.7-1.2l-.1-2.1-5.9.4z"/><path class="st9" d="M514.6 415.4l.4 5.3.1 1.2c.3 2.9 2.7 5.1 5.6 5.1.3 0 .6 0 .9-.1.1 0 .2-.1.3-.1h.1c.4-.2.8-.4 1.1-.8.7-.8 1.1-1.6 1.5-2.5.3-.7.6-1.5.8-2.2.2-.9.4-1.8.2-2.8l-.4-4.6-9-.7-1.6 2.2z"/><path class="st3" d="M523.9 414s-10.3.6-8.2 9.7c0 0-3.2-6.5.1-10.9 3.6-4.8 8.5-3.2 10.2-.9 1.7 2.3 3 6.1-1.8 8.9-.1-.1 1.5-3.5-.3-6.8z"/><path class="st9" d="M523.7 419.5c.1 1.2 1.1 2.1 2.3 2 1.2-.1 2.1-1.1 2-2.3-.1-1.2-1.1-2.1-2.3-2-1.2.1-2.1 1.1-2 2.3z"/><g><path class="st3" d="M503.8 450.8l-7.4-8c4.5-4.2 6.9-9.8 6.9-15.9h10.9c0 9.1-3.8 17.8-10.4 23.9z"/><path class="st4" d="M514.2 427h-10.9c0-12-9.7-21.7-21.7-21.7-2.6 0-5.1.4-7.5 1.3l-3.8-10.2c3.6-1.3 7.4-2 11.3-2 18-.1 32.6 14.6 32.6 32.6z"/><path class="st2" d="M481.6 459.6c-18 0-32.6-14.6-32.6-32.6 0-13.6 8.6-25.9 21.4-30.6l3.8 10.2c-8.5 3.1-14.2 11.3-14.2 20.4 0 12 9.7 21.7 21.7 21.7 5.5 0 10.8-2.1 14.8-5.8l7.4 8c-6.1 5.6-14 8.7-22.3 8.7z"/></g><g><path class="st9" d="M471.1 455.3c0-.8.5-1.5 1.3-1.5 4.4-.5 8.8-1.1 13.1-2.1 4.3-.9 8.5-2.1 12.6-3.5 4.1-1.5 8-3.2 11.8-5.2 1.9-1 3.7-2.1 5.5-3.3.4-.3.9-.6 1.3-.9l1.3-.9c.8-.6 1.7-1.2 2.5-1.9.3-.2.6-.4.8-.5l.9-.3c.6-.1 1.3-.1 1.9 0 1.3.2 2.6.9 3.5 2.1.9 1.2 1.2 2.6 1 3.9-.1.7-.4 1.3-.8 1.9-.2.3-.4.6-.7.8-.3.3-.6.5-.9.7-1 .6-2.1 1.2-3.1 1.7l-1.6.8c-.5.3-1.1.5-1.6.8-2.1 1-4.3 2-6.5 2.8-4.4 1.7-9 3-13.5 3.9-4.6.9-9.2 1.5-13.7 1.9-4.6.3-9.1.4-13.7.3-.8 0-1.4-.7-1.4-1.5z"/><path class="st2" d="M515.5 452.5l10.1-6.2c3.1-1.9 4.3-5.7 2.4-8.8-1.9-3.1-6.1-4.2-9.1-2.3l-10.1 6.2 6.7 11.1z"/><path class="st1" d="M529.1 439.4c-.1-.7-.4-1.4-.8-2-.9-1.5-2.5-2.7-4.3-3.1-.3-.1-.6-.1-.9-.2v.3c2 .3 3.9 1.4 4.9 3.2 1.4 2.3 1.1 5-.5 7l.2.1c1.3-1.5 1.8-3.4 1.4-5.3zm-3.3 7.1s.1 0 .1-.1l-.3-.1-3 1.8.2.2 3-1.8zm-4.2 2.6l-.2-.2-2.9 1.7.2.2 2.9-1.7zm-4.4 2.6l-.2-.2-1.5.9-5.2-8.5-.2.1 5.3 8.8 1.8-1.1zm.2-15.9l-7.4 4.5.1.2 7.4-4.5-.1-.2z"/></g></g><g><path class="st10" d="M234.4 464c0-.8-.5-1.5-1.3-1.6-2.3-.3-4.6-.6-6.9-1-2.3-.4-4.5-.8-6.7-1.3s-4.3-1.2-6.2-2c-1.9-.8-3.7-1.9-5.3-3.1-3.2-2.5-5.7-6-8-9.7-.3-.5-.6-.9-.9-1.4l-.8-1.4c-.6-1-1.1-2-1.7-3-1.1-2-2.2-4-3.2-6.1-1.4-2.6-4.7-3.5-7.2-2s-3.3 4.8-1.7 7.3c1.4 1.9 2.7 3.9 4.1 5.8.7 1 1.4 1.9 2.2 2.9l1.1 1.4c.4.5.8.9 1.1 1.4 1.6 1.9 3.2 3.7 5 5.5 1.8 1.8 3.9 3.4 6.1 4.8 2.3 1.3 4.7 2.3 7.2 3 2.5.7 4.9 1.1 7.3 1.3 2.4.2 4.8.4 7.1.4 2.4.1 4.7.1 7 .1 1 0 1.7-.6 1.7-1.3z"/><path class="st3" d="M190.5 450.4l-6.3-10c-1.9-3-1.3-7 1.8-8.9 3-1.9 7.3-1.1 9.2 2l6.3 10-11 6.9z"/><path class="st9" d="M181.4 505.2L189.7 554.4 192.6 553.9 193.4 504.8z"/><path class="st4" d="M194.2 504.7l-13.6.5c-3.7-9-6.9-28.9-3.1-38.1l15.2 3.4 1.5 34.2z"/><circle transform="rotate(-16.739 184.847 470.406)" class="st4" cx="184.8" cy="470.4" r="7.9"/><g class="st11"><path class="st4" d="M184.8 470.4L184.8 470.4 184.8 470.4 184.8 470.3z"/></g><path class="st9" d="M165.9 503.2L161.1 553.4 164.1 553.6 177.6 505.8z"/><path class="st4" d="M180.4 462.7c-3.2-1-6.5.2-8.5 2.7-.1.2-.3.4-.4.6-5.7 8.3-7.5 27.6-6.3 37l13.2 3 7.3-33.4c1.3-4.2-1.1-8.6-5.3-9.9z"/><path class="st2" d="M180.4 497.1l-1.9 8.9-2.2-.5v.3l2.4.5 2-9.1-.3-.1zm-11.9-25.8c-1.3 3.5-2.4 7.8-3.1 12.8v.3h.3c.6-4.6 1.7-9.1 3.1-12.9l-.3-.2zm-3.9 23.7h.3c0-2.2.2-4.5.4-6.8h-.3c-.3 2.3-.4 4.6-.4 6.8zm.6 8c-.2-1.3-.3-2.8-.3-4.4h-.3c.1 1.6.2 3.1.3 4.4v.2l8 1.8.1-.2-7.8-1.8zm18.6-21.8l-1.7 7.9h.3l1.7-7.9h-.3z"/><path class="st3" d="M170.4 556.6c-.2-.4-.6-.6-1-.7-.4-.1-.8-.1-1.2-.2-1-.2-2-.6-2.8-1.2-.4-.3-.8-.7-1.3-.9l-2.9-.3c-.4.4-.8.9-1 1.5-.2.5-.2 1.1-.3 1.6l-.1.6 10.7 1.2.1-.6c.1-.3 0-.7-.2-1zM199.5 555.1c-.3-.3-.7-.4-1.2-.4-.4 0-.8.1-1.3.1-1 .1-2.1 0-3-.4l-1.5-.6-2.9.5c-.3.5-.5 1.1-.5 1.7 0 .5 0 1.1.1 1.6l.1.6 10.7-1.6-.1-.6c0-.3-.1-.6-.4-.9zM182 428.8c9 .1 15.8 8.2 14.4 17.1l-3.6 24c-6.5 2.3-15.6 1.5-23.1-.7v-27.4c-.5-7.1 5.2-13.1 12.3-13z"/><path class="st1" d="M169.4 457.4v10.4h.3v-10.4h-.3zm12.6-28.8h-.1c-.4 0-.8 0-1.2.1v.3c.4 0 .8-.1 1.3-.1 2.1 0 4 .5 5.8 1.2l.1-.2c-1.8-.9-3.8-1.3-5.9-1.3zm11.3 5.3c-.8-.9-1.7-1.8-2.7-2.5l-.2.2c3.7 2.7 6.1 7.1 6.1 11.9 0 .8-.1 1.6-.2 2.4l-.8 5.3.3.1.8-5.3c.7-4.4-.5-8.8-3.3-12.1zm-.6 36c-5.9 2.1-13.9 1.6-20.9-.1v.3c4 1 8.1 1.5 11.8 1.5 3.5 0 6.6-.5 9.2-1.4l.1-.1 2.1-13.8h-.3l-2 13.6zm-16.9-39.5l-.1-.3c-1.1.6-2.1 1.4-2.9 2.3-2.4 2.5-3.6 5.9-3.3 9.3v6.4h.3v-6.4c-.4-4.7 2.1-9 6-11.3z"/><g><path class="st9" d="M186.2 424.7l-.4 7.3c-2.1 1.1-4 1-5.9-.4l.4-7.3 5.9.4z"/><path class="st10" d="M186.1 426.9v.8c-.9.5-2 .7-3.1.7h-.2c-1-.1-1.9-.5-2.6-1.2l.1-2.1 5.8 1.8z"/><path class="st9" d="M189.3 416.4l-.5 5.2-.1 1.2c-.3 2.9-2.8 5.1-5.7 5-.3 0-.6-.1-.9-.1-.1 0-.2-.1-.3-.1h-.1c-.4-.2-.8-.5-1.1-.8-.6-.8-1-1.6-1.4-2.5-.3-.8-.6-1.5-.8-2.3-.2-.9-.3-1.8-.2-2.8l.2-3.6 9.3-1.5 1.6 2.3z"/><path class="st3" d="M189 424.6s0-3.1-.1-4.6c-.1-1.4-.4-2.8-1.5-2.6-2.1.4-2.9-1.4-2.9-1.4-.6 0-1.2.1-1.9.3-3.1.8-3.6 0-4-.5-.8 2.4-.5 5.5-.5 5.8 0 .1.1.3.1.4.2.8.5 1.5.8 2.3.3.7.6 1.4 1 2v.5c-2.2-.4-4.9-2.8-5.6-4.7-2.3-7.2 1.6-11.5 7.1-12.6 4.8-.9 7.4 3.5 8.4 7.5.8 2.3-.3 7-.9 7.6z"/><path class="st9" d="M180.2 420.3c-.1 1.2-1.1 2.1-2.3 2-1.2-.1-2.1-1.1-2-2.3.1-1.2 1.1-2.1 2.3-2 1.2.1 2 1.1 2 2.3z"/></g><g><path transform="rotate(-180 274.437 454.01)" class="st2" d="M269 446.1H279.8V462H269z"/><path transform="rotate(-180 260.511 447.387)" class="st2" d="M255.1 432.8H265.9V461.90000000000003H255.1z"/><path transform="rotate(-180 246.585 443.424)" class="st4" d="M241.2 424.9H252V461.9H241.2z"/><path transform="rotate(-180 232.659 439.712)" class="st4" d="M227.2 417.5H238V461.9H227.2z"/><path transform="rotate(-180 218.732 441.217)" class="st4" d="M213.3 420.5H224.10000000000002V461.9H213.3z"/><path transform="rotate(-180 204.806 443.424)" class="st2" d="M199.4 424.9H210.20000000000002V461.9H199.4z"/><path transform="rotate(-180 190.88 447.387)" class="st4" d="M185.5 432.8H196.3V461.90000000000003H185.5z"/><g><path transform="rotate(-180 232.659 462.663)" class="st3" d="M183.1 461.9H282.3V463.4H183.1z"/></g></g><g><path class="st9" d="M227.5 461.9c-.1-.8-.7-1.4-1.5-1.4h-6.9c-2.3-.1-4.6-.2-6.8-.4s-4.4-.6-6.4-1.1c-2-.6-3.9-1.3-5.7-2.4-3.5-2.1-6.5-5.1-9.3-8.5-.4-.4-.7-.8-1.1-1.3l-1-1.3c-.7-.9-1.4-1.8-2-2.7-1.4-1.8-2.7-3.7-4-5.6-1.7-2.3-5.1-2.8-7.4-.9-2.3 1.8-2.6 5.3-.6 7.5 1.6 1.7 3.2 3.4 4.9 5.2.8.9 1.7 1.7 2.5 2.6l1.3 1.3c.4.4.9.8 1.3 1.2 1.8 1.7 3.7 3.3 5.8 4.8 2.1 1.5 4.3 2.8 6.7 3.9 2.4 1 5 1.7 7.5 2 2.5.3 5 .4 7.4.3 2.4-.1 4.8-.3 7.1-.6s4.7-.6 7-.9c.7-.3 1.2-1 1.2-1.7z"/><path class="st3" d="M181.9 454.2l-7.7-9c-2.3-2.7-2.2-6.7.5-9.1 2.7-2.3 7.1-2.1 9.4.7l7.7 9-9.9 8.4z"/><path class="st1" d="M179.6 434.3c-1.2-.1-2.3.1-3.4.6l.1.2c2.6-1.2 5.9-.6 7.8 1.7l.7.8.2-.2-.7-.8c-1.1-1.3-2.8-2.2-4.7-2.3zm12.2 11.6l-8.1 6.8.2.2 8.3-7-4.4-5.2-.2.2 4.2 5zm-18-9.1c-1.8 2.1-1.9 5.2-.4 7.7l.2-.1c-1.4-2.3-1.4-5.3.3-7.4l-.1-.2zm7.4 17l.2-.2-3.7-4.4-.2.2 3.7 4.4z"/></g></g><g><path class="st3" d="M630.9 587.7H74.2c-1.6 0-2.9-1.3-2.9-2.9 0-1.6 1.3-2.9 2.9-2.9H631c1.6 0 2.9 1.3 2.9 2.9-.1 1.6-1.4 2.9-3 2.9z"/></g><g><path transform="rotate(-40.957 194.403 297.627)" class="st2" d="M179.5 288.7H209.2V306.4H179.5z"/><path transform="rotate(-40.957 148.955 337.083)" class="st4" d="M103.6 323.8H194.2V350.40000000000003H103.6z"/><path class="st4" d="M294.2 300.4c28.1-24.4 31.2-67.2 6.7-95.3-24.4-28.1-67.2-31.2-95.3-6.7-25.9 22.5-30.5 60.4-12.1 88.2 1.6 2.4 3.4 4.8 5.4 7.1 2 2.3 4.1 4.4 6.2 6.3 25 22.1 63.3 22.9 89.1.4zm-76.9-88.6c20.7-18 52.3-15.8 70.3 5s15.8 52.3-5 70.3-52.3 15.8-70.3-5-15.8-52.3 5-70.3z"/><g style="opacity:.5"><path class="st2" d="M212.3 282.1c-18-20.8-15.8-52.3 5-70.3 20.7-18 52.3-15.8 70.3 5s15.8 52.3-5 70.3c-20.7 17.9-52.3 15.7-70.3-5z"/></g><g><path class="st1" d="M263.6 217c.2-.4.4-.7.8-1 1-.8 2.5-.5 3.2.5l20.8 28.3c.8 1 .5 2.5-.5 3.2-1 .8-2.5.5-3.2-.5l-20.8-28.3c-.5-.6-.6-1.5-.3-2.2zM252.5 225.2c.2-.4.4-.7.8-1 1-.8 2.5-.5 3.2.5l20.8 28.3c.8 1 .5 2.5-.5 3.2-1 .8-2.5.5-3.2-.5l-20.8-28.3c-.5-.6-.6-1.5-.3-2.2z"/></g></g><g><path class="st3" d="M410 551.8l-12.9 6.5c-.2-.4-.3-.9-.2-1.4.1-.6.5-1 .9-1.3.5-.3 1-.6 1.5-.8 1.2-.7 2.2-1.6 3-2.8.4-.6.7-1.2 1.2-1.8l3.6-1.7c.7.3 1.4.8 1.9 1.4.4.5.7 1.2 1 1.9zM422.6 556.4l-14.4 1.9c-.1-.5 0-1 .2-1.4.3-.5.8-.8 1.3-1 .5-.2 1.1-.2 1.7-.3 1.4-.2 2.6-.8 3.7-1.6.6-.4 1.1-.9 1.7-1.3l3.9-.4c.6.5 1.1 1.2 1.3 2 .4.7.5 1.4.6 2.1zM416.5 508.6L416.5 508.6 416.5 508.6z"/><g class="st11"><path class="st3" d="M414.6 478.3L414.6 478.3 414.6 478.3 414.6 478.3z"/></g><path class="st3" d="M416.5 508.6L416.5 508.6 416.5 508.6zM416.5 508.6L416.5 508.6 416.5 508.6z"/><path class="st2" d="M384.1 510.1l18.8 40.3 4.7-1.9-12-37.6 9.7-15.3 11 57.2 5.1-.3.1-73.2c.1-.7-30.9-2.6-30.9-2.6l-6.8 30.4c-.1 1.1 0 2 .3 3zm32.4-1.5z"/><g class="st11"><path class="st3" d="M416.5 508.6L416.5 508.6 416.5 508.6z"/></g><path class="st10" d="M352.8 484.7c1.5-1.5 3-2.8 4.5-4.2.4-.3.7-.7 1.1-1 .4-.4.7-.7 1.1-1l1-1.1.5-.5.5-.5c2.7-2.9 5.1-6 7.3-9.3 1-1.7 2.1-3.3 3.1-5l.7-1.3c.1-.2.2-.4.4-.6l.3-.7 1.3-2.6c.1-.2.2-.4.3-.7l.3-.7.6-1.3.6-1.3.3-.7.2-.3.1-.3 1.1-2.7c.4-.9.7-1.8 1.1-2.8.4-1.2 1.4-2 2.6-2.4 1.2-.4 2.6-.4 3.9.2 1.3.6 2.3 1.6 2.8 2.7.5 1.2.5 2.5-.1 3.7-.5.9-1 1.8-1.6 2.8l-1.6 2.7-.2.3-.2.3-.4.7-.9 1.3-.9 1.3-.4.7c-.1.2-.3.4-.5.7l-1.8 2.6-.5.6c-.2.2-.3.4-.5.6l-1 1.3c-1.3 1.7-2.7 3.3-4.1 4.9-2.9 3.1-6 6.1-9.2 8.7l-.6.5-.6.5-1.3.9c-.4.3-.8.6-1.3.9-.4.3-.8.6-1.3.9-1.7 1.2-3.4 2.3-5 3.4-.5.4-1.3.3-1.9-.3-.3-.6-.3-1.4.2-1.9z"/><path class="st4" d="M383.8 465.5l5.6-12.2c1.7-3.7.4-8-3.3-9.7-3.7-1.7-8.4-.1-10 3.6l-5.6 12.2 13.3 6.1z"/><g><path class="st4" d="M389.8 435.7c-7.7 1.9-12.2 9.9-9.6 17.5l11.9 36.1c13.9 1.7 20.3-2.7 29.7-9.7l-12.4-33.5c-3-7.9-11.4-12.4-19.6-10.4z"/></g><g><path class="st1" d="M403.3 438.2c1.4 1 2.7 2.2 3.8 3.7l.3-.3c-1.1-1.4-2.4-2.7-3.8-3.7l-.3.3zm16 43.1l.2.3c.8-.6 1.5-1.1 2.3-1.7l.2-.2-3.2-8.7-.4.1 3.1 8.5c-.6.5-1.4 1.1-2.2 1.7zm-27.6 8.3h.3c2.7.3 5.1.5 7.3.4l-.1-.4c-2.2.1-4.5 0-7.2-.3l-7-21.2-.4.1 7.1 21.4zm20.6-3.7c-1 .5-1.9 1-3 1.4l.2.4c1.1-.5 2.1-1 3.1-1.5l-.3-.3zm2.1-26l.4-.2-3-8-.4.2 3 8zm-32.6-.2l.4-.1-2.1-6.5c-2.3-6.8 1.2-14.1 7.6-16.8.3-.1.6-.3 1-.4l-.2-.4-.9.3c-6.8 2.8-10.3 10.4-7.9 17.4l2.1 6.5z"/></g><g><path class="st4" d="M353.2 491.4c8.2 7.9 20.6 10.6 31.7 6l-11.3-26.9-20.4 20.9z"/><path class="st3" d="M373.6 470.4l29.2-1.4c-.2-3.3-.9-6.7-2.3-9.9-2.7-6.4-7.4-11.3-13.1-14.4l-13.8 25.7zM362.2 443.5l11.3 26.9 13.9-25.7c-7.4-4-16.7-4.8-25.2-1.2z"/><path class="st3" d="M373.6 470.4l11.3 26.9c11.6-4.9 18.4-16.4 17.8-28.3l-29.1 1.4z"/><path class="st4" d="M346.7 481.8c1.6 3.7 3.8 7 6.6 9.6l20.4-21-29-3.4c-.7 4.9-.1 9.9 2 14.8z"/></g><g><path class="st9" d="M371.3 467.3c.5-.6 1.3-.7 1.9-.3 1.8 1.2 3.7 2.4 5.6 3.4.9.5 1.9 1 2.9 1.4 1 .4 1.9.8 2.9 1.1 1 .3 1.9.5 2.8.5h1.2c.4 0 .8-.1 1.1-.2.3-.1.6-.3.9-.4.3-.2.5-.4.8-.6.2-.2.5-.5.7-.8.2-.3.4-.6.6-1 .4-.7.7-1.6 1-2.5.3-.9.5-1.8.7-2.8.4-2 .6-4 .7-6.1.1-2.1.2-4.2.2-6.4 0-2.1-.1-4.3-.2-6.5 0-1.4.5-2.6 1.5-3.6.9-.9 2.2-1.5 3.7-1.5 1.4 0 2.7.6 3.7 1.6.9 1 1.4 2.3 1.3 3.7-.2 2.3-.4 4.6-.8 6.8-.3 2.3-.7 4.6-1.1 6.9-.5 2.3-1 4.6-1.8 7-.4 1.2-.8 2.3-1.4 3.5-.6 1.2-1.2 2.3-2 3.4-.4.6-.9 1.1-1.4 1.6-.5.5-1.1 1-1.7 1.4-.6.4-1.3.8-2.1 1.1-.7.3-1.5.5-2.2.6-.7.1-1.5.1-2.2.1h-.6l-.5-.1c-.3-.1-.7-.1-1-.2-1.3-.3-2.5-.7-3.6-1.2s-2.2-1.1-3.2-1.7c-1-.6-2-1.3-2.9-2-1.9-1.4-3.6-2.8-5.3-4.3-.6-.5-.7-1.3-.2-1.9z"/><path class="st4" d="M406.8 461.8l.4-13.4c.1-4.1-2.8-7.6-6.8-7.7-4.1-.1-7.7 3.2-7.9 7.2l-.4 13.4 14.7.5z"/><path class="st1" d="M392.2 449.3h.4V448c0-.5.1-.9.2-1.4l-.4-.1c-.1.5-.2 1-.2 1.5v1.3zm11.3 12.8l3.7.1.1-4.1-.4-.1-.1 3.8-3.3-.1v.4zm3.7-11.4l-.2 4.9h.4l.2-5-.4.1zm-12.7-7.9l.4.1c1.5-1.4 3.4-2.2 5.5-2.2 1.7.1 3.3.7 4.4 1.8l.2-.3c-1.3-1.2-2.9-1.8-4.6-1.9-1.1 0-2.2.2-3.3.6-1 .5-1.9 1.1-2.6 1.9zm-2.6 15.2l.4.1.1-3.4h-.4l-.1 3.3zm4.5 3.9l3.4.1v-.4l-3.3-.1-.1.4z"/></g><g><path class="st9" d="M383 434.5l4.8 8.4c2.5.2 4.3-.9 5.5-3.1l-4.8-8.4-5.5 3.1z"/><path class="st10" d="M383 434.5l2.2 3.8c.5-.1.9-.2 1.4-.4.1-.1.3-.1.4-.2 1.5-.9 2.5-2.4 2.8-4l-1.3-2.3-5.5 3.1z"/><path class="st9" d="M377.6 430.8l2.1 3.5.9 1.4c1.2 1.9 3.7 2.7 5.7 1.7.7-.3 1.3-.8 1.7-1.3.1-.1.2-.3.3-.4.1-.1.1-.2.2-.3.5-.9.8-1.8.7-2.9-.1-.9-.2-1.8-.3-2.8-.1-.6-.2-1.1-.3-1.7-.5-2.5-2.3-4.6-4.7-5.3-2.1-.6-4-.1-5.6 1.6-1.7 1.8-2 4.4-.7 6.5z"/><path class="st3" d="M385.7 429.1h-.8s0-3.2-.8-3.9c-1.2-1-4.5.3-5.9 1.4-.4.4-.7.7-.7 1.3.1 1.2.5 3.5 1.7 5.5 0 0-5-5.2-4.5-9.4.3-2.8.8-4.8 4.3-4.8 5.5 0 9.6 2.7 11.2 8.4l-3.5.2-1 1.3z"/><g><path class="st9" d="M385.9 429.5c.6 1 2 1.3 3.1.7s1.6-2 1-3c-.6-1-2-1.3-3.1-.7-1.2.6-1.6 2-1 3z"/></g></g></g><g><path class="st5" d="M305 499.7H375.1V558.3H305z"/><path class="st13" d="M281.2 499.7H305V558.3H281.2z"/><path class="st13" d="M305 499.7L295.6 517.6 269.4 517.6 281.2 499.7zM386 519.6L316 524.1 305 499.7 375.1 504.2z"/><path class="st5" d="M386 519.6L316 519.6 305 499.7 375.1 499.7zM305 499.7L299.7 519.7 269.4 517.6 295.6 517.6z"/></g><g><path class="st0" d="M38.9 241.8c3.5-18.6 10.8-36.5 20.7-52.7 5-8.1 10.7-15.8 17.1-22.9 3.2-3.6 6.5-7 10-10.3 3.5-3.3 7.1-6.4 10.8-9.4 15-11.9 32.3-20.9 50.6-26.7 9.2-2.9 18.6-4.9 28.1-6.1 2.4-.3 4.8-.5 7.1-.8l3.6-.3c1.2-.1 2.4-.1 3.6-.2 4.8-.2 9.6-.2 14.4 0 4.8.2 9.6.7 14.3 1.3 4.8.6 9.5 1.5 14.2 2.5 2.3.5 4.7 1.1 7 1.7l3.5 1c1.2.3 2.3.7 3.4 1.1.6.2 1.1.4 1.7.5l1.7.6c1.1.4 2.3.8 3.4 1.2 1.1.4 2.2.8 3.4 1.3l3.3 1.4c.6.2 1.1.5 1.7.7l1.6.7 3.3 1.5 3.2 1.6 1.6.8 1.6.8 3.2 1.7 3.1 1.7 1.6.9 1.5.9 3.1 1.8c4.1 2.4 8.1 4.9 12.1 7.5 4 2.6 7.9 5.2 11.9 7.9 7.8 5.3 15.6 10.7 23.5 15.9 3.9 2.6 7.9 5.1 11.9 7.6 4 2.4 8.1 4.8 12.2 7.1 2 1.2 4.1 2.2 6.2 3.3 1 .6 2.1 1 3.2 1.6 1.1.5 2.1 1.1 3.2 1.5 2.1 1 4.3 2 6.5 2.8 1.1.4 2.2.9 3.3 1.3l3.3 1.2 3.3 1.2c1.1.4 2.2.8 3.4 1.1l3.4 1c.6.2 1.1.3 1.7.5l1.7.4c1.1.3 2.3.6 3.4.8l3.5.7c.6.1 1.2.2 1.7.3l1.7.3 3.5.5c-9.4-.8-18.8-2.7-27.8-5.6-9-2.9-17.8-6.7-26.3-11-4.3-2.1-8.4-4.4-12.5-6.8-4.1-2.4-8.2-4.8-12.2-7.3s-8-5.1-12-7.6l-11.9-7.7c-4-2.6-7.9-5.1-11.9-7.6s-8-4.9-12.1-7.3l-3.1-1.7-1.5-.9-1.5-.8-3.1-1.7-3.1-1.6-1.6-.8-1.6-.8-3.2-1.5-3.2-1.4-1.6-.7c-.5-.2-1.1-.4-1.6-.7-17.2-7.2-35.7-11.2-54.3-11.9-18.6-.8-37.4 1.5-55.2 6.9-4.5 1.3-8.9 2.9-13.2 4.6-4.3 1.7-8.6 3.7-12.7 5.8-8.3 4.2-16.2 9.2-23.7 14.8-7.4 5.7-14.4 11.9-20.8 18.8-6.4 6.8-12.2 14.2-17.4 22-10.6 15.9-18.3 33.3-22.9 51.7z"/></g><g><path class="st0" d="M658 370.2c6.5 13.9 10.3 29.1 11.5 44.5 1.1 15.4-.4 31.1-4.6 46.1-4.2 14.9-11.2 29.1-20.3 41.6-9.1 12.5-20.3 23.5-33.2 31.9 11.9-9.7 22.3-21 30.7-33.6 8.4-12.6 14.9-26.4 19-41 4.1-14.5 5.9-29.7 5.3-44.9-.4-15.1-3.3-30.2-8.4-44.6z"/></g><g><path class="st1" d="M639.8 422.2c.4 9.5-.9 19.2-3.6 28.3-1.4 4.6-3.1 9.1-5.2 13.4-2.1 4.3-4.6 8.5-7.3 12.4-2.8 3.9-5.9 7.6-9.2 11.1-3.4 3.4-7 6.6-10.9 9.4-7.7 5.7-16.4 10.1-25.5 12.9 8.8-3.5 17.1-8.3 24.6-14.1 3.7-2.9 7.2-6.1 10.5-9.5 3.3-3.4 6.3-7 9-10.9 2.7-3.8 5.1-7.9 7.3-12.1 2.1-4.2 3.9-8.6 5.4-13.1 2.9-8.8 4.5-18.2 4.9-27.8z"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/member_balance.svg b/src/assets/svgs/member_balance.svg
new file mode 100644
index 0000000..5395b23
--- /dev/null
+++ b/src/assets/svgs/member_balance.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="1693028338187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22985" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M983.8 312.7C958 251.7 921 197 874 150c-47-47-101.8-83.9-162.7-109.7C648.2 13.5 581.1 0 512 0S375.8 13.5 312.7 40.2C251.7 66 197 102.9 150 150c-47 47-83.9 101.8-109.7 162.7C13.5 375.8 0 442.9 0 512s13.5 136.2 40.2 199.3C66 772.3 102.9 827 150 874c47 47 101.8 83.9 162.7 109.7 63.1 26.7 130.2 40.2 199.3 40.2s136.2-13.5 199.3-40.2C772.3 958 827 921 874 874c47-47 83.9-101.8 109.7-162.7 26.7-63.1 40.2-130.2 40.2-199.3s-13.4-136.2-40.1-199.3z m-55.3 375.2c-22.8 53.8-55.4 102.2-96.9 143.7s-89.9 74.1-143.7 96.9C632.2 952.1 573 964 512 964s-120.2-11.9-175.9-35.5c-53.8-22.8-102.2-55.4-143.7-96.9s-74.1-89.9-96.9-143.7C71.9 632.2 60 573 60 512s11.9-120.2 35.5-175.9c22.8-53.8 55.4-102.2 96.9-143.7s89.9-74.1 143.7-96.9C391.8 71.9 451 60 512 60s120.2 11.9 175.9 35.5c53.8 22.8 102.2 55.4 143.7 96.9s74.1 89.9 96.9 143.7C952.1 391.8 964 451 964 512s-11.9 120.2-35.5 175.9z" fill="#000000" p-id="22986"></path><path d="M706 469.1H574.7l84.2-180.6c7-15 0.4-32.9-14.5-39.9-15-7-32.9-0.4-39.9 14.5L512 461.5l-92.5-198.3c-7-15-24.9-21.5-39.9-14.5s-21.5 24.9-14.5 39.9l84.2 180.6H318c-16.5 0-30 13.5-30 30s13.5 30 30 30h164v64h-92.5c-20.6 0-37.5 13.5-37.5 30s16.9 30 37.5 30H482v95c0 16.5 13.5 30 30 30s30-13.5 30-30v-95h92.5c20.6 0 37.5-13.5 37.5-30s-16.9-30-37.5-30H542v-64h164c16.5 0 30-13.5 30-30 0-16.6-13.5-30.1-30-30.1z" fill="#000000" p-id="22987"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/member_expenditure_balance.svg b/src/assets/svgs/member_expenditure_balance.svg
new file mode 100644
index 0000000..02d498c
--- /dev/null
+++ b/src/assets/svgs/member_expenditure_balance.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="1693028553383" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="28918" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M510.72 962.56C262.4 960 61.44 757.76 64 509.44 66.56 263.68 264.96 65.28 510.72 62.72c17.92 0 34.56 14.08 34.56 32s-14.08 34.56-32 34.56h-2.56C299.52 130.56 128 300.8 128 512s171.52 382.72 382.72 382.72S893.44 723.2 893.44 512c0-17.92 16.64-33.28 34.56-32 17.92 0 32 15.36 32 32 0 248.32-200.96 450.56-449.28 450.56z" fill="#000000" p-id="28919"></path><path d="M645.12 480H375.04c-17.92 0-34.56-14.08-34.56-32s14.08-34.56 32-34.56h272.64c17.92 0 33.28 16.64 32 34.56 0 17.92-14.08 32-32 32z m0 130.56H375.04c-17.92 0-33.28-16.64-32-34.56 0-17.92 15.36-32 32-32h270.08c17.92 0 33.28 16.64 32 34.56 0 16.64-14.08 32-32 32z" fill="#000000" p-id="28920"></path><path d="M510.72 746.24c-17.92 0-33.28-15.36-33.28-33.28V441.6c0-17.92 16.64-33.28 34.56-32 17.92 0 32 15.36 32 32v270.08c0 19.2-15.36 34.56-33.28 34.56z" fill="#000000" p-id="28921"></path><path d="M510.72 458.24c-8.96 0-17.92-3.84-24.32-10.24l-111.36-111.36c-14.08-12.8-15.36-33.28-2.56-47.36s33.28-15.36 47.36-2.56l2.56 2.56 111.36 111.36c12.8 12.8 12.8 34.56 0 47.36-6.4 6.4-15.36 10.24-23.04 10.24z" fill="#000000" p-id="28922"></path><path d="M510.72 458.24c-8.96 0-17.92-3.84-24.32-10.24-12.8-12.8-12.8-34.56 0-47.36l111.36-111.36c14.08-12.8 35.84-10.24 47.36 2.56 11.52 12.8 11.52 32 0 44.8L533.76 448c-6.4 6.4-15.36 10.24-23.04 10.24zM925.44 241.92c17.92 0 33.28-15.36 33.28-33.28 0-8.96-3.84-17.92-10.24-24.32l-111.36-111.36c-12.8-14.08-33.28-14.08-47.36-1.28s-14.08 33.28-1.28 47.36l1.28 1.28 111.36 111.36c7.68 6.4 15.36 10.24 24.32 10.24z" fill="#000000" p-id="28923"></path><path d="M815.36 353.28c8.96 0 17.92-3.84 24.32-10.24l111.36-111.36c12.8-14.08 10.24-35.84-2.56-47.36-12.8-11.52-32-11.52-44.8 0l-111.36 111.36c-12.8 12.8-12.8 34.56 0 47.36 5.12 6.4 14.08 10.24 23.04 10.24z" fill="#000000" p-id="28924"></path><path d="M920.32 241.92c17.92 0 34.56-14.08 34.56-32s-14.08-34.56-32-34.56H695.04c-17.92 0-33.28 16.64-32 34.56 0 17.92 15.36 32 32 32h225.28z" fill="#000000" p-id="28925"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/member_level.svg b/src/assets/svgs/member_level.svg
new file mode 100644
index 0000000..cbcc686
--- /dev/null
+++ b/src/assets/svgs/member_level.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="1693027700643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8876" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M936.96 385.877333l-203.434667-204.8-18.090667-7.68L308.565333 173.397333l-18.090667 7.68L87.04 385.877333c-9.728 9.898667-9.898667 25.941333-0.170667 35.84l406.869333 421.034667c4.778667 4.949333 11.434667 7.850667 18.432 7.850667 6.997333 0 13.653333-2.901333 18.432-7.850667l406.869333-421.034667C946.858667 411.648 946.688 395.776 936.96 385.877333zM868.522667 389.632l-141.994667 0-163.84-165.034667 141.994667 0L868.522667 389.632zM319.317333 224.768l143.018667 0-163.84 165.034667L155.477333 389.802667 319.317333 224.768zM176.469333 440.832l132.608 0 18.090667-7.509333 185.173333-186.538667 185.173333 186.538667 18.090667 7.509333 131.584 0L512 787.968 176.469333 440.832z" p-id="8877" fill="#000000"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/member_point.svg b/src/assets/svgs/member_point.svg
new file mode 100644
index 0000000..b849ddb
--- /dev/null
+++ b/src/assets/svgs/member_point.svg
@@ -0,0 +1 @@
+<svg t="1693027780777" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10083" width="128" height="128"><path d="M509.091764 501.653351c241.775532 0 424.086741-78.085426 424.086741-181.63992 0-103.543238-182.311209-181.628664-424.086741-181.628664S84.993766 216.471217 84.993766 320.014454C84.993766 423.568948 267.316232 501.653351 509.091764 501.653351zM509.091764 184.220698c222.908836 0 378.251833 71.561849 378.251833 135.793756S732.001623 455.818443 509.091764 455.818443c-222.920092 0-378.26309-71.573105-378.26309-135.803989S286.171672 184.220698 509.091764 184.220698z" fill="#000000" p-id="10084"></path><path d="M509.083577 694.061522c241.1155 0 422.937568-77.598332 422.937568-180.482561 0-27.169803-13.127995-52.453652-36.241412-75.131141-0.148379-0.153496-0.26606-0.320295-0.418532-0.468674-0.170892-0.166799-0.285502-0.345877-0.456395-0.51063l-0.11461 0.125867c-3.717671-3.40761-8.576329-5.608741-14.017248-5.608741-11.542894 0-20.898982 9.356089-20.898982 20.898982 0 6.110161 2.721994 11.481496 6.901177 15.302521l-0.082888 0.091074c13.948687 14.024411 21.809725 31.154557 21.809725 45.300742 0 64.785515-155.813718 136.966465-379.419426 136.966465-223.595474 0-379.410216-72.180949-379.410216-136.966465 0-16.139585 4.53734-29.952172 22.323425-45.670156 0.213871-0.204661 0.429789-0.381693 0.635473-0.594541 0.137123-0.118704 0.240477-0.233314 0.378623-0.354064l-0.084934-0.080841c3.416819-3.719718 5.623068-8.588609 5.623068-14.037714 0-11.542894-9.356089-20.898982-20.898982-20.898982-5.770424 0-10.993378 2.340301-14.773472 6.119371l-0.122797-0.118704c-23.408129 22.797215-36.594453 48.27754-36.594453 75.635631C86.158289 616.462167 267.979334 694.061522 509.083577 694.061522z" fill="#000000" p-id="10085"></path><path d="M895.577119 629.529787c-0.168846-0.164752-0.282433-0.342808-0.453325-0.50756l-0.11461 0.124843c-3.717671-3.40761-8.577353-5.608741-14.018272-5.608741-11.540847 0-20.897959 9.356089-20.897959 20.898982 0 6.110161 2.720971 11.482519 6.901177 15.302521l-0.083911 0.091074c13.94971 14.024411 21.810748 31.154557 21.810748 45.300742 0 64.787562-155.813718 136.966465-379.419426 136.966465-223.595474 0-379.410216-72.179926-379.410216-136.966465 0-16.139585 4.53734-29.952172 22.321378-45.670156 0.213871-0.202615 0.429789-0.381693 0.635473-0.594541 0.137123-0.118704 0.240477-0.233314 0.378623-0.354064l-0.084934-0.080841c3.416819-3.719718 5.623068-8.588609 5.623068-14.037714 0-11.542894-9.356089-20.898982-20.897959-20.898982-5.770424 0-10.993378 2.340301-14.773472 6.119371l-0.122797-0.118704c-23.410176 22.797215-36.594453 48.278563-36.594453 75.635631 0 102.884228 181.821045 180.482561 422.926312 180.482561 241.114476 0 422.935522-77.598332 422.935522-180.482561 0-27.166733-13.125949-52.452629-36.235272-75.127048C895.851365 629.847012 895.730615 629.681236 895.577119 629.529787z" fill="#000000" p-id="10086"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/member_recharge_balance.svg b/src/assets/svgs/member_recharge_balance.svg
new file mode 100644
index 0000000..7519bb2
--- /dev/null
+++ b/src/assets/svgs/member_recharge_balance.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="1693028440322" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25843" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 750.509317c-19.080745 0-31.801242-12.720497-31.801242-31.801242L480.198758 432.496894c0-19.080745 12.720497-31.801242 31.801242-31.801242s31.801242 12.720497 31.801242 31.801242l0 286.21118C537.440994 737.78882 524.720497 750.509317 512 750.509317z" fill="#000000" p-id="25844"></path><path d="M651.925466 534.26087 365.714286 534.26087c-19.080745 0-31.801242-12.720497-31.801242-31.801242 0-19.080745 12.720497-31.801242 31.801242-31.801242l286.21118 0c19.080745 0 31.801242 12.720497 31.801242 31.801242C683.726708 521.540373 671.006211 534.26087 651.925466 534.26087z" fill="#000000" p-id="25845"></path><path d="M651.925466 648.745342 365.714286 648.745342c-19.080745 0-31.801242-12.720497-31.801242-31.801242 0-19.080745 12.720497-31.801242 31.801242-31.801242l286.21118 0c19.080745 0 31.801242 12.720497 31.801242 31.801242C683.726708 636.024845 671.006211 648.745342 651.925466 648.745342z" fill="#000000" p-id="25846"></path><path d="M512 464.298137c-6.360248 0-19.080745 0-25.440994-6.360248L352.993789 324.372671c-12.720497-12.720497-12.720497-31.801242 0-44.521739 12.720497-12.720497 31.801242-12.720497 44.521739 0l133.565217 133.565217c12.720497 12.720497 12.720497 31.801242 0 44.521739C524.720497 464.298137 518.360248 464.298137 512 464.298137z" fill="#000000" p-id="25847"></path><path d="M512 464.298137c-6.360248 0-19.080745 0-25.440994-6.360248-12.720497-12.720497-12.720497-31.801242 0-44.521739l133.565217-133.565217c12.720497-12.720497 31.801242-12.720497 44.521739 0 12.720497 12.720497 12.720497 31.801242 0 44.521739L531.080745 457.937888C524.720497 464.298137 518.360248 464.298137 512 464.298137z" fill="#000000" p-id="25848"></path><path d="M512 1017.639752c-279.850932 0-508.819876-228.968944-508.819876-508.819876s228.968944-508.819876 508.819876-508.819876 508.819876 228.968944 508.819876 508.819876c0 25.440994 0 50.881988-6.360248 82.68323 0 19.080745-19.080745 31.801242-38.161491 25.440994-19.080745 0-31.801242-19.080745-25.440994-38.161491 6.360248-25.440994 6.360248-44.521739 6.360248-69.962733 0-248.049689-197.167702-445.217391-445.217391-445.217391S66.782609 267.130435 66.782609 515.180124s197.167702 445.217391 445.217391 445.217391c25.440994 0 57.242236 0 82.68323-6.360248 19.080745-6.360248 31.801242 6.360248 38.161491 25.440994 6.360248 19.080745-6.360248 31.801242-25.440994 38.161491C575.602484 1017.639752 543.801242 1017.639752 512 1017.639752z" fill="#000000" p-id="25849"></path><path d="M989.018634 864.993789l-318.012422 0c-19.080745 0-31.801242-12.720497-31.801242-31.801242s12.720497-31.801242 31.801242-31.801242l318.012422 0c19.080745 0 31.801242 12.720497 31.801242 31.801242S1001.73913 864.993789 989.018634 864.993789z" fill="#000000" p-id="25850"></path><path d="M830.012422 1024c-19.080745 0-31.801242-12.720497-31.801242-31.801242l0-318.012422c0-19.080745 12.720497-31.801242 31.801242-31.801242s31.801242 12.720497 31.801242 31.801242l0 318.012422C861.813665 1004.919255 842.732919 1024 830.012422 1024z" fill="#000000" p-id="25851"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/message.svg b/src/assets/svgs/message.svg
new file mode 100644
index 0000000..14ca817
--- /dev/null
+++ b/src/assets/svgs/message.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/money.svg b/src/assets/svgs/money.svg
new file mode 100644
index 0000000..c1580de
--- /dev/null
+++ b/src/assets/svgs/money.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/alipay_app.svg b/src/assets/svgs/pay/icon/alipay_app.svg
new file mode 100644
index 0000000..ebf1188
--- /dev/null
+++ b/src/assets/svgs/pay/icon/alipay_app.svg
@@ -0,0 +1 @@
+<svg t="1627279997305" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11904" width="40" height="40"><path d="M938.7008 669.525333L938.7008 249.412267c0-90.555733-73.5232-164.078933-164.1472-164.078933L249.378133 85.333333c-90.555733 0-164.078933 73.48906699-164.078933 164.078933l0 525.2096c0 90.555733 73.454933 164.078933 164.07893301 164.078933l525.20959999 0c80.725333 0 147.8656-58.368 161.553067-135.099733-43.52-18.8416-232.106667-100.283733-330.376533-147.182933-74.786133 90.589867-153.088 144.930133-271.121067 144.930133s-196.81279999-72.704-187.357867-161.655467c6.2464-58.402133 46.2848-153.9072 220.296533-137.5232 91.682133 8.6016 133.666133 25.736533 208.418133 50.414933 19.3536-35.4304 35.4304-74.513067 47.616-116.0192L292.0448 436.565333l0-32.8704 164.0448 0 0-58.9824L256 344.712533l1e-8-36.181333 200.12373299 0L456.123733 223.3344c0 0 1.809067-13.312 16.520533-13.31200001l82.056533 1e-8 0 98.474667 213.333333 0 0 36.181333-213.333333 1e-8 0 58.98239999 174.045867 0c-16.00853301 65.1264-40.277333 124.962133-70.690133 177.220267C708.608 599.176533 938.7008 669.525333 938.7008 669.525333L938.7008 669.525333 938.7008 669.525333 938.7008 669.525333zM321.57013299 744.994133c-124.7232 0-144.452267-78.7456-137.83039999-111.65013299 6.5536-32.733867 42.666667-75.502933 112.0256-75.50293301 79.6672 0 151.04 20.445867 236.714667 62.088533C472.302933 698.333867 398.370133 744.994133 321.57013299 744.994133L321.57013299 744.994133 321.57013299 744.994133zM321.57013299 744.994133" fill="#1296db" p-id="11905"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/alipay_bar.svg b/src/assets/svgs/pay/icon/alipay_bar.svg
new file mode 100644
index 0000000..eb1e1e8
--- /dev/null
+++ b/src/assets/svgs/pay/icon/alipay_bar.svg
@@ -0,0 +1,2 @@
+<?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="1627279586085" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6737" xmlns:xlink="http://www.w3.org/1999/xlink" width="40.46875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
+</style></defs><path d="M27.587124 336.619083h69.148134a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916A13.978733 13.978733 0 0 0 96.735258 0.011183H27.587124a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m165.880969 0h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-27.584701a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m138.109886 322.629167h-110.525185a27.771084 27.771084 0 0 0-27.584701 28.14385v111.829867a27.771084 27.771084 0 0 0 27.584701 28.14385h110.525185a27.957467 27.957467 0 0 0 27.584701-28.14385v-111.829867a27.957467 27.957467 0 0 0-27.584701-28.14385z m484.596091-322.629167h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-27.5847a13.978733 13.978733 0 0 0-13.978734 13.978733v308.650434a13.978733 13.978733 0 0 0 13.978734 13.978733z m-469.871825 0H428.68358a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916A13.978733 13.978733 0 0 0 428.68358 0.011183h-83.126867a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m594.189361 0h69.148134a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-69.148135a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m-412.279444 126.181367H66.91396A67.470687 67.470687 0 0 0 0.002423 530.830286v425.139878a67.470687 67.470687 0 0 0 66.911537 68.029836h418.802853a67.470687 67.470687 0 0 0 66.911537-68.029836V487.775787a24.788954 24.788954 0 0 0-24.416188-24.975337z m-58.337914 433.899885a42.681733 42.681733 0 0 1-42.495349 43.054498H125.438257a42.681733 42.681733 0 0 1-42.495349-43.054498V590.100115a42.681733 42.681733 0 0 1 42.495349-43.054498h301.940642a42.681733 42.681733 0 0 1 42.495349 43.054498z m525.22761-433.899885a41.749817 41.749817 0 0 0-41.377051 42.122583v55.914934a41.377051 41.377051 0 1 0 82.940485 0v-55.914934a41.749817 41.749817 0 0 0-41.563434-42.122583z m0 223.659734a41.749817 41.749817 0 0 0-41.377051 42.122584V894.65012a45.477479 45.477479 0 0 1-45.291096 45.850246h-159.730327a43.240882 43.240882 0 0 0-43.613649 37.276622A41.9362 41.9362 0 0 0 745.534871 1024h233.538039a57.778765 57.778765 0 0 0 57.405999-58.337914V729.3283a41.749817 41.749817 0 0 0-41.377051-41.9362zM732.488053 322.64035V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-82.940485a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733h82.940485a13.978733 13.978733 0 0 0 13.79235-13.978733zM532.126208 0.011183c-11.36937 0-20.688525 6.337026-20.688526 13.978733v308.650434c0 7.828091 9.319156 13.978733 20.688526 13.978733s20.688525-6.337026 20.688525-13.978733V13.989916c0-7.641708-9.319156-13.978733-20.688525-13.978733z" p-id="6738" fill="#1977FD"></path><path d="M745.534871 462.80045a41.749817 41.749817 0 0 0-41.377051 42.122583v252.549117a41.377051 41.377051 0 1 0 82.940485 0V504.923033A41.749817 41.749817 0 0 0 745.534871 462.80045" p-id="6739" fill="#1977FD"></path></svg>
diff --git a/src/assets/svgs/pay/icon/alipay_pc.svg b/src/assets/svgs/pay/icon/alipay_pc.svg
new file mode 100644
index 0000000..2a75277
--- /dev/null
+++ b/src/assets/svgs/pay/icon/alipay_pc.svg
@@ -0,0 +1 @@
+<svg t="1627279878333" class="icon" viewBox="0 0 1285 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8535" width="40" height="40"><path d="M1141.76 855.04h-286.72c0 40.96 30.72 71.68 71.68 71.68h107.52c20.48 0 35.84 15.36 35.84 35.84s-15.36 35.84-35.84 35.84h-783.36c-20.48 0-35.84-15.36-35.84-35.84s15.36-35.84 35.84-35.84h107.52c40.96 0 71.68-30.72 71.68-71.68h-286.72c-76.8 0-143.36-61.44-143.36-143.36v-568.32c0-76.8 61.44-143.36 143.36-143.36h993.28c76.8 0 143.36 61.44 143.36 143.36v568.32c5.12 76.8-56.32 143.36-138.24 143.36z m71.68-711.68c0-40.96-30.72-71.68-71.68-71.68h-993.28c-40.96 0-71.68 30.72-71.68 71.68v568.32c0 40.96 30.72 71.68 71.68 71.68h993.28c40.96 0 71.68-30.72 71.68-71.68v-568.32z m-143.36 568.32h-855.04c-40.96 0-71.68-30.72-71.68-71.68v-424.96c0-40.96 30.72-71.68 71.68-71.68h855.04c40.96 0 71.68 30.72 71.68 71.68v424.96c0 40.96-30.72 71.68-71.68 71.68z" p-id="8536" fill="#1977FD"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/alipay_qr.svg b/src/assets/svgs/pay/icon/alipay_qr.svg
new file mode 100644
index 0000000..4833750
--- /dev/null
+++ b/src/assets/svgs/pay/icon/alipay_qr.svg
@@ -0,0 +1,2 @@
+<?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="1627279238245" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4112" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
+</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#1977FD" p-id="4113"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/alipay_wap.svg b/src/assets/svgs/pay/icon/alipay_wap.svg
new file mode 100644
index 0000000..87075db
--- /dev/null
+++ b/src/assets/svgs/pay/icon/alipay_wap.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="1645964864184" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8460" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" height="40"><defs><style type="text/css"></style></defs><path d="M768.3 0 255.7 0c-70.8 0-128.1 57.4-128.1 128.1l0 767.8c0 70.8 57.4 128.1 128.1 128.1L512 1024l256.3 0c70.8 0 128.1-57.4 128.1-128.1L896.4 128.1C896.4 57.3 839 0 768.3 0zM383.9 96.1c0-17.7 14.3-32 32-32l192.2 0c17.7 0 32 14.3 32 32l0 0c0 17.7-14.3 32-32 32L415.9 128.1C398.2 128.1 383.9 113.8 383.9 96.1L383.9 96.1zM512 959.9 512 959.9 512 959.9c-35.4 0-64.1-28.8-64.1-64.1 0-35.4 28.7-64.1 64.1-64.1l0 0 0 0c35.4 0 64.1 28.7 64.1 64.1C576.1 931.1 547.4 959.9 512 959.9zM832.3 755.6c0 6.7-5.4 12.2-12.2 12.2L203.9 767.8c-6.7 0-12.2-5.4-12.2-12.2L191.7 204.3c0-6.7 5.4-12.2 12.2-12.2l616.3 0c6.7 0 12.2 5.4 12.2 12.2L832.4 755.6z" p-id="8461" fill="#1977FD"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/mock.svg b/src/assets/svgs/pay/icon/mock.svg
new file mode 100644
index 0000000..e0a6857
--- /dev/null
+++ b/src/assets/svgs/pay/icon/mock.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="1747409043186" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4834" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M44.416 853.333333v-85.205333a170.666667 170.666667 0 0 1 170.666667-170.666667h170.837333a37637.589333 37637.589333 0 0 1 0-206.165333C324.309333 352.170667 281.6 285.141333 281.6 211.968c0-116.906667 90.197333-211.072 231.168-211.072 140.970667 0 230.741333 94.208 230.741333 211.072 0 73.216-40.96 140.245333-102.528 179.328 0.256 0.170667 0.256 68.906667 0 206.165333h171.989334a170.666667 170.666667 0 0 1 170.666666 170.666667V853.333333a170.666667 170.666667 0 0 1-170.666666 170.666667H215.082667a170.666667 170.666667 0 0 1-170.666667-170.666667z m84.266667-84.650666v104.277333a85.333333 85.333333 0 0 0 85.333333 85.333333H811.52a85.333333 85.333333 0 0 0 85.333333-85.333333v-104.277333a85.333333 85.333333 0 0 0-85.333333-85.333334h-256.64l8.96-342.698666c66.944-21.333333 100.394667-64.256 100.394667-128.682667 0-61.952-57.344-129.322667-151.466667-129.322667-94.122667 0-146.645333 61.610667-146.645333 129.322667 0 71.466667 34.816 114.346667 104.362666 128.682667v342.698666H214.016a85.333333 85.333333 0 0 0-85.333333 85.333334z m167.125333 138.368c-50.432 0-91.434667-41.557333-91.434667-92.586667s41.002667-92.586667 91.434667-92.586667c50.389333 0 91.434667 41.557333 91.434667 92.586667 0 24.832-9.6 48.170667-27.008 65.706667-17.237333 17.322667-40.106667 26.88-64.426667 26.88z m0-119.466667a27.093333 27.093333 0 0 0-27.306667 26.88c0 14.805333 12.245333 26.88 27.306667 26.88a27.306667 27.306667 0 0 0 19.498667-8.106667 26.453333 26.453333 0 0 0 7.808-18.773333 27.093333 27.093333 0 0 0-27.306667-26.88z" fill="#1296db" p-id="4835"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/wallet.svg b/src/assets/svgs/pay/icon/wallet.svg
new file mode 100644
index 0000000..27b09ea
--- /dev/null
+++ b/src/assets/svgs/pay/icon/wallet.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="1676209854312" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3033" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M173.077333 362.666667l91.114667-214.677334a65.6 65.6 0 0 1 86.016-34.773333c11.584 4.906667 24.96 10.282667 40.896 16.448 8.277333 3.2 16.789333 6.464 27.904 10.666667 28.202667 10.709333 39.296 14.933333 46.144 17.642666l51.477333-51.669333c28.181333-28.16 74.112-27.946667 102.570667 0.533333l195.925333 195.925334c16.426667 16.426667 23.445333 38.634667 21.056 59.904H896a42.666667 42.666667 0 0 1 42.666667 42.666666v490.666667a42.666667 42.666667 0 0 1-42.666667 42.666667H128a42.666667 42.666667 0 0 1-42.666667-42.666667V405.333333a42.666667 42.666667 0 0 1 42.666667-42.666666h45.077333z m48.96 0h39.104l169.194667-169.770667-27.328-10.389333c-11.2-4.245333-19.818667-7.530667-28.224-10.794667a1459.2 1459.2 0 0 1-42.197333-17.002667 20.522667 20.522667 0 0 0-26.901334 10.88L222.037333 362.666667z m108.842667 0h454.954667a23.509333 23.509333 0 0 0-5.290667-25.322667l-195.925333-195.925333a23.36 23.36 0 0 0-33.024-0.213334L330.88 362.666667zM128 405.333333v490.666667h768V405.333333H128z m597.333333 320a85.333333 85.333333 0 1 1 0-170.666666 85.333333 85.333333 0 0 1 0 170.666666z m0-42.666666a42.666667 42.666667 0 1 0 0-85.333334 42.666667 42.666667 0 0 0 0 85.333334z" fill="#4296d5" p-id="3034"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/wx_app.svg b/src/assets/svgs/pay/icon/wx_app.svg
new file mode 100644
index 0000000..ad40b2a
--- /dev/null
+++ b/src/assets/svgs/pay/icon/wx_app.svg
@@ -0,0 +1,2 @@
+<?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="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
+</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/wx_bar.svg b/src/assets/svgs/pay/icon/wx_bar.svg
new file mode 100644
index 0000000..11292e6
--- /dev/null
+++ b/src/assets/svgs/pay/icon/wx_bar.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="1627279586085" class="icon" viewBox="0 0 1036 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6737" xmlns:xlink="http://www.w3.org/1999/xlink" width="40.46875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</style></defs><path d="M27.587124 336.619083h69.148134a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916A13.978733 13.978733 0 0 0 96.735258 0.011183H27.587124a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m165.880969 0h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-27.584701a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m138.109886 322.629167h-110.525185a27.771084 27.771084 0 0 0-27.584701 28.14385v111.829867a27.771084 27.771084 0 0 0 27.584701 28.14385h110.525185a27.957467 27.957467 0 0 0 27.584701-28.14385v-111.829867a27.957467 27.957467 0 0 0-27.584701-28.14385z m484.596091-322.629167h27.584701a13.978733 13.978733 0 0 0 13.79235-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-27.5847a13.978733 13.978733 0 0 0-13.978734 13.978733v308.650434a13.978733 13.978733 0 0 0 13.978734 13.978733z m-469.871825 0H428.68358a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916A13.978733 13.978733 0 0 0 428.68358 0.011183h-83.126867a13.978733 13.978733 0 0 0-13.792351 13.978733v308.650434a13.978733 13.978733 0 0 0 13.792351 13.978733z m594.189361 0h69.148134a13.978733 13.978733 0 0 0 13.792351-13.978733V13.989916a13.978733 13.978733 0 0 0-14.537883-13.978733h-69.148135a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733z m-412.279444 126.181367H66.91396A67.470687 67.470687 0 0 0 0.002423 530.830286v425.139878a67.470687 67.470687 0 0 0 66.911537 68.029836h418.802853a67.470687 67.470687 0 0 0 66.911537-68.029836V487.775787a24.788954 24.788954 0 0 0-24.416188-24.975337z m-58.337914 433.899885a42.681733 42.681733 0 0 1-42.495349 43.054498H125.438257a42.681733 42.681733 0 0 1-42.495349-43.054498V590.100115a42.681733 42.681733 0 0 1 42.495349-43.054498h301.940642a42.681733 42.681733 0 0 1 42.495349 43.054498z m525.22761-433.899885a41.749817 41.749817 0 0 0-41.377051 42.122583v55.914934a41.377051 41.377051 0 1 0 82.940485 0v-55.914934a41.749817 41.749817 0 0 0-41.563434-42.122583z m0 223.659734a41.749817 41.749817 0 0 0-41.377051 42.122584V894.65012a45.477479 45.477479 0 0 1-45.291096 45.850246h-159.730327a43.240882 43.240882 0 0 0-43.613649 37.276622A41.9362 41.9362 0 0 0 745.534871 1024h233.538039a57.778765 57.778765 0 0 0 57.405999-58.337914V729.3283a41.749817 41.749817 0 0 0-41.377051-41.9362zM732.488053 322.64035V13.989916a13.978733 13.978733 0 0 0-13.79235-13.978733h-82.940485a13.978733 13.978733 0 0 0-13.79235 13.978733v308.650434a13.978733 13.978733 0 0 0 13.79235 13.978733h82.940485a13.978733 13.978733 0 0 0 13.79235-13.978733zM532.126208 0.011183c-11.36937 0-20.688525 6.337026-20.688526 13.978733v308.650434c0 7.828091 9.319156 13.978733 20.688526 13.978733s20.688525-6.337026 20.688525-13.978733V13.989916c0-7.641708-9.319156-13.978733-20.688525-13.978733z" p-id="6738" fill="#04C361"/><path d="M745.534871 462.80045a41.749817 41.749817 0 0 0-41.377051 42.122583v252.549117a41.377051 41.377051 0 1 0 82.940485 0V504.923033A41.749817 41.749817 0 0 0 745.534871 462.80045" p-id="6739" fill="#04C361"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/wx_lite.svg b/src/assets/svgs/pay/icon/wx_lite.svg
new file mode 100644
index 0000000..0c925cf
--- /dev/null
+++ b/src/assets/svgs/pay/icon/wx_lite.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="1676209433089" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2990" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M608.6 290.3c67.1 0 121.7 50.5 121.7 112.9 0 19.4-5.6 38.4-15.7 55.5-15.3 25-39.8 43.5-69.4 52.3-7.9 2.3-13.9 3.2-19.4 3.2-13 0-23.1-10.2-23.1-23.1 0-13 10.2-23.1 23.1-23.1 0.9 0 2.8 0 5.1-0.9 19.9-5.6 35.6-17.1 44.4-32.4 6-9.7 8.8-20.4 8.8-31.5 0-36.6-33.8-66.6-75-66.6-14.4 0-28.2 3.7-40.7 10.6-21.8 12.5-34.7 33.3-34.7 56v193.9c0 39.3-21.8 75.4-57.9 95.8-19.4 11.1-41.2 16.7-63.4 16.7-67.1 0-121.7-50.5-121.7-112.9 0-19.4 5.6-38.4 15.7-55.5 15.3-25 39.8-43.5 69.4-52.3 8.3-2.3 13.9-3.2 19.4-3.2 13 0 23.1 10.2 23.1 23.1 0 13-10.2 23.1-23.1 23.1-0.9 0-2.8 0-5.1 0.9-19.9 6-35.6 17.6-44.4 32.4-6 9.7-8.8 20.4-8.8 31.5 0 36.6 33.8 66.6 75.4 66.6 14.4 0 28.2-3.7 40.7-10.6 21.8-12.5 34.7-33.3 34.7-56V403.3c0-39.3 21.8-75.4 57.9-95.8 19-11.6 40.7-17.2 63-17.2zM510.8 929c231.1 0 418.4-187.3 418.4-418.4S741.9 92.1 510.8 92.1 92.4 279.5 92.4 510.6 279.7 929 510.8 929z m0 22C267.5 951 70.3 753.8 70.3 510.6S267.5 70.1 510.8 70.1s440.5 197.2 440.5 440.5S754.1 951 510.8 951z" p-id="2991" fill="#58bf6b"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/wx_native.svg b/src/assets/svgs/pay/icon/wx_native.svg
new file mode 100644
index 0000000..bf3ba2b
--- /dev/null
+++ b/src/assets/svgs/pay/icon/wx_native.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="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/pay/icon/wx_pub.svg b/src/assets/svgs/pay/icon/wx_pub.svg
new file mode 100644
index 0000000..3a6d15b
--- /dev/null
+++ b/src/assets/svgs/pay/icon/wx_pub.svg
@@ -0,0 +1,2 @@
+<?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="1627279797174" class="icon" viewBox="0 0 1260 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7665" xmlns:xlink="http://www.w3.org/1999/xlink" width="49.21875" height="40"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix") format("embedded-opentype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2") format("woff2"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff") format("woff"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf") format("truetype"), url("//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont") format("svg"); }
+</style></defs><path d="M797.14798 481.753a269.194 269.194 0 0 0 102.892-211.929C900.03998 120.99 779.02998 0 630.15698 0 481.28298 0 360.27398 120.99 360.27398 269.824c0 85.878 40.33 162.462 102.912 211.929A450.974 450.974 0 0 0 309.84198 582.774c-85.543 85.524-132.608 199.208-132.608 320.236 0 25.01 0 51.712 0.197 76.367a44.898 44.898 0 0 0 44.82 44.623h816.01a44.8 44.8 0 0 0 44.82-44.623V903.01c0-121.009-47.066-234.732-132.609-320.236a451.072 451.072 0 0 0-153.344-101.021z" p-id="7666" fill="#04C361"></path><path d="M1186.18898 580.391A378.644 378.644 0 0 0 1061.81198 473.03a223.783 223.783 0 0 0 64.237-157.657c0-49.742-15.872-96.67-45.746-136.074A225.34 225.34 0 0 0 964.70998 99.9a37.297 37.297 0 0 0-46.14 25.718c-5.592 19.89 5.79 40.724 25.6 46.356 63.114 18.196 107.363 77.135 107.363 143.4a148.913 148.913 0 0 1-81.23 133.06 38.065 38.065 0 0 0-20.363 36.608c1.32 15.203 11.58 28.16 25.975 32.65 125.479 39.601 209.703 155.038 209.703 287.173v63.074c0 20.638 16.62 37.534 37.16 37.711h0.196a37.396 37.396 0 0 0 37.337-37.336V805.06c-0.197-81.644-25.777-159.35-74.142-224.69z m-901.77-62.503a36.982 36.982 0 0 0 25.955-32.65 37.455 37.455 0 0 0-20.362-36.628 148.913 148.913 0 0 1-81.231-133.06c0-66.245 44.071-125.184 107.382-143.4a37.612 37.612 0 0 0 25.58-46.356 37.376 37.376 0 0 0-46.139-25.718 225.32 225.32 0 0 0-115.593 79.4 223.252 223.252 0 0 0-45.746 136.074c0 60.258 23.533 116.381 64.237 157.676A380.475 380.475 0 0 0 74.14498 580.569 373.839 373.839 0 0 0 0.00198 805.258v63.232c0 20.657 16.798 37.356 37.356 37.356h0.197a37.317 37.317 0 0 0 37.14-37.73V805.06c0-132.332 84.401-247.769 209.723-287.173z" p-id="7667" fill="#04C361"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/peoples.svg b/src/assets/svgs/peoples.svg
new file mode 100644
index 0000000..aab852e
--- /dev/null
+++ b/src/assets/svgs/peoples.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M95.648 118.762c0 5.035-3.563 9.121-7.979 9.121H7.98c-4.416 0-7.979-4.086-7.979-9.121C0 100.519 15.408 83.47 31.152 76.75c-9.099-6.43-15.216-17.863-15.216-30.987v-9.128c0-20.16 14.293-36.518 31.893-36.518s31.894 16.358 31.894 36.518v9.122c0 13.137-6.123 24.556-15.216 30.993 15.738 6.726 31.141 23.769 31.141 42.012z"/><path d="M106.032 118.252h15.867c3.376 0 6.101-3.125 6.101-6.972 0-13.957-11.787-26.984-23.819-32.123 6.955-4.919 11.638-13.66 11.638-23.704v-6.985c0-15.416-10.928-27.926-24.39-27.926-1.674 0-3.306.193-4.89.561 1.936 4.713 3.018 9.974 3.018 15.526v9.121c0 13.137-3.056 23.111-11.066 30.993 14.842 4.41 27.312 23.42 27.541 41.509z"/></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/send.svg b/src/assets/svgs/send.svg
new file mode 100644
index 0000000..6fbc984
--- /dev/null
+++ b/src/assets/svgs/send.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="1724297262365" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1396" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M707.91 103c16.28 0 29.522 13.007 29.897 29.195l0.009 0.706v111.878a29.96 29.96 0 0 1-0.898 7.3l171.177-0.001c16.28 0 29.522 13.007 29.897 29.195l0.008 0.706v637.12c0 16.278-13.01 29.518-29.2 29.893l-0.705 0.008H270.884c-16.28 0-29.522-13.007-29.897-29.195l-0.008-0.706V787.274c0-16.514 13.389-29.9 29.905-29.9 16.28 0 29.522 13.007 29.897 29.194l0.008 0.706v101.924h577.4V311.88h-577.4v88.787c0 16.278-13.009 29.518-29.2 29.893l-0.705 0.008c-16.28 0-29.522-13.008-29.897-29.195l-0.008-0.706V281.979c0-16.278 13.009-29.518 29.2-29.893l0.705-0.008h408.019a29.916 29.916 0 0 1-0.89-6.593l-0.008-0.706v-81.978H132.808v407.113h385.787L408.223 456.982c-11.36-11.624-11.329-30.143-0.066-41.729l0.554-0.555c11.625-11.358 30.147-11.327 41.734-0.066l0.555 0.554 161.028 164.762c11.244 11.504 11.344 29.793 0.362 41.42l-0.55 0.565-161.027 161.849c-11.648 11.707-30.583 11.757-42.292 0.11-11.524-11.461-11.754-29.979-0.657-41.723l0.546-0.563 111.319-111.89H102.905c-16.28 0-29.522-13.007-29.897-29.195l-0.008-0.705V132.9c0-16.278 13.01-29.518 29.2-29.893l0.705-0.008H707.91z" p-id="1397"></path></svg>
\ No newline at end of file
diff --git a/src/assets/svgs/shopping.svg b/src/assets/svgs/shopping.svg
new file mode 100644
index 0000000..f395bc7
--- /dev/null
+++ b/src/assets/svgs/shopping.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M42.913 101.36c1.642 0 3.198.332 4.667.996a12.28 12.28 0 013.89 2.772c1.123 1.184 1.987 2.582 2.592 4.193.605 1.612.908 3.318.908 5.118 0 1.8-.303 3.507-.908 5.118-.605 1.611-1.469 3.01-2.593 4.194a13.3 13.3 0 01-3.889 2.843 10.582 10.582 0 01-4.667 1.066c-1.729 0-3.306-.355-4.732-1.066a13.604 13.604 0 01-3.825-2.843c-1.123-1.185-1.988-2.583-2.593-4.194a14.437 14.437 0 01-.907-5.118c0-1.8.302-3.506.907-5.118.605-1.61 1.47-3.009 2.593-4.193a12.515 12.515 0 013.825-2.772c1.426-.664 3.003-.996 4.732-.996zm53.932.285c1.643 0 3.22.331 4.733.995a11.386 11.386 0 013.889 2.772c1.08 1.185 1.945 2.583 2.593 4.194.648 1.61.972 3.317.972 5.118 0 1.8-.324 3.506-.972 5.117-.648 1.611-1.513 3.01-2.593 4.194a12.253 12.253 0 01-3.89 2.843 11 11 0 01-4.732 1.066 10.58 10.58 0 01-4.667-1.066 12.478 12.478 0 01-3.824-2.843c-1.08-1.185-1.945-2.583-2.593-4.194a13.581 13.581 0 01-.973-5.117c0-1.801.325-3.507.973-5.118.648-1.611 1.512-3.01 2.593-4.194a11.559 11.559 0 013.824-2.772 11.212 11.212 0 014.667-.995zm21.781-80.747c2.42 0 4.3.355 5.64 1.066 1.34.71 2.29 1.587 2.852 2.63a6.427 6.427 0 01.778 3.34c-.044 1.185-.195 2.204-.454 3.057-.26.853-.8 2.606-1.62 5.26a589.268 589.268 0 01-2.788 8.743 1236.373 1236.373 0 00-3.047 9.453c-.994 3.128-1.75 5.592-2.269 7.393-1.123 3.79-2.55 6.42-4.278 7.89-1.728 1.469-3.846 2.203-6.352 2.203H39.023l1.945 12.795h65.342c4.148 0 6.223 1.943 6.223 5.828 0 1.896-.41 3.53-1.232 4.905-.821 1.374-2.442 2.061-4.862 2.061H38.505c-1.729 0-3.176-.426-4.343-1.28-1.167-.852-2.14-1.966-2.917-3.34a21.277 21.277 0 01-1.88-4.478 44.128 44.128 0 01-1.102-4.55c-.087-.568-.324-1.942-.713-4.122-.39-2.18-.865-4.904-1.426-8.174l-1.88-10.947c-.692-4.027-1.383-8.079-2.075-12.154-1.642-9.572-3.5-20.234-5.574-31.986H6.87c-1.296 0-2.377-.356-3.24-1.067a9.024 9.024 0 01-2.14-2.558 10.416 10.416 0 01-1.167-3.2C.108 8.53 0 7.488 0 6.54c0-1.896.583-3.46 1.75-4.69C2.917.615 4.494 0 6.482 0h13.095c1.728 0 3.111.284 4.148.853 1.037.569 1.858 1.28 2.463 2.132a8.548 8.548 0 011.297 2.701c.26.948.475 1.754.648 2.417.173.758.346 1.825.519 3.199.173 1.374.345 2.772.518 4.193.26 1.706.519 3.507.778 5.403h88.678z"/></svg>
\ No newline at end of file
diff --git a/src/components/AppLinkInput/AppLinkSelectDialog.vue b/src/components/AppLinkInput/AppLinkSelectDialog.vue
new file mode 100644
index 0000000..5211f74
--- /dev/null
+++ b/src/components/AppLinkInput/AppLinkSelectDialog.vue
@@ -0,0 +1,211 @@
+<template>
+ <Dialog v-model="dialogVisible" title="閫夋嫨閾炬帴" width="65%">
+ <div class="h-500px flex gap-8px">
+ <!-- 宸︿晶鍒嗙粍鍒楄〃 -->
+ <el-scrollbar wrap-class="h-full" ref="groupScrollbar" view-class="flex flex-col">
+ <el-button
+ v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
+ :key="groupIndex"
+ :class="[
+ 'm-r-16px m-l-0px! justify-start! w-90px',
+ { active: activeGroup === group.name }
+ ]"
+ ref="groupBtnRefs"
+ :text="activeGroup !== group.name"
+ :type="activeGroup === group.name ? 'primary' : 'default'"
+ @click="handleGroupSelected(group.name)"
+ >
+ {{ group.name }}
+ </el-button>
+ </el-scrollbar>
+ <!-- 鍙充晶閾炬帴鍒楄〃 -->
+ <el-scrollbar class="h-full flex-1" @scroll="handleScroll" ref="linkScrollbar">
+ <div v-for="(group, groupIndex) in APP_LINK_GROUP_LIST" :key="groupIndex">
+ <!-- 鍒嗙粍鏍囬 -->
+ <div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div>
+ <!-- 閾炬帴鍒楄〃 -->
+ <el-tooltip
+ v-for="(appLink, appLinkIndex) in group.links"
+ :key="appLinkIndex"
+ :content="appLink.path"
+ placement="bottom"
+ :show-after="300"
+ >
+ <el-button
+ class="m-b-8px m-r-8px m-l-0px!"
+ :type="isSameLink(appLink.path, activeAppLink.path) ? 'primary' : 'default'"
+ @click="handleAppLinkSelected(appLink)"
+ >
+ {{ appLink.name }}
+ </el-button>
+ </el-tooltip>
+ </div>
+ </el-scrollbar>
+ </div>
+ <!-- 搴曢儴瀵硅瘽妗嗘搷浣滄寜閽� -->
+ <template #footer>
+ <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <Dialog v-model="detailSelectDialog.visible" title="" width="50%">
+ <el-form class="min-h-200px">
+ <el-form-item
+ label="閫夋嫨鍒嗙被"
+ v-if="detailSelectDialog.type === APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST"
+ >
+ <ProductCategorySelect
+ v-model="detailSelectDialog.id"
+ :parent-id="0"
+ @update:model-value="handleProductCategorySelected"
+ />
+ </el-form-item>
+ </el-form>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM, AppLink } from './data'
+import { ButtonInstance, ScrollbarInstance } from 'element-plus'
+import { split } from 'lodash-es'
+import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
+import { getUrlNumberValue } from '@/utils'
+
+// APP 閾炬帴閫夋嫨寮规
+defineOptions({ name: 'AppLinkSelectDialog' })
+// 閫変腑鐨勫垎缁勶紝榛樿閫変腑绗竴涓�
+const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
+// 閫変腑鐨� APP 閾炬帴
+const activeAppLink = ref({} as AppLink)
+
+/** 鎵撳紑寮圭獥 */
+const dialogVisible = ref(false)
+const open = (link: string) => {
+ // 杩涘叆椤甸潰鏃跺厛閲嶇疆 activeAppLink
+ activeAppLink.value = { name: '', path: '' }
+ dialogVisible.value = true
+
+ // 婊氬姩鍒板綋鍓嶇殑閾炬帴
+ const group = APP_LINK_GROUP_LIST.find((group) =>
+ group.links.some((linkItem) => {
+ const sameLink = isSameLink(linkItem.path, link)
+ if (sameLink) {
+ activeAppLink.value = { ...linkItem, path: link }
+ }
+ return sameLink
+ })
+ )
+ if (group) {
+ // 浣跨敤 nextTick 鐨勫師鍥狅細鍙兘 Dom 杩樻病鐢熸垚锛屽鑷存粴鍔ㄥけ璐�
+ nextTick(() => handleGroupSelected(group.name))
+ }
+}
+defineExpose({ open })
+
+// 澶勭悊 APP 閾炬帴閫変腑
+const handleAppLinkSelected = (appLink: AppLink) => {
+ // 鍙湁涓嶅悓閾炬帴鏃舵墠鏇存柊锛堥伩鍏嶉噸澶嶈Е鍙戯級
+ if (!isSameLink(appLink.path, activeAppLink.value.path)) {
+ // 濡傛灉鏂伴摼鎺ョ殑 path 涓虹┖锛屽垯娌跨敤褰撳墠 activeAppLink 鐨� path
+ const path = appLink.path || activeAppLink.value.path
+ activeAppLink.value = { ...appLink, path: path }
+ }
+ switch (appLink.type) {
+ case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
+ detailSelectDialog.value.visible = true
+ detailSelectDialog.value.type = appLink.type
+ // 杩旀樉
+ detailSelectDialog.value.id =
+ getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value.path) || undefined
+ break
+ default:
+ break
+ }
+}
+
+// 澶勭悊缁戝畾鍊兼洿鏂�
+const emit = defineEmits<{
+ change: [link: string]
+ appLinkChange: [appLink: AppLink]
+}>()
+const handleSubmit = () => {
+ dialogVisible.value = false
+ emit('change', activeAppLink.value.path)
+ emit('appLinkChange', activeAppLink.value)
+}
+
+// 鍒嗙粍鏍囬寮曠敤鍒楄〃
+const groupTitleRefs = ref<HTMLInputElement[]>([])
+/**
+ * 澶勭悊鍙充晶閾炬帴鍒楄〃婊氬姩
+ * @param scrollTop 婊氬姩鏉$殑浣嶇疆
+ */
+const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
+ const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
+ // 鑾峰彇鏍囬鐨勪綅缃俊鎭�
+ const { offsetHeight, offsetTop } = titleEl
+ // 鍒ゆ柇鏍囬鏄惁鍦ㄥ彲瑙嗚寖鍥村唴
+ return scrollTop >= offsetTop && scrollTop < offsetTop + offsetHeight
+ })
+ // 鍙渶澶勭悊涓�娆�
+ if (titleEl && activeGroup.value !== titleEl.textContent) {
+ activeGroup.value = titleEl.textContent || ''
+ // 鍚屾宸︿晶鐨勬粴鍔ㄦ潯浣嶇疆
+ scrollToGroupBtn(activeGroup.value)
+ }
+}
+
+// 鍙充晶婊氬姩鏉�
+const linkScrollbar = ref<ScrollbarInstance>()
+// 澶勭悊鍒嗙粍閫変腑
+const handleGroupSelected = (group: string) => {
+ activeGroup.value = group
+ const titleRef = groupTitleRefs.value.find((item: HTMLInputElement) => item.textContent === group)
+ if (titleRef) {
+ // 婊氬姩鍒嗙粍鏍囬
+ linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
+ }
+}
+
+// 鍒嗙粍婊氬姩鏉�
+const groupScrollbar = ref<ScrollbarInstance>()
+// 鍒嗙粍寮曠敤鍒楄〃
+const groupBtnRefs = ref<ButtonInstance[]>([])
+// 鑷姩婊氬姩鍒嗙粍鎸夐挳锛岀‘淇濆垎缁勬寜閽繚鎸佸湪鍙鍖哄煙鍐�
+const scrollToGroupBtn = (group: string) => {
+ const groupBtn = groupBtnRefs.value
+ .map((btn: ButtonInstance) => btn['ref'])
+ .find((ref: HTMLButtonElement) => ref.textContent === group)
+ if (groupBtn) {
+ groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
+ }
+}
+
+// 鏄惁涓虹浉鍚岀殑閾炬帴锛堜笉姣旇緝鍙傛暟锛屽彧姣旇緝閾炬帴锛�
+const isSameLink = (link1: string, link2: string) => {
+ return split(link1, '?', 1)[0] === split(link2, '?', 1)[0]
+}
+
+// 璇︽儏閫夋嫨瀵硅瘽妗�
+const detailSelectDialog = ref<{
+ visible: boolean
+ id?: number
+ type?: APP_LINK_TYPE_ENUM
+}>({
+ visible: false,
+ id: undefined,
+ type: undefined
+})
+// 澶勭悊璇︽儏閫夋嫨
+const handleProductCategorySelected = (id: number) => {
+ const url = new URL(activeAppLink.value.path, 'http://127.0.0.1')
+ // 淇敼 id 鍙傛暟
+ url.searchParams.set('id', `${id}`)
+ // 鎺掗櫎鍩熷悕
+ activeAppLink.value.path = `${url.pathname}${url.search}`
+ // 鍏抽棴瀵硅瘽妗�
+ detailSelectDialog.value.visible = false
+ // 閲嶇疆 id
+ detailSelectDialog.value.id = undefined
+}
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/components/AppLinkInput/data.ts b/src/components/AppLinkInput/data.ts
new file mode 100644
index 0000000..c9e3678
--- /dev/null
+++ b/src/components/AppLinkInput/data.ts
@@ -0,0 +1,236 @@
+// APP 閾炬帴鍒嗙粍
+export interface AppLinkGroup {
+ // 鍒嗙粍鍚嶇О
+ name: string
+ // 閾炬帴鍒楄〃
+ links: AppLink[]
+}
+
+// APP 閾炬帴
+export interface AppLink {
+ // 閾炬帴鍚嶇О
+ name: string
+ // 閾炬帴鍦板潃
+ path: string
+ // 閾炬帴鐨勭被鍨�
+ type?: APP_LINK_TYPE_ENUM
+}
+
+// APP 閾炬帴绫诲瀷锛堥渶瑕佺壒娈婂鐞嗭紝渚嬪鍟嗗搧璇︽儏锛�
+export const enum APP_LINK_TYPE_ENUM {
+ // 鎷煎洟娲诲姩
+ ACTIVITY_COMBINATION,
+ // 绉掓潃娲诲姩
+ ACTIVITY_SECKILL,
+ // 绉垎鍟嗗煄娲诲姩
+ ACTIVITY_POINT,
+ // 鏂囩珷璇︽儏
+ ARTICLE_DETAIL,
+ // 浼樻儬鍒歌鎯�
+ COUPON_DETAIL,
+ // 鑷畾涔夐〉闈㈣鎯�
+ DIY_PAGE_DETAIL,
+ // 鍝佺被鍒楄〃
+ PRODUCT_CATEGORY_LIST,
+ // 鍟嗗搧鍒楄〃
+ PRODUCT_LIST,
+ // 鍟嗗搧璇︽儏
+ PRODUCT_DETAIL_NORMAL,
+ // 鎷煎洟鍟嗗搧璇︽儏
+ PRODUCT_DETAIL_COMBINATION,
+ // 绉掓潃鍟嗗搧璇︽儏
+ PRODUCT_DETAIL_SECKILL
+}
+
+// APP 閾炬帴鍒楄〃锛堝仛涓�涓嬫寔涔呭寲锛燂級
+export const APP_LINK_GROUP_LIST = [
+ {
+ name: '鍟嗗煄',
+ links: [
+ {
+ name: '棣栭〉',
+ path: '/pages/index/index'
+ },
+ {
+ name: '鍟嗗搧鍒嗙被',
+ path: '/pages/index/category',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST
+ },
+ {
+ name: '璐墿杞�',
+ path: '/pages/index/cart'
+ },
+ {
+ name: '涓汉涓績',
+ path: '/pages/index/user'
+ },
+ {
+ name: '鍟嗗搧鎼滅储',
+ path: '/pages/index/search'
+ },
+ {
+ name: '鑷畾涔夐〉闈�',
+ path: '/pages/index/page',
+ type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL
+ },
+ {
+ name: '瀹㈡湇',
+ path: '/pages/chat/index'
+ },
+ {
+ name: '绯荤粺璁剧疆',
+ path: '/pages/public/setting'
+ },
+ {
+ name: '甯歌闂',
+ path: '/pages/public/faq'
+ }
+ ]
+ },
+ {
+ name: '鍟嗗搧',
+ links: [
+ {
+ name: '鍟嗗搧鍒楄〃',
+ path: '/pages/goods/list',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_LIST
+ },
+ {
+ name: '鍟嗗搧璇︽儏',
+ path: '/pages/goods/index',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL
+ },
+ {
+ name: '鎷煎洟鍟嗗搧璇︽儏',
+ path: '/pages/goods/groupon',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION
+ },
+ {
+ name: '绉掓潃鍟嗗搧璇︽儏',
+ path: '/pages/goods/seckill',
+ type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL
+ }
+ ]
+ },
+ {
+ name: '钀ラ攢娲诲姩',
+ links: [
+ {
+ name: '鎷煎洟璁㈠崟',
+ path: '/pages/activity/groupon/order'
+ },
+ {
+ name: '钀ラ攢鍟嗗搧',
+ path: '/pages/activity/index'
+ },
+ {
+ name: '鎷煎洟娲诲姩',
+ path: '/pages/activity/groupon/list',
+ type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION
+ },
+ {
+ name: '绉掓潃娲诲姩',
+ path: '/pages/activity/seckill/list',
+ type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL
+ },
+ {
+ name: '绉垎鍟嗗煄娲诲姩',
+ path: '/pages/activity/point/list',
+ type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT
+ },
+ {
+ name: '绛惧埌涓績',
+ path: '/pages/app/sign'
+ },
+ {
+ name: '浼樻儬鍒镐腑蹇�',
+ path: '/pages/coupon/list'
+ },
+ {
+ name: '浼樻儬鍒歌鎯�',
+ path: '/pages/coupon/detail',
+ type: APP_LINK_TYPE_ENUM.COUPON_DETAIL
+ },
+ {
+ name: '鏂囩珷璇︽儏',
+ path: '/pages/public/richtext',
+ type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL
+ }
+ ]
+ },
+ {
+ name: '鍒嗛攢鍟嗗煄',
+ links: [
+ {
+ name: '鍒嗛攢涓績',
+ path: '/pages/commission/index'
+ },
+ {
+ name: '鎺ㄥ箍鍟嗗搧',
+ path: '/pages/commission/goods'
+ },
+ {
+ name: '鍒嗛攢璁㈠崟',
+ path: '/pages/commission/order'
+ },
+ {
+ name: '鎴戠殑鍥㈤槦',
+ path: '/pages/commission/team'
+ }
+ ]
+ },
+ {
+ name: '鏀粯',
+ links: [
+ {
+ name: '鍏呭�间綑棰�',
+ path: '/pages/pay/recharge'
+ },
+ {
+ name: '鍏呭�艰褰�',
+ path: '/pages/pay/recharge-log'
+ }
+ ]
+ },
+ {
+ name: '鐢ㄦ埛涓績',
+ links: [
+ {
+ name: '鐢ㄦ埛淇℃伅',
+ path: '/pages/user/info'
+ },
+ {
+ name: '鐢ㄦ埛璁㈠崟',
+ path: '/pages/order/list'
+ },
+ {
+ name: '鍞悗璁㈠崟',
+ path: '/pages/order/aftersale/list'
+ },
+ {
+ name: '鍟嗗搧鏀惰棌',
+ path: '/pages/user/goods-collect'
+ },
+ {
+ name: '娴忚璁板綍',
+ path: '/pages/user/goods-log'
+ },
+ {
+ name: '鍦板潃绠$悊',
+ path: '/pages/user/address/list'
+ },
+ {
+ name: '鐢ㄦ埛浣i噾',
+ path: '/pages/user/wallet/commission'
+ },
+ {
+ name: '鐢ㄦ埛浣欓',
+ path: '/pages/user/wallet/money'
+ },
+ {
+ name: '鐢ㄦ埛绉垎',
+ path: '/pages/user/wallet/score'
+ }
+ ]
+ }
+] as AppLinkGroup[]
diff --git a/src/components/AppLinkInput/index.vue b/src/components/AppLinkInput/index.vue
new file mode 100644
index 0000000..ff71382
--- /dev/null
+++ b/src/components/AppLinkInput/index.vue
@@ -0,0 +1,43 @@
+<template>
+ <el-input v-model="appLink" placeholder="杈撳叆鎴栭�夋嫨閾炬帴">
+ <template #append>
+ <el-button @click="handleOpenDialog">閫夋嫨</el-button>
+ </template>
+ </el-input>
+ <AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+// APP 閾炬帴杈撳叆妗�
+defineOptions({ name: 'AppLinkInput' })
+// 瀹氫箟灞炴��
+const props = defineProps({
+ // 褰撳墠閫変腑鐨勯摼鎺�
+ modelValue: propTypes.string.def('')
+})
+// 褰撳墠鐨勯摼鎺�
+const appLink = ref('')
+// 閫夋嫨瀵硅瘽妗�
+const dialogRef = ref()
+// 澶勭悊鎵撳紑瀵硅瘽妗�
+const handleOpenDialog = () => dialogRef.value?.open(appLink.value)
+// 澶勭悊 APP 閾炬帴閫変腑
+const handleLinkSelected = (link: string) => (appLink.value = link)
+
+// getter
+watch(
+ () => props.modelValue,
+ () => (appLink.value = props.modelValue),
+ { immediate: true }
+)
+
+// setter
+const emit = defineEmits<{
+ 'update:modelValue': [link: string]
+}>()
+watch(
+ () => appLink.value,
+ () => emit('update:modelValue', appLink.value)
+)
+</script>
diff --git a/src/components/Backtop/index.ts b/src/components/Backtop/index.ts
new file mode 100644
index 0000000..96de88d
--- /dev/null
+++ b/src/components/Backtop/index.ts
@@ -0,0 +1,3 @@
+import Backtop from './src/Backtop.vue'
+
+export { Backtop }
diff --git a/src/components/Backtop/src/Backtop.vue b/src/components/Backtop/src/Backtop.vue
new file mode 100644
index 0000000..5d79f51
--- /dev/null
+++ b/src/components/Backtop/src/Backtop.vue
@@ -0,0 +1,17 @@
+<script lang="ts" setup>
+import { ElBacktop } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'BackTop' })
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('backtop')
+</script>
+
+<template>
+ <ElBacktop
+ :class="`${prefixCls}-backtop`"
+ :target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
+ />
+</template>
diff --git a/src/components/Card/index.ts b/src/components/Card/index.ts
new file mode 100644
index 0000000..f4c0d86
--- /dev/null
+++ b/src/components/Card/index.ts
@@ -0,0 +1,3 @@
+import CardTitle from './src/CardTitle.vue'
+
+export { CardTitle }
diff --git a/src/components/Card/src/CardTitle.vue b/src/components/Card/src/CardTitle.vue
new file mode 100644
index 0000000..76a8356
--- /dev/null
+++ b/src/components/Card/src/CardTitle.vue
@@ -0,0 +1,37 @@
+<script lang="ts" setup>
+defineComponent({
+ name: 'CardTitle'
+})
+
+defineProps({
+ title: {
+ type: String,
+ required: true
+ }
+})
+</script>
+
+<template>
+ <span class="card-title">{{ title }}</span>
+</template>
+
+<style scoped lang="scss">
+.card-title {
+ font-size: 14px;
+ font-weight: 600;
+
+ &::before {
+ position: relative;
+ top: 8px;
+ left: -5px;
+ display: inline-block;
+ width: 3px;
+ height: 14px;
+ //background-color: #105cfb;
+ background: var(--el-color-primary);
+ border-radius: 5px;
+ content: '';
+ transform: translateY(-50%);
+ }
+}
+</style>
diff --git a/src/components/ColorInput/index.vue b/src/components/ColorInput/index.vue
new file mode 100644
index 0000000..63ff73c
--- /dev/null
+++ b/src/components/ColorInput/index.vue
@@ -0,0 +1,34 @@
+<template>
+ <el-input v-model="color">
+ <template #prepend>
+ <el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
+ </template>
+ </el-input>
+</template>
+
+<script setup lang="ts">
+import { propTypes } from '@/utils/propTypes'
+import { PREDEFINE_COLORS } from '@/utils/color'
+
+// 棰滆壊杈撳叆妗�
+defineOptions({ name: 'ColorInput' })
+
+const props = defineProps({
+ modelValue: propTypes.string.def('')
+})
+const emit = defineEmits(['update:modelValue'])
+const color = computed({
+ get: () => {
+ return props.modelValue
+ },
+ set: (val: string) => {
+ emit('update:modelValue', val)
+ }
+})
+</script>
+
+<style scoped lang="scss">
+:deep(.el-input-group__prepend) {
+ padding: 0;
+}
+</style>
diff --git a/src/components/ConfigGlobal/index.ts b/src/components/ConfigGlobal/index.ts
new file mode 100644
index 0000000..dda2462
--- /dev/null
+++ b/src/components/ConfigGlobal/index.ts
@@ -0,0 +1,3 @@
+import ConfigGlobal from './src/ConfigGlobal.vue'
+
+export { ConfigGlobal }
diff --git a/src/components/ConfigGlobal/src/ConfigGlobal.vue b/src/components/ConfigGlobal/src/ConfigGlobal.vue
new file mode 100644
index 0000000..af543df
--- /dev/null
+++ b/src/components/ConfigGlobal/src/ConfigGlobal.vue
@@ -0,0 +1,62 @@
+<script setup lang="ts">
+import { provide, computed, watch, onMounted } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { ComponentSize, ElConfigProvider } from 'element-plus'
+import { useLocaleStore } from '@/store/modules/locale'
+import { useWindowSize } from '@vueuse/core'
+import { useAppStore } from '@/store/modules/app'
+import { setCssVar } from '@/utils'
+import { useDesign } from '@/hooks/web/useDesign'
+
+const { variables } = useDesign()
+
+const appStore = useAppStore()
+
+const props = defineProps({
+ size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
+})
+
+provide('configGlobal', props)
+
+// 鍒濆鍖栨墍鏈変富棰樿壊
+onMounted(() => {
+ appStore.setCssVarTheme()
+})
+
+const { width } = useWindowSize()
+
+// 鐩戝惉绐楀彛鍙樺寲
+watch(
+ () => width.value,
+ (width: number) => {
+ if (width < 768) {
+ !appStore.getMobile ? appStore.setMobile(true) : undefined
+ setCssVar('--left-menu-min-width', '0')
+ appStore.setCollapse(true)
+ appStore.getLayout !== 'classic' ? appStore.setLayout('classic') : undefined
+ } else {
+ appStore.getMobile ? appStore.setMobile(false) : undefined
+ setCssVar('--left-menu-min-width', '64px')
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+// 澶氳瑷�鐩稿叧
+const localeStore = useLocaleStore()
+
+const currentLocale = computed(() => localeStore.currentLocale)
+</script>
+
+<template>
+ <ElConfigProvider
+ :namespace="variables.elNamespace"
+ :locale="currentLocale.elLocale"
+ :message="{ max: 5 }"
+ :size="size"
+ >
+ <slot></slot>
+ </ElConfigProvider>
+</template>
diff --git a/src/components/ContentDetailWrap/index.ts b/src/components/ContentDetailWrap/index.ts
new file mode 100644
index 0000000..1871cac
--- /dev/null
+++ b/src/components/ContentDetailWrap/index.ts
@@ -0,0 +1,3 @@
+import ContentDetailWrap from './src/ContentDetailWrap.vue'
+
+export { ContentDetailWrap }
diff --git a/src/components/ContentDetailWrap/src/ContentDetailWrap.vue b/src/components/ContentDetailWrap/src/ContentDetailWrap.vue
new file mode 100644
index 0000000..a9eacc0
--- /dev/null
+++ b/src/components/ContentDetailWrap/src/ContentDetailWrap.vue
@@ -0,0 +1,58 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'ContentDetailWrap' })
+
+const { t } = useI18n()
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('content-detail-wrap')
+
+defineProps({
+ title: propTypes.string.def(''),
+ message: propTypes.string.def('')
+})
+const emit = defineEmits(['back'])
+const offset = ref(85)
+const contentDetailWrap = ref()
+onMounted(() => {
+ offset.value = contentDetailWrap.value.getBoundingClientRect().top
+})
+</script>
+
+<template>
+ <div ref="contentDetailWrap" :class="[`${prefixCls}-container`]">
+ <Sticky :offset="offset">
+ <div
+ :class="[
+ `${prefixCls}-header`,
+ 'flex b-b-1 h-50px items-center text-center bg-white pr-10px'
+ ]"
+ >
+ <div :class="[`${prefixCls}-header__back`, 'flex pl-10px pr-10px ']">
+ <ElButton @click="emit('back')">
+ <Icon class="mr-5px" icon="ep:arrow-left" />
+ {{ t('common.back') }}
+ </ElButton>
+ </div>
+ <div :class="[`${prefixCls}-header__title`, 'flex flex-1 justify-center']">
+ <slot name="title">
+ <label class="text-16px font-700">{{ title }}</label>
+ </slot>
+ </div>
+ <div :class="[`${prefixCls}-header__right`, 'flex pl-10px pr-10px']">
+ <slot name="right"></slot>
+ </div>
+ </div>
+ </Sticky>
+ <div style="padding: var(--app-content-padding)">
+ <ElCard :class="[`${prefixCls}-body`, 'mb-20px']" shadow="never">
+ <div>
+ <slot></slot>
+ </div>
+ </ElCard>
+ </div>
+ </div>
+</template>
diff --git a/src/components/ContentWrap/index.ts b/src/components/ContentWrap/index.ts
new file mode 100644
index 0000000..8c22cc8
--- /dev/null
+++ b/src/components/ContentWrap/index.ts
@@ -0,0 +1,3 @@
+import ContentWrap from './src/ContentWrap.vue'
+
+export { ContentWrap }
diff --git a/src/components/ContentWrap/src/ContentWrap.vue b/src/components/ContentWrap/src/ContentWrap.vue
new file mode 100644
index 0000000..e603596
--- /dev/null
+++ b/src/components/ContentWrap/src/ContentWrap.vue
@@ -0,0 +1,36 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'ContentWrap' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('content-wrap')
+
+defineProps({
+ title: propTypes.string.def(''),
+ message: propTypes.string.def(''),
+ bodyStyle: propTypes.object.def({ padding: '10px' })
+})
+</script>
+
+<template>
+ <ElCard :body-style="bodyStyle" :class="[prefixCls, 'mb-15px']" shadow="never">
+ <template v-if="title" #header>
+ <div class="flex items-center">
+ <span class="text-16px font-700">{{ title }}</span>
+ <ElTooltip v-if="message" effect="dark" placement="right">
+ <template #content>
+ <div class="max-w-200px">{{ message }}</div>
+ </template>
+ <Icon :size="14" class="ml-5px" icon="ep:question-filled" />
+ </ElTooltip>
+ <div class="flex flex-grow pl-20px">
+ <slot name="header"></slot>
+ </div>
+ </div>
+ </template>
+ <slot></slot>
+ </ElCard>
+</template>
diff --git a/src/components/CountTo/index.ts b/src/components/CountTo/index.ts
new file mode 100644
index 0000000..2119f02
--- /dev/null
+++ b/src/components/CountTo/index.ts
@@ -0,0 +1,3 @@
+import CountTo from './src/CountTo.vue'
+
+export { CountTo }
diff --git a/src/components/CountTo/src/CountTo.vue b/src/components/CountTo/src/CountTo.vue
new file mode 100644
index 0000000..7a19bec
--- /dev/null
+++ b/src/components/CountTo/src/CountTo.vue
@@ -0,0 +1,182 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { isNumber } from '@/utils/is'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'CountTo' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('count-to')
+
+const props = defineProps({
+ startVal: propTypes.number.def(0), // 寮�濮嬫挱鏀惧��
+ endVal: propTypes.number.def(2021), // 鏈�缁堝��
+ duration: propTypes.number.def(3000), // 鍔ㄧ敾鏃堕暱
+ autoplay: propTypes.bool.def(true), // 鏄惁鑷姩鎾斁鍔ㄧ敾, 榛樿鎾斁
+ decimals: propTypes.number.validate((value: number) => value >= 0).def(0), // 鏄剧ず鐨勫皬鏁颁綅鏁�, 榛樿涓嶆樉绀哄皬鏁�
+ decimal: propTypes.string.def('.'), // 灏忔暟鍒嗛殧绗﹀彿, 榛樿涓虹偣
+ separator: propTypes.string.def(','), // 鏁板瓧姣忎笁浣嶇殑鍒嗛殧绗�, 榛樿涓洪�楀彿
+ prefix: propTypes.string.def(''), // 鍓嶇紑, 鏁板�煎墠闈㈡樉绀虹殑鍐呭
+ suffix: propTypes.string.def(''), // 鍚庣紑, 鏁板�煎悗闈㈡樉绀虹殑鍐呭
+ useEasing: propTypes.bool.def(true), // 鏄惁浣跨敤缂撳姩鏁堟灉, 榛樿鍚敤
+ easingFn: {
+ type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
+ default(t: number, b: number, c: number, d: number) {
+ return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
+ } // 缂撳姩鍑芥暟
+ }
+})
+
+const emit = defineEmits(['mounted', 'callback'])
+
+const formatNumber = (num: number | string) => {
+ const { decimals, decimal, separator, suffix, prefix } = props
+ num = Number(num).toFixed(decimals)
+ num += ''
+ const x = num.split('.')
+ let x1 = x[0]
+ const x2 = x.length > 1 ? decimal + x[1] : ''
+ const rgx = /(\d+)(\d{3})/
+ if (separator && !isNumber(separator)) {
+ while (rgx.test(x1)) {
+ x1 = x1.replace(rgx, '$1' + separator + '$2')
+ }
+ }
+ return prefix + x1 + x2 + suffix
+}
+
+const state = reactive<{
+ localStartVal: number
+ printVal: number | null
+ displayValue: string
+ paused: boolean
+ localDuration: number | null
+ startTime: number | null
+ timestamp: number | null
+ rAF: any
+ remaining: number | null
+}>({
+ localStartVal: props.startVal,
+ displayValue: formatNumber(props.startVal),
+ printVal: null,
+ paused: false,
+ localDuration: props.duration,
+ startTime: null,
+ timestamp: null,
+ remaining: null,
+ rAF: null
+})
+
+const displayValue = toRef(state, 'displayValue')
+
+onMounted(() => {
+ if (props.autoplay) {
+ start()
+ }
+ emit('mounted')
+})
+
+const getCountDown = computed(() => {
+ return props.startVal > props.endVal
+})
+
+watch([() => props.startVal, () => props.endVal], () => {
+ if (props.autoplay) {
+ start()
+ }
+})
+
+const start = () => {
+ const { startVal, duration } = props
+ state.localStartVal = startVal
+ state.startTime = null
+ state.localDuration = duration
+ state.paused = false
+ state.rAF = requestAnimationFrame(count)
+}
+
+const pauseResume = () => {
+ if (state.paused) {
+ resume()
+ state.paused = false
+ } else {
+ pause()
+ state.paused = true
+ }
+}
+
+const pause = () => {
+ cancelAnimationFrame(state.rAF)
+}
+
+const resume = () => {
+ state.startTime = null
+ state.localDuration = +(state.remaining as number)
+ state.localStartVal = +(state.printVal as number)
+ requestAnimationFrame(count)
+}
+
+const reset = () => {
+ state.startTime = null
+ cancelAnimationFrame(state.rAF)
+ state.displayValue = formatNumber(props.startVal)
+}
+
+const count = (timestamp: number) => {
+ const { useEasing, easingFn, endVal } = props
+ if (!state.startTime) state.startTime = timestamp
+ state.timestamp = timestamp
+ const progress = timestamp - state.startTime
+ state.remaining = (state.localDuration as number) - progress
+ if (useEasing) {
+ if (unref(getCountDown)) {
+ state.printVal =
+ state.localStartVal -
+ easingFn(progress, 0, state.localStartVal - endVal, state.localDuration as number)
+ } else {
+ state.printVal = easingFn(
+ progress,
+ state.localStartVal,
+ endVal - state.localStartVal,
+ state.localDuration as number
+ )
+ }
+ } else {
+ if (unref(getCountDown)) {
+ state.printVal =
+ state.localStartVal -
+ (state.localStartVal - endVal) * (progress / (state.localDuration as number))
+ } else {
+ state.printVal =
+ state.localStartVal +
+ (endVal - state.localStartVal) * (progress / (state.localDuration as number))
+ }
+ }
+ if (unref(getCountDown)) {
+ state.printVal = state.printVal < endVal ? endVal : state.printVal
+ } else {
+ state.printVal = state.printVal > endVal ? endVal : state.printVal
+ }
+ state.displayValue = formatNumber(state.printVal!)
+ if (progress < (state.localDuration as number)) {
+ state.rAF = requestAnimationFrame(count)
+ } else {
+ emit('callback')
+ }
+}
+
+defineExpose({
+ pauseResume,
+ reset,
+ start,
+ pause
+})
+</script>
+
+<template>
+ <span :class="prefixCls">
+ {{ displayValue }}
+ </span>
+</template>
diff --git a/src/components/Crontab/index.ts b/src/components/Crontab/index.ts
new file mode 100644
index 0000000..6beeef8
--- /dev/null
+++ b/src/components/Crontab/index.ts
@@ -0,0 +1,2 @@
+import Crontab from './src/Crontab.vue'
+export { Crontab }
diff --git a/src/components/Crontab/src/Crontab.vue b/src/components/Crontab/src/Crontab.vue
new file mode 100644
index 0000000..0914bb7
--- /dev/null
+++ b/src/components/Crontab/src/Crontab.vue
@@ -0,0 +1,1015 @@
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus'
+import { PropType } from 'vue'
+
+defineOptions({ name: 'Crontab' })
+
+interface shortcutsType {
+ text: string
+ value: string
+}
+
+const props = defineProps({
+ modelValue: {
+ type: String,
+ default: '* * * * * ?'
+ },
+ shortcuts: { type: Array as PropType<shortcutsType[]>, default: () => [] }
+})
+const defaultValue = ref('')
+const dialogVisible = ref(false)
+const getYear = () => {
+ let v: number[] = []
+ let y = new Date().getFullYear()
+ for (let i = 0; i < 11; i++) {
+ v.push(y + i)
+ }
+ return v
+}
+const cronValue = reactive({
+ second: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2
+ },
+ loop: {
+ start: 0,
+ end: 1
+ },
+ appoint: [] as string[]
+ },
+ minute: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2
+ },
+ loop: {
+ start: 0,
+ end: 1
+ },
+ appoint: [] as string[]
+ },
+ hour: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2
+ },
+ loop: {
+ start: 0,
+ end: 1
+ },
+ appoint: [] as string[]
+ },
+ day: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2
+ },
+ loop: {
+ start: 1,
+ end: 1
+ },
+ appoint: [] as string[]
+ },
+ month: {
+ type: '0',
+ range: {
+ start: 1,
+ end: 2
+ },
+ loop: {
+ start: 1,
+ end: 1
+ },
+ appoint: [] as string[]
+ },
+ week: {
+ type: '5',
+ range: {
+ start: '2',
+ end: '3'
+ },
+ loop: {
+ start: 0,
+ end: '2'
+ },
+ last: '2',
+ appoint: [] as string[]
+ },
+ year: {
+ type: '-1',
+ range: {
+ start: getYear()[0],
+ end: getYear()[1]
+ },
+ loop: {
+ start: getYear()[0],
+ end: 1
+ },
+ appoint: [] as string[]
+ }
+})
+const data = reactive({
+ second: ['0', '5', '15', '20', '25', '30', '35', '40', '45', '50', '55', '59'],
+ minute: ['0', '5', '15', '20', '25', '30', '35', '40', '45', '50', '55', '59'],
+ hour: [
+ '0',
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ '12',
+ '13',
+ '14',
+ '15',
+ '16',
+ '17',
+ '18',
+ '19',
+ '20',
+ '21',
+ '22',
+ '23'
+ ],
+ day: [
+ '1',
+ '2',
+ '3',
+ '4',
+ '5',
+ '6',
+ '7',
+ '8',
+ '9',
+ '10',
+ '11',
+ '12',
+ '13',
+ '14',
+ '15',
+ '16',
+ '17',
+ '18',
+ '19',
+ '20',
+ '21',
+ '22',
+ '23',
+ '24',
+ '25',
+ '26',
+ '27',
+ '28',
+ '29',
+ '30',
+ '31'
+ ],
+ month: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+ week: [
+ {
+ value: '1',
+ label: '鍛ㄦ棩'
+ },
+ {
+ value: '2',
+ label: '鍛ㄤ竴'
+ },
+ {
+ value: '3',
+ label: '鍛ㄤ簩'
+ },
+ {
+ value: '4',
+ label: '鍛ㄤ笁'
+ },
+ {
+ value: '5',
+ label: '鍛ㄥ洓'
+ },
+ {
+ value: '6',
+ label: '鍛ㄤ簲'
+ },
+ {
+ value: '7',
+ label: '鍛ㄥ叚'
+ }
+ ],
+ year: getYear()
+})
+
+const value_second = computed(() => {
+ let v = cronValue.second
+ if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.start + '/' + v.loop.end
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : '*'
+ } else {
+ return '*'
+ }
+})
+const value_minute = computed(() => {
+ let v = cronValue.minute
+ if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.start + '/' + v.loop.end
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : '*'
+ } else {
+ return '*'
+ }
+})
+const value_hour = computed(() => {
+ let v = cronValue.hour
+ if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.start + '/' + v.loop.end
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : '*'
+ } else {
+ return '*'
+ }
+})
+const value_day = computed(() => {
+ let v = cronValue.day
+ if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.start + '/' + v.loop.end
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : '*'
+ } else if (v.type == '4') {
+ return 'L'
+ } else if (v.type == '5') {
+ return '?'
+ } else {
+ return '*'
+ }
+})
+const value_month = computed(() => {
+ let v = cronValue.month
+ if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.start + '/' + v.loop.end
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : '*'
+ } else {
+ return '*'
+ }
+})
+const value_week = computed(() => {
+ let v = cronValue.week
+ if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.end + '#' + v.loop.start
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : '*'
+ } else if (v.type == '4') {
+ return v.last + 'L'
+ } else if (v.type == '5') {
+ return '?'
+ } else {
+ return '*'
+ }
+})
+const value_year = computed(() => {
+ let v = cronValue.year
+ if (v.type == '-1') {
+ return ''
+ } else if (v.type == '0') {
+ return '*'
+ } else if (v.type == '1') {
+ return v.range.start + '-' + v.range.end
+ } else if (v.type == '2') {
+ return v.loop.start + '/' + v.loop.end
+ } else if (v.type == '3') {
+ return v.appoint.length > 0 ? v.appoint.join(',') : ''
+ } else {
+ return ''
+ }
+})
+watch(
+ () => cronValue.week.type,
+ (val) => {
+ if (val != '5') {
+ cronValue.day.type = '5'
+ }
+ }
+)
+watch(
+ () => cronValue.day.type,
+ (val) => {
+ if (val != '5') {
+ cronValue.week.type = '5'
+ }
+ }
+)
+watch(
+ () => props.modelValue,
+ () => {
+ defaultValue.value = props.modelValue
+ }
+)
+onMounted(() => {
+ defaultValue.value = props.modelValue
+})
+const emit = defineEmits(['update:modelValue'])
+const select = ref()
+watch(
+ () => select.value,
+ () => {
+ if (select.value == 'custom') {
+ open()
+ } else {
+ defaultValue.value = select.value
+ emit('update:modelValue', defaultValue.value)
+ }
+ }
+)
+const open = () => {
+ set()
+ dialogVisible.value = true
+}
+const set = () => {
+ defaultValue.value = props.modelValue
+ let arr = (props.modelValue || '* * * * * ?').split(' ')
+ //绠�鍗曟鏌�
+ if (arr.length < 6) {
+ ElMessage.warning('cron琛ㄨ揪寮忛敊璇紝宸茶浆鎹负榛樿琛ㄨ揪寮�')
+ arr = '* * * * * ?'.split(' ')
+ }
+
+ //绉�
+ if (arr[0] == '*') {
+ cronValue.second.type = '0'
+ } else if (arr[0].includes('-')) {
+ cronValue.second.type = '1'
+ cronValue.second.range.start = Number(arr[0].split('-')[0])
+ cronValue.second.range.end = Number(arr[0].split('-')[1])
+ } else if (arr[0].includes('/')) {
+ cronValue.second.type = '2'
+ cronValue.second.loop.start = Number(arr[0].split('/')[0])
+ cronValue.second.loop.end = Number(arr[0].split('/')[1])
+ } else {
+ cronValue.second.type = '3'
+ cronValue.second.appoint = arr[0].split(',')
+ }
+ //鍒�
+ if (arr[1] == '*') {
+ cronValue.minute.type = '0'
+ } else if (arr[1].includes('-')) {
+ cronValue.minute.type = '1'
+ cronValue.minute.range.start = Number(arr[1].split('-')[0])
+ cronValue.minute.range.end = Number(arr[1].split('-')[1])
+ } else if (arr[1].includes('/')) {
+ cronValue.minute.type = '2'
+ cronValue.minute.loop.start = Number(arr[1].split('/')[0])
+ cronValue.minute.loop.end = Number(arr[1].split('/')[1])
+ } else {
+ cronValue.minute.type = '3'
+ cronValue.minute.appoint = arr[1].split(',')
+ }
+ //灏忔椂
+ if (arr[2] == '*') {
+ cronValue.hour.type = '0'
+ } else if (arr[2].includes('-')) {
+ cronValue.hour.type = '1'
+ cronValue.hour.range.start = Number(arr[2].split('-')[0])
+ cronValue.hour.range.end = Number(arr[2].split('-')[1])
+ } else if (arr[2].includes('/')) {
+ cronValue.hour.type = '2'
+ cronValue.hour.loop.start = Number(arr[2].split('/')[0])
+ cronValue.hour.loop.end = Number(arr[2].split('/')[1])
+ } else {
+ cronValue.hour.type = '3'
+ cronValue.hour.appoint = arr[2].split(',')
+ }
+ //鏃�
+ if (arr[3] == '*') {
+ cronValue.day.type = '0'
+ } else if (arr[3] == 'L') {
+ cronValue.day.type = '4'
+ } else if (arr[3] == '?') {
+ cronValue.day.type = '5'
+ } else if (arr[3].includes('-')) {
+ cronValue.day.type = '1'
+ cronValue.day.range.start = Number(arr[3].split('-')[0])
+ cronValue.day.range.end = Number(arr[3].split('-')[1])
+ } else if (arr[3].includes('/')) {
+ cronValue.day.type = '2'
+ cronValue.day.loop.start = Number(arr[3].split('/')[0])
+ cronValue.day.loop.end = Number(arr[3].split('/')[1])
+ } else {
+ cronValue.day.type = '3'
+ cronValue.day.appoint = arr[3].split(',')
+ }
+ //鏈�
+ if (arr[4] == '*') {
+ cronValue.month.type = '0'
+ } else if (arr[4].includes('-')) {
+ cronValue.month.type = '1'
+ cronValue.month.range.start = Number(arr[4].split('-')[0])
+ cronValue.month.range.end = Number(arr[4].split('-')[1])
+ } else if (arr[4].includes('/')) {
+ cronValue.month.type = '2'
+ cronValue.month.loop.start = Number(arr[4].split('/')[0])
+ cronValue.month.loop.end = Number(arr[4].split('/')[1])
+ } else {
+ cronValue.month.type = '3'
+ cronValue.month.appoint = arr[4].split(',')
+ }
+ //鍛�
+ if (arr[5] == '*') {
+ cronValue.week.type = '0'
+ } else if (arr[5] == '?') {
+ cronValue.week.type = '5'
+ } else if (arr[5].includes('-')) {
+ cronValue.week.type = '1'
+ cronValue.week.range.start = arr[5].split('-')[0]
+ cronValue.week.range.end = arr[5].split('-')[1]
+ } else if (arr[5].includes('#')) {
+ cronValue.week.type = '2'
+ cronValue.week.loop.start = Number(arr[5].split('#')[1])
+ cronValue.week.loop.end = arr[5].split('#')[0]
+ } else if (arr[5].includes('L')) {
+ cronValue.week.type = '4'
+ cronValue.week.last = arr[5].split('L')[0]
+ } else {
+ cronValue.week.type = '3'
+ cronValue.week.appoint = arr[5].split(',')
+ }
+ //骞�
+ if (!arr[6]) {
+ cronValue.year.type = '-1'
+ } else if (arr[6] == '*') {
+ cronValue.year.type = '0'
+ } else if (arr[6].includes('-')) {
+ cronValue.year.type = '1'
+ cronValue.year.range.start = Number(arr[6].split('-')[0])
+ cronValue.year.range.end = Number(arr[6].split('-')[1])
+ } else if (arr[6].includes('/')) {
+ cronValue.year.type = '2'
+ cronValue.year.loop.start = Number(arr[6].split('/')[1])
+ cronValue.year.loop.end = Number(arr[6].split('/')[0])
+ } else {
+ cronValue.year.type = '3'
+ cronValue.year.appoint = arr[6].split(',')
+ }
+}
+const submit = () => {
+ let year = value_year.value ? ' ' + value_year.value : ''
+ defaultValue.value =
+ value_second.value +
+ ' ' +
+ value_minute.value +
+ ' ' +
+ value_hour.value +
+ ' ' +
+ value_day.value +
+ ' ' +
+ value_month.value +
+ ' ' +
+ value_week.value +
+ year
+ emit('update:modelValue', defaultValue.value)
+ dialogVisible.value = false
+}
+
+const inputChange = () => {
+ emit('update:modelValue', defaultValue.value)
+}
+</script>
+<template>
+ <el-input v-model="defaultValue" class="input-with-select" v-bind="$attrs" @input="inputChange">
+ <template #append>
+ <el-select v-model="select" placeholder="鐢熸垚鍣�" style="width: 115px">
+ <el-option label="姣忓垎閽�" value="0 * * * * ?" />
+ <el-option label="姣忓皬鏃�" value="0 0 * * * ?" />
+ <el-option label="姣忓ぉ闆剁偣" value="0 0 0 * * ?" />
+ <el-option label="姣忔湀涓�鍙烽浂鐐�" value="0 0 0 1 * ?" />
+ <el-option label="姣忔湀鏈�鍚庝竴澶╅浂鐐�" value="0 0 0 L * ?" />
+ <el-option label="姣忓懆鏄熸湡鏃ラ浂鐐�" value="0 0 0 ? * 1" />
+ <el-option
+ v-for="(item, index) in shortcuts"
+ :key="index"
+ :label="item.text"
+ :value="item.value"
+ />
+ <el-option label="鑷畾涔�" value="custom" />
+ </el-select>
+ </template>
+ </el-input>
+
+ <el-dialog
+ v-model="dialogVisible"
+ :width="580"
+ append-to-body
+ destroy-on-close
+ title="cron瑙勫垯鐢熸垚鍣�"
+ >
+ <div class="sc-cron">
+ <el-tabs>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>绉�</h2>
+ <h4>{{ value_second }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.second.type">
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.second.type == '1'" label="鑼冨洿">
+ <el-input-number
+ v-model="cronValue.second.range.start"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ <span style="padding: 0 15px">-</span>
+ <el-input-number
+ v-model="cronValue.second.range.end"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-form-item v-if="cronValue.second.type == '2'" label="闂撮殧">
+ <el-input-number
+ v-model="cronValue.second.loop.start"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ 绉掑紑濮嬶紝姣�
+ <el-input-number
+ v-model="cronValue.second.loop.end"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ 绉掓墽琛屼竴娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.second.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.second.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.second"
+ :key="index"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>鍒嗛挓</h2>
+ <h4>{{ value_minute }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.minute.type">
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.minute.type == '1'" label="鑼冨洿">
+ <el-input-number
+ v-model="cronValue.minute.range.start"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ <span style="padding: 0 15px">-</span>
+ <el-input-number
+ v-model="cronValue.minute.range.end"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-form-item v-if="cronValue.minute.type == '2'" label="闂撮殧">
+ <el-input-number
+ v-model="cronValue.minute.loop.start"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ 鍒嗛挓寮�濮嬶紝姣�
+ <el-input-number
+ v-model="cronValue.minute.loop.end"
+ :max="59"
+ :min="0"
+ controls-position="right"
+ />
+ 鍒嗛挓鎵ц涓�娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.minute.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.minute.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.minute"
+ :key="index"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>灏忔椂</h2>
+ <h4>{{ value_hour }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.hour.type">
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.hour.type == '1'" label="鑼冨洿">
+ <el-input-number
+ v-model="cronValue.hour.range.start"
+ :max="23"
+ :min="0"
+ controls-position="right"
+ />
+ <span style="padding: 0 15px">-</span>
+ <el-input-number
+ v-model="cronValue.hour.range.end"
+ :max="23"
+ :min="0"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-form-item v-if="cronValue.hour.type == '2'" label="闂撮殧">
+ <el-input-number
+ v-model="cronValue.hour.loop.start"
+ :max="23"
+ :min="0"
+ controls-position="right"
+ />
+ 灏忔椂寮�濮嬶紝姣�
+ <el-input-number
+ v-model="cronValue.hour.loop.end"
+ :max="23"
+ :min="0"
+ controls-position="right"
+ />
+ 灏忔椂鎵ц涓�娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.hour.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.hour.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.hour"
+ :key="index"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>鏃�</h2>
+ <h4>{{ value_day }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.day.type">
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ <el-radio-button value="4">鏈湀鏈�鍚庝竴澶�</el-radio-button>
+ <el-radio-button value="5">涓嶆寚瀹�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.day.type == '1'" label="鑼冨洿">
+ <el-input-number
+ v-model="cronValue.day.range.start"
+ :max="31"
+ :min="1"
+ controls-position="right"
+ />
+ <span style="padding: 0 15px">-</span>
+ <el-input-number
+ v-model="cronValue.day.range.end"
+ :max="31"
+ :min="1"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-form-item v-if="cronValue.day.type == '2'" label="闂撮殧">
+ <el-input-number
+ v-model="cronValue.day.loop.start"
+ :max="31"
+ :min="1"
+ controls-position="right"
+ />
+ 鍙峰紑濮嬶紝姣�
+ <el-input-number
+ v-model="cronValue.day.loop.end"
+ :max="31"
+ :min="1"
+ controls-position="right"
+ />
+ 澶╂墽琛屼竴娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.day.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.day.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.day"
+ :key="index"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>鏈�</h2>
+ <h4>{{ value_month }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.month.type">
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.month.type == '1'" label="鑼冨洿">
+ <el-input-number
+ v-model="cronValue.month.range.start"
+ :max="12"
+ :min="1"
+ controls-position="right"
+ />
+ <span style="padding: 0 15px">-</span>
+ <el-input-number
+ v-model="cronValue.month.range.end"
+ :max="12"
+ :min="1"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-form-item v-if="cronValue.month.type == '2'" label="闂撮殧">
+ <el-input-number
+ v-model="cronValue.month.loop.start"
+ :max="12"
+ :min="1"
+ controls-position="right"
+ />
+ 鏈堝紑濮嬶紝姣�
+ <el-input-number
+ v-model="cronValue.month.loop.end"
+ :max="12"
+ :min="1"
+ controls-position="right"
+ />
+ 鏈堟墽琛屼竴娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.month.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.month.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.month"
+ :key="index"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>鍛�</h2>
+ <h4>{{ value_week }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.week.type">
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ <el-radio-button value="4">鏈湀鏈�鍚庝竴鍛�</el-radio-button>
+ <el-radio-button value="5">涓嶆寚瀹�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.week.type == '1'" label="鑼冨洿">
+ <el-select v-model="cronValue.week.range.start">
+ <el-option
+ v-for="(item, index) in data.week"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <span style="padding: 0 15px">-</span>
+ <el-select v-model="cronValue.week.range.end">
+ <el-option
+ v-for="(item, index) in data.week"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="cronValue.week.type == '2'" label="闂撮殧">
+ 绗�
+ <el-input-number
+ v-model="cronValue.week.loop.start"
+ :max="4"
+ :min="1"
+ controls-position="right"
+ />
+ 鍛ㄧ殑鏄熸湡
+ <el-select v-model="cronValue.week.loop.end">
+ <el-option
+ v-for="(item, index) in data.week"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ 鎵ц涓�娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.week.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.week.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.week"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="cronValue.week.type == '4'" label="鏈�鍚庝竴鍛�">
+ <el-select v-model="cronValue.week.last">
+ <el-option
+ v-for="(item, index) in data.week"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-form>
+ </el-tab-pane>
+ <el-tab-pane>
+ <template #label>
+ <div class="sc-cron-num">
+ <h2>骞�</h2>
+ <h4>{{ value_year }}</h4>
+ </div>
+ </template>
+ <el-form>
+ <el-form-item label="绫诲瀷">
+ <el-radio-group v-model="cronValue.year.type">
+ <el-radio-button value="-1">蹇界暐</el-radio-button>
+ <el-radio-button value="0">浠绘剰鍊�</el-radio-button>
+ <el-radio-button value="1">鑼冨洿</el-radio-button>
+ <el-radio-button value="2">闂撮殧</el-radio-button>
+ <el-radio-button value="3">鎸囧畾</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="cronValue.year.type == '1'" label="鑼冨洿">
+ <el-input-number v-model="cronValue.year.range.start" controls-position="right" />
+ <span style="padding: 0 15px">-</span>
+ <el-input-number v-model="cronValue.year.range.end" controls-position="right" />
+ </el-form-item>
+ <el-form-item v-if="cronValue.year.type == '2'" label="闂撮殧">
+ <el-input-number v-model="cronValue.year.loop.start" controls-position="right" />
+ 骞村紑濮嬶紝姣�
+ <el-input-number
+ v-model="cronValue.year.loop.end"
+ :min="1"
+ controls-position="right"
+ />
+ 骞存墽琛屼竴娆�
+ </el-form-item>
+ <el-form-item v-if="cronValue.year.type == '3'" label="鎸囧畾">
+ <el-select v-model="cronValue.year.appoint" multiple style="width: 100%">
+ <el-option
+ v-for="(item, index) in data.year"
+ :key="index"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+
+ <template #footer>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submit()">纭� 璁�</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<style scoped>
+.sc-cron:deep(.el-tabs__item) {
+ height: auto;
+ padding: 0 7px;
+ line-height: 1;
+ vertical-align: bottom;
+}
+
+.sc-cron-num {
+ width: 100%;
+ margin-bottom: 15px;
+ text-align: center;
+}
+
+.sc-cron-num h2 {
+ margin-bottom: 15px;
+ font-size: 12px;
+ font-weight: normal;
+}
+
+.sc-cron-num h4 {
+ display: block;
+ width: 100%;
+ height: 32px;
+ padding: 0 15px;
+ font-size: 12px;
+ line-height: 30px;
+ background: var(--el-color-primary-light-9);
+ border-radius: 4px;
+}
+
+.sc-cron:deep(.el-tabs__item.is-active) .sc-cron-num h4 {
+ color: #fff;
+ background: var(--el-color-primary);
+}
+
+[data-theme='dark'] .sc-cron-num h4 {
+ background: var(--el-color-white);
+}
+
+.input-with-select .el-input-group__prepend {
+ background-color: var(--el-fill-color-blank);
+}
+</style>
diff --git a/src/components/Cropper/index.ts b/src/components/Cropper/index.ts
new file mode 100644
index 0000000..8fcc618
--- /dev/null
+++ b/src/components/Cropper/index.ts
@@ -0,0 +1,4 @@
+import CropperImage from './src/Cropper.vue'
+import CropperAvatar from './src/CropperAvatar.vue'
+
+export { CropperImage, CropperAvatar }
diff --git a/src/components/Cropper/src/CopperModal.vue b/src/components/Cropper/src/CopperModal.vue
new file mode 100644
index 0000000..d9a4e34
--- /dev/null
+++ b/src/components/Cropper/src/CopperModal.vue
@@ -0,0 +1,261 @@
+<template>
+ <div @click.stop>
+ <Dialog
+ v-model="dialogVisible"
+ :canFullscreen="false"
+ :title="t('cropper.modalTitle')"
+ maxHeight="380px"
+ width="800px"
+ >
+ <div :class="prefixCls">
+ <div :class="`${prefixCls}-left`">
+ <div :class="`${prefixCls}-cropper`">
+ <CropperImage
+ v-if="src"
+ :circled="circled"
+ :src="src"
+ height="300px"
+ @cropend="handleCropend"
+ @ready="handleReady"
+ />
+ </div>
+
+ <div :class="`${prefixCls}-toolbar`">
+ <el-upload :beforeUpload="handleBeforeUpload" :fileList="[]" accept="image/*">
+ <el-tooltip :content="t('cropper.selectImage')" placement="bottom">
+ <XButton preIcon="ant-design:upload-outlined" type="primary" />
+ </el-tooltip>
+ </el-upload>
+ <el-space>
+ <el-tooltip :content="t('cropper.btn_reset')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="ant-design:reload-outlined"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('reset')"
+ />
+ </el-tooltip>
+ <el-tooltip :content="t('cropper.btn_rotate_left')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="ant-design:rotate-left-outlined"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('rotate', -45)"
+ />
+ </el-tooltip>
+ <el-tooltip :content="t('cropper.btn_rotate_right')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="ant-design:rotate-right-outlined"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('rotate', 45)"
+ />
+ </el-tooltip>
+ <el-tooltip :content="t('cropper.btn_scale_x')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="vaadin:arrows-long-h"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('scaleX')"
+ />
+ </el-tooltip>
+ <el-tooltip :content="t('cropper.btn_scale_y')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="vaadin:arrows-long-v"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('scaleY')"
+ />
+ </el-tooltip>
+ <el-tooltip :content="t('cropper.btn_zoom_in')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="ant-design:zoom-in-outlined"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('zoom', 0.1)"
+ />
+ </el-tooltip>
+ <el-tooltip :content="t('cropper.btn_zoom_out')" placement="bottom">
+ <XButton
+ :disabled="!src"
+ preIcon="ant-design:zoom-out-outlined"
+ size="small"
+ type="primary"
+ @click="handlerToolbar('zoom', -0.1)"
+ />
+ </el-tooltip>
+ </el-space>
+ </div>
+ </div>
+ <div :class="`${prefixCls}-right`">
+ <div :class="`${prefixCls}-preview`">
+ <img v-if="previewSource" :alt="t('cropper.preview')" :src="previewSource" />
+ </div>
+ <template v-if="previewSource">
+ <div :class="`${prefixCls}-group`">
+ <el-avatar :src="previewSource" size="large" />
+ <el-avatar :size="48" :src="previewSource" />
+ <el-avatar :size="64" :src="previewSource" />
+ <el-avatar :size="80" :src="previewSource" />
+ </div>
+ </template>
+ </div>
+ </div>
+ <template #footer>
+ <el-button type="primary" @click="handleOk">{{ t('cropper.okText') }}</el-button>
+ </template>
+ </Dialog>
+ </div>
+</template>
+<script lang="ts" setup>
+import { useDesign } from '@/hooks/web/useDesign'
+import { dataURLtoBlob } from '@/utils/filt'
+import { useI18n } from 'vue-i18n'
+import type { CropendResult, Cropper } from './types'
+import { propTypes } from '@/utils/propTypes'
+import { CropperImage } from '@/components/Cropper'
+
+defineOptions({ name: 'CopperModal' })
+
+const props = defineProps({
+ srcValue: propTypes.string.def(''),
+ circled: propTypes.bool.def(true)
+})
+const emit = defineEmits(['uploadSuccess'])
+const { t } = useI18n()
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('cropper-am')
+
+const src = ref(props.srcValue)
+const previewSource = ref('')
+const cropper = ref<Cropper>()
+const dialogVisible = ref(false)
+let filename = ''
+let scaleX = 1
+let scaleY = 1
+
+// Block upload
+function handleBeforeUpload(file: File) {
+ const reader = new FileReader()
+ reader.readAsDataURL(file)
+ src.value = ''
+ previewSource.value = ''
+ reader.onload = function (e) {
+ src.value = (e.target?.result as string) ?? ''
+ filename = file.name
+ }
+ return false
+}
+
+function handleCropend({ imgBase64 }: CropendResult) {
+ previewSource.value = imgBase64
+}
+
+function handleReady(cropperInstance: Cropper) {
+ cropper.value = cropperInstance
+}
+
+function handlerToolbar(event: string, arg?: number) {
+ if (event === 'scaleX') {
+ scaleX = arg = scaleX === -1 ? 1 : -1
+ }
+ if (event === 'scaleY') {
+ scaleY = arg = scaleY === -1 ? 1 : -1
+ }
+ cropper?.value?.[event]?.(arg)
+}
+
+async function handleOk() {
+ const blob = dataURLtoBlob(previewSource.value)
+ emit('uploadSuccess', { source: previewSource.value, data: blob, filename: filename })
+}
+
+function openModal() {
+ dialogVisible.value = true
+}
+
+function closeModal() {
+ dialogVisible.value = false
+}
+
+defineExpose({ openModal, closeModal })
+</script>
+<style lang="scss">
+$prefix-cls: #{$namespace}-cropper-am;
+
+.#{$prefix-cls} {
+ display: flex;
+
+ &-left,
+ &-right {
+ height: 340px;
+ }
+
+ &-left {
+ width: 55%;
+ }
+
+ &-right {
+ width: 45%;
+ }
+
+ &-cropper {
+ height: 300px;
+ background: #eee;
+ background-image: linear-gradient(
+ 45deg,
+ rgb(0 0 0 / 25%) 25%,
+ transparent 0,
+ transparent 75%,
+ rgb(0 0 0 / 25%) 0
+ ),
+ linear-gradient(
+ 45deg,
+ rgb(0 0 0 / 25%) 25%,
+ transparent 0,
+ transparent 75%,
+ rgb(0 0 0 / 25%) 0
+ );
+ background-position:
+ 0 0,
+ 12px 12px;
+ background-size: 24px 24px;
+ }
+
+ &-toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 10px;
+ }
+
+ &-preview {
+ width: 220px;
+ height: 220px;
+ margin: 0 auto;
+ overflow: hidden;
+ border: 1px solid;
+ border-radius: 50%;
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ &-group {
+ display: flex;
+ padding-top: 8px;
+ margin-top: 8px;
+ border-top: 1px solid;
+ justify-content: space-around;
+ align-items: center;
+ }
+}
+</style>
diff --git a/src/components/Cropper/src/Cropper.vue b/src/components/Cropper/src/Cropper.vue
new file mode 100644
index 0000000..871aed8
--- /dev/null
+++ b/src/components/Cropper/src/Cropper.vue
@@ -0,0 +1,183 @@
+<template>
+ <div :class="getClass" :style="getWrapperStyle">
+ <img
+ v-show="isReady"
+ ref="imgElRef"
+ :alt="alt"
+ :crossorigin="crossorigin"
+ :src="src"
+ :style="getImageStyle"
+ />
+ </div>
+</template>
+<script lang="ts" setup>
+import { CSSProperties, PropType } from 'vue'
+import Cropper from 'cropperjs'
+import 'cropperjs/dist/cropper.css'
+import { useDesign } from '@/hooks/web/useDesign'
+import { propTypes } from '@/utils/propTypes'
+import { useDebounceFn } from '@vueuse/core'
+
+defineOptions({ name: 'Cropper' })
+
+type Options = Cropper.Options
+
+const defaultOptions: Options = {
+ aspectRatio: 1,
+ zoomable: true,
+ zoomOnTouch: true,
+ zoomOnWheel: true,
+ cropBoxMovable: true,
+ cropBoxResizable: true,
+ toggleDragModeOnDblclick: true,
+ autoCrop: true,
+ background: true,
+ highlight: true,
+ center: true,
+ responsive: true,
+ restore: true,
+ checkCrossOrigin: true,
+ checkOrientation: true,
+ scalable: true,
+ modal: true,
+ guides: true,
+ movable: true,
+ rotatable: true
+}
+
+const props = defineProps({
+ src: propTypes.string.def(''),
+ alt: propTypes.string.def(''),
+ circled: propTypes.bool.def(false),
+ realTimePreview: propTypes.bool.def(true),
+ height: propTypes.string.def('360px'),
+ crossorigin: {
+ type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
+ default: undefined
+ },
+ imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
+ options: { type: Object as PropType<Options>, default: () => ({}) }
+})
+
+const emit = defineEmits(['cropend', 'ready', 'cropendError'])
+const attrs = useAttrs()
+const imgElRef = ref<ElRef<HTMLImageElement>>()
+const cropper = ref<Nullable<Cropper>>()
+const isReady = ref(false)
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('cropper-image')
+const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)
+
+const getImageStyle = computed((): CSSProperties => {
+ return {
+ height: props.height,
+ maxWidth: '100%',
+ ...props.imageStyle
+ }
+})
+
+const getClass = computed(() => {
+ return [
+ prefixCls,
+ attrs.class,
+ {
+ [`${prefixCls}--circled`]: props.circled
+ }
+ ]
+})
+const getWrapperStyle = computed((): CSSProperties => {
+ return { height: `${props.height}`.replace(/px/, '') + 'px' }
+})
+
+onMounted(init)
+
+onUnmounted(() => {
+ cropper.value?.destroy()
+})
+
+async function init() {
+ const imgEl = unref(imgElRef)
+ if (!imgEl) {
+ return
+ }
+ cropper.value = new Cropper(imgEl, {
+ ...defaultOptions,
+ ready: () => {
+ isReady.value = true
+ realTimeCroppered()
+ emit('ready', cropper.value)
+ },
+ crop() {
+ debounceRealTimeCroppered()
+ },
+ zoom() {
+ debounceRealTimeCroppered()
+ },
+ cropmove() {
+ debounceRealTimeCroppered()
+ },
+ ...props.options
+ })
+}
+
+// Real-time display preview
+function realTimeCroppered() {
+ props.realTimePreview && croppered()
+}
+
+// event: return base64 and width and height information after cropping
+function croppered() {
+ if (!cropper.value) {
+ return
+ }
+ let imgInfo = cropper.value.getData()
+ const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
+ canvas.toBlob((blob) => {
+ if (!blob) {
+ return
+ }
+ let fileReader: FileReader = new FileReader()
+ fileReader.readAsDataURL(blob)
+ fileReader.onloadend = (e) => {
+ emit('cropend', {
+ imgBase64: e.target?.result ?? '',
+ imgInfo
+ })
+ }
+ fileReader.onerror = () => {
+ emit('cropendError')
+ }
+ }, 'image/png')
+}
+
+// Get a circular picture canvas
+function getRoundedCanvas() {
+ const sourceCanvas = cropper.value!.getCroppedCanvas()
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext('2d')!
+ const width = sourceCanvas.width
+ const height = sourceCanvas.height
+ canvas.width = width
+ canvas.height = height
+ context.imageSmoothingEnabled = true
+ context.drawImage(sourceCanvas, 0, 0, width, height)
+ context.globalCompositeOperation = 'destination-in'
+ context.beginPath()
+ context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
+ context.fill()
+ return canvas
+}
+</script>
+<style lang="scss">
+$prefix-cls: #{$namespace}-cropper-image;
+
+.#{$prefix-cls} {
+ &--circled {
+ .cropper-view-box,
+ .cropper-face {
+ border-radius: 50%;
+ }
+ }
+}
+</style>
diff --git a/src/components/Cropper/src/CropperAvatar.vue b/src/components/Cropper/src/CropperAvatar.vue
new file mode 100644
index 0000000..9464c2a
--- /dev/null
+++ b/src/components/Cropper/src/CropperAvatar.vue
@@ -0,0 +1,142 @@
+<template>
+ <div class="user-info-head" @click="open()">
+ <el-avatar v-if="sourceValue" :src="sourceValue" alt="avatar" class="img-circle img-lg" />
+ <el-avatar v-if="!sourceValue" :src="avatar" alt="avatar" class="img-circle img-lg" />
+ <el-button v-if="showBtn" :class="`${prefixCls}-upload-btn`" @click="open()">
+ {{ btnText ? btnText : t('cropper.selectImage') }}
+ </el-button>
+ <CopperModal
+ ref="cropperModelRef"
+ :srcValue="sourceValue"
+ @upload-success="handleUploadSuccess"
+ />
+ </div>
+</template>
+<script lang="ts" setup>
+import { useDesign } from '@/hooks/web/useDesign'
+
+import { propTypes } from '@/utils/propTypes'
+import { useI18n } from 'vue-i18n'
+import CopperModal from './CopperModal.vue'
+import avatar from '@/assets/imgs/avatar.gif'
+
+defineOptions({ name: 'CropperAvatar' })
+
+const props = defineProps({
+ width: propTypes.string.def('200px'),
+ value: propTypes.string.def(''),
+ showBtn: propTypes.bool.def(true),
+ btnText: propTypes.string.def('')
+})
+
+const emit = defineEmits(['update:value', 'change'])
+const sourceValue = ref(props.value)
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('cropper-avatar')
+const message = useMessage()
+const { t } = useI18n()
+
+const cropperModelRef = ref()
+
+watchEffect(() => {
+ sourceValue.value = props.value
+})
+
+watch(
+ () => sourceValue.value,
+ (v: string) => {
+ emit('update:value', v)
+ }
+)
+
+function handleUploadSuccess({ source, data, filename }) {
+ sourceValue.value = source
+ emit('change', { source, data, filename })
+ message.success(t('cropper.uploadSuccess'))
+}
+
+function open() {
+ cropperModelRef.value.openModal()
+}
+
+function close() {
+ cropperModelRef.value.closeModal()
+}
+
+defineExpose({
+ open,
+ close
+})
+</script>
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}--cropper-avatar;
+
+.#{$prefix-cls} {
+ display: inline-block;
+ text-align: center;
+
+ &-image-wrapper {
+ overflow: hidden;
+ cursor: pointer;
+ border: 1px solid;
+ border-radius: 50%;
+
+ img {
+ width: 100%;
+ }
+ }
+
+ &-image-mask {
+ position: absolute;
+ width: inherit;
+ height: inherit;
+ cursor: pointer;
+ background: rgb(0 0 0 / 40%);
+ border: inherit;
+ border-radius: inherit;
+ opacity: 0;
+ transition: opacity 0.4s;
+
+ ::v-deep(svg) {
+ margin: auto;
+ }
+ }
+
+ &-image-mask:hover {
+ opacity: 40;
+ }
+
+ &-upload-btn {
+ margin: 10px auto;
+ }
+}
+
+.user-info-head {
+ position: relative;
+ display: inline-block;
+}
+
+.img-circle {
+ border-radius: 50%;
+}
+
+.img-lg {
+ width: 120px;
+ height: 120px;
+}
+
+.user-info-head:hover::after {
+ position: absolute;
+ inset: 0;
+ font-size: 24px;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-style: normal;
+ line-height: 110px;
+ color: #eee;
+ cursor: pointer;
+ background: rgb(0 0 0 / 50%);
+ border-radius: 50%;
+ content: '+';
+}
+</style>
diff --git a/src/components/Cropper/src/types.ts b/src/components/Cropper/src/types.ts
new file mode 100644
index 0000000..bcad3b4
--- /dev/null
+++ b/src/components/Cropper/src/types.ts
@@ -0,0 +1,8 @@
+import type Cropper from 'cropperjs'
+
+export interface CropendResult {
+ imgBase64: string
+ imgInfo: Cropper.Data
+}
+
+export type { Cropper }
diff --git a/src/components/DeptSelectForm/index.vue b/src/components/DeptSelectForm/index.vue
new file mode 100644
index 0000000..140f495
--- /dev/null
+++ b/src/components/DeptSelectForm/index.vue
@@ -0,0 +1,122 @@
+<template>
+ <Dialog v-model="dialogVisible" title="閮ㄩ棬閫夋嫨" width="600">
+ <el-row v-loading="formLoading">
+ <el-col :span="24">
+ <ContentWrap class="h-1/1">
+ <el-tree
+ ref="treeRef"
+ :data="deptTree"
+ :props="defaultProps"
+ show-checkbox
+ :check-strictly="checkStrictly"
+ check-on-click-node
+ default-expand-all
+ highlight-current
+ node-key="id"
+ @check="handleCheck"
+ />
+ </ContentWrap>
+ </el-col>
+ </el-row>
+ <template #footer>
+ <el-button
+ :disabled="formLoading || !selectedDeptIds?.length"
+ type="primary"
+ @click="submitForm"
+ >
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+defineOptions({ name: 'DeptSelectForm' })
+
+const emit = defineEmits<{
+ confirm: [deptList: any[]]
+}>()
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+ // 鏄惁涓ユ牸鐨勯伒寰埗瀛愪笉浜掔浉鍏宠仈
+ checkStrictly: {
+ type: Boolean,
+ default: false
+ },
+ // 鏄惁鏀寔澶氶��
+ multiple: {
+ type: Boolean,
+ default: true
+ }
+})
+
+const treeRef = ref()
+const deptTree = ref<Tree[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯
+const selectedDeptIds = ref<number[]>([]) // 閫変腑鐨勯儴闂� ID 鍒楄〃
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+
+/** 鎵撳紑寮圭獥 */
+const open = async (selectedList?: DeptApi.DeptVO[]) => {
+ resetForm()
+ formLoading.value = true
+ try {
+ // 鍔犺浇閮ㄩ棬鍒楄〃
+ const deptData = await DeptApi.getSimpleDeptList()
+ deptTree.value = handleTree(deptData)
+ } finally {
+ formLoading.value = false
+ }
+ dialogVisible.value = true
+ // 璁剧疆宸查�夋嫨鐨勯儴闂�
+ if (selectedList?.length) {
+ await nextTick()
+ const selectedIds = selectedList
+ .map((dept) => dept.id)
+ .filter((id): id is number => id !== undefined)
+ selectedDeptIds.value = selectedIds
+ treeRef.value?.setCheckedKeys(selectedIds)
+ }
+}
+
+/** 澶勭悊閫変腑鐘舵�佸彉鍖� */
+const handleCheck = (data: any, checked: any) => {
+ selectedDeptIds.value = treeRef.value.getCheckedKeys()
+ if (!props.multiple && selectedDeptIds.value.length > 1) {
+ // 鍗曢�夋ā寮忎笅锛屽彧淇濈暀鏈�鍚庨�夋嫨鐨勮妭鐐�
+ const lastSelectedId = selectedDeptIds.value[selectedDeptIds.value.length - 1]
+ selectedDeptIds.value = [lastSelectedId]
+ treeRef.value.setCheckedKeys([lastSelectedId])
+ }
+}
+
+/** 鎻愪氦閫夋嫨 */
+const submitForm = async () => {
+ try {
+ // 鑾峰彇閫変腑鐨勫畬鏁撮儴闂ㄦ暟鎹�
+ const checkedNodes = treeRef.value.getCheckedNodes()
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ emit('confirm', checkedNodes)
+ } finally {
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ deptTree.value = []
+ selectedDeptIds.value = []
+ if (treeRef.value) {
+ treeRef.value.setCheckedKeys([])
+ }
+}
+
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/components/Descriptions/index.ts b/src/components/Descriptions/index.ts
new file mode 100644
index 0000000..243bc39
--- /dev/null
+++ b/src/components/Descriptions/index.ts
@@ -0,0 +1,4 @@
+import Descriptions from './src/Descriptions.vue'
+import DescriptionsItemLabel from './src/DescriptionsItemLabel.vue'
+
+export { Descriptions, DescriptionsItemLabel }
diff --git a/src/components/Descriptions/src/Descriptions.vue b/src/components/Descriptions/src/Descriptions.vue
new file mode 100644
index 0000000..184d95c
--- /dev/null
+++ b/src/components/Descriptions/src/Descriptions.vue
@@ -0,0 +1,167 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import dayjs from 'dayjs'
+import { useDesign } from '@/hooks/web/useDesign'
+import { propTypes } from '@/utils/propTypes'
+import { useAppStore } from '@/store/modules/app'
+import { DescriptionsSchema } from '@/types/descriptions'
+
+defineOptions({ name: 'Descriptions' })
+
+const appStore = useAppStore()
+
+const mobile = computed(() => appStore.getMobile)
+
+const attrs = useAttrs()
+
+const slots = useSlots()
+
+const props = defineProps({
+ title: propTypes.string.def(''),
+ message: propTypes.string.def(''),
+ collapse: propTypes.bool.def(true),
+ columns: propTypes.number.def(1),
+ schema: {
+ type: Array as PropType<DescriptionsSchema[]>,
+ default: () => []
+ },
+ data: {
+ type: Object as PropType<any>,
+ default: () => ({})
+ }
+})
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('descriptions')
+
+const getBindValue = computed(() => {
+ const delArr: string[] = ['title', 'message', 'collapse', 'schema', 'data', 'class']
+ const obj = { ...attrs, ...props }
+ for (const key in obj) {
+ if (delArr.indexOf(key) !== -1) {
+ delete obj[key]
+ }
+ }
+ return obj
+})
+
+const getBindItemValue = (item: DescriptionsSchema) => {
+ const delArr: string[] = ['field']
+ const obj = { ...item }
+ for (const key in obj) {
+ if (delArr.indexOf(key) !== -1) {
+ delete obj[key]
+ }
+ }
+ return obj
+}
+
+// 鎶樺彔
+const show = ref(true)
+
+const toggleClick = () => {
+ if (props.collapse) {
+ show.value = !unref(show)
+ }
+}
+</script>
+
+<template>
+ <div
+ :class="[
+ prefixCls,
+ 'bg-[var(--el-color-white)] dark:bg-[var(--el-bg-color)] dark:border-[var(--el-border-color)] dark:border-1px'
+ ]"
+ >
+ <div
+ v-if="title"
+ :class="[
+ `${prefixCls}-header`,
+ 'h-50px flex justify-between items-center b-b-1 border-solid border-[var(--el-border-color)] px-10px cursor-pointer dark:border-[var(--el-border-color)]'
+ ]"
+ @click="toggleClick"
+ >
+ <div :class="[`${prefixCls}-header__title`, 'relative font-18px font-bold ml-10px']">
+ <div class="flex items-center">
+ {{ title }}
+ <ElTooltip v-if="message" :content="message" placement="right">
+ <Icon class="ml-5px" icon="ep:warning" />
+ </ElTooltip>
+ </div>
+ </div>
+ <Icon v-if="collapse" :icon="show ? 'ep:arrow-down' : 'ep:arrow-up'" />
+ </div>
+
+ <ElCollapseTransition>
+ <div v-show="show" :class="[`${prefixCls}-content`, 'p-10px']">
+ <ElDescriptions
+ :column="props.columns"
+ :direction="mobile ? 'vertical' : 'horizontal'"
+ border
+ v-bind="getBindValue"
+ >
+ <template v-if="slots['extra']" #extra>
+ <slot name="extra"></slot>
+ </template>
+ <ElDescriptionsItem
+ v-for="item in schema"
+ :key="item.field"
+ min-width="80"
+ v-bind="getBindItemValue(item)"
+ >
+ <template #label>
+ <slot
+ :name="`${item.field}-label`"
+ :row="{
+ label: item.label
+ }"
+ >{{ item.label }}
+ </slot>
+ </template>
+
+ <template #default>
+ <slot v-if="item.dateFormat">
+ {{
+ data[item.field] !== null ? dayjs(data[item.field]).format(item.dateFormat) : ''
+ }}
+ </slot>
+ <slot v-else-if="item.dictType">
+ <DictTag :type="item.dictType" :value="data[item.field] + ''" />
+ </slot>
+ <slot v-else :name="item.field" :row="data">
+ {{
+ item.mappedField ? data[item.mappedField] : data[item.field]
+ }}
+ </slot>
+ </template>
+ </ElDescriptionsItem>
+ </ElDescriptions>
+ </div>
+ </ElCollapseTransition>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-descriptions;
+
+.#{$prefix-cls}-header {
+ &__title {
+ &::after {
+ position: absolute;
+ top: 3px;
+ left: -10px;
+ width: 4px;
+ height: 70%;
+ background: var(--el-color-primary);
+ content: '';
+ }
+ }
+}
+
+.#{$prefix-cls}-content {
+ :deep(.#{$elNamespace}-descriptions__cell) {
+ width: 0;
+ }
+}
+</style>
diff --git a/src/components/Descriptions/src/DescriptionsItemLabel.vue b/src/components/Descriptions/src/DescriptionsItemLabel.vue
new file mode 100644
index 0000000..4efb2fb
--- /dev/null
+++ b/src/components/Descriptions/src/DescriptionsItemLabel.vue
@@ -0,0 +1,29 @@
+<script setup lang="ts">
+const { label } = defineProps({
+ label: {
+ type: String,
+ required: true
+ },
+ icon: {
+ type: String,
+ required: false
+ }
+})
+</script>
+
+<template>
+ <div class="cell-item">
+ <Icon :icon="icon" v-if="icon" style="vertical-align: middle" :size="18" />
+ {{ label }}
+ </div>
+</template>
+
+<style scoped lang="scss">
+.cell-item {
+ display: inline;
+}
+
+.cell-item::after {
+ content: ':';
+}
+</style>
diff --git a/src/components/Dialog/index.ts b/src/components/Dialog/index.ts
new file mode 100644
index 0000000..1655dad
--- /dev/null
+++ b/src/components/Dialog/index.ts
@@ -0,0 +1,3 @@
+import Dialog from './src/Dialog.vue'
+
+export { Dialog }
diff --git a/src/components/Dialog/src/Dialog.vue b/src/components/Dialog/src/Dialog.vue
new file mode 100644
index 0000000..019971c
--- /dev/null
+++ b/src/components/Dialog/src/Dialog.vue
@@ -0,0 +1,157 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { isNumber } from '@/utils/is'
+
+defineOptions({ name: 'Dialog' })
+
+const slots = useSlots()
+const emits = defineEmits(['update:modelValue'])
+
+const props = defineProps({
+ modelValue: propTypes.bool.def(false),
+ title: propTypes.string.def('Dialog'),
+ fullscreen: propTypes.bool.def(true),
+ width: propTypes.oneOfType([String, Number]).def('40%'),
+ scroll: propTypes.bool.def(false), // 鏄惁寮�鍚粴鍔ㄦ潯銆傚鏋滄槸鐨勮瘽锛屾寜鐓� maxHeight 璁剧疆鏈�澶ч珮搴�
+ maxHeight: propTypes.oneOfType([String, Number]).def('400px')
+})
+
+const getBindValue = computed(() => {
+ const delArr: string[] = ['fullscreen', 'title', 'maxHeight', 'appendToBody']
+ const attrs = useAttrs()
+ const obj = { ...attrs, ...props }
+ for (const key in obj) {
+ if (delArr.indexOf(key) !== -1) {
+ delete obj[key]
+ }
+ }
+ return obj
+})
+
+const isFullscreen = ref(false)
+
+const toggleFull = () => {
+ isFullscreen.value = !unref(isFullscreen)
+}
+
+const dialogHeight = ref(isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight)
+
+watch(
+ () => isFullscreen.value,
+ async (val: boolean) => {
+ await nextTick()
+ if (val) {
+ const windowHeight = document.documentElement.offsetHeight
+ dialogHeight.value = `${windowHeight - 55 - 60 - (slots.footer ? 63 : 0)}px`
+ } else {
+ dialogHeight.value = isNumber(props.maxHeight) ? `${props.maxHeight}px` : props.maxHeight
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+const dialogStyle = computed(() => {
+ return {
+ height: unref(dialogHeight)
+ }
+})
+
+const closing = ref(false)
+
+function closeHandler() {
+ emits('update:modelValue', false)
+ closing.value = true
+}
+
+function closedHandler() {
+ closing.value = false
+}
+</script>
+
+<template>
+ <ElDialog
+ v-bind="getBindValue"
+ :close-on-click-modal="true"
+ :fullscreen="isFullscreen"
+ :width="width"
+ destroy-on-close
+ lock-scroll
+ draggable
+ class="com-dialog"
+ :show-close="false"
+ @close="closeHandler"
+ @closed="closedHandler"
+ >
+ <template #header="{ close }">
+ <div class="relative h-54px flex items-center justify-between pl-15px pr-15px">
+ <slot name="title">
+ {{ title }}
+ </slot>
+ <div
+ class="absolute right-15px top-[50%] h-54px flex translate-y-[-50%] items-center justify-between"
+ >
+ <Icon
+ v-if="fullscreen"
+ class="is-hover mr-10px cursor-pointer"
+ :icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
+ color="var(--el-color-info)"
+ hover-color="var(--el-color-primary)"
+ @click="toggleFull"
+ />
+ <Icon
+ class="is-hover cursor-pointer"
+ icon="ep:close"
+ hover-color="var(--el-color-primary)"
+ color="var(--el-color-info)"
+ @click.stop="close"
+ />
+ </div>
+ </div>
+ </template>
+
+ <ElScrollbar v-if="scroll" :style="dialogStyle">
+ <slot></slot>
+ </ElScrollbar>
+ <slot v-else></slot>
+ <template v-if="slots.footer" #footer>
+ <div :style="{ 'pointer-events': closing ? 'none' : 'auto' }">
+ <slot name="footer"></slot>
+ </div>
+ </template>
+ </ElDialog>
+</template>
+
+<style lang="scss">
+.com-dialog {
+ .#{$elNamespace}-overlay-dialog {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .#{$elNamespace}-dialog {
+ margin: 0 !important;
+
+ &__header {
+ height: 54px;
+ padding: 0;
+ margin-right: 0 !important;
+ border-bottom: 1px solid var(--el-border-color);
+ }
+
+ &__body {
+ padding: 15px !important;
+ }
+
+ &__footer {
+ border-top: 1px solid var(--el-border-color);
+ }
+
+ &__headerbtn {
+ top: 0;
+ }
+ }
+}
+</style>
diff --git a/src/components/DictTag/index.ts b/src/components/DictTag/index.ts
new file mode 100644
index 0000000..4db2742
--- /dev/null
+++ b/src/components/DictTag/index.ts
@@ -0,0 +1,3 @@
+import DictTag from './src/DictTag.vue'
+
+export { DictTag }
diff --git a/src/components/DictTag/src/DictTag.vue b/src/components/DictTag/src/DictTag.vue
new file mode 100644
index 0000000..6414eaa
--- /dev/null
+++ b/src/components/DictTag/src/DictTag.vue
@@ -0,0 +1,90 @@
+<script lang="tsx">
+import { computed, defineComponent, PropType } from 'vue'
+import { isHexColor } from '@/utils/color'
+import { ElTag } from 'element-plus'
+import { DictDataType, getDictOptions } from '@/utils/dict'
+import { isArray, isBoolean, isNumber, isString } from '@/utils/is'
+
+export default defineComponent({
+ name: 'DictTag',
+ props: {
+ type: {
+ type: String as PropType<string>,
+ required: true
+ },
+ value: {
+ type: [String, Number, Boolean, Array],
+ required: true
+ },
+ // 瀛楃涓插垎闅旂 鍙湁褰� props.value 浼犲叆鍊间负瀛楃涓叉椂鏈夋晥
+ separator: {
+ type: String as PropType<string>,
+ default: ','
+ },
+ // 姣忎釜 tag 涔嬮棿鐨勯棿闅旓紝榛樿涓� 5px锛屽弬鑰冪殑 el-row 鐨� gutter
+ gutter: {
+ type: String as PropType<string>,
+ default: '5px'
+ }
+ },
+ setup(props) {
+ const valueArr: any = computed(() => {
+ // 1. 鏄� Number 绫诲瀷鍜� Boolean 绫诲瀷鐨勬儏鍐�
+ if (isNumber(props.value) || isBoolean(props.value)) {
+ return [String(props.value)]
+ }
+ // 2. 鏄瓧绗︿覆锛堣繘涓�姝ュ垽鏂槸鍚︽湁鍖呭惈鍒嗛殧绗﹀彿 -> props.sepSymbol 锛�
+ else if (isString(props.value)) {
+ return props.value.split(props.separator)
+ }
+ // 3. 鏁扮粍
+ else if (isArray(props.value)) {
+ return props.value.map(String)
+ }
+ return []
+ })
+ const renderDictTag = () => {
+ if (!props.type) {
+ return null
+ }
+ // 瑙e喅鑷畾涔夊瓧鍏告爣绛惧�间负闆舵椂鏍囩涓嶆覆鏌撶殑闂
+ if (props.value === undefined || props.value === null || props.value === '') {
+ return null
+ }
+ const dictOptions = getDictOptions(props.type)
+
+ return (
+ <div
+ class="dict-tag"
+ style={{
+ display: 'inline-flex',
+ gap: props.gutter,
+ justifyContent: 'center',
+ alignItems: 'center'
+ }}
+ >
+ {dictOptions.map((dict: DictDataType) => {
+ if (valueArr.value.includes(dict.value)) {
+ if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
+ dict.colorType = ''
+ }
+ return (
+ // 娣诲姞鏍囩鐨勬枃瀛楅鑹蹭负鐧借壊锛岃В鍐宠嚜瀹氫箟鑳屾櫙棰滆壊鏃舵爣绛炬枃瀛楃湅涓嶆竻鐨勯棶棰�
+ <ElTag
+ style={dict?.cssClass ? 'color: #fff' : ''}
+ type={dict?.colorType || null}
+ color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''}
+ disableTransitions={true}
+ >
+ {dict?.label}
+ </ElTag>
+ )
+ }
+ })}
+ </div>
+ )
+ }
+ return () => renderDictTag()
+ }
+})
+</script>
diff --git a/src/components/DiyEditor/components/ComponentContainer.vue b/src/components/DiyEditor/components/ComponentContainer.vue
new file mode 100644
index 0000000..4856722
--- /dev/null
+++ b/src/components/DiyEditor/components/ComponentContainer.vue
@@ -0,0 +1,239 @@
+<template>
+ <div :class="['component', { active: active }]">
+ <div
+ :style="{
+ ...style
+ }"
+ >
+ <component :is="component.id" :property="component.property" />
+ </div>
+ <div class="component-wrap">
+ <!-- 宸︿晶锛氱粍浠跺悕锛堟偓娴殑灏忚创鏉★級 -->
+ <div class="component-name" v-if="component.name">
+ {{ component.name }}
+ </div>
+ <!-- 鍙充晶锛氱粍浠舵搷浣滃伐鍏锋爮 -->
+ <div class="component-toolbar" v-if="showToolbar && component.name && active">
+ <VerticalButtonGroup type="primary">
+ <el-tooltip content="涓婄Щ" placement="right">
+ <el-button :disabled="!canMoveUp" @click.stop="handleMoveComponent(-1)">
+ <Icon icon="ep:arrow-up" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="涓嬬Щ" placement="right">
+ <el-button :disabled="!canMoveDown" @click.stop="handleMoveComponent(1)">
+ <Icon icon="ep:arrow-down" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="澶嶅埗" placement="right">
+ <el-button @click.stop="handleCopyComponent()">
+ <Icon icon="ep:copy-document" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="right">
+ <el-button @click.stop="handleDeleteComponent()">
+ <Icon icon="ep:delete" />
+ </el-button>
+ </el-tooltip>
+ </VerticalButtonGroup>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+// 娉ㄥ唽鎵�鏈夌殑缁勪欢
+import { components } from '../components/mobile/index'
+export default {
+ components: { ...components }
+}
+</script>
+<script setup lang="ts">
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { propTypes } from '@/utils/propTypes'
+import { object } from 'vue-types'
+
+/**
+ * 缁勪欢瀹瑰櫒锛氱洰鍓嶅湪涓棿閮ㄥ垎
+ * 鐢ㄤ簬鍖呰9缁勪欢锛屼负缁勪欢鎻愪緵 鑳屾櫙銆佸杈硅窛銆佸唴杈硅窛銆佽竟妗嗙瓑鏍峰紡
+ */
+defineOptions({ name: 'ComponentContainer' })
+
+type DiyComponentWithStyle = DiyComponent<any> & { property: { style?: ComponentStyle } }
+const props = defineProps({
+ component: object<DiyComponentWithStyle>().isRequired,
+ active: propTypes.bool.def(false),
+ canMoveUp: propTypes.bool.def(false),
+ canMoveDown: propTypes.bool.def(false),
+ showToolbar: propTypes.bool.def(true)
+})
+
+/**
+ * 缁勪欢鏍峰紡
+ */
+const style = computed(() => {
+ let componentStyle = props.component.property.style
+ if (!componentStyle) {
+ return {}
+ }
+ return {
+ marginTop: `${componentStyle.marginTop || 0}px`,
+ marginBottom: `${componentStyle.marginBottom || 0}px`,
+ marginLeft: `${componentStyle.marginLeft || 0}px`,
+ marginRight: `${componentStyle.marginRight || 0}px`,
+ paddingTop: `${componentStyle.paddingTop || 0}px`,
+ paddingRight: `${componentStyle.paddingRight || 0}px`,
+ paddingBottom: `${componentStyle.paddingBottom || 0}px`,
+ paddingLeft: `${componentStyle.paddingLeft || 0}px`,
+ borderTopLeftRadius: `${componentStyle.borderTopLeftRadius || 0}px`,
+ borderTopRightRadius: `${componentStyle.borderTopRightRadius || 0}px`,
+ borderBottomRightRadius: `${componentStyle.borderBottomRightRadius || 0}px`,
+ borderBottomLeftRadius: `${componentStyle.borderBottomLeftRadius || 0}px`,
+ overflow: 'hidden',
+ background:
+ componentStyle.bgType === 'color' ? componentStyle.bgColor : `url(${componentStyle.bgImg})`
+ }
+})
+
+const emits = defineEmits<{
+ (e: 'move', direction: number): void
+ (e: 'copy'): void
+ (e: 'delete'): void
+}>()
+
+/**
+ * 绉诲姩缁勪欢
+ * @param direction 绉诲姩鏂瑰悜
+ */
+const handleMoveComponent = (direction: number) => {
+ emits('move', direction)
+}
+
+/**
+ * 澶嶅埗缁勪欢
+ */
+const handleCopyComponent = () => {
+ emits('copy')
+}
+
+/**
+ * 鍒犻櫎缁勪欢
+ */
+const handleDeleteComponent = () => {
+ emits('delete')
+}
+</script>
+
+<style scoped lang="scss">
+$active-border-width: 2px;
+$hover-border-width: 1px;
+$name-position: -85px;
+$toolbar-position: -55px;
+
+/* 缁勪欢 */
+.component {
+ position: relative;
+ cursor: move;
+
+ .component-wrap {
+ position: absolute;
+ top: 0;
+ left: -$active-border-width;
+ display: block;
+ width: 100%;
+ height: 100%;
+
+ /* 榧犳爣鏀惧埌缁勪欢涓婃椂 */
+ &:hover {
+ border: $hover-border-width dashed var(--el-color-primary);
+ box-shadow: 0 0 5px 0 rgb(24 144 255 / 30%);
+
+ .component-name {
+ top: $hover-border-width;
+
+ /* 闃叉鍔犱簡杈规涔嬪悗锛屼綅缃Щ鍔� */
+ left: $name-position - $hover-border-width;
+ }
+ }
+
+ /* 宸︿晶锛氱粍浠跺悕绉� */
+ .component-name {
+ position: absolute;
+ top: $active-border-width;
+ left: $name-position;
+ display: block;
+ width: 80px;
+ height: 25px;
+ font-size: 12px;
+ color: #6a6a6a;
+ line-height: 25px;
+ text-align: center;
+ background: #fff;
+ box-shadow:
+ 0 0 4px #00000014,
+ 0 2px 6px #0000000f,
+ 0 4px 8px 2px #0000000a;
+
+ /* 鍙充晶灏忎笁瑙� */
+ &::after {
+ position: absolute;
+ top: 7.5px;
+ right: -10px;
+ width: 0;
+ height: 0;
+ border: 5px solid transparent;
+ border-left-color: #fff;
+ content: ' ';
+ }
+ }
+
+ /* 鍙充晶锛氱粍浠舵搷浣滃伐鍏锋爮 */
+ .component-toolbar {
+ position: absolute;
+ top: 0;
+ right: $toolbar-position;
+ display: none;
+
+ /* 宸︿晶灏忎笁瑙� */
+ &::before {
+ position: absolute;
+ top: 10px;
+ left: -10px;
+ width: 0;
+ height: 0;
+ border: 5px solid transparent;
+ border-right-color: #2d8cf0;
+ content: ' ';
+ }
+ }
+ }
+
+ /* 缁勪欢閫変腑鏃� */
+ &.active {
+ margin-bottom: 4px;
+
+ .component-wrap {
+ margin-bottom: $active-border-width + $active-border-width;
+ border: $active-border-width solid var(--el-color-primary) !important;
+ box-shadow: 0 0 10px 0 rgb(24 144 255 / 30%);
+
+ .component-name {
+ top: 0 !important;
+
+ /* 闃叉鍔犱簡杈规涔嬪悗锛屼綅缃Щ鍔� */
+ left: $name-position - $active-border-width !important;
+ color: #fff;
+ background: var(--el-color-primary);
+
+ &::after {
+ border-left-color: var(--el-color-primary);
+ }
+ }
+
+ .component-toolbar {
+ display: block;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/ComponentContainerProperty.vue b/src/components/DiyEditor/components/ComponentContainerProperty.vue
new file mode 100644
index 0000000..5d18785
--- /dev/null
+++ b/src/components/DiyEditor/components/ComponentContainerProperty.vue
@@ -0,0 +1,168 @@
+<template>
+ <el-tabs stretch>
+ <!-- 姣忎釜缁勪欢鐨勮嚜瀹氫箟鍐呭 -->
+ <el-tab-pane label="鍐呭" v-if="$slots.default">
+ <slot></slot>
+ </el-tab-pane>
+
+ <!-- 姣忎釜缁勪欢鐨勯�氱敤鍐呭 -->
+ <el-tab-pane label="鏍峰紡" lazy>
+ <el-card header="缁勪欢鏍峰紡" class="property-group">
+ <el-form :model="formData" label-width="80px">
+ <el-form-item label="缁勪欢鑳屾櫙" prop="bgType">
+ <el-radio-group v-model="formData.bgType">
+ <el-radio value="color">绾壊</el-radio>
+ <el-radio value="img">鍥剧墖</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="閫夋嫨棰滆壊" prop="bgColor" v-if="formData.bgType === 'color'">
+ <ColorInput v-model="formData.bgColor" />
+ </el-form-item>
+ <el-form-item label="涓婁紶鍥剧墖" prop="bgImg" v-else>
+ <UploadImg v-model="formData.bgImg" :limit="1">
+ <template #tip>寤鸿瀹藉害 750px</template>
+ </UploadImg>
+ </el-form-item>
+ <el-tree :data="treeData" :expand-on-click-node="false" default-expand-all>
+ <template #default="{ node, data }">
+ <el-form-item
+ :label="data.label"
+ :prop="data.prop"
+ :label-width="node.level === 1 ? '80px' : '62px'"
+ class="w-full m-b-0!"
+ >
+ <el-slider
+ v-model="formData[data.prop]"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ @input="handleSliderChange(data.prop)"
+ />
+ </el-form-item>
+ </template>
+ </el-tree>
+ <slot name="style" :style="formData"></slot>
+ </el-form>
+ </el-card>
+ </el-tab-pane>
+ </el-tabs>
+</template>
+
+<script setup lang="ts">
+import { ComponentStyle } from '@/components/DiyEditor/util'
+import { useVModel } from '@vueuse/core'
+
+/**
+ * 缁勪欢瀹瑰櫒灞炴�э細鐩墠鍙宠竟閮ㄥ垎
+ * 鐢ㄤ簬鍖呰9缁勪欢锛屼负缁勪欢鎻愪緵 鑳屾櫙銆佸杈硅窛銆佸唴杈硅窛銆佽竟妗嗙瓑鏍峰紡
+ */
+defineOptions({ name: 'ComponentContainer' })
+
+const props = defineProps<{ modelValue: ComponentStyle }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+const treeData = [
+ {
+ label: '澶栭儴杈硅窛',
+ prop: 'margin',
+ children: [
+ {
+ label: '涓�',
+ prop: 'marginTop'
+ },
+ {
+ label: '鍙�',
+ prop: 'marginRight'
+ },
+ {
+ label: '涓�',
+ prop: 'marginBottom'
+ },
+ {
+ label: '宸�',
+ prop: 'marginLeft'
+ }
+ ]
+ },
+ {
+ label: '鍐呴儴杈硅窛',
+ prop: 'padding',
+ children: [
+ {
+ label: '涓�',
+ prop: 'paddingTop'
+ },
+ {
+ label: '鍙�',
+ prop: 'paddingRight'
+ },
+ {
+ label: '涓�',
+ prop: 'paddingBottom'
+ },
+ {
+ label: '宸�',
+ prop: 'paddingLeft'
+ }
+ ]
+ },
+ {
+ label: '杈规鍦嗚',
+ prop: 'borderRadius',
+ children: [
+ {
+ label: '涓婂乏',
+ prop: 'borderTopLeftRadius'
+ },
+ {
+ label: '涓婂彸',
+ prop: 'borderTopRightRadius'
+ },
+ {
+ label: '涓嬪彸',
+ prop: 'borderBottomRightRadius'
+ },
+ {
+ label: '涓嬪乏',
+ prop: 'borderBottomLeftRadius'
+ }
+ ]
+ }
+]
+
+const handleSliderChange = (prop: string) => {
+ switch (prop) {
+ case 'margin':
+ formData.value.marginTop = formData.value.margin
+ formData.value.marginRight = formData.value.margin
+ formData.value.marginBottom = formData.value.margin
+ formData.value.marginLeft = formData.value.margin
+ break
+ case 'padding':
+ formData.value.paddingTop = formData.value.padding
+ formData.value.paddingRight = formData.value.padding
+ formData.value.paddingBottom = formData.value.padding
+ formData.value.paddingLeft = formData.value.padding
+ break
+ case 'borderRadius':
+ formData.value.borderTopLeftRadius = formData.value.borderRadius
+ formData.value.borderTopRightRadius = formData.value.borderRadius
+ formData.value.borderBottomRightRadius = formData.value.borderRadius
+ formData.value.borderBottomLeftRadius = formData.value.borderRadius
+ break
+ }
+}
+</script>
+
+<style scoped lang="scss">
+:deep(.el-slider__runway) {
+ margin-right: 16px;
+}
+
+:deep(.el-input-number) {
+ width: 50px;
+}
+</style>
diff --git a/src/components/DiyEditor/components/ComponentLibrary.vue b/src/components/DiyEditor/components/ComponentLibrary.vue
new file mode 100644
index 0000000..06f2312
--- /dev/null
+++ b/src/components/DiyEditor/components/ComponentLibrary.vue
@@ -0,0 +1,211 @@
+<template>
+ <el-aside class="editor-left" width="261px">
+ <el-scrollbar>
+ <el-collapse v-model="extendGroups">
+ <el-collapse-item
+ v-for="group in groups"
+ :key="group.name"
+ :name="group.name"
+ :title="group.name"
+ >
+ <draggable
+ class="component-container"
+ ghost-class="draggable-ghost"
+ item-key="index"
+ :list="group.components"
+ :sort="false"
+ :group="{ name: 'component', pull: 'clone', put: false }"
+ :clone="handleCloneComponent"
+ :animation="200"
+ :force-fallback="false"
+ >
+ <template #item="{ element }">
+ <div>
+ <div class="drag-placement">缁勪欢鏀剧疆鍖哄煙</div>
+ <div class="component">
+ <Icon :icon="element.icon" :size="32" />
+ <span class="mt-4px text-12px">{{ element.name }}</span>
+ </div>
+ </div>
+ </template>
+ </draggable>
+ </el-collapse-item>
+ </el-collapse>
+ </el-scrollbar>
+ </el-aside>
+</template>
+
+<script setup lang="ts">
+import draggable from 'vuedraggable'
+import { componentConfigs } from '../components/mobile/index'
+import { cloneDeep } from 'lodash-es'
+import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util'
+
+/** 缁勪欢搴擄細鐩墠宸︿晶鐨勩�愬熀纭�缁勪欢銆戙�併�愬浘鏂囩粍浠躲�戦儴鍒� */
+defineOptions({ name: 'ComponentLibrary' })
+
+// 缁勪欢鍒楄〃
+const props = defineProps<{
+ list: DiyComponentLibrary[]
+}>()
+// 缁勪欢鍒嗙粍
+const groups = reactive<any[]>([])
+// 灞曞紑鐨勬姌鍙犻潰鏉�
+const extendGroups = reactive<string[]>([])
+
+// 鐩戝惉 list 灞炴�э紝鎸夌収 DiyComponentLibrary 鐨� name 鍒嗙粍
+watch(
+ () => props.list,
+ () => {
+ // 娓呴櫎鏃ф暟鎹�
+ extendGroups.length = 0
+ groups.length = 0
+ // 閲嶆柊鐢熸垚鏁版嵁
+ props.list.forEach((group) => {
+ // 鏄惁灞曞紑鍒嗙粍
+ if (group.extended) {
+ extendGroups.push(group.name)
+ }
+ // 鏌ユ壘缁勪欢
+ const components = group.components
+ .map((name) => componentConfigs[name] as DiyComponent<any>)
+ .filter((component) => component)
+ if (components.length > 0) {
+ groups.push({
+ name: group.name,
+ components
+ })
+ }
+ })
+ },
+ {
+ immediate: true
+ }
+)
+
+// 鍏嬮殕缁勪欢
+const handleCloneComponent = (component: DiyComponent<any>) => {
+ const instance = cloneDeep(component)
+ instance.uid = new Date().getTime()
+ return instance
+}
+</script>
+
+<style scoped lang="scss">
+.editor-left {
+ z-index: 1;
+ flex-shrink: 0;
+ user-select: none;
+ box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
+
+ :deep(.el-collapse) {
+ border-top: none;
+ }
+
+ :deep(.el-collapse-item__wrap) {
+ border-bottom: none;
+ }
+
+ :deep(.el-collapse-item__content) {
+ padding-bottom: 0;
+ }
+
+ :deep(.el-collapse-item__header) {
+ height: 32px;
+ padding: 0 24px;
+ line-height: 32px;
+ background-color: var(--el-bg-color-page);
+ border-bottom: none;
+ }
+
+ .component-container {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ .component {
+ display: flex;
+ width: 86px;
+ height: 86px;
+ cursor: move;
+ border-right: 1px solid var(--el-border-color-lighter);
+ border-bottom: 1px solid var(--el-border-color-lighter);
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ .el-icon {
+ margin-bottom: 4px;
+ color: gray;
+ }
+ }
+
+ .component.active,
+ .component:hover {
+ color: var(--el-color-white);
+ background: var(--el-color-primary);
+
+ .el-icon {
+ color: var(--el-color-white);
+ }
+ }
+
+ .component:nth-of-type(3n) {
+ border-right: none;
+ }
+}
+
+/* 鎷栨嫿鍗犱綅鎻愮ず锛岄粯璁や笉鏄剧ず */
+.drag-placement {
+ display: none;
+ color: #fff;
+}
+
+.drag-area {
+ /* 鎷栨嫿鍒版墜鏈哄尯鍩熸椂鐨勬牱寮� */
+ .draggable-ghost {
+ display: flex;
+ width: 100%;
+ height: 40px;
+
+ /* 鏉$汗鑳屾櫙 */
+ background: linear-gradient(
+ 45deg,
+ #91a8d5 0,
+ #91a8d5 10%,
+ #94b4eb 10%,
+ #94b4eb 50%,
+ #91a8d5 50%,
+ #91a8d5 60%,
+ #94b4eb 60%,
+ #94b4eb
+ );
+ background-size: 1rem 1rem;
+ transition: all 0.5s;
+ justify-content: center;
+ align-items: center;
+
+ span {
+ display: inline-block;
+ width: 140px;
+ height: 25px;
+ font-size: 12px;
+ line-height: 25px;
+ color: #fff;
+ text-align: center;
+ background: #5487df;
+ }
+
+ /* 鎷栨嫿鏃堕殣钘忕粍浠� */
+ .component {
+ display: none;
+ }
+
+ /* 鎷栨嫿鏃舵樉绀哄崰浣嶆彁绀� */
+ .drag-placement {
+ display: block;
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/Carousel/config.ts b/src/components/DiyEditor/components/mobile/Carousel/config.ts
new file mode 100644
index 0000000..1ca9de8
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Carousel/config.ts
@@ -0,0 +1,53 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 杞挱鍥惧睘鎬� */
+export interface CarouselProperty {
+ // 绫诲瀷锛氶粯璁� | 鍗$墖
+ type: 'default' | 'card'
+ // 鎸囩ず鍣ㄦ牱寮忥細鐐� | 鏁板瓧
+ indicator: 'dot' | 'number'
+ // 鏄惁鑷姩鎾斁
+ autoplay: boolean
+ // 鎾斁闂撮殧
+ interval: number
+ // 杞挱楂樺害
+ height: number
+ // 杞挱鍐呭
+ items: CarouselItemProperty[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+// 杞挱鍐呭灞炴��
+export interface CarouselItemProperty {
+ // 绫诲瀷锛氬浘鐗� | 瑙嗛
+ type: 'img' | 'video'
+ // 鍥剧墖閾炬帴
+ imgUrl: string
+ // 瑙嗛閾炬帴
+ videoUrl: string
+ // 璺宠浆閾炬帴
+ url: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'Carousel',
+ name: '杞挱鍥�',
+ icon: 'system-uicons:carousel',
+ property: {
+ type: 'default',
+ indicator: 'dot',
+ autoplay: false,
+ interval: 3,
+ height: 174,
+ items: [
+ { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
+ { type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }
+ ] as CarouselItemProperty[],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<CarouselProperty>
diff --git a/src/components/DiyEditor/components/mobile/Carousel/index.vue b/src/components/DiyEditor/components/mobile/Carousel/index.vue
new file mode 100644
index 0000000..cafb534
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Carousel/index.vue
@@ -0,0 +1,43 @@
+<template>
+ <!-- 鏃犲浘鐗� -->
+ <div
+ class="h-250px flex items-center justify-center bg-gray-3"
+ v-if="property.items.length === 0"
+ >
+ <Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
+ </div>
+ <div v-else class="relative">
+ <el-carousel
+ :height="property.height + 'px'"
+ :type="property.type === 'card' ? 'card' : ''"
+ :autoplay="property.autoplay"
+ :interval="property.interval * 1000"
+ :indicator-position="property.indicator === 'number' ? 'none' : undefined"
+ @change="handleIndexChange"
+ >
+ <el-carousel-item v-for="(item, index) in property.items" :key="index">
+ <el-image class="h-full w-full" :src="item.imgUrl" />
+ </el-carousel-item>
+ </el-carousel>
+ <div
+ v-if="property.indicator === 'number'"
+ class="absolute bottom-10px right-10px rounded-xl bg-black p-x-8px p-y-2px text-10px text-white opacity-40"
+ >{{ currentIndex }} / {{ property.items.length }}</div
+ >
+ </div>
+</template>
+<script setup lang="ts">
+import { CarouselProperty } from './config'
+
+/** 杞挱鍥� */
+defineOptions({ name: 'Carousel' })
+
+defineProps<{ property: CarouselProperty }>()
+
+const currentIndex = ref(0)
+const handleIndexChange = (index: number) => {
+ currentIndex.value = index + 1
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/Carousel/property.vue b/src/components/DiyEditor/components/mobile/Carousel/property.vue
new file mode 100644
index 0000000..8da98ff
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Carousel/property.vue
@@ -0,0 +1,109 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-card header="鏍峰紡璁剧疆" class="property-group" shadow="never">
+ <el-form-item label="鏍峰紡" prop="type">
+ <el-radio-group v-model="formData.type">
+ <el-tooltip class="item" content="榛樿" placement="bottom">
+ <el-radio-button value="default">
+ <Icon icon="system-uicons:carousel" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍗$墖" placement="bottom">
+ <el-radio-button value="card">
+ <Icon icon="ic:round-view-carousel" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="楂樺害" prop="height">
+ <el-input-number class="!w-50% mr-10px" controls-position="right" v-model="formData.height" /> px
+ </el-form-item>
+ <el-form-item label="鎸囩ず鍣�" prop="indicator">
+ <el-radio-group v-model="formData.indicator">
+ <el-radio value="dot">灏忓渾鐐�</el-radio>
+ <el-radio value="number">鏁板瓧</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏄惁杞挱" prop="autoplay">
+ <el-switch v-model="formData.autoplay" />
+ </el-form-item>
+ <el-form-item label="鎾斁闂撮殧" prop="interval" v-if="formData.autoplay">
+ <el-slider
+ v-model="formData.interval"
+ :max="10"
+ :min="0.5"
+ :step="0.5"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ <el-text type="info">鍗曚綅锛氱</el-text>
+ </el-form-item>
+ </el-card>
+ <el-card header="鍐呭璁剧疆" class="property-group" shadow="never">
+ <Draggable v-model="formData.items" :empty-item="{ type: 'img' }">
+ <template #default="{ element }">
+ <el-form-item label="绫诲瀷" prop="type" class="m-b-8px!" label-width="40px">
+ <el-radio-group v-model="element.type">
+ <el-radio value="img">鍥剧墖</el-radio>
+ <el-radio value="video">瑙嗛</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ label="鍥剧墖"
+ class="m-b-8px!"
+ label-width="40px"
+ v-if="element.type === 'img'"
+ >
+ <UploadImg
+ v-model="element.imgUrl"
+ draggable="false"
+ height="80px"
+ width="100%"
+ class="min-w-80px"
+ />
+ </el-form-item>
+ <template v-else>
+ <el-form-item label="灏侀潰" class="m-b-8px!" label-width="40px">
+ <UploadImg
+ v-model="element.imgUrl"
+ draggable="false"
+ height="80px"
+ width="100%"
+ class="min-w-80px"
+ />
+ </el-form-item>
+ <el-form-item label="瑙嗛" class="m-b-8px!" label-width="40px">
+ <UploadFile
+ v-model="element.videoUrl"
+ :file-type="['mp4']"
+ :limit="1"
+ :file-size="100"
+ class="min-w-80px"
+ />
+ </el-form-item>
+ </template>
+ <el-form-item label="閾炬帴" class="m-b-8px!" label-width="40px">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ </template>
+ </Draggable>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { CarouselProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 杞挱鍥惧睘鎬ч潰鏉�
+defineOptions({ name: 'CarouselProperty' })
+
+const props = defineProps<{ modelValue: CarouselProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/CouponCard/component.tsx b/src/components/DiyEditor/components/mobile/CouponCard/component.tsx
new file mode 100644
index 0000000..afe5dfd
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/CouponCard/component.tsx
@@ -0,0 +1,73 @@
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
+import { floatToFixed2 } from '@/utils'
+import { formatDate } from '@/utils/formatTime'
+import { object } from 'vue-types'
+
+// 浼樻儬鍊�
+export const CouponDiscount = defineComponent({
+ name: 'CouponDiscount',
+ props: {
+ coupon: object<CouponTemplateApi.CouponTemplateVO>()
+ },
+ setup(props) {
+ const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
+ // 鎶樻墸
+ let value = coupon.discountPercent / 10 + ''
+ let suffix = ' 鎶�'
+ // 婊″噺
+ if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+ value = floatToFixed2(coupon.discountPrice)
+ suffix = ' 鍏�'
+ }
+ return () => (
+ <div>
+ <span class={'text-20px font-bold'}>{value}</span>
+ <span>{suffix}</span>
+ </div>
+ )
+ }
+})
+
+// 浼樻儬鎻忚堪
+export const CouponDiscountDesc = defineComponent({
+ name: 'CouponDiscountDesc',
+ props: {
+ coupon: object<CouponTemplateApi.CouponTemplateVO>()
+ },
+ setup(props) {
+ const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
+ // 浣跨敤鏉′欢
+ const useCondition = coupon.usePrice > 0 ? `婊�${floatToFixed2(coupon.usePrice)}鍏冿紝` : ''
+ // 浼樻儬鎻忚堪
+ const discountDesc =
+ coupon.discountType === PromotionDiscountTypeEnum.PRICE.type
+ ? `鍑�${floatToFixed2(coupon.discountPrice)}鍏僠
+ : `鎵�${coupon.discountPercent / 10.0}鎶榒
+ return () => (
+ <div>
+ <span>{useCondition}</span>
+ <span>{discountDesc}</span>
+ </div>
+ )
+ }
+})
+
+// 鏈夋晥鏈�
+export const CouponValidTerm = defineComponent({
+ name: 'CouponValidTerm',
+ props: {
+ coupon: object<CouponTemplateApi.CouponTemplateVO>()
+ },
+ setup(props) {
+ const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
+ const text =
+ coupon.validityType === CouponTemplateValidityTypeEnum.DATE.type
+ ? `鏈夋晥鏈燂細${formatDate(coupon.validStartTime, 'YYYY-MM-DD')} 鑷� ${formatDate(
+ coupon.validEndTime,
+ 'YYYY-MM-DD'
+ )}`
+ : `棰嗗彇鍚庣 ${coupon.fixedStartTerm} - ${coupon.fixedEndTerm} 澶╁唴鍙敤`
+ return () => <div>{text}</div>
+ }
+})
diff --git a/src/components/DiyEditor/components/mobile/CouponCard/config.ts b/src/components/DiyEditor/components/mobile/CouponCard/config.ts
new file mode 100644
index 0000000..304533d
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/CouponCard/config.ts
@@ -0,0 +1,47 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鍟嗗搧鍗$墖灞炴�� */
+export interface CouponCardProperty {
+ // 鍒楁暟
+ columns: number
+ // 鑳屾櫙鍥�
+ bgImg: string
+ // 鏂囧瓧棰滆壊
+ textColor: string
+ // 鎸夐挳鏍峰紡
+ button: {
+ // 棰滆壊
+ color: string
+ // 鑳屾櫙棰滆壊
+ bgColor: string
+ }
+ // 闂磋窛
+ space: number
+ // 浼樻儬鍒哥紪鍙峰垪琛�
+ couponIds: number[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'CouponCard',
+ name: '浼樻儬鍒�',
+ icon: 'ep:ticket',
+ property: {
+ columns: 1,
+ bgImg: '',
+ textColor: '#E9B461',
+ button: {
+ color: '#434343',
+ bgColor: ''
+ },
+ space: 0,
+ couponIds: [],
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<CouponCardProperty>
diff --git a/src/components/DiyEditor/components/mobile/CouponCard/index.vue b/src/components/DiyEditor/components/mobile/CouponCard/index.vue
new file mode 100644
index 0000000..48d01c5
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/CouponCard/index.vue
@@ -0,0 +1,149 @@
+<template>
+ <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+ <div
+ class="flex flex-row text-12px"
+ :style="{
+ gap: `${property.space}px`,
+ width: scrollbarWidth
+ }"
+ >
+ <div
+ class="box-content"
+ :style="{
+ background: property.bgImg
+ ? `url(${property.bgImg}) 100% center / 100% 100% no-repeat`
+ : '#fff',
+ width: `${couponWidth}px`,
+ color: property.textColor
+ }"
+ v-for="(coupon, index) in couponList"
+ :key="index"
+ >
+ <!-- 甯冨眬1锛�1鍒�-->
+ <div v-if="property.columns === 1" class="m-l-16px flex flex-row justify-between p-8px">
+ <div class="flex flex-col justify-evenly gap-4px">
+ <!-- 浼樻儬鍊� -->
+ <CouponDiscount :coupon="coupon" />
+ <!-- 浼樻儬鎻忚堪 -->
+ <CouponDiscountDesc :coupon="coupon" />
+ <!-- 鏈夋晥鏈� -->
+ <CouponValidTerm :coupon="coupon" />
+ </div>
+ <div class="flex flex-col justify-evenly">
+ <div
+ class="rounded-20px p-x-8px p-y-2px"
+ :style="{
+ color: property.button.color,
+ background: property.button.bgColor
+ }"
+ >
+ 绔嬪嵆棰嗗彇
+ </div>
+ </div>
+ </div>
+ <!-- 甯冨眬2锛�2鍒�-->
+ <div
+ v-else-if="property.columns === 2"
+ class="m-l-16px flex flex-row justify-between p-8px"
+ >
+ <div class="flex flex-col justify-evenly gap-4px">
+ <!-- 浼樻儬鍊� -->
+ <CouponDiscount :coupon="coupon" />
+ <!-- 浼樻儬鎻忚堪 -->
+ <CouponDiscountDesc :coupon="coupon" />
+ <!-- 棰嗗彇璇存槑 -->
+ <div v-if="coupon.totalCount >= 0">
+ 浠呭墿锛歿{ coupon.totalCount - coupon.takeCount }}寮�
+ </div>
+ <div v-else-if="coupon.totalCount === -1">浠呭墿锛氫笉闄愬埗</div>
+ </div>
+ <div class="flex flex-col">
+ <div
+ class="h-full w-20px rounded-20px p-x-2px p-y-8px text-center"
+ :style="{
+ color: property.button.color,
+ background: property.button.bgColor
+ }"
+ >
+ 绔嬪嵆棰嗗彇
+ </div>
+ </div>
+ </div>
+ <!-- 甯冨眬3锛�3鍒�-->
+ <div v-else class="flex flex-col items-center justify-around gap-4px p-4px">
+ <!-- 浼樻儬鍊� -->
+ <CouponDiscount :coupon="coupon" />
+ <!-- 浼樻儬鎻忚堪 -->
+ <CouponDiscountDesc :coupon="coupon" />
+ <div
+ class="rounded-20px p-x-8px p-y-2px"
+ :style="{
+ color: property.button.color,
+ background: property.button.bgColor
+ }"
+ >
+ 绔嬪嵆棰嗗彇
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-scrollbar>
+</template>
+<script setup lang="ts">
+import { CouponCardProperty } from './config'
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import { CouponDiscount } from './component'
+import {
+ CouponDiscountDesc,
+ CouponValidTerm
+} from '@/components/DiyEditor/components/mobile/CouponCard/component'
+
+/** 鍟嗗搧鍗$墖 */
+defineOptions({ name: 'CouponCard' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: CouponCardProperty }>()
+// 鍟嗗搧鍒楄〃
+const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([])
+watch(
+ () => props.property.couponIds,
+ async () => {
+ if (props.property.couponIds?.length > 0) {
+ couponList.value = await CouponTemplateApi.getCouponTemplateList(props.property.couponIds)
+ }
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+
+// 鎵嬫満瀹藉害
+const phoneWidth = ref(375)
+// 瀹瑰櫒
+const containerRef = ref()
+// 婊氬姩鏉″搴�
+const scrollbarWidth = ref('100%')
+// 浼樻儬鍒哥殑瀹藉害
+const couponWidth = ref(375)
+// 璁$畻甯冨眬鍙傛暟
+watch(
+ () => [props.property, phoneWidth, couponList.value.length],
+ () => {
+ // 姣忓垪鐨勫搴︿负锛氾紙鎬诲搴� - 闂磋窛 * (鍒楁暟 - 1)锛�/ 鍒楁暟
+ couponWidth.value =
+ (phoneWidth.value - props.property.space * (props.property.columns - 1)) /
+ props.property.columns
+ // 鏄剧ず婊氬姩鏉�
+ scrollbarWidth.value = `${
+ couponWidth.value * couponList.value.length +
+ props.property.space * (couponList.value.length - 1)
+ }px`
+ },
+ { immediate: true, deep: true }
+)
+onMounted(() => {
+ // 鎻愬彇鎵嬫満瀹藉害
+ phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
+})
+</script>
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/CouponCard/property.vue b/src/components/DiyEditor/components/mobile/CouponCard/property.vue
new file mode 100644
index 0000000..604afe9
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/CouponCard/property.vue
@@ -0,0 +1,119 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-card header="浼樻儬鍒稿垪琛�" class="property-group" shadow="never">
+ <div
+ v-for="(coupon, index) in couponList"
+ :key="index"
+ class="flex items-center justify-between"
+ >
+ <el-text size="large" truncated>{{ coupon.name }}</el-text>
+ <el-text type="info" truncated>
+ <span v-if="coupon.usePrice > 0">婊{ floatToFixed2(coupon.usePrice) }}鍏冿紝</span>
+ <span v-if="coupon.discountType === PromotionDiscountTypeEnum.PRICE.type">
+ 鍑弡{ floatToFixed2(coupon.discountPrice) }}鍏�
+ </span>
+ <span v-else> 鎵搟{ coupon.discountPercent }}鎶� </span>
+ </el-text>
+ </div>
+ <el-form-item label-width="0">
+ <el-button @click="handleAddCoupon" type="primary" plain class="m-t-8px w-full">
+ <Icon icon="ep:plus" class="mr-5px" /> 娣诲姞
+ </el-button>
+ </el-form-item>
+ </el-card>
+ <el-card header="浼樻儬鍒告牱寮�" class="property-group" shadow="never">
+ <el-form-item label="鍒楁暟" prop="type">
+ <el-radio-group v-model="formData.columns">
+ <el-tooltip class="item" content="涓�鍒�" placement="bottom">
+ <el-radio-button :value="1">
+ <Icon icon="fluent:text-column-one-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="浜屽垪" placement="bottom">
+ <el-radio-button :value="2">
+ <Icon icon="fluent:text-column-two-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="涓夊垪" placement="bottom">
+ <el-radio-button :value="3">
+ <Icon icon="fluent:text-column-three-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鑳屾櫙鍥剧墖" prop="bgImg">
+ <UploadImg v-model="formData.bgImg" height="80px" width="100%" class="min-w-160px" />
+ </el-form-item>
+ <el-form-item label="鏂囧瓧棰滆壊" prop="textColor">
+ <ColorInput v-model="formData.textColor" />
+ </el-form-item>
+ <el-form-item label="鎸夐挳鑳屾櫙" prop="button.bgColor">
+ <ColorInput v-model="formData.button.bgColor" />
+ </el-form-item>
+ <el-form-item label="鎸夐挳鏂囧瓧" prop="button.color">
+ <ColorInput v-model="formData.button.color" />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+ <!-- 浼樻儬鍒搁�夋嫨 -->
+ <CouponSelect
+ ref="couponSelectDialog"
+ v-model:multiple-selection="couponList"
+ :take-type="CouponTemplateTakeTypeEnum.USER.type"
+ @change="handleCouponSelect"
+ />
+</template>
+
+<script setup lang="ts">
+import { CouponCardProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import { floatToFixed2 } from '@/utils'
+import { CouponTemplateTakeTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
+import CouponSelect from '@/views/mall/promotion/coupon/components/CouponSelect.vue'
+
+// 浼樻儬鍒稿崱鐗囧睘鎬ч潰鏉�
+defineOptions({ name: 'CouponCardProperty' })
+
+const props = defineProps<{ modelValue: CouponCardProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 浼樻儬鍒稿垪琛�
+const couponList = ref<CouponTemplateApi.CouponTemplateVO[]>([])
+const couponSelectDialog = ref()
+// 娣诲姞浼樻儬鍒�
+const handleAddCoupon = () => {
+ couponSelectDialog.value.open()
+}
+const handleCouponSelect = () => {
+ formData.value.couponIds = couponList.value.map((coupon) => coupon.id)
+}
+
+watch(
+ () => formData.value.couponIds,
+ async () => {
+ if (formData.value.couponIds?.length > 0) {
+ couponList.value = await CouponTemplateApi.getCouponTemplateList(formData.value.couponIds)
+ }
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/Divider/config.ts b/src/components/DiyEditor/components/mobile/Divider/config.ts
new file mode 100644
index 0000000..9b55360
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Divider/config.ts
@@ -0,0 +1,29 @@
+import { DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鍒嗗壊绾垮睘鎬� */
+export interface DividerProperty {
+ // 楂樺害
+ height: number
+ // 绾垮
+ lineWidth: number
+ // 杈硅窛绫诲瀷
+ paddingType: 'none' | 'horizontal'
+ // 棰滆壊
+ lineColor: string
+ // 绫诲瀷
+ borderType: 'solid' | 'dashed' | 'dotted' | 'none'
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'Divider',
+ name: '鍒嗗壊绾�',
+ icon: 'tdesign:component-divider-vertical',
+ property: {
+ height: 30,
+ lineWidth: 1,
+ paddingType: 'none',
+ lineColor: '#dcdfe6',
+ borderType: 'solid'
+ }
+} as DiyComponent<DividerProperty>
diff --git a/src/components/DiyEditor/components/mobile/Divider/index.vue b/src/components/DiyEditor/components/mobile/Divider/index.vue
new file mode 100644
index 0000000..f778504
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Divider/index.vue
@@ -0,0 +1,29 @@
+<template>
+ <div
+ class="flex items-center"
+ :style="{
+ height: property.height + 'px'
+ }"
+ >
+ <div
+ class="w-full"
+ :style="{
+ borderTopStyle: property.borderType,
+ borderTopColor: property.lineColor,
+ borderTopWidth: `${property.lineWidth}px`,
+ margin: property.paddingType === 'none' ? '0' : '0px 16px'
+ }"
+ ></div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { DividerProperty } from './config'
+
+/** 椤甸潰椤堕儴瀵艰埅鏍� */
+defineOptions({ name: 'Divider' })
+
+defineProps<{ property: DividerProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/Divider/property.vue b/src/components/DiyEditor/components/mobile/Divider/property.vue
new file mode 100644
index 0000000..dc2a4da
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Divider/property.vue
@@ -0,0 +1,80 @@
+<template>
+ <el-form label-width="80px" :model="formData">
+ <el-form-item label="楂樺害" prop="height">
+ <el-slider v-model="formData.height" :min="1" :max="100" show-input input-size="small" />
+ </el-form-item>
+ <el-form-item label="閫夋嫨鏍峰紡" prop="borderType">
+ <el-radio-group v-model="formData!.borderType">
+ <el-tooltip
+ placement="top"
+ v-for="(item, index) in BORDER_TYPES"
+ :key="index"
+ :content="item.text"
+ >
+ <el-radio-button :value="item.type">
+ <Icon :icon="item.icon" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <template v-if="formData.borderType !== 'none'">
+ <el-form-item label="绾垮" prop="lineWidth">
+ <el-slider v-model="formData.lineWidth" :min="1" :max="30" show-input input-size="small" />
+ </el-form-item>
+ <el-form-item label="宸﹀彸杈硅窛" prop="paddingType">
+ <el-radio-group v-model="formData!.paddingType">
+ <el-tooltip content="鏃犺竟璺�" placement="top">
+ <el-radio-button value="none">
+ <Icon icon="tabler:box-padding" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip content="宸﹀彸鐣欒竟" placement="top">
+ <el-radio-button value="horizontal">
+ <Icon icon="vaadin:padding" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="棰滆壊">
+ <!-- 鍒嗗壊绾块鑹� -->
+ <ColorInput v-model="formData.lineColor" />
+ </el-form-item>
+ </template>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { DividerProperty } from './config'
+import { useVModel } from '@vueuse/core'
+// 瀵艰埅鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'DividerProperty' })
+const props = defineProps<{ modelValue: DividerProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+//绾跨被鍨�
+const BORDER_TYPES = [
+ {
+ icon: 'vaadin:line-h',
+ text: '瀹炵嚎',
+ type: 'solid'
+ },
+ {
+ icon: 'tabler:line-dashed',
+ text: '铏氱嚎',
+ type: 'dashed'
+ },
+ {
+ icon: 'tabler:line-dotted',
+ text: '鐐圭嚎',
+ type: 'dotted'
+ },
+ {
+ icon: 'entypo:progress-empty',
+ text: '鏃�',
+ type: 'none'
+ }
+]
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/config.ts b/src/components/DiyEditor/components/mobile/FloatingActionButton/config.ts
new file mode 100644
index 0000000..fcf129f
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/config.ts
@@ -0,0 +1,36 @@
+import { DiyComponent } from '@/components/DiyEditor/util'
+
+// 鎮诞鎸夐挳灞炴��
+export interface FloatingActionButtonProperty {
+ // 灞曞紑鏂瑰悜
+ direction: 'horizontal' | 'vertical'
+ // 鏄惁鏄剧ず鏂囧瓧
+ showText: boolean
+ // 鎸夐挳鍒楄〃
+ list: FloatingActionButtonItemProperty[]
+}
+
+// 鎮诞鎸夐挳椤瑰睘鎬�
+export interface FloatingActionButtonItemProperty {
+ // 鍥剧墖鍦板潃
+ imgUrl: string
+ // 璺宠浆杩炴帴
+ url: string
+ // 鏂囧瓧
+ text: string
+ // 鏂囧瓧棰滆壊
+ textColor: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'FloatingActionButton',
+ name: '鎮诞鎸夐挳',
+ icon: 'tabler:float-right',
+ position: 'fixed',
+ property: {
+ direction: 'vertical',
+ showText: true,
+ list: [{ textColor: '#fff' }]
+ }
+} as DiyComponent<FloatingActionButtonProperty>
diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue b/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue
new file mode 100644
index 0000000..c2b9926
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/index.vue
@@ -0,0 +1,74 @@
+<template>
+ <div
+ :class="[
+ 'absolute bottom-32px right-[calc(50%-375px/2+32px)] flex z-12 gap-12px items-center',
+ {
+ 'flex-row': property.direction === 'horizontal',
+ 'flex-col': property.direction === 'vertical'
+ }
+ ]"
+ >
+ <template v-if="expanded">
+ <div
+ v-for="(item, index) in property.list"
+ :key="index"
+ class="flex flex-col items-center"
+ @click="handleActive(index)"
+ >
+ <el-image :src="item.imgUrl" fit="contain" class="h-27px w-27px">
+ <template #error>
+ <div class="h-full w-full flex items-center justify-center">
+ <Icon icon="ep:picture" :color="item.textColor" />
+ </div>
+ </template>
+ </el-image>
+ <span v-if="property.showText" class="mt-4px text-12px" :style="{ color: item.textColor }">
+ {{ item.text }}
+ </span>
+ </div>
+ </template>
+ <!-- todo: @owen 浣跨敤APP涓婚鑹� -->
+ <el-button type="primary" size="large" circle @click="handleToggleFab">
+ <Icon icon="ep:plus" :class="['fab-icon', { active: expanded }]" />
+ </el-button>
+ </div>
+ <!-- 妯℃�佽儗鏅細灞曞紑鏃舵樉绀猴紝鐐瑰嚮鍚庢姌鍙� -->
+ <div v-if="expanded" class="modal-bg" @click="handleToggleFab"></div>
+</template>
+<script setup lang="ts">
+import { FloatingActionButtonProperty } from './config'
+
+/** 鎮诞鎸夐挳 */
+defineOptions({ name: 'FloatingActionButton' })
+// 瀹氫箟灞炴��
+defineProps<{ property: FloatingActionButtonProperty }>()
+
+// 鏄惁灞曞紑
+const expanded = ref(false)
+// 澶勭悊灞曞紑/鎶樺彔
+const handleToggleFab = () => {
+ expanded.value = !expanded.value
+}
+</script>
+
+<style scoped lang="scss">
+/* 妯℃�佽儗鏅� */
+.modal-bg {
+ position: absolute;
+ left: calc(50% - 375px / 2);
+ top: 0;
+ z-index: 11;
+ width: 375px;
+ height: 100%;
+ background-color: rgba(#000000, 0.4);
+}
+
+.fab-icon {
+ transform: rotate(0deg);
+ transition: transform 0.3s;
+
+ &.active {
+ transform: rotate(135deg);
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue b/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue
new file mode 100644
index 0000000..6eeb217
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/FloatingActionButton/property.vue
@@ -0,0 +1,44 @@
+<template>
+ <el-form label-width="80px" :model="formData">
+ <el-card header="鎸夐挳閰嶇疆" class="property-group" shadow="never">
+ <el-form-item label="灞曞紑鏂瑰悜" prop="direction">
+ <el-radio-group v-model="formData.direction">
+ <el-radio value="vertical">鍨傜洿</el-radio>
+ <el-radio value="horizontal">姘村钩</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏄剧ず鏂囧瓧" prop="showText">
+ <el-switch v-model="formData.showText" />
+ </el-form-item>
+ </el-card>
+ <el-card header="鎸夐挳鍒楄〃" class="property-group" shadow="never">
+ <Draggable v-model="formData.list" :empty-item="{ textColor: '#fff' }">
+ <template #default="{ element, index }">
+ <el-form-item label="鍥炬爣" :prop="`list[${index}].imgUrl`">
+ <UploadImg v-model="element.imgUrl" height="56px" width="56px" />
+ </el-form-item>
+ <el-form-item label="鏂囧瓧" :prop="`list[${index}].text`">
+ <InputWithColor v-model="element.text" v-model:color="element.textColor" />
+ </el-form-item>
+ <el-form-item label="璺宠浆閾炬帴" :prop="`list[${index}].url`">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ </template>
+ </Draggable>
+ </el-card>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { FloatingActionButtonProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 鎮诞鎸夐挳灞炴�ч潰鏉�
+defineOptions({ name: 'FloatingActionButtonProperty' })
+
+const props = defineProps<{ modelValue: FloatingActionButtonProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/controller.ts b/src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/controller.ts
new file mode 100644
index 0000000..a7bd762
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/controller.ts
@@ -0,0 +1,143 @@
+import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
+import { StyleValue } from 'vue'
+
+// 鐑尯鐨勬渶灏忓楂�
+export const HOT_ZONE_MIN_SIZE = 100
+
+// 鎺у埗鐨勭被鍨�
+export enum CONTROL_TYPE_ENUM {
+ LEFT,
+ TOP,
+ WIDTH,
+ HEIGHT
+}
+
+// 瀹氫箟鐑尯鐨勬帶鍒剁偣
+export interface ControlDot {
+ position: string
+ types: CONTROL_TYPE_ENUM[]
+ style: StyleValue
+}
+
+// 鐑尯鐨�8涓帶鍒剁偣
+export const CONTROL_DOT_LIST = [
+ {
+ position: '宸︿笂瑙�',
+ types: [
+ CONTROL_TYPE_ENUM.LEFT,
+ CONTROL_TYPE_ENUM.TOP,
+ CONTROL_TYPE_ENUM.WIDTH,
+ CONTROL_TYPE_ENUM.HEIGHT
+ ],
+ style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' }
+ },
+ {
+ position: '涓婃柟涓棿',
+ types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT],
+ style: { left: '50%', top: '-5px', cursor: 'n-resize', transform: 'translateX(-50%)' }
+ },
+ {
+ position: '鍙充笂瑙�',
+ types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+ style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' }
+ },
+ {
+ position: '鍙充晶涓棿',
+ types: [CONTROL_TYPE_ENUM.WIDTH],
+ style: { right: '-5px', top: '50%', cursor: 'e-resize', transform: 'translateX(-50%)' }
+ },
+ {
+ position: '鍙充笅瑙�',
+ types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+ style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' }
+ },
+ {
+ position: '涓嬫柟涓棿',
+ types: [CONTROL_TYPE_ENUM.HEIGHT],
+ style: { left: '50%', bottom: '-5px', cursor: 's-resize', transform: 'translateX(-50%)' }
+ },
+ {
+ position: '宸︿笅瑙�',
+ types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
+ style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' }
+ },
+ {
+ position: '宸︿晶涓棿',
+ types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH],
+ style: { left: '-5px', top: '50%', cursor: 'w-resize', transform: 'translateX(-50%)' }
+ }
+] as ControlDot[]
+
+//region 鐑尯鐨勭缉鏀�
+// 鐑尯鐨勭缉鏀炬瘮渚�
+export const HOT_ZONE_SCALE_RATE = 2
+// 缂╁皬锛氱缉鍥為�傚悎鎵嬫満灞忓箷鐨勫ぇ灏�
+export const zoomOut = (list?: HotZoneItemProperty[]) => {
+ return (
+ list?.map((hotZone) => ({
+ ...hotZone,
+ left: (hotZone.left /= HOT_ZONE_SCALE_RATE),
+ top: (hotZone.top /= HOT_ZONE_SCALE_RATE),
+ width: (hotZone.width /= HOT_ZONE_SCALE_RATE),
+ height: (hotZone.height /= HOT_ZONE_SCALE_RATE)
+ })) || []
+ )
+}
+// 鏀惧ぇ锛氫綔鐢ㄦ槸涓轰簡鏂逛究鍦ㄧ數鑴戝睆骞曚笂缂栬緫
+export const zoomIn = (list?: HotZoneItemProperty[]) => {
+ return (
+ list?.map((hotZone) => ({
+ ...hotZone,
+ left: (hotZone.left *= HOT_ZONE_SCALE_RATE),
+ top: (hotZone.top *= HOT_ZONE_SCALE_RATE),
+ width: (hotZone.width *= HOT_ZONE_SCALE_RATE),
+ height: (hotZone.height *= HOT_ZONE_SCALE_RATE)
+ })) || []
+ )
+}
+//endregion
+
+/**
+ * 灏佽鐑尯鎷栨嫿
+ *
+ * 娉細涓轰粈涔堜笉浣跨敤vueuse鐨剈seDraggable銆傚湪鏈満鏅笅锛屽叾浣跨敤鏂瑰紡姣旇緝澶嶆潅
+ * @param hotZone 鐑尯
+ * @param downEvent 榧犳爣鎸変笅浜嬩欢
+ * @param callback 鍥炶皟鍑芥暟
+ */
+export const useDraggable = (
+ hotZone: HotZoneItemProperty,
+ downEvent: MouseEvent,
+ callback: (
+ left: number,
+ top: number,
+ width: number,
+ height: number,
+ moveWidth: number,
+ moveHeight: number
+ ) => void
+) => {
+ // 闃绘浜嬩欢鍐掓场
+ downEvent.stopPropagation()
+
+ // 绉诲姩鍓嶇殑榧犳爣鍧愭爣
+ const { clientX: startX, clientY: startY } = downEvent
+ // 绉诲姩鍓嶇殑鐑尯鍧愭爣銆佸ぇ灏�
+ const { left, top, width, height } = hotZone
+
+ // 鐩戝惉榧犳爣绉诲姩
+ document.onmousemove = (e) => {
+ // 绉诲姩瀹藉害
+ const moveWidth = e.clientX - startX
+ // 绉诲姩楂樺害
+ const moveHeight = e.clientY - startY
+ // 绉诲姩鍥炶皟
+ callback(left, top, width, height, moveWidth, moveHeight)
+ }
+
+ // 鏉惧紑榧犳爣鍚庯紝缁撴潫鎷栨嫿
+ document.onmouseup = () => {
+ document.onmousemove = null
+ document.onmouseup = null
+ }
+}
diff --git a/src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/index.vue b/src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/index.vue
new file mode 100644
index 0000000..3925057
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/HotZone/components/HotZoneEditDialog/index.vue
@@ -0,0 +1,236 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璁剧疆鐑尯" width="780" @close="handleClose">
+ <div ref="container" class="relative h-full w-750px">
+ <el-image :src="imgUrl" class="pointer-events-none h-full w-750px select-none" />
+ <div
+ v-for="(item, hotZoneIndex) in formData"
+ :key="hotZoneIndex"
+ class="hot-zone"
+ :style="{
+ width: `${item.width}px`,
+ height: `${item.height}px`,
+ top: `${item.top}px`,
+ left: `${item.left}px`
+ }"
+ @mousedown="handleMove(item, $event)"
+ @dblclick="handleShowAppLinkDialog(item)"
+ >
+ <span class="pointer-events-none select-none">{{ item.name || '鍙屽嚮閫夋嫨閾炬帴' }}</span>
+ <Icon icon="ep:close" class="delete" :size="14" @click="handleRemove(item)" />
+
+ <!-- 8涓帶鍒剁偣 -->
+ <span
+ class="ctrl-dot"
+ v-for="(dot, dotIndex) in CONTROL_DOT_LIST"
+ :key="dotIndex"
+ :style="dot.style"
+ @mousedown="handleResize(item, dot, $event)"
+ ></span>
+ </div>
+ </div>
+ <template #footer>
+ <el-button @click="handleAdd" type="primary" plain>
+ <Icon icon="ep:plus" class="mr-5px" />
+ 娣诲姞鐑尯
+ </el-button>
+ <el-button @click="handleSubmit" type="primary" plain>
+ <Icon icon="ep:check" class="mr-5px" />
+ 纭畾
+ </el-button>
+ </template>
+ </Dialog>
+ <AppLinkSelectDialog ref="appLinkDialogRef" @app-link-change="handleAppLinkChange" />
+</template>
+
+<script setup lang="ts">
+import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
+import { array, string } from 'vue-types'
+import {
+ CONTROL_DOT_LIST,
+ CONTROL_TYPE_ENUM,
+ ControlDot,
+ HOT_ZONE_MIN_SIZE,
+ useDraggable,
+ zoomIn,
+ zoomOut
+} from './controller'
+import { AppLink } from '@/components/AppLinkInput/data'
+import { remove } from 'lodash-es'
+
+/** 鐑尯缂栬緫瀵硅瘽妗� */
+defineOptions({ name: 'HotZoneEditDialog' })
+
+// 瀹氫箟灞炴��
+const props = defineProps({
+ modelValue: array<HotZoneItemProperty>(),
+ imgUrl: string().def('')
+})
+const emit = defineEmits(['update:modelValue'])
+const formData = ref<HotZoneItemProperty[]>([])
+
+// 寮圭獥鐨勬槸鍚︽樉绀�
+const dialogVisible = ref(false)
+// 鎵撳紑寮圭獥
+const open = () => {
+ // 鏀惧ぇ
+ formData.value = zoomIn(props.modelValue)
+ dialogVisible.value = true
+}
+// 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+defineExpose({ open })
+
+// 鐑尯瀹瑰櫒
+const container = ref<HTMLDivElement>()
+
+// 澧炲姞鐑尯
+const handleAdd = () => {
+ formData.value.push({
+ width: HOT_ZONE_MIN_SIZE,
+ height: HOT_ZONE_MIN_SIZE,
+ top: 0,
+ left: 0
+ } as HotZoneItemProperty)
+}
+// 鍒犻櫎鐑尯
+const handleRemove = (hotZone: HotZoneItemProperty) => {
+ remove(formData.value, hotZone)
+}
+
+// 绉诲姩鐑尯
+const handleMove = (item: HotZoneItemProperty, e: MouseEvent) => {
+ useDraggable(item, e, (left, top, _, __, moveWidth, moveHeight) => {
+ setLeft(item, left + moveWidth)
+ setTop(item, top + moveHeight)
+ })
+}
+
+// 璋冩暣鐑尯澶у皬銆佷綅缃�
+const handleResize = (item: HotZoneItemProperty, ctrlDot: ControlDot, e: MouseEvent) => {
+ useDraggable(item, e, (left, top, width, height, moveWidth, moveHeight) => {
+ ctrlDot.types.forEach((type) => {
+ switch (type) {
+ case CONTROL_TYPE_ENUM.LEFT:
+ setLeft(item, left + moveWidth)
+ break
+ case CONTROL_TYPE_ENUM.TOP:
+ setTop(item, top + moveHeight)
+ break
+ case CONTROL_TYPE_ENUM.WIDTH:
+ {
+ // 涓婄Щ鏃讹紝楂樺害涓哄噺灏�
+ const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.LEFT) ? -1 : 1
+ setWidth(item, width + moveWidth * direction)
+ }
+ break
+ case CONTROL_TYPE_ENUM.HEIGHT:
+ {
+ // 宸︾Щ鏃讹紝瀹藉害涓哄噺灏�
+ const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.TOP) ? -1 : 1
+ setHeight(item, height + moveHeight * direction)
+ }
+ break
+ }
+ })
+ })
+}
+
+// 璁剧疆X杞村潗鏍�
+const setLeft = (item: HotZoneItemProperty, left: number) => {
+ // 涓嶈兘瓒呭嚭瀹瑰櫒
+ if (left >= 0 && left <= container.value!.offsetWidth - item.width) {
+ item.left = left
+ }
+}
+// 璁剧疆Y杞村潗鏍�
+const setTop = (item: HotZoneItemProperty, top: number) => {
+ // 涓嶈兘瓒呭嚭瀹瑰櫒
+ if (top >= 0 && top <= container.value!.offsetHeight - item.height) {
+ item.top = top
+ }
+}
+// 璁剧疆瀹藉害
+const setWidth = (item: HotZoneItemProperty, width: number) => {
+ // 涓嶈兘灏忎簬鏈�灏忓搴� && 涓嶈兘瓒呭嚭瀹瑰櫒鍙宠竟
+ if (width >= HOT_ZONE_MIN_SIZE && item.left + width <= container.value!.offsetWidth) {
+ item.width = width
+ }
+}
+// 璁剧疆楂樺害
+const setHeight = (item: HotZoneItemProperty, height: number) => {
+ // 涓嶈兘灏忎簬鏈�灏忛珮搴� && 涓嶈兘瓒呭嚭瀹瑰櫒搴曢儴
+ if (height >= HOT_ZONE_MIN_SIZE && item.top + height <= container.value!.offsetHeight) {
+ item.height = height
+ }
+}
+
+// 澶勭悊瀵硅瘽妗嗗叧闂�
+const handleSubmit = () => {
+ // 浼氳嚜鍔ㄨЕ鍙慼andleClose
+ dialogVisible.value = false
+}
+
+// 澶勭悊瀵硅瘽妗嗗叧闂�
+const handleClose = () => {
+ // 缂╁皬
+ const list = zoomOut(formData.value)
+ emit('update:modelValue', list)
+}
+
+const activeHotZone = ref<HotZoneItemProperty>()
+const appLinkDialogRef = ref()
+const handleShowAppLinkDialog = (hotZone: HotZoneItemProperty) => {
+ activeHotZone.value = hotZone
+ appLinkDialogRef.value.open(hotZone.url)
+}
+const handleAppLinkChange = (appLink: AppLink) => {
+ if (!appLink || !activeHotZone.value) return
+ activeHotZone.value.name = appLink.name
+ activeHotZone.value.url = appLink.path
+}
+</script>
+
+<style scoped lang="scss">
+.hot-zone {
+ position: absolute;
+ background: var(--el-color-primary-light-7);
+ opacity: 0.8;
+ border: 1px solid var(--el-color-primary);
+ color: var(--el-color-primary);
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: move;
+ z-index: 10;
+
+ /* 鎺у埗鐐� */
+ .ctrl-dot {
+ position: absolute;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ border: inherit;
+ background-color: #fff;
+ z-index: 11;
+ }
+
+ .delete {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 2px 2px 6px 6px;
+ background-color: var(--el-color-primary);
+ border-radius: 0 0 0 80%;
+ cursor: pointer;
+ color: #fff;
+ text-align: right;
+ }
+
+ &:hover {
+ .delete {
+ display: block;
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/HotZone/config.ts b/src/components/DiyEditor/components/mobile/HotZone/config.ts
new file mode 100644
index 0000000..80ed855
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/HotZone/config.ts
@@ -0,0 +1,43 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鐑尯灞炴�� */
+export interface HotZoneProperty {
+ // 鍥剧墖鍦板潃
+ imgUrl: string
+ // 瀵艰埅鑿滃崟鍒楄〃
+ list: HotZoneItemProperty[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+/** 鐑尯椤圭洰灞炴�� */
+export interface HotZoneItemProperty {
+ // 閾炬帴鐨勫悕绉�
+ name: string
+ // 閾炬帴
+ url: string
+ // 瀹�
+ width: number
+ // 楂�
+ height: number
+ // 涓�
+ top: number
+ // 宸�
+ left: number
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'HotZone',
+ name: '鐑尯',
+ icon: 'tabler:hand-click',
+ property: {
+ imgUrl: '',
+ list: [] as HotZoneItemProperty[],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<HotZoneProperty>
diff --git a/src/components/DiyEditor/components/mobile/HotZone/index.vue b/src/components/DiyEditor/components/mobile/HotZone/index.vue
new file mode 100644
index 0000000..3a9b842
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/HotZone/index.vue
@@ -0,0 +1,42 @@
+<template>
+ <div class="relative h-full min-h-30px w-full">
+ <el-image :src="property.imgUrl" class="pointer-events-none h-full w-full select-none" />
+ <div
+ v-for="(item, index) in property.list"
+ :key="index"
+ class="hot-zone"
+ :style="{
+ width: `${item.width}px`,
+ height: `${item.height}px`,
+ top: `${item.top}px`,
+ left: `${item.left}px`
+ }"
+ >
+ {{ item.name }}
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { HotZoneProperty } from './config'
+
+/** 鐑尯 */
+defineOptions({ name: 'HotZone' })
+const props = defineProps<{ property: HotZoneProperty }>()
+</script>
+
+<style scoped lang="scss">
+.hot-zone {
+ position: absolute;
+ background: var(--el-color-primary-light-7);
+ opacity: 0.8;
+ border: 1px solid var(--el-color-primary);
+ color: var(--el-color-primary);
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: move;
+ z-index: 10;
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/HotZone/property.vue b/src/components/DiyEditor/components/mobile/HotZone/property.vue
new file mode 100644
index 0000000..65892f8
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/HotZone/property.vue
@@ -0,0 +1,63 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <!-- 琛ㄥ崟 -->
+ <el-form label-width="80px" :model="formData" class="m-t-8px">
+ <el-form-item label="涓婁紶鍥剧墖" prop="imgUrl">
+ <UploadImg v-model="formData.imgUrl" height="50px" width="auto" class="min-w-80px">
+ <template #tip>
+ <el-text type="info" size="small"> 鎺ㄨ崘瀹藉害 750</el-text>
+ </template>
+ </UploadImg>
+ </el-form-item>
+ </el-form>
+
+ <el-button type="primary" plain class="w-full" @click="handleOpenEditDialog">
+ 璁剧疆鐑尯
+ </el-button>
+ </ComponentContainerProperty>
+ <!-- 鐑尯缂栬緫瀵硅瘽妗� -->
+ <HotZoneEditDialog ref="editDialogRef" v-model="formData.list" :img-url="formData.imgUrl" />
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { HotZoneProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
+import HotZoneEditDialog from './components/HotZoneEditDialog/index.vue'
+
+/** 鐑尯灞炴�ч潰鏉� */
+defineOptions({ name: 'HotZoneProperty' })
+
+const props = defineProps<{ modelValue: HotZoneProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 鐑尯缂栬緫瀵硅瘽妗�
+const editDialogRef = ref()
+// 鎵撳紑鐑尯缂栬緫瀵硅瘽妗�
+const handleOpenEditDialog = () => {
+ editDialogRef.value.open()
+}
+</script>
+
+<style scoped lang="scss">
+.hot-zone {
+ position: absolute;
+ background: #409effbf;
+ border: 1px solid var(--el-color-primary);
+ color: #fff;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: move;
+
+ /* 鎺у埗鐐� */
+ .ctrl-dot {
+ position: absolute;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: #fff;
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/ImageBar/config.ts b/src/components/DiyEditor/components/mobile/ImageBar/config.ts
new file mode 100644
index 0000000..68edf72
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ImageBar/config.ts
@@ -0,0 +1,27 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鍥剧墖灞曠ず灞炴�� */
+export interface ImageBarProperty {
+ // 鍥剧墖閾炬帴
+ imgUrl: string
+ // 璺宠浆閾炬帴
+ url: string
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'ImageBar',
+ name: '鍥剧墖灞曠ず',
+ icon: 'ep:picture',
+ property: {
+ imgUrl: '',
+ url: '',
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<ImageBarProperty>
diff --git a/src/components/DiyEditor/components/mobile/ImageBar/index.vue b/src/components/DiyEditor/components/mobile/ImageBar/index.vue
new file mode 100644
index 0000000..d9685b5
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ImageBar/index.vue
@@ -0,0 +1,24 @@
+<template>
+ <!-- 鏃犲浘鐗� -->
+ <div class="h-50px flex items-center justify-center bg-gray-3" v-if="!property.imgUrl">
+ <Icon icon="ep:picture" class="text-gray-8 text-30px!" />
+ </div>
+ <el-image class="min-h-30px" v-else :src="property.imgUrl" />
+</template>
+<script setup lang="ts">
+import { ImageBarProperty } from './config'
+
+/** 鍥剧墖灞曠ず */
+defineOptions({ name: 'ImageBar' })
+
+defineProps<{ property: ImageBarProperty }>()
+</script>
+
+<style scoped lang="scss">
+/* 鍥剧墖 */
+img {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/ImageBar/property.vue b/src/components/DiyEditor/components/mobile/ImageBar/property.vue
new file mode 100644
index 0000000..fe08756
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ImageBar/property.vue
@@ -0,0 +1,34 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-form-item label="涓婁紶鍥剧墖" prop="imgUrl">
+ <UploadImg
+ v-model="formData.imgUrl"
+ draggable="false"
+ height="80px"
+ width="100%"
+ class="min-w-80px"
+ >
+ <template #tip> 寤鸿瀹藉害750 </template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="閾炬帴" prop="url">
+ <AppLinkInput v-model="formData.url" />
+ </el-form-item>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { ImageBarProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 鍥剧墖灞曠ず灞炴�ч潰鏉�
+defineOptions({ name: 'ImageBarProperty' })
+
+const props = defineProps<{ modelValue: ImageBarProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/MagicCube/config.ts b/src/components/DiyEditor/components/mobile/MagicCube/config.ts
new file mode 100644
index 0000000..5e10ab5
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MagicCube/config.ts
@@ -0,0 +1,49 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 骞垮憡榄旀柟灞炴�� */
+export interface MagicCubeProperty {
+ // 涓婂渾瑙�
+ borderRadiusTop: number
+ // 涓嬪渾瑙�
+ borderRadiusBottom: number
+ // 闂撮殧
+ space: number
+ // 瀵艰埅鑿滃崟鍒楄〃
+ list: MagicCubeItemProperty[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+/** 骞垮憡榄旀柟椤圭洰灞炴�� */
+export interface MagicCubeItemProperty {
+ // 鍥炬爣閾炬帴
+ imgUrl: string
+ // 閾炬帴
+ url: string
+ // 瀹�
+ width: number
+ // 楂�
+ height: number
+ // 涓�
+ top: number
+ // 宸�
+ left: number
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'MagicCube',
+ name: '骞垮憡榄旀柟',
+ icon: 'bi:columns',
+ property: {
+ borderRadiusTop: 0,
+ borderRadiusBottom: 0,
+ space: 0,
+ list: [],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<MagicCubeProperty>
diff --git a/src/components/DiyEditor/components/mobile/MagicCube/index.vue b/src/components/DiyEditor/components/mobile/MagicCube/index.vue
new file mode 100644
index 0000000..0abb353
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MagicCube/index.vue
@@ -0,0 +1,76 @@
+<template>
+ <div
+ class="relative"
+ :style="{
+ height: `${rowCount * CUBE_SIZE}px`,
+ width: `${4 * CUBE_SIZE}px`,
+ padding: `${property.space}px`
+ }"
+ >
+ <div
+ v-for="(item, index) in property.list"
+ :key="index"
+ class="absolute"
+ :style="{
+ width: `${item.width * CUBE_SIZE - property.space}px`,
+ height: `${item.height * CUBE_SIZE - property.space}px`,
+ top: `${item.top * CUBE_SIZE}px`,
+ left: `${item.left * CUBE_SIZE}px`
+ }"
+ >
+ <el-image
+ class="h-full w-full"
+ fit="cover"
+ :src="item.imgUrl"
+ :style="{
+ borderTopLeftRadius: `${property.borderRadiusTop}px`,
+ borderTopRightRadius: `${property.borderRadiusTop}px`,
+ borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+ borderBottomRightRadius: `${property.borderRadiusBottom}px`
+ }"
+ >
+ <template #error>
+ <div class="image-slot">
+ <div
+ class="flex items-center justify-center"
+ :style="{
+ width: `${item.width * CUBE_SIZE}px`,
+ height: `${item.height * CUBE_SIZE}px`
+ }"
+ >
+ <Icon icon="ep-picture" color="gray" :size="CUBE_SIZE" />
+ </div>
+ </div>
+ </template>
+ </el-image>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { MagicCubeProperty } from './config'
+
+/** 骞垮憡榄旀柟 */
+defineOptions({ name: 'MagicCube' })
+const props = defineProps<{ property: MagicCubeProperty }>()
+// 涓�涓柟鍧楃殑澶у皬
+const CUBE_SIZE = 93.75
+/**
+ * 璁$畻鏂瑰潡鐨勮鏁�
+ * 琛屾暟鐢ㄤ簬璁$畻榄旀柟鐨勬�讳綋楂樺害锛屽瓨鍦ㄤ互涓嬫儏鍐碉細
+ * 1. 娌℃湁鏁版嵁鏃讹紝榛樿灏卞彧鏄剧ず涓�琛岀殑楂樺害
+ * 2. 搴曢儴鐨勭┖鐧戒笉绠楅珮搴︼紝渚嬪鍙湁绗竴琛屾湁鏁版嵁锛岄偅涔堝氨鍙樉绀轰竴琛岀殑楂樺害
+ * 3. 椤堕儴鍙婁腑闂寸殑绌虹櫧绠楅珮搴︼紝渚嬪涓�鍏辨湁鍥涜锛屽彧鏈夋渶鍚庝竴琛屾湁鏁版嵁锛岄偅涔堜篃鏄剧ず鍥涜鐨勯珮搴�
+ */
+const rowCount = computed(() => {
+ let count = 0
+ if (props.property.list.length > 0) {
+ // 鏈�澶ц鍙�
+ count = Math.max(...props.property.list.map((item) => item.top + item.height))
+ }
+ // 淇濊瘉鑷冲皯鏈変竴琛�
+ return count == 0 ? 1 : count
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/MagicCube/property.vue b/src/components/DiyEditor/components/mobile/MagicCube/property.vue
new file mode 100644
index 0000000..dee3117
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MagicCube/property.vue
@@ -0,0 +1,76 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <!-- 琛ㄥ崟 -->
+ <el-form label-width="80px" :model="formData" class="m-t-8px">
+ <el-text tag="p"> 榄旀柟璁剧疆 </el-text>
+ <el-text type="info" size="small"> 姣忔牸灏哄187 * 187 </el-text>
+ <MagicCubeEditor
+ class="m-y-16px"
+ v-model="formData.list"
+ :rows="4"
+ :cols="4"
+ @hot-area-selected="handleHotAreaSelected"
+ />
+ <template v-for="(hotArea, index) in formData.list" :key="index">
+ <template v-if="selectedHotAreaIndex === index">
+ <el-form-item label="涓婁紶鍥剧墖" :prop="`list[${index}].imgUrl`">
+ <UploadImg v-model="hotArea.imgUrl" height="80px" width="80px" />
+ </el-form-item>
+ <el-form-item label="閾炬帴" :prop="`list[${index}].url`">
+ <AppLinkInput v-model="hotArea.url" />
+ </el-form-item>
+ </template>
+ </template>
+ <el-form-item label="涓婂渾瑙�" prop="borderRadiusTop">
+ <el-slider
+ v-model="formData.borderRadiusTop"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="涓嬪渾瑙�" prop="borderRadiusBottom">
+ <el-slider
+ v-model="formData.borderRadiusBottom"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { MagicCubeProperty } from '@/components/DiyEditor/components/mobile/MagicCube/config'
+
+/** 骞垮憡榄旀柟灞炴�ч潰鏉� */
+defineOptions({ name: 'MagicCubeProperty' })
+
+const props = defineProps<{ modelValue: MagicCubeProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 閫変腑鐨勭儹鍖�
+const selectedHotAreaIndex = ref(-1)
+const handleHotAreaSelected = (_: any, index: number) => {
+ selectedHotAreaIndex.value = index
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/MenuGrid/config.ts b/src/components/DiyEditor/components/mobile/MenuGrid/config.ts
new file mode 100644
index 0000000..9f91ceb
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuGrid/config.ts
@@ -0,0 +1,79 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { cloneDeep } from 'lodash-es'
+
+/** 瀹牸瀵艰埅灞炴�� */
+export interface MenuGridProperty {
+ // 鍒楁暟
+ column: number
+ // 瀵艰埅鑿滃崟鍒楄〃
+ list: MenuGridItemProperty[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+/** 瀹牸瀵艰埅椤圭洰灞炴�� */
+export interface MenuGridItemProperty {
+ // 鍥炬爣閾炬帴
+ iconUrl: string
+ // 鏍囬
+ title: string
+ // 鏍囬棰滆壊
+ titleColor: string
+ // 鍓爣棰�
+ subtitle: string
+ // 鍓爣棰橀鑹�
+ subtitleColor: string
+ // 閾炬帴
+ url: string
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鏂囧瓧
+ text: string
+ // 瑙掓爣鏂囧瓧棰滆壊
+ textColor: string
+ // 瑙掓爣鑳屾櫙棰滆壊
+ bgColor: string
+ }
+}
+
+export const EMPTY_MENU_GRID_ITEM_PROPERTY = {
+ title: '鏍囬',
+ titleColor: '#333',
+ subtitle: '鍓爣棰�',
+ subtitleColor: '#bbb',
+ badge: {
+ show: false,
+ textColor: '#fff',
+ bgColor: '#FF6000'
+ }
+} as MenuGridItemProperty
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'MenuGrid',
+ name: '瀹牸瀵艰埅',
+ icon: 'bi:grid-3x3-gap',
+ property: {
+ column: 3,
+ list: [cloneDeep(EMPTY_MENU_GRID_ITEM_PROPERTY)],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ marginLeft: 8,
+ marginRight: 8,
+ padding: 8,
+ paddingTop: 8,
+ paddingRight: 8,
+ paddingBottom: 8,
+ paddingLeft: 8,
+ borderRadius: 8,
+ borderTopLeftRadius: 8,
+ borderTopRightRadius: 8,
+ borderBottomRightRadius: 8,
+ borderBottomLeftRadius: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<MenuGridProperty>
diff --git a/src/components/DiyEditor/components/mobile/MenuGrid/index.vue b/src/components/DiyEditor/components/mobile/MenuGrid/index.vue
new file mode 100644
index 0000000..1c5ef1d
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuGrid/index.vue
@@ -0,0 +1,35 @@
+<template>
+ <div class="flex flex-row flex-wrap">
+ <div
+ v-for="(item, index) in property.list"
+ :key="index"
+ class="relative flex flex-col items-center p-b-14px p-t-20px"
+ :style="{ width: `${100 * (1 / property.column)}%` }"
+ >
+ <!-- 鍙充笂瑙掕鏍� -->
+ <span
+ v-if="item.badge?.show"
+ class="absolute left-50% top-10px z-1 h-20px rounded-50% p-x-6px text-center text-12px leading-20px"
+ :style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
+ >
+ {{ item.badge.text }}
+ </span>
+ <el-image v-if="item.iconUrl" class="h-28px w-28px" :src="item.iconUrl" />
+ <span class="m-t-8px h-16px text-12px leading-16px" :style="{ color: item.titleColor }">
+ {{ item.title }}
+ </span>
+ <span class="m-t-6px h-12px text-10px leading-12px" :style="{ color: item.subtitleColor }">
+ {{ item.subtitle }}
+ </span>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { MenuGridProperty } from './config'
+/** 瀹牸瀵艰埅 */
+defineOptions({ name: 'MenuGrid' })
+defineProps<{ property: MenuGridProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/MenuGrid/property.vue b/src/components/DiyEditor/components/mobile/MenuGrid/property.vue
new file mode 100644
index 0000000..e05988e
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuGrid/property.vue
@@ -0,0 +1,65 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <!-- 琛ㄥ崟 -->
+ <el-form label-width="80px" :model="formData" class="m-t-8px">
+ <el-form-item label="姣忚鏁伴噺" prop="column">
+ <el-radio-group v-model="formData.column">
+ <el-radio :value="3">3涓�</el-radio>
+ <el-radio :value="4">4涓�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <el-card header="鑿滃崟璁剧疆" class="property-group" shadow="never">
+ <Draggable v-model="formData.list" :empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY">
+ <template #default="{ element }">
+ <el-form-item label="鍥炬爣" prop="iconUrl">
+ <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+ <template #tip> 寤鸿灏哄锛�44 * 44 </template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="鏍囬" prop="title">
+ <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+ </el-form-item>
+ <el-form-item label="鍓爣棰�" prop="subtitle">
+ <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
+ </el-form-item>
+ <el-form-item label="閾炬帴" prop="url">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず瑙掓爣" prop="badge.show">
+ <el-switch v-model="element.badge.show" />
+ </el-form-item>
+ <template v-if="element.badge.show">
+ <el-form-item label="瑙掓爣鍐呭" prop="badge.text">
+ <InputWithColor
+ v-model="element.badge.text"
+ v-model:color="element.badge.textColor"
+ />
+ </el-form-item>
+ <el-form-item label="鑳屾櫙棰滆壊" prop="badge.bgColor">
+ <ColorInput v-model="element.badge.bgColor" />
+ </el-form-item>
+ </template>
+ </template>
+ </Draggable>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import {
+ EMPTY_MENU_GRID_ITEM_PROPERTY,
+ MenuGridProperty
+} from '@/components/DiyEditor/components/mobile/MenuGrid/config'
+
+/** 瀹牸瀵艰埅灞炴�ч潰鏉� */
+defineOptions({ name: 'MenuGridProperty' })
+
+const props = defineProps<{ modelValue: MenuGridProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/MenuList/config.ts b/src/components/DiyEditor/components/mobile/MenuList/config.ts
new file mode 100644
index 0000000..f96fd0a
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuList/config.ts
@@ -0,0 +1,48 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { cloneDeep } from 'lodash-es'
+
+/** 鍒楄〃瀵艰埅灞炴�� */
+export interface MenuListProperty {
+ // 瀵艰埅鑿滃崟鍒楄〃
+ list: MenuListItemProperty[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+/** 鍒楄〃瀵艰埅椤圭洰灞炴�� */
+export interface MenuListItemProperty {
+ // 鍥炬爣閾炬帴
+ iconUrl: string
+ // 鏍囬
+ title: string
+ // 鏍囬棰滆壊
+ titleColor: string
+ // 鍓爣棰�
+ subtitle: string
+ // 鍓爣棰橀鑹�
+ subtitleColor: string
+ // 閾炬帴
+ url: string
+}
+
+export const EMPTY_MENU_LIST_ITEM_PROPERTY = {
+ title: '鏍囬',
+ titleColor: '#333',
+ subtitle: '鍓爣棰�',
+ subtitleColor: '#bbb'
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'MenuList',
+ name: '鍒楄〃瀵艰埅',
+ icon: 'fa-solid:list',
+ property: {
+ list: [cloneDeep(EMPTY_MENU_LIST_ITEM_PROPERTY)],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<MenuListProperty>
diff --git a/src/components/DiyEditor/components/mobile/MenuList/index.vue b/src/components/DiyEditor/components/mobile/MenuList/index.vue
new file mode 100644
index 0000000..9a56fd9
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuList/index.vue
@@ -0,0 +1,31 @@
+<template>
+ <div class="min-h-42px flex flex-col">
+ <div
+ v-for="(item, index) in property.list"
+ :key="index"
+ class="item h-42px flex flex-row items-center justify-between gap-4px p-x-12px"
+ >
+ <div class="flex flex-1 flex-row items-center gap-8px">
+ <el-image v-if="item.iconUrl" class="h-16px w-16px" :src="item.iconUrl" />
+ <span class="text-16px" :style="{ color: item.titleColor }">{{ item.title }}</span>
+ </div>
+ <div class="item-center flex flex-row justify-center gap-4px">
+ <span class="text-12px" :style="{ color: item.subtitleColor }">{{ item.subtitle }}</span>
+ <Icon icon="ep-arrow-right" color="#000" :size="16" />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { MenuListProperty } from './config'
+/** 鍒楄〃瀵艰埅 */
+defineOptions({ name: 'MenuList' })
+defineProps<{ property: MenuListProperty }>()
+</script>
+
+<style scoped lang="scss">
+.item + .item {
+ border-top: 1px solid #eee;
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/MenuList/property.vue b/src/components/DiyEditor/components/mobile/MenuList/property.vue
new file mode 100644
index 0000000..b665b32
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuList/property.vue
@@ -0,0 +1,45 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-text tag="p"> 鑿滃崟璁剧疆 </el-text>
+ <el-text type="info" size="small"> 鎷栧姩宸︿晶鐨勫皬鍦嗙偣鍙互璋冩暣椤哄簭 </el-text>
+
+ <!-- 琛ㄥ崟 -->
+ <el-form label-width="60px" :model="formData" class="m-t-8px">
+ <Draggable v-model="formData.list" :empty-item="EMPTY_MENU_LIST_ITEM_PROPERTY">
+ <template #default="{ element }">
+ <el-form-item label="鍥炬爣" prop="iconUrl">
+ <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+ <template #tip> 寤鸿灏哄锛�44 * 44 </template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="鏍囬" prop="title">
+ <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+ </el-form-item>
+ <el-form-item label="鍓爣棰�" prop="subtitle">
+ <InputWithColor v-model="element.subtitle" v-model:color="element.subtitleColor" />
+ </el-form-item>
+ <el-form-item label="閾炬帴" prop="url">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ </template>
+ </Draggable>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import {
+ EMPTY_MENU_LIST_ITEM_PROPERTY,
+ MenuListProperty
+} from '@/components/DiyEditor/components/mobile/MenuList/config'
+
+/** 鍒楄〃瀵艰埅灞炴�ч潰鏉� */
+defineOptions({ name: 'MenuListProperty' })
+
+const props = defineProps<{ modelValue: MenuListProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/MenuSwiper/config.ts b/src/components/DiyEditor/components/mobile/MenuSwiper/config.ts
new file mode 100644
index 0000000..fe5f4e8
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuSwiper/config.ts
@@ -0,0 +1,66 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+import { cloneDeep } from 'lodash-es'
+
+/** 鑿滃崟瀵艰埅灞炴�� */
+export interface MenuSwiperProperty {
+ // 甯冨眬锛� 鍥炬爣+鏂囧瓧 | 鍥炬爣
+ layout: 'iconText' | 'icon'
+ // 琛屾暟
+ row: number
+ // 鍒楁暟
+ column: number
+ // 瀵艰埅鑿滃崟鍒楄〃
+ list: MenuSwiperItemProperty[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+/** 鑿滃崟瀵艰埅椤圭洰灞炴�� */
+export interface MenuSwiperItemProperty {
+ // 鍥炬爣閾炬帴
+ iconUrl: string
+ // 鏍囬
+ title: string
+ // 鏍囬棰滆壊
+ titleColor: string
+ // 閾炬帴
+ url: string
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鏂囧瓧
+ text: string
+ // 瑙掓爣鏂囧瓧棰滆壊
+ textColor: string
+ // 瑙掓爣鑳屾櫙棰滆壊
+ bgColor: string
+ }
+}
+
+export const EMPTY_MENU_SWIPER_ITEM_PROPERTY = {
+ title: '鏍囬',
+ titleColor: '#333',
+ badge: {
+ show: false,
+ textColor: '#fff',
+ bgColor: '#FF6000'
+ }
+} as MenuSwiperItemProperty
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'MenuSwiper',
+ name: '鑿滃崟瀵艰埅',
+ icon: 'bi:grid-3x2-gap',
+ property: {
+ layout: 'iconText',
+ row: 1,
+ column: 3,
+ list: [cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<MenuSwiperProperty>
diff --git a/src/components/DiyEditor/components/mobile/MenuSwiper/index.vue b/src/components/DiyEditor/components/mobile/MenuSwiper/index.vue
new file mode 100644
index 0000000..fc6d718
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuSwiper/index.vue
@@ -0,0 +1,119 @@
+<template>
+ <el-carousel
+ :height="`${carouselHeight}px`"
+ :autoplay="false"
+ arrow="hover"
+ indicator-position="outside"
+ >
+ <el-carousel-item v-for="(page, pageIndex) in pages" :key="pageIndex">
+ <div class="flex flex-row flex-wrap">
+ <div
+ v-for="(item, index) in page"
+ :key="index"
+ class="relative flex flex-col items-center justify-center"
+ :style="{ width: columnWidth, height: `${rowHeight}px` }"
+ >
+ <!-- 鍥炬爣 + 瑙掓爣 -->
+ <div class="relative" :class="`h-${ICON_SIZE}px w-${ICON_SIZE}px`">
+ <!-- 鍙充笂瑙掕鏍� -->
+ <span
+ v-if="item.badge?.show"
+ class="absolute right--10px top--10px z-1 h-20px rounded-10px p-x-6px text-center text-12px leading-20px"
+ :style="{ color: item.badge.textColor, backgroundColor: item.badge.bgColor }"
+ >
+ {{ item.badge.text }}
+ </span>
+ <el-image v-if="item.iconUrl" :src="item.iconUrl" class="h-full w-full" />
+ </div>
+ <!-- 鏍囬 -->
+ <span
+ v-if="property.layout === 'iconText'"
+ class="text-12px"
+ :style="{
+ color: item.titleColor,
+ height: `${TITLE_HEIGHT}px`,
+ lineHeight: `${TITLE_HEIGHT}px`
+ }"
+ >
+ {{ item.title }}
+ </span>
+ </div>
+ </div>
+ </el-carousel-item>
+ </el-carousel>
+</template>
+
+<script setup lang="ts">
+import { MenuSwiperProperty, MenuSwiperItemProperty } from './config'
+/** 鑿滃崟瀵艰埅 */
+defineOptions({ name: 'MenuSwiper' })
+const props = defineProps<{ property: MenuSwiperProperty }>()
+// 鏍囬鐨勯珮搴�
+const TITLE_HEIGHT = 20
+// 鍥炬爣鐨勯珮搴�
+const ICON_SIZE = 32
+// 鍨傜洿闂磋窛锛氫竴琛屼笂涓嬬殑闂磋窛
+const SPACE_Y = 16
+
+// 鍒嗛〉
+const pages = ref<MenuSwiperItemProperty[][]>([])
+// 杞挱鍥鹃珮搴�
+const carouselHeight = ref(0)
+// 琛岄珮
+const rowHeight = ref(0)
+// 鍒楀
+const columnWidth = ref('')
+watch(
+ () => props.property,
+ () => {
+ // 璁$畻鍒楀锛氭瘡涓�鍒楃殑鐧惧垎姣�
+ columnWidth.value = `${100 * (1 / props.property.column)}%`
+ // 璁$畻琛岄珮锛氬浘鏍� + 鏂囧瓧锛堜粎鏄剧ず鍥剧墖鏃朵负0锛� + 鍨傜洿闂磋窛 * 2
+ rowHeight.value =
+ (props.property.layout === 'iconText' ? ICON_SIZE + TITLE_HEIGHT : ICON_SIZE) + SPACE_Y * 2
+ // 璁$畻杞挱鐨勯珮搴︼細琛屾暟 * 琛岄珮
+ carouselHeight.value = props.property.row * rowHeight.value
+
+ // 姣忛〉鏁伴噺锛氳鏁� * 鍒楁暟
+ const pageSize = props.property.row * props.property.column
+ // 娓呯┖鍒嗛〉
+ pages.value = []
+ // 姣忎竴椤电殑鑿滃崟
+ let pageItems: MenuSwiperItemProperty[] = []
+ for (const item of props.property.list) {
+ // 鏈〉婊″憳锛屾柊寤轰笅涓�椤�
+ if (pageItems.length === pageSize) {
+ pageItems = []
+ }
+ // 澧炲姞涓�椤�
+ if (pageItems.length === 0) {
+ pages.value.push(pageItems)
+ }
+ // 鏈〉澧炲姞涓�涓�
+ pageItems.push(item)
+ }
+ },
+ { immediate: true, deep: true }
+)
+</script>
+
+<style lang="scss">
+// 閲嶅啓鎸囩ず鍣ㄦ牱寮忥紝涓� APP 淇濇寔涓�鑷�
+:root {
+ .el-carousel__indicator {
+ padding-top: 0;
+ padding-bottom: 0;
+ .el-carousel__button {
+ --el-carousel-indicator-height: 6px;
+ --el-carousel-indicator-width: 6px;
+ --el-carousel-indicator-out-color: #ff6000;
+ border-radius: 6px;
+ }
+ }
+ .el-carousel__indicator.is-active {
+ .el-carousel__button {
+ --el-carousel-indicator-width: 12px;
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue b/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue
new file mode 100644
index 0000000..3dd3f7c
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/MenuSwiper/property.vue
@@ -0,0 +1,76 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <!-- 琛ㄥ崟 -->
+ <el-form label-width="80px" :model="formData" class="m-t-8px">
+ <el-form-item label="甯冨眬" prop="layout">
+ <el-radio-group v-model="formData.layout">
+ <el-radio value="iconText">鍥炬爣+鏂囧瓧</el-radio>
+ <el-radio value="icon">浠呭浘鏍�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="琛屾暟" prop="row">
+ <el-radio-group v-model="formData.row">
+ <el-radio :value="1">1琛�</el-radio>
+ <el-radio :value="2">2琛�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍒楁暟" prop="column">
+ <el-radio-group v-model="formData.column">
+ <el-radio :value="3">3鍒�</el-radio>
+ <el-radio :value="4">4鍒�</el-radio>
+ <el-radio :value="5">5鍒�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <el-card header="鑿滃崟璁剧疆" class="property-group" shadow="never">
+ <Draggable v-model="formData.list" :empty-item="cloneDeep(EMPTY_MENU_SWIPER_ITEM_PROPERTY)">
+ <template #default="{ element }">
+ <el-form-item label="鍥炬爣" prop="iconUrl">
+ <UploadImg v-model="element.iconUrl" height="80px" width="80px">
+ <template #tip> 寤鸿灏哄锛�98 * 98 </template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="鏍囬" prop="title">
+ <InputWithColor v-model="element.title" v-model:color="element.titleColor" />
+ </el-form-item>
+ <el-form-item label="閾炬帴" prop="url">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず瑙掓爣" prop="badge.show">
+ <el-switch v-model="element.badge.show" />
+ </el-form-item>
+ <template v-if="element.badge.show">
+ <el-form-item label="瑙掓爣鍐呭" prop="badge.text">
+ <InputWithColor
+ v-model="element.badge.text"
+ v-model:color="element.badge.textColor"
+ />
+ </el-form-item>
+ <el-form-item label="鑳屾櫙棰滆壊" prop="badge.bgColor">
+ <ColorInput v-model="element.badge.bgColor" />
+ </el-form-item>
+ </template>
+ </template>
+ </Draggable>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import {
+ EMPTY_MENU_SWIPER_ITEM_PROPERTY,
+ MenuSwiperProperty
+} from '@/components/DiyEditor/components/mobile/MenuSwiper/config'
+import { cloneDeep } from 'lodash-es'
+
+/** 鑿滃崟瀵艰埅灞炴�ч潰鏉� */
+defineOptions({ name: 'MenuSwiperProperty' })
+
+const props = defineProps<{ modelValue: MenuSwiperProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue b/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue
new file mode 100644
index 0000000..6c671d3
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue
@@ -0,0 +1,128 @@
+<template>
+ <div class="h-40px flex items-center justify-center">
+ <MagicCubeEditor
+ v-model="cellList"
+ :cols="cellCount"
+ :cube-size="38"
+ :rows="1"
+ class="m-b-16px"
+ @hot-area-selected="handleHotAreaSelected"
+ />
+ <img v-if="isMp" alt="" class="h-30px w-76px" src="@/assets/imgs/diy/app-nav-bar-mp.png" />
+ </div>
+ <template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
+ <template v-if="selectedHotAreaIndex === cellIndex">
+ <el-form-item :prop="`cell[${cellIndex}].type`" label="绫诲瀷">
+ <el-radio-group v-model="cell.type" @change="handleHotAreaSelected(cell, cellIndex)">
+ <el-radio value="text">鏂囧瓧</el-radio>
+ <el-radio value="image">鍥剧墖</el-radio>
+ <el-radio value="search">鎼滅储妗�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <!-- 1. 鏂囧瓧 -->
+ <template v-if="cell.type === 'text'">
+ <el-form-item :prop="`cell[${cellIndex}].text`" label="鍐呭">
+ <el-input v-model="cell!.text" maxlength="10" show-word-limit />
+ </el-form-item>
+ <el-form-item :prop="`cell[${cellIndex}].text`" label="棰滆壊">
+ <ColorInput v-model="cell!.textColor" />
+ </el-form-item>
+ <el-form-item :prop="`cell[${cellIndex}].url`" label="閾炬帴">
+ <AppLinkInput v-model="cell.url" />
+ </el-form-item>
+ </template>
+ <!-- 2. 鍥剧墖 -->
+ <template v-else-if="cell.type === 'image'">
+ <el-form-item :prop="`cell[${cellIndex}].imgUrl`" label="鍥剧墖">
+ <UploadImg v-model="cell.imgUrl" :limit="1" height="56px" width="56px">
+ <template #tip>寤鸿灏哄 56*56</template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item :prop="`cell[${cellIndex}].url`" label="閾炬帴">
+ <AppLinkInput v-model="cell.url" />
+ </el-form-item>
+ </template>
+ <!-- 3. 鎼滅储妗� -->
+ <template v-else>
+ <el-form-item label="妗嗕綋棰滆壊" prop="backgroundColor">
+ <ColorInput v-model="cell.backgroundColor" />
+ </el-form-item>
+ <el-form-item class="lef" label="鏂囨湰棰滆壊" prop="textColor">
+ <ColorInput v-model="cell.textColor" />
+ </el-form-item>
+ <el-form-item :prop="`cell[${cellIndex}].placeholder`" label="鎻愮ず鏂囧瓧">
+ <el-input v-model="cell.placeholder" maxlength="10" show-word-limit />
+ </el-form-item>
+ <el-form-item label="鏂囨湰浣嶇疆" prop="placeholderPosition">
+ <el-radio-group v-model="cell!.placeholderPosition">
+ <el-tooltip content="灞呭乏" placement="top">
+ <el-radio-button value="left">
+ <Icon icon="ant-design:align-left-outlined" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip content="灞呬腑" placement="top">
+ <el-radio-button value="center">
+ <Icon icon="ant-design:align-center-outlined" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鎵竴鎵�" prop="showScan">
+ <el-switch v-model="cell!.showScan" />
+ </el-form-item>
+ <el-form-item :prop="`cell[${cellIndex}].borderRadius`" label="鍦嗚">
+ <el-slider
+ v-model="cell.borderRadius"
+ :max="100"
+ :min="0"
+ :show-input-controls="false"
+ input-size="small"
+ show-input
+ />
+ </el-form-item>
+ </template>
+ </template>
+ </template>
+</template>
+
+<script lang="ts" setup>
+import { NavigationBarCellProperty } from '../config'
+import { useVModel } from '@vueuse/core'
+// 瀵艰埅鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'NavigationBarCellProperty' })
+
+const props = withDefaults(
+ defineProps<{
+ modelValue: NavigationBarCellProperty[]
+ isMp: boolean
+ }>(),
+ {
+ modelValue: () => [],
+ isMp: true
+ }
+)
+const emit = defineEmits(['update:modelValue'])
+const cellList = useVModel(props, 'modelValue', emit)
+
+// 鍗曞厓鏍兼暟閲忥細灏忕▼搴�6涓紙鍙充晶鑳跺泭鎸夐挳鍗犱簡2涓級锛屽叾瀹冨钩鍙�8涓�
+const cellCount = computed(() => (props.isMp ? 6 : 8))
+
+// 閫変腑鐨勭儹鍖�
+const selectedHotAreaIndex = ref(0)
+const handleHotAreaSelected = (cellValue: NavigationBarCellProperty, index: number) => {
+ selectedHotAreaIndex.value = index
+ // 榛樿璁剧疆涓洪�変腑鏂囧瓧锛屽苟璁剧疆灞炴��
+ if (!cellValue.type) {
+ cellValue.type = 'text'
+ cellValue.textColor = '#111111'
+ }
+ // 濡傛灉鐐瑰嚮鐨勬槸鎼滅储妗嗭紝鍒欏垵濮嬪寲鎼滅储妗嗙殑灞炴��
+ if (cellValue.type === 'search') {
+ cellValue.placeholderPosition = 'left'
+ cellValue.backgroundColor = '#EEEEEE'
+ cellValue.textColor = '#969799'
+ }
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/config.ts b/src/components/DiyEditor/components/mobile/NavigationBar/config.ts
new file mode 100644
index 0000000..4fb5e7c
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NavigationBar/config.ts
@@ -0,0 +1,88 @@
+import { DiyComponent } from '@/components/DiyEditor/util'
+
+/** 椤堕儴瀵艰埅鏍忓睘鎬� */
+export interface NavigationBarProperty {
+ // 鑳屾櫙绫诲瀷
+ bgType: 'color' | 'img'
+ // 鑳屾櫙棰滆壊
+ bgColor: string
+ // 鍥剧墖閾炬帴
+ bgImg: string
+ // 鏍峰紡绫诲瀷锛氶粯璁� | 娌夋蹈寮�
+ styleType: 'normal' | 'inner'
+ // 甯搁┗鏄剧ず
+ alwaysShow: boolean
+ // 灏忕▼搴忓崟鍏冩牸鍒楄〃
+ mpCells: NavigationBarCellProperty[]
+ // 鍏跺畠骞冲彴鍗曞厓鏍煎垪琛�
+ otherCells: NavigationBarCellProperty[]
+ // 鏈湴鍙橀噺
+ _local: {
+ // 棰勮椤堕儴瀵艰埅锛堝皬绋嬪簭锛�
+ previewMp: boolean
+ // 棰勮椤堕儴瀵艰埅锛堥潪灏忕▼搴忥級
+ previewOther: boolean
+ }
+}
+
+/** 椤堕儴瀵艰埅鏍� - 鍗曞厓鏍� 灞炴�� */
+export interface NavigationBarCellProperty {
+ // 绫诲瀷锛氭枃瀛� | 鍥剧墖 | 鎼滅储妗�
+ type: 'text' | 'image' | 'search'
+ // 瀹藉害
+ width: number
+ // 楂樺害
+ height: number
+ // 椤堕儴浣嶇疆
+ top: number
+ // 宸︿晶浣嶇疆
+ left: number
+ // 鏂囧瓧鍐呭
+ text: string
+ // 鏂囧瓧棰滆壊
+ textColor: string
+ // 鍥剧墖鍦板潃
+ imgUrl: string
+ // 鍥剧墖閾炬帴
+ url: string
+ // 鎼滅储妗嗭細妗嗕綋棰滆壊
+ backgroundColor: string
+ // 鎼滅储妗嗭細鎻愮ず鏂囧瓧
+ placeholder: string
+ // 鎼滅储妗嗭細鎻愮ず鏂囧瓧浣嶇疆
+ placeholderPosition: string
+ // 鎼滅储妗嗭細鏄惁鏄剧ず鎵竴鎵�
+ showScan: boolean
+ // 鎼滅储妗嗭細杈规鍦嗚鍗婂緞
+ borderRadius: number
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'NavigationBar',
+ name: '椤堕儴瀵艰埅鏍�',
+ icon: 'tabler:layout-navbar',
+ property: {
+ bgType: 'color',
+ bgColor: '#fff',
+ bgImg: '',
+ styleType: 'normal',
+ alwaysShow: true,
+ mpCells: [
+ {
+ type: 'text',
+ textColor: '#111111'
+ }
+ ],
+ otherCells: [
+ {
+ type: 'text',
+ textColor: '#111111'
+ }
+ ],
+ _local: {
+ previewMp: true,
+ previewOther: false
+ }
+ }
+} as DiyComponent<NavigationBarProperty>
diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/index.vue b/src/components/DiyEditor/components/mobile/NavigationBar/index.vue
new file mode 100644
index 0000000..104c600
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NavigationBar/index.vue
@@ -0,0 +1,93 @@
+<template>
+ <div class="navigation-bar" :style="bgStyle">
+ <div class="h-full w-full flex items-center">
+ <div v-for="(cell, cellIndex) in cellList" :key="cellIndex" :style="getCellStyle(cell)">
+ <span v-if="cell.type === 'text'">{{ cell.text }}</span>
+ <img v-else-if="cell.type === 'image'" :src="cell.imgUrl" alt="" class="h-full w-full" />
+ <SearchBar v-else :property="getSearchProp(cell)" />
+ </div>
+ </div>
+ <img
+ v-if="property._local?.previewMp"
+ src="@/assets/imgs/diy/app-nav-bar-mp.png"
+ alt=""
+ class="h-30px w-86px"
+ />
+ </div>
+</template>
+<script setup lang="ts">
+import { NavigationBarCellProperty, NavigationBarProperty } from './config'
+import SearchBar from '@/components/DiyEditor/components/mobile/SearchBar/index.vue'
+import { StyleValue } from 'vue'
+import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
+
+/** 椤甸潰椤堕儴瀵艰埅鏍� */
+defineOptions({ name: 'NavigationBar' })
+
+const props = defineProps<{ property: NavigationBarProperty }>()
+
+// 鑳屾櫙
+const bgStyle = computed(() => {
+ const background =
+ props.property.bgType === 'img' && props.property.bgImg
+ ? `url(${props.property.bgImg}) no-repeat top center / 100% 100%`
+ : props.property.bgColor
+ return { background }
+})
+// 鍗曞厓鏍煎垪琛�
+const cellList = computed(() =>
+ props.property._local?.previewMp ? props.property.mpCells : props.property.otherCells
+)
+// 鍗曞厓鏍煎搴�
+const cellWidth = computed(() => {
+ return props.property._local?.previewMp ? (375 - 80 - 86) / 6 : (375 - 90) / 8
+})
+// 鑾峰緱鍗曞厓鏍兼牱寮�
+const getCellStyle = (cell: NavigationBarCellProperty) => {
+ return {
+ width: cell.width * cellWidth.value + (cell.width - 1) * 10 + 'px',
+ left: cell.left * cellWidth.value + (cell.left + 1) * 10 + 'px',
+ position: 'absolute'
+ } as StyleValue
+}
+// 鑾峰緱鎼滅储妗嗗睘鎬�
+const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
+ return {
+ height: 30,
+ backgroundColor: cell.backgroundColor,
+ showScan: cell.showScan,
+ placeholder: cell.placeholder,
+ borderRadius: cell.borderRadius,
+ textColor: cell.textColor,
+ placeholderPosition: cell.placeholderPosition
+ } as SearchProperty
+})
+</script>
+<style lang="scss" scoped>
+.navigation-bar {
+ display: flex;
+ height: 50px;
+ background: #fff;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 6px;
+
+ /* 宸﹁竟 */
+ .left {
+ margin-left: 8px;
+ }
+
+ .center {
+ font-size: 14px;
+ line-height: 35px;
+ color: #333;
+ text-align: center;
+ flex: 1;
+ }
+
+ /* 鍙宠竟 */
+ .right {
+ margin-right: 8px;
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/property.vue b/src/components/DiyEditor/components/mobile/NavigationBar/property.vue
new file mode 100644
index 0000000..654b3b2
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NavigationBar/property.vue
@@ -0,0 +1,91 @@
+<template>
+ <el-form label-width="80px" :model="formData" :rules="rules">
+ <el-form-item label="鏍峰紡" prop="styleType">
+ <el-radio-group v-model="formData!.styleType">
+ <el-radio value="normal">鏍囧噯</el-radio>
+ <el-tooltip
+ content="娌変镜寮忓ご閮ㄤ粎鏀寔寰俊灏忕▼搴忋�丄PP锛屽缓璁〉闈㈢涓�涓粍浠朵负鍥剧墖灞曠ず绫荤粍浠�"
+ placement="top"
+ >
+ <el-radio value="inner">娌夋蹈寮�</el-radio>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="甯搁┗鏄剧ず" prop="alwaysShow" v-if="formData.styleType === 'inner'">
+ <el-radio-group v-model="formData!.alwaysShow">
+ <el-radio :value="false">鍏抽棴</el-radio>
+ <el-tooltip content="甯搁┗鏄剧ず鍏抽棴鍚�,澶撮儴灏忕粍浠跺皢鍦ㄩ〉闈㈡粦鍔ㄦ椂娣″叆" placement="top">
+ <el-radio :value="true">寮�鍚�</el-radio>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鑳屾櫙绫诲瀷" prop="bgType">
+ <el-radio-group v-model="formData.bgType">
+ <el-radio value="color">绾壊</el-radio>
+ <el-radio value="img">鍥剧墖</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鑳屾櫙棰滆壊" prop="bgColor" v-if="formData.bgType === 'color'">
+ <ColorInput v-model="formData.bgColor" />
+ </el-form-item>
+ <el-form-item label="鑳屾櫙鍥剧墖" prop="bgImg" v-else>
+ <div class="flex items-center">
+ <UploadImg v-model="formData.bgImg" :limit="1" width="56px" height="56px" />
+ <span class="text-xs text-gray-400 ml-2 mb-2">寤鸿瀹藉害锛�750</span>
+ </div>
+ </el-form-item>
+ <el-card class="property-group" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <span>鍐呭锛堝皬绋嬪簭锛�</span>
+ <el-form-item prop="_local.previewMp" class="m-b-0!">
+ <el-checkbox
+ v-model="formData._local.previewMp"
+ @change="formData._local.previewOther = !formData._local.previewMp"
+ >
+ 棰勮
+ </el-checkbox>
+ </el-form-item>
+ </div>
+ </template>
+ <NavigationBarCellProperty v-model="formData.mpCells" is-mp />
+ </el-card>
+ <el-card class="property-group" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <span>鍐呭锛堥潪灏忕▼搴忥級</span>
+ <el-form-item prop="_local.previewOther" class="m-b-0!">
+ <el-checkbox
+ v-model="formData._local.previewOther"
+ @change="formData._local.previewMp = !formData._local.previewOther"
+ >
+ 棰勮
+ </el-checkbox>
+ </el-form-item>
+ </div>
+ </template>
+ <NavigationBarCellProperty v-model="formData.otherCells" :is-mp="false" />
+ </el-card>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { NavigationBarProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import NavigationBarCellProperty from '@/components/DiyEditor/components/mobile/NavigationBar/components/CellProperty.vue'
+// 瀵艰埅鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'NavigationBarProperty' })
+// 琛ㄥ崟鏍¢獙
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ラ〉闈㈠悕绉�', trigger: 'blur' }]
+}
+
+const props = defineProps<{ modelValue: NavigationBarProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+if (!formData.value._local) {
+ formData.value._local = { previewMp: true, previewOther: false }
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/NoticeBar/config.ts b/src/components/DiyEditor/components/mobile/NoticeBar/config.ts
new file mode 100644
index 0000000..b6b0860
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NoticeBar/config.ts
@@ -0,0 +1,46 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鍏憡鏍忓睘鎬� */
+export interface NoticeBarProperty {
+ // 鍥炬爣鍦板潃
+ iconUrl: string
+ // 鍏憡鍐呭鍒楄〃
+ contents: NoticeContentProperty[]
+ // 鑳屾櫙棰滆壊
+ backgroundColor: string
+ // 鏂囧瓧棰滆壊
+ textColor: string
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+/** 鍐呭灞炴�� */
+export interface NoticeContentProperty {
+ // 鍐呭鏂囧瓧
+ text: string
+ // 閾炬帴鍦板潃
+ url: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'NoticeBar',
+ name: '鍏憡鏍�',
+ icon: 'ep:bell',
+ property: {
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
+ contents: [
+ {
+ text: '',
+ url: ''
+ }
+ ],
+ backgroundColor: '#fff',
+ textColor: '#333',
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<NoticeBarProperty>
diff --git a/src/components/DiyEditor/components/mobile/NoticeBar/index.vue b/src/components/DiyEditor/components/mobile/NoticeBar/index.vue
new file mode 100644
index 0000000..fce1afb
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NoticeBar/index.vue
@@ -0,0 +1,26 @@
+<template>
+ <div
+ class="flex items-center p-y-4px text-12px"
+ :style="{ backgroundColor: property.backgroundColor, color: property.textColor }"
+ >
+ <el-image :src="property.iconUrl" class="h-18px" />
+ <el-divider direction="vertical" />
+ <el-carousel height="24px" direction="vertical" :autoplay="true" class="flex-1 p-r-8px">
+ <el-carousel-item v-for="(item, index) in property.contents" :key="index">
+ <div class="h-24px truncate leading-24px">{{ item.text }}</div>
+ </el-carousel-item>
+ </el-carousel>
+ <Icon icon="ep:arrow-right" />
+ </div>
+</template>
+
+<script setup lang="ts">
+import { NoticeBarProperty } from './config'
+
+/** 鍏憡鏍� */
+defineOptions({ name: 'NoticeBar' })
+
+defineProps<{ property: NoticeBarProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/NoticeBar/property.vue b/src/components/DiyEditor/components/mobile/NoticeBar/property.vue
new file mode 100644
index 0000000..99d04b0
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/NoticeBar/property.vue
@@ -0,0 +1,46 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData" :rules="rules">
+ <el-form-item label="鍏憡鍥炬爣" prop="iconUrl">
+ <UploadImg v-model="formData.iconUrl" height="48px">
+ <template #tip>寤鸿灏哄锛�24 * 24</template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="鑳屾櫙棰滆壊" prop="backgroundColor">
+ <ColorInput v-model="formData.backgroundColor" />
+ </el-form-item>
+ <el-form-item label="鏂囧瓧棰滆壊" prop="鏂囧瓧棰滆壊">
+ <ColorInput v-model="formData.textColor" />
+ </el-form-item>
+ <el-card header="鍏憡鍐呭" class="property-group" shadow="never">
+ <Draggable v-model="formData.contents">
+ <template #default="{ element }">
+ <el-form-item label="鍏憡" prop="text" label-width="40px">
+ <el-input v-model="element.text" placeholder="璇疯緭鍏ュ叕鍛�" />
+ </el-form-item>
+ <el-form-item label="閾炬帴" prop="url" label-width="40px">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ </template>
+ </Draggable>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { NoticeBarProperty } from './config'
+import { useVModel } from '@vueuse/core'
+// 閫氱煡鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'NoticeBarProperty' })
+// 琛ㄥ崟鏍¢獙
+const rules = {
+ content: [{ required: true, message: '璇疯緭鍏ュ叕鍛�', trigger: 'blur' }]
+}
+
+const props = defineProps<{ modelValue: NoticeBarProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PageConfig/config.ts b/src/components/DiyEditor/components/mobile/PageConfig/config.ts
new file mode 100644
index 0000000..f8e45e4
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PageConfig/config.ts
@@ -0,0 +1,23 @@
+import { DiyComponent } from '@/components/DiyEditor/util'
+
+/** 椤甸潰璁剧疆灞炴�� */
+export interface PageConfigProperty {
+ // 椤甸潰鎻忚堪
+ description: string
+ // 椤甸潰鑳屾櫙棰滆壊
+ backgroundColor: string
+ // 椤甸潰鑳屾櫙鍥剧墖
+ backgroundImage: string
+}
+
+// 瀹氫箟椤甸潰缁勪欢
+export const component = {
+ id: 'PageConfig',
+ name: '椤甸潰璁剧疆',
+ icon: 'ep:document',
+ property: {
+ description: '',
+ backgroundColor: '#f5f5f5',
+ backgroundImage: ''
+ }
+} as DiyComponent<PageConfigProperty>
diff --git a/src/components/DiyEditor/components/mobile/PageConfig/property.vue b/src/components/DiyEditor/components/mobile/PageConfig/property.vue
new file mode 100644
index 0000000..d8f51d2
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PageConfig/property.vue
@@ -0,0 +1,34 @@
+<template>
+ <el-form label-width="80px" :model="formData" :rules="rules">
+ <el-form-item label="椤甸潰鎻忚堪" prop="description">
+ <el-input
+ type="textarea"
+ v-model="formData!.description"
+ placeholder="鐢ㄦ埛閫氳繃寰俊鍒嗕韩缁欐湅鍙嬫椂锛屼細鑷姩鏄剧ず椤甸潰鎻忚堪"
+ />
+ </el-form-item>
+ <el-form-item label="鑳屾櫙棰滆壊" prop="backgroundColor">
+ <ColorInput v-model="formData!.backgroundColor" />
+ </el-form-item>
+ <el-form-item label="鑳屾櫙鍥剧墖" prop="backgroundImage">
+ <UploadImg v-model="formData!.backgroundImage" :limit="1">
+ <template #tip>寤鸿瀹藉害 750px</template>
+ </UploadImg>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { PageConfigProperty } from './config'
+import { useVModel } from '@vueuse/core'
+// 瀵艰埅鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'PageConfigProperty' })
+// 琛ㄥ崟鏍¢獙
+const rules = {}
+
+const props = defineProps<{ modelValue: PageConfigProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/Popover/config.ts b/src/components/DiyEditor/components/mobile/Popover/config.ts
new file mode 100644
index 0000000..e814090
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Popover/config.ts
@@ -0,0 +1,26 @@
+import { DiyComponent } from '@/components/DiyEditor/util'
+
+/** 寮圭獥骞垮憡灞炴�� */
+export interface PopoverProperty {
+ list: PopoverItemProperty[]
+}
+
+export interface PopoverItemProperty {
+ // 鍥剧墖鍦板潃
+ imgUrl: string
+ // 璺宠浆杩炴帴
+ url: string
+ // 鏄剧ず绫诲瀷锛氫粎鏄剧ず涓�娆°�佹瘡娆″惎鍔ㄩ兘浼氭樉绀�
+ showType: 'once' | 'always'
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'Popover',
+ name: '寮圭獥骞垮憡',
+ icon: 'carbon:popup',
+ position: 'fixed',
+ property: {
+ list: [{ showType: 'once' }]
+ }
+} as DiyComponent<PopoverProperty>
diff --git a/src/components/DiyEditor/components/mobile/Popover/index.vue b/src/components/DiyEditor/components/mobile/Popover/index.vue
new file mode 100644
index 0000000..347599b
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Popover/index.vue
@@ -0,0 +1,38 @@
+<template>
+ <div
+ v-for="(item, index) in property.list"
+ :key="index"
+ class="absolute bottom-50% right-50% h-454px w-292px border-1px border-gray border-rounded-4px border-solid bg-white p-1px"
+ :style="{
+ zIndex: 100 + index + (activeIndex === index ? 100 : 0),
+ marginRight: `${-146 - index * 20}px`,
+ marginBottom: `${-227 - index * 20}px`
+ }"
+ @click="handleActive(index)"
+ >
+ <el-image :src="item.imgUrl" fit="contain" class="h-full w-full">
+ <template #error>
+ <div class="h-full w-full flex items-center justify-center">
+ <Icon icon="ep:picture" />
+ </div>
+ </template>
+ </el-image>
+ <div class="absolute right-1 top-1 text-12px">{{ index + 1 }}</div>
+ </div>
+</template>
+<script setup lang="ts">
+import { PopoverProperty } from './config'
+
+/** 寮圭獥骞垮憡 */
+defineOptions({ name: 'Popover' })
+// 瀹氫箟灞炴��
+defineProps<{ property: PopoverProperty }>()
+
+// 澶勭悊閫変腑
+const activeIndex = ref(0)
+const handleActive = (index: number) => {
+ activeIndex.value = index
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/Popover/property.vue b/src/components/DiyEditor/components/mobile/Popover/property.vue
new file mode 100644
index 0000000..21be46e
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/Popover/property.vue
@@ -0,0 +1,38 @@
+<template>
+ <el-form label-width="80px" :model="formData">
+ <Draggable v-model="formData.list" :empty-item="{ showType: 'once' }">
+ <template #default="{ element, index }">
+ <el-form-item label="鍥剧墖" :prop="`list[${index}].imgUrl`">
+ <UploadImg v-model="element.imgUrl" height="56px" width="56px" />
+ </el-form-item>
+ <el-form-item label="璺宠浆閾炬帴" :prop="`list[${index}].url`">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず娆℃暟" :prop="`list[${index}].showType`">
+ <el-radio-group v-model="element.showType">
+ <el-tooltip content="鍙樉绀轰竴娆★紝涓嬫鎵撳紑鏃朵笉鏄剧ず" placement="bottom">
+ <el-radio value="once">涓�娆�</el-radio>
+ </el-tooltip>
+ <el-tooltip content="姣忔鎵撳紑鏃堕兘浼氭樉绀�" placement="bottom">
+ <el-radio value="always">涓嶉檺</el-radio>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ </template>
+ </Draggable>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { PopoverProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 寮圭獥骞垮憡灞炴�ч潰鏉�
+defineOptions({ name: 'PopoverProperty' })
+
+const props = defineProps<{ modelValue: PopoverProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/ProductCard/config.ts b/src/components/DiyEditor/components/mobile/ProductCard/config.ts
new file mode 100644
index 0000000..0a19124
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ProductCard/config.ts
@@ -0,0 +1,97 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鍟嗗搧鍗$墖灞炴�� */
+export interface ProductCardProperty {
+ // 甯冨眬绫诲瀷锛氬崟鍒楀ぇ鍥� | 鍗曞垪灏忓浘 | 鍙屽垪
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
+ // 鍟嗗搧瀛楁
+ fields: {
+ // 鍟嗗搧鍚嶇О
+ name: ProductCardFieldProperty
+ // 鍟嗗搧绠�浠�
+ introduction: ProductCardFieldProperty
+ // 鍟嗗搧浠锋牸
+ price: ProductCardFieldProperty
+ // 鍟嗗搧甯傚満浠�
+ marketPrice: ProductCardFieldProperty
+ // 鍟嗗搧閿�閲�
+ salesCount: ProductCardFieldProperty
+ // 鍟嗗搧搴撳瓨
+ stock: ProductCardFieldProperty
+ }
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鍥剧墖
+ imgUrl: string
+ }
+ // 鎸夐挳
+ btnBuy: {
+ // 绫诲瀷锛氭枃瀛� | 鍥剧墖
+ type: 'text' | 'img'
+ // 鏂囧瓧
+ text: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樿捣濮嬮鑹�
+ bgBeginColor: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樼粨鏉熼鑹�
+ bgEndColor: string
+ // 鍥剧墖鎸夐挳锛氬浘鐗囧湴鍧�
+ imgUrl: string
+ }
+ // 涓婂渾瑙�
+ borderRadiusTop: number
+ // 涓嬪渾瑙�
+ borderRadiusBottom: number
+ // 闂磋窛
+ space: number
+ // 鍟嗗搧缂栧彿鍒楄〃
+ spuIds: number[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+// 鍟嗗搧瀛楁
+export interface ProductCardFieldProperty {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 棰滆壊
+ color: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'ProductCard',
+ name: '鍟嗗搧鍗$墖',
+ icon: 'fluent:text-column-two-left-24-filled',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' }
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '绔嬪嵆璐拱',
+ // todo: @owen 鏍规嵁涓婚鑹查厤缃�
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: ''
+ },
+ borderRadiusTop: 6,
+ borderRadiusBottom: 6,
+ space: 8,
+ spuIds: [],
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<ProductCardProperty>
diff --git a/src/components/DiyEditor/components/mobile/ProductCard/index.vue b/src/components/DiyEditor/components/mobile/ProductCard/index.vue
new file mode 100644
index 0000000..93f6e07
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ProductCard/index.vue
@@ -0,0 +1,170 @@
+<template>
+ <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
+ <div
+ class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+ :style="{
+ ...calculateSpace(index),
+ ...calculateWidth(),
+ borderTopLeftRadius: `${property.borderRadiusTop}px`,
+ borderTopRightRadius: `${property.borderRadiusTop}px`,
+ borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+ borderBottomRightRadius: `${property.borderRadiusBottom}px`
+ }"
+ v-for="(spu, index) in spuList"
+ :key="index"
+ >
+ <!-- 瑙掓爣 -->
+ <div
+ v-if="property.badge.show && property.badge.imgUrl"
+ class="absolute left-0 top-0 z-1 items-center justify-center"
+ >
+ <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+ </div>
+ <!-- 鍟嗗搧灏侀潰鍥� -->
+ <div
+ :class="[
+ 'h-140px',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-140px': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
+ </div>
+ <div
+ :class="[
+ ' flex flex-col gap-8px p-8px box-border',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <!-- 鍟嗗搧鍚嶇О -->
+ <div
+ v-if="property.fields.name.show"
+ :class="[
+ 'text-14px ',
+ {
+ truncate: property.layoutType !== 'oneColSmallImg',
+ 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ :style="{ color: property.fields.name.color }"
+ >
+ {{ spu.name }}
+ </div>
+ <!-- 鍟嗗搧绠�浠� -->
+ <div
+ v-if="property.fields.introduction.show"
+ class="truncate text-12px"
+ :style="{ color: property.fields.introduction.color }"
+ >
+ {{ spu.introduction }}
+ </div>
+ <div>
+ <!-- 浠锋牸 -->
+ <span
+ v-if="property.fields.price.show"
+ class="text-16px"
+ :style="{ color: property.fields.price.color }"
+ >
+ 锟{ fenToYuan(spu.price as any) }}
+ </span>
+ <!-- 甯傚満浠� -->
+ <span
+ v-if="property.fields.marketPrice.show && spu.marketPrice"
+ class="ml-4px text-10px line-through"
+ :style="{ color: property.fields.marketPrice.color }"
+ >锟{ fenToYuan(spu.marketPrice) }}
+ </span>
+ </div>
+ <div class="text-12px">
+ <!-- 閿�閲� -->
+ <span
+ v-if="property.fields.salesCount.show"
+ :style="{ color: property.fields.salesCount.color }"
+ >
+ 宸插敭{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}浠�
+ </span>
+ <!-- 搴撳瓨 -->
+ <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
+ 搴撳瓨{{ spu.stock || 0 }}
+ </span>
+ </div>
+ </div>
+ <!-- 璐拱鎸夐挳 -->
+ <div class="absolute bottom-8px right-8px">
+ <!-- 鏂囧瓧鎸夐挳 -->
+ <span
+ v-if="property.btnBuy.type === 'text'"
+ class="rounded-full p-x-12px p-y-4px text-12px text-white"
+ :style="{
+ background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
+ }"
+ >
+ {{ property.btnBuy.text }}
+ </span>
+ <!-- 鍥剧墖鎸夐挳 -->
+ <el-image
+ v-else
+ class="h-28px w-28px rounded-full"
+ fit="cover"
+ :src="property.btnBuy.imgUrl"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { ProductCardProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { fenToYuan } from '../../../../../utils'
+
+/** 鍟嗗搧鍗$墖 */
+defineOptions({ name: 'ProductCard' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: ProductCardProperty }>()
+// 鍟嗗搧鍒楄〃
+const spuList = ref<ProductSpuApi.Spu[]>([])
+watch(
+ () => props.property.spuIds,
+ async () => {
+ spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+
+/**
+ * 璁$畻鍟嗗搧鐨勯棿璺�
+ * @param index 鍟嗗搧绱㈠紩
+ */
+const calculateSpace = (index: number) => {
+ // 鍟嗗搧鐨勫垪鏁�
+ const columns = props.property.layoutType === 'twoCol' ? 2 : 1
+ // 绗竴鍒楁病鏈夊乏杈硅窛
+ const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
+ // 绗竴琛屾病鏈変笂杈硅窛
+ const marginTop = index < columns ? '0' : props.property.space + 'px'
+
+ return { marginLeft, marginTop }
+}
+
+// 瀹瑰櫒
+const containerRef = ref()
+// 璁$畻鍟嗗搧鐨勫搴�
+const calculateWidth = () => {
+ let width = '100%'
+ // 鍙屽垪鏃舵瘡鍒楃殑瀹藉害涓猴細锛堟�诲搴� - 闂磋窛锛�/ 2
+ if (props.property.layoutType === 'twoCol') {
+ width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
+ }
+ return { width }
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/ProductCard/property.vue b/src/components/DiyEditor/components/mobile/ProductCard/property.vue
new file mode 100644
index 0000000..91846e6
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ProductCard/property.vue
@@ -0,0 +1,149 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-card header="鍟嗗搧鍒楄〃" class="property-group" shadow="never">
+ <SpuShowcase v-model="formData.spuIds" />
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="甯冨眬" prop="type">
+ <el-radio-group v-model="formData.layoutType">
+ <el-tooltip class="item" content="鍗曞垪澶у浘" placement="bottom">
+ <el-radio-button value="oneColBigImg">
+ <Icon icon="fluent:text-column-one-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍗曞垪灏忓浘" placement="bottom">
+ <el-radio-button value="oneColSmallImg">
+ <Icon icon="fluent:text-column-two-left-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍙屽垪" placement="bottom">
+ <el-radio-button value="twoCol">
+ <Icon icon="fluent:text-column-two-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="fields.name.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.name.color" />
+ <el-checkbox v-model="formData.fields.name.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧绠�浠�" prop="fields.introduction.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.introduction.color" />
+ <el-checkbox v-model="formData.fields.introduction.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧浠锋牸" prop="fields.price.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.price.color" />
+ <el-checkbox v-model="formData.fields.price.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="甯傚満浠�" prop="fields.marketPrice.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.marketPrice.color" />
+ <el-checkbox v-model="formData.fields.marketPrice.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧閿�閲�" prop="fields.salesCount.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.salesCount.color" />
+ <el-checkbox v-model="formData.fields.salesCount.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧搴撳瓨" prop="fields.stock.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.stock.color" />
+ <el-checkbox v-model="formData.fields.stock.show" />
+ </div>
+ </el-form-item>
+ </el-card>
+ <el-card header="瑙掓爣" class="property-group" shadow="never">
+ <el-form-item label="瑙掓爣" prop="badge.show">
+ <el-switch v-model="formData.badge.show" />
+ </el-form-item>
+ <el-form-item label="瑙掓爣" prop="badge.imgUrl" v-if="formData.badge.show">
+ <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+ <template #tip> 寤鸿灏哄锛�36 * 22 </template>
+ </UploadImg>
+ </el-form-item>
+ </el-card>
+ <el-card header="鎸夐挳" class="property-group" shadow="never">
+ <el-form-item label="鎸夐挳绫诲瀷" prop="btnBuy.type">
+ <el-radio-group v-model="formData.btnBuy.type">
+ <el-radio-button value="text">鏂囧瓧</el-radio-button>
+ <el-radio-button value="img">鍥剧墖</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <template v-if="formData.btnBuy.type === 'text'">
+ <el-form-item label="鎸夐挳鏂囧瓧" prop="btnBuy.text">
+ <el-input v-model="formData.btnBuy.text" />
+ </el-form-item>
+ <el-form-item label="宸︿晶鑳屾櫙" prop="btnBuy.bgBeginColor">
+ <ColorInput v-model="formData.btnBuy.bgBeginColor" />
+ </el-form-item>
+ <el-form-item label="鍙充晶鑳屾櫙" prop="btnBuy.bgEndColor">
+ <ColorInput v-model="formData.btnBuy.bgEndColor" />
+ </el-form-item>
+ </template>
+ <template v-else>
+ <el-form-item label="鍥剧墖" prop="btnBuy.imgUrl">
+ <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
+ <template #tip> 寤鸿灏哄锛�56 * 56 </template>
+ </UploadImg>
+ </el-form-item>
+ </template>
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="涓婂渾瑙�" prop="borderRadiusTop">
+ <el-slider
+ v-model="formData.borderRadiusTop"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="涓嬪渾瑙�" prop="borderRadiusBottom">
+ <el-slider
+ v-model="formData.borderRadiusBottom"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { ProductCardProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
+
+// 鍟嗗搧鍗$墖灞炴�ч潰鏉�
+defineOptions({ name: 'ProductCardProperty' })
+
+const props = defineProps<{ modelValue: ProductCardProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/ProductList/config.ts b/src/components/DiyEditor/components/mobile/ProductList/config.ts
new file mode 100644
index 0000000..1f16832
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ProductList/config.ts
@@ -0,0 +1,64 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鍟嗗搧鏍忓睘鎬� */
+export interface ProductListProperty {
+ // 甯冨眬绫诲瀷锛氬弻鍒� | 涓夊垪 | 姘村钩婊戝姩
+ layoutType: 'twoCol' | 'threeCol' | 'horizSwiper'
+ // 鍟嗗搧瀛楁
+ fields: {
+ // 鍟嗗搧鍚嶇О
+ name: ProductListFieldProperty
+ // 鍟嗗搧浠锋牸
+ price: ProductListFieldProperty
+ }
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鍥剧墖
+ imgUrl: string
+ }
+ // 涓婂渾瑙�
+ borderRadiusTop: number
+ // 涓嬪渾瑙�
+ borderRadiusBottom: number
+ // 闂磋窛
+ space: number
+ // 鍟嗗搧缂栧彿鍒楄〃
+ spuIds: number[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+// 鍟嗗搧瀛楁
+export interface ProductListFieldProperty {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 棰滆壊
+ color: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'ProductList',
+ name: '鍟嗗搧鏍�',
+ icon: 'fluent:text-column-two-24-filled',
+ property: {
+ layoutType: 'twoCol',
+ fields: {
+ name: { show: true, color: '#000' },
+ price: { show: true, color: '#ff3000' }
+ },
+ badge: { show: false, imgUrl: '' },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ spuIds: [],
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<ProductListProperty>
diff --git a/src/components/DiyEditor/components/mobile/ProductList/index.vue b/src/components/DiyEditor/components/mobile/ProductList/index.vue
new file mode 100644
index 0000000..a51fc07
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ProductList/index.vue
@@ -0,0 +1,132 @@
+<template>
+ <el-scrollbar class="z-1 min-h-30px" wrap-class="w-full" ref="containerRef">
+ <!-- 鍟嗗搧缃戞牸 -->
+ <div
+ class="grid overflow-x-auto"
+ :style="{
+ gridGap: `${property.space}px`,
+ gridTemplateColumns,
+ width: scrollbarWidth
+ }"
+ >
+ <!-- 鍟嗗搧 -->
+ <div
+ class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+ :style="{
+ borderTopLeftRadius: `${property.borderRadiusTop}px`,
+ borderTopRightRadius: `${property.borderRadiusTop}px`,
+ borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+ borderBottomRightRadius: `${property.borderRadiusBottom}px`
+ }"
+ v-for="(spu, index) in spuList"
+ :key="index"
+ >
+ <!-- 瑙掓爣 -->
+ <div
+ v-if="property.badge.show"
+ class="absolute left-0 top-0 z-1 items-center justify-center"
+ >
+ <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+ </div>
+ <!-- 鍟嗗搧灏侀潰鍥� -->
+ <el-image fit="cover" :src="spu.picUrl" :style="{ width: imageSize, height: imageSize }" />
+ <div
+ :class="[
+ 'flex flex-col gap-8px p-8px box-border',
+ {
+ 'w-[calc(100%-64px)]': columns === 2,
+ 'w-full': columns === 3
+ }
+ ]"
+ >
+ <!-- 鍟嗗搧鍚嶇О -->
+ <div
+ v-if="property.fields.name.show"
+ class="truncate text-12px"
+ :style="{ color: property.fields.name.color }"
+ >
+ {{ spu.name }}
+ </div>
+ <div>
+ <!-- 鍟嗗搧浠锋牸 -->
+ <span
+ v-if="property.fields.price.show"
+ class="text-12px"
+ :style="{ color: property.fields.price.color }"
+ >
+ 锟{ fenToYuan(spu.price) }}
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-scrollbar>
+</template>
+<script setup lang="ts">
+import { ProductListProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { fenToYuan } from '@/utils'
+
+/** 鍟嗗搧鏍� */
+defineOptions({ name: 'ProductList' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: ProductListProperty }>()
+// 鍟嗗搧鍒楄〃
+const spuList = ref<ProductSpuApi.Spu[]>([])
+watch(
+ () => props.property.spuIds,
+ async () => {
+ spuList.value = await ProductSpuApi.getSpuDetailList(props.property.spuIds)
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+// 鎵嬫満瀹藉害
+const phoneWidth = ref(375)
+// 瀹瑰櫒
+const containerRef = ref()
+// 鍟嗗搧鐨勫垪鏁�
+const columns = ref(2)
+// 婊氬姩鏉″搴�
+const scrollbarWidth = ref('100%')
+// 鍟嗗搧鍥惧ぇ灏�
+const imageSize = ref('0')
+// 鍟嗗搧缃戠粶鍒楁暟
+const gridTemplateColumns = ref('')
+// 璁$畻甯冨眬鍙傛暟
+watch(
+ () => [props.property, phoneWidth, spuList.value.length],
+ () => {
+ // 璁$畻鍒楁暟
+ columns.value = props.property.layoutType === 'twoCol' ? 2 : 3
+ // 姣忓垪鐨勫搴︿负锛氾紙鎬诲搴� - 闂磋窛 * (鍒楁暟 - 1)锛�/ 鍒楁暟
+ const productWidth =
+ (phoneWidth.value - props.property.space * (columns.value - 1)) / columns.value
+ // 鍟嗗搧鍥惧竷灞�锛�2鍒楁椂锛屽乏鍙冲竷灞� 3鍒楁椂锛屼笂涓嬪竷灞�
+ imageSize.value = columns.value === 2 ? '64px' : `${productWidth}px`
+ // 鏍规嵁甯冨眬绫诲瀷锛岃绠楄鏁般�佸垪鏁�
+ if (props.property.layoutType === 'horizSwiper') {
+ // 鍗曡鏄剧ず
+ gridTemplateColumns.value = `repeat(auto-fill, ${productWidth}px)`
+ // 鏄剧ず婊氬姩鏉�
+ scrollbarWidth.value = `${
+ productWidth * spuList.value.length + props.property.space * (spuList.value.length - 1)
+ }px`
+ } else {
+ // 鎸囧畾鍒楁暟
+ gridTemplateColumns.value = `repeat(${columns.value}, auto)`
+ // 涓嶆粴鍔�
+ scrollbarWidth.value = '100%'
+ }
+ },
+ { immediate: true, deep: true }
+)
+onMounted(() => {
+ // 鎻愬彇鎵嬫満瀹藉害
+ phoneWidth.value = containerRef.value?.wrapRef?.offsetWidth || 375
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/ProductList/property.vue b/src/components/DiyEditor/components/mobile/ProductList/property.vue
new file mode 100644
index 0000000..d7a5a7c
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/ProductList/property.vue
@@ -0,0 +1,99 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-card header="鍟嗗搧鍒楄〃" class="property-group" shadow="never">
+ <SpuShowcase v-model="formData.spuIds" />
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="甯冨眬" prop="type">
+ <el-radio-group v-model="formData.layoutType">
+ <el-tooltip class="item" content="鍙屽垪" placement="bottom">
+ <el-radio-button value="twoCol">
+ <Icon icon="fluent:text-column-two-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="涓夊垪" placement="bottom">
+ <el-radio-button value="threeCol">
+ <Icon icon="fluent:text-column-three-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="姘村钩婊戝姩" placement="bottom">
+ <el-radio-button value="horizSwiper">
+ <Icon icon="system-uicons:carousel" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="fields.name.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.name.color" />
+ <el-checkbox v-model="formData.fields.name.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧浠锋牸" prop="fields.price.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.price.color" />
+ <el-checkbox v-model="formData.fields.price.show" />
+ </div>
+ </el-form-item>
+ </el-card>
+ <el-card header="瑙掓爣" class="property-group" shadow="never">
+ <el-form-item label="瑙掓爣" prop="badge.show">
+ <el-switch v-model="formData.badge.show" />
+ </el-form-item>
+ <el-form-item label="瑙掓爣" prop="badge.imgUrl" v-if="formData.badge.show">
+ <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+ <template #tip> 寤鸿灏哄锛�36 * 22 </template>
+ </UploadImg>
+ </el-form-item>
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="涓婂渾瑙�" prop="borderRadiusTop">
+ <el-slider
+ v-model="formData.borderRadiusTop"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="涓嬪渾瑙�" prop="borderRadiusBottom">
+ <el-slider
+ v-model="formData.borderRadiusBottom"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { ProductListProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
+
+// 鍟嗗搧鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'ProductListProperty' })
+
+const props = defineProps<{ modelValue: ProductListProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionArticle/config.ts b/src/components/DiyEditor/components/mobile/PromotionArticle/config.ts
new file mode 100644
index 0000000..c6270c2
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionArticle/config.ts
@@ -0,0 +1,25 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 钀ラ攢鏂囩珷灞炴�� */
+export interface PromotionArticleProperty {
+ // 鏂囩珷缂栧彿
+ id: number
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'PromotionArticle',
+ name: '钀ラ攢鏂囩珷',
+ icon: 'ph:article-medium',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<PromotionArticleProperty>
diff --git a/src/components/DiyEditor/components/mobile/PromotionArticle/index.vue b/src/components/DiyEditor/components/mobile/PromotionArticle/index.vue
new file mode 100644
index 0000000..e003b08
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionArticle/index.vue
@@ -0,0 +1,27 @@
+<template>
+ <div class="min-h-30px" v-html="article?.content"></div>
+</template>
+<script setup lang="ts">
+import { PromotionArticleProperty } from './config'
+import * as ArticleApi from '@/api/mall/promotion/article/index'
+
+/** 钀ラ攢鏂囩珷 */
+defineOptions({ name: 'PromotionArticle' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: PromotionArticleProperty }>()
+// 鍟嗗搧鍒楄〃
+const article = ref<ArticleApi.ArticleVO>()
+watch(
+ () => props.property.id,
+ async () => {
+ if (props.property.id) {
+ article.value = await ArticleApi.getArticle(props.property.id)
+ }
+ },
+ {
+ immediate: true
+ }
+)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionArticle/property.vue b/src/components/DiyEditor/components/mobile/PromotionArticle/property.vue
new file mode 100644
index 0000000..10c5840
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionArticle/property.vue
@@ -0,0 +1,56 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="40px" :model="formData">
+ <el-form-item label="鏂囩珷" prop="id">
+ <el-select
+ v-model="formData.id"
+ placeholder="璇烽�夋嫨鏂囩珷"
+ class="w-full"
+ filterable
+ remote
+ :remote-method="queryArticleList"
+ :loading="loading"
+ >
+ <el-option
+ v-for="article in articles"
+ :key="article.id"
+ :label="article.title"
+ :value="article.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionArticleProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import * as ArticleApi from '@/api/mall/promotion/article/index'
+
+// 钀ラ攢鏂囩珷灞炴�ч潰鏉�
+defineOptions({ name: 'PromotionArticleProperty' })
+
+const props = defineProps<{ modelValue: PromotionArticleProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+// 鏂囩珷鍒楄〃
+const articles = ref<ArticleApi.ArticleVO>([])
+
+// 鍔犺浇涓�
+const loading = ref(false)
+// 鏌ヨ鏂囩珷鍒楄〃
+const queryArticleList = async (title?: string) => {
+ loading.value = true
+ const { list } = await ArticleApi.getArticlePage({ title, pageSize: 10 })
+ articles.value = list
+ loading.value = false
+}
+
+// 鍒濆鍖�
+onMounted(() => {
+ queryArticleList()
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts b/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts
new file mode 100644
index 0000000..f4fdf6e
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionCombination/config.ts
@@ -0,0 +1,96 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鎷煎洟灞炴�� */
+export interface PromotionCombinationProperty {
+ // 甯冨眬绫诲瀷锛氬崟鍒� | 涓夊垪
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
+ // 鍟嗗搧瀛楁
+ fields: {
+ // 鍟嗗搧鍚嶇О
+ name: PromotionCombinationFieldProperty
+ // 鍟嗗搧绠�浠�
+ introduction: PromotionCombinationFieldProperty
+ // 鍟嗗搧浠锋牸
+ price: PromotionCombinationFieldProperty
+ // 甯傚満浠�
+ marketPrice: PromotionCombinationFieldProperty
+ // 鍟嗗搧閿�閲�
+ salesCount: PromotionCombinationFieldProperty
+ // 鍟嗗搧搴撳瓨
+ stock: PromotionCombinationFieldProperty
+ }
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鍥剧墖
+ imgUrl: string
+ }
+ // 鎸夐挳
+ btnBuy: {
+ // 绫诲瀷锛氭枃瀛� | 鍥剧墖
+ type: 'text' | 'img'
+ // 鏂囧瓧
+ text: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樿捣濮嬮鑹�
+ bgBeginColor: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樼粨鏉熼鑹�
+ bgEndColor: string
+ // 鍥剧墖鎸夐挳锛氬浘鐗囧湴鍧�
+ imgUrl: string
+ }
+ // 涓婂渾瑙�
+ borderRadiusTop: number
+ // 涓嬪渾瑙�
+ borderRadiusBottom: number
+ // 闂磋窛
+ space: number
+ // 鎷煎洟娲诲姩缂栧彿
+ activityIds: number[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 鍟嗗搧瀛楁
+export interface PromotionCombinationFieldProperty {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 棰滆壊
+ color: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'PromotionCombination',
+ name: '鎷煎洟',
+ icon: 'mdi:account-group',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' }
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '鍘绘嫾鍥�',
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: ''
+ },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<PromotionCombinationProperty>
diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue b/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue
new file mode 100644
index 0000000..d41bf1c
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionCombination/index.vue
@@ -0,0 +1,201 @@
+<template>
+ <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
+ <div
+ class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+ :style="{
+ ...calculateSpace(index),
+ ...calculateWidth(),
+ borderTopLeftRadius: `${property.borderRadiusTop}px`,
+ borderTopRightRadius: `${property.borderRadiusTop}px`,
+ borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+ borderBottomRightRadius: `${property.borderRadiusBottom}px`
+ }"
+ v-for="(spu, index) in spuList"
+ :key="index"
+ >
+ <!-- 瑙掓爣 -->
+ <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
+ <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+ </div>
+ <!-- 鍟嗗搧灏侀潰鍥� -->
+ <div
+ :class="[
+ 'h-140px',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-140px': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
+ </div>
+ <div
+ :class="[
+ ' flex flex-col gap-8px p-8px box-border',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <!-- 鍟嗗搧鍚嶇О -->
+ <div
+ v-if="property.fields.name.show"
+ :class="[
+ 'text-14px ',
+ {
+ truncate: property.layoutType !== 'oneColSmallImg',
+ 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ :style="{ color: property.fields.name.color }"
+ >
+ {{ spu.name }}
+ </div>
+ <!-- 鍟嗗搧绠�浠� -->
+ <div
+ v-if="property.fields.introduction.show"
+ class="truncate text-12px"
+ :style="{ color: property.fields.introduction.color }"
+ >
+ {{ spu.introduction }}
+ </div>
+ <div>
+ <!-- 浠锋牸 -->
+ <span
+ v-if="property.fields.price.show"
+ class="text-16px"
+ :style="{ color: property.fields.price.color }"
+ >
+ 锟{ fenToYuan(spu.price || Infinity) }}
+ </span>
+ <!-- 甯傚満浠� -->
+ <span
+ v-if="property.fields.marketPrice.show && spu.marketPrice"
+ class="ml-4px text-10px line-through"
+ :style="{ color: property.fields.marketPrice.color }"
+ >锟{ fenToYuan(spu.marketPrice) }}</span
+ >
+ </div>
+ <div class="text-12px">
+ <!-- 閿�閲� -->
+ <span
+ v-if="property.fields.salesCount.show"
+ :style="{ color: property.fields.salesCount.color }"
+ >
+ 宸插敭{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}浠�
+ </span>
+ <!-- 搴撳瓨 -->
+ <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
+ 搴撳瓨{{ spu.stock || 0 }}
+ </span>
+ </div>
+ </div>
+ <!-- 璐拱鎸夐挳 -->
+ <div class="absolute bottom-8px right-8px">
+ <!-- 鏂囧瓧鎸夐挳 -->
+ <span
+ v-if="property.btnBuy.type === 'text'"
+ class="rounded-full p-x-12px p-y-4px text-12px text-white"
+ :style="{
+ background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
+ }"
+ >
+ {{ property.btnBuy.text }}
+ </span>
+ <!-- 鍥剧墖鎸夐挳 -->
+ <el-image
+ v-else
+ class="h-28px w-28px rounded-full"
+ fit="cover"
+ :src="property.btnBuy.imgUrl"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { PromotionCombinationProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { fenToYuan } from '@/utils'
+
+/** 鎷煎洟鍗$墖 */
+defineOptions({ name: 'PromotionCombination' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: PromotionCombinationProperty }>()
+// 鍟嗗搧鍒楄〃
+const spuList = ref<ProductSpuApi.Spu[]>([])
+const spuIdList = ref<number[]>([])
+const combinationActivityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
+
+watch(
+ () => props.property.activityIds,
+ async () => {
+ try {
+ // 鏂版坊鍔犵殑鎷煎洟缁勪欢锛屾槸娌℃湁娲诲姩ID鐨�
+ const activityIds = props.property.activityIds
+ // 妫�鏌ユ椿鍔↖D鐨勬湁鏁堟��
+ if (Array.isArray(activityIds) && activityIds.length > 0) {
+ // 鑾峰彇鎷煎洟娲诲姩璇︽儏鍒楄〃
+ combinationActivityList.value =
+ await CombinationActivityApi.getCombinationActivityListByIds(activityIds)
+
+ // 鑾峰彇鎷煎洟娲诲姩鐨� SPU 璇︽儏鍒楄〃
+ spuList.value = []
+ spuIdList.value = combinationActivityList.value
+ .map((activity) => activity.spuId)
+ .filter((spuId): spuId is number => typeof spuId === 'number')
+ if (spuIdList.value.length > 0) {
+ spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
+ }
+
+ // 鏇存柊 SPU 鐨勬渶浣庝环鏍�
+ combinationActivityList.value.forEach((activity) => {
+ // 鍖归厤spuId
+ const spu = spuList.value.find((spu) => spu.id === activity.spuId)
+ if (spu) {
+ // 璧嬪�兼椿鍔ㄤ环鏍硷紝鍝釜鏈�渚垮疁灏辫祴鍊煎摢涓�
+ spu.price = Math.min(activity.combinationPrice || Infinity, spu.price || Infinity)
+ }
+ })
+ }
+ } catch (error) {
+ console.error('鑾峰彇鎷煎洟娲诲姩缁嗚妭鎴� SPU 缁嗚妭鏃跺嚭閿�:', error)
+ }
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+
+/**
+ * 璁$畻鍟嗗搧鐨勯棿璺�
+ * @param index 鍟嗗搧绱㈠紩
+ */
+const calculateSpace = (index: number) => {
+ // 鍟嗗搧鐨勫垪鏁�
+ const columns = props.property.layoutType === 'twoCol' ? 2 : 1
+ // 绗竴鍒楁病鏈夊乏杈硅窛
+ const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
+ // 绗竴琛屾病鏈変笂杈硅窛
+ const marginTop = index < columns ? '0' : props.property.space + 'px'
+
+ return { marginLeft, marginTop }
+}
+
+// 瀹瑰櫒
+const containerRef = ref()
+// 璁$畻鍟嗗搧鐨勫搴�
+const calculateWidth = () => {
+ let width = '100%'
+ // 鍙屽垪鏃舵瘡鍒楃殑瀹藉害涓猴細锛堟�诲搴� - 闂磋窛锛�/ 2
+ if (props.property.layoutType === 'twoCol') {
+ width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
+ }
+ return { width }
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue b/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue
new file mode 100644
index 0000000..b796e6a
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionCombination/property.vue
@@ -0,0 +1,164 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-card header="鎷煎洟娲诲姩" class="property-group" shadow="never">
+ <CombinationShowcase v-model="formData.activityIds" />
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="甯冨眬" prop="type">
+ <el-radio-group v-model="formData.layoutType">
+ <el-tooltip class="item" content="鍗曞垪澶у浘" placement="bottom">
+ <el-radio-button value="oneColBigImg">
+ <Icon icon="fluent:text-column-one-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍗曞垪灏忓浘" placement="bottom">
+ <el-radio-button value="oneColSmallImg">
+ <Icon icon="fluent:text-column-two-left-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍙屽垪" placement="bottom">
+ <el-radio-button value="twoCol">
+ <Icon icon="fluent:text-column-two-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <!--<el-tooltip class="item" content="涓夊垪" placement="bottom">
+ <el-radio-button value="threeCol">
+ <Icon icon="fluent:text-column-three-24-filled" />
+ </el-radio-button>
+ </el-tooltip>-->
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="fields.name.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.name.color" />
+ <el-checkbox v-model="formData.fields.name.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧绠�浠�" prop="fields.introduction.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.introduction.color" />
+ <el-checkbox v-model="formData.fields.introduction.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧浠锋牸" prop="fields.price.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.price.color" />
+ <el-checkbox v-model="formData.fields.price.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="甯傚満浠�" prop="fields.marketPrice.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.marketPrice.color" />
+ <el-checkbox v-model="formData.fields.marketPrice.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧閿�閲�" prop="fields.salesCount.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.salesCount.color" />
+ <el-checkbox v-model="formData.fields.salesCount.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧搴撳瓨" prop="fields.stock.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.stock.color" />
+ <el-checkbox v-model="formData.fields.stock.show" />
+ </div>
+ </el-form-item>
+ </el-card>
+ <el-card header="瑙掓爣" class="property-group" shadow="never">
+ <el-form-item label="瑙掓爣" prop="badge.show">
+ <el-switch v-model="formData.badge.show" />
+ </el-form-item>
+ <el-form-item label="瑙掓爣" prop="badge.imgUrl" v-if="formData.badge.show">
+ <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+ <template #tip> 寤鸿灏哄锛�36 * 22</template>
+ </UploadImg>
+ </el-form-item>
+ </el-card>
+ <el-card header="鎸夐挳" class="property-group" shadow="never">
+ <el-form-item label="鎸夐挳绫诲瀷" prop="btnBuy.type">
+ <el-radio-group v-model="formData.btnBuy.type">
+ <el-radio-button value="text">鏂囧瓧</el-radio-button>
+ <el-radio-button value="img">鍥剧墖</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <template v-if="formData.btnBuy.type === 'text'">
+ <el-form-item label="鎸夐挳鏂囧瓧" prop="btnBuy.text">
+ <el-input v-model="formData.btnBuy.text" />
+ </el-form-item>
+ <el-form-item label="宸︿晶鑳屾櫙" prop="btnBuy.bgBeginColor">
+ <ColorInput v-model="formData.btnBuy.bgBeginColor" />
+ </el-form-item>
+ <el-form-item label="鍙充晶鑳屾櫙" prop="btnBuy.bgEndColor">
+ <ColorInput v-model="formData.btnBuy.bgEndColor" />
+ </el-form-item>
+ </template>
+ <template v-else>
+ <el-form-item label="鍥剧墖" prop="btnBuy.imgUrl">
+ <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
+ <template #tip> 寤鸿灏哄锛�56 * 56</template>
+ </UploadImg>
+ </el-form-item>
+ </template>
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="涓婂渾瑙�" prop="borderRadiusTop">
+ <el-slider
+ v-model="formData.borderRadiusTop"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="涓嬪渾瑙�" prop="borderRadiusBottom">
+ <el-slider
+ v-model="formData.borderRadiusBottom"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionCombinationProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { CommonStatusEnum } from '@/utils/constants'
+import CombinationShowcase from '@/views/mall/promotion/combination/components/CombinationShowcase.vue'
+
+// 鎷煎洟灞炴�ч潰鏉�
+defineOptions({ name: 'PromotionCombinationProperty' })
+
+const props = defineProps<{ modelValue: PromotionCombinationProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+// 娲诲姩鍒楄〃
+const activityList = ref<CombinationActivityApi.CombinationActivityVO[]>([])
+onMounted(async () => {
+ const { list } = await CombinationActivityApi.getCombinationActivityPage({
+ status: CommonStatusEnum.ENABLE
+ })
+ activityList.value = list
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts b/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts
new file mode 100644
index 0000000..75aa0ff
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionPoint/config.ts
@@ -0,0 +1,96 @@
+import {ComponentStyle, DiyComponent} from '@/components/DiyEditor/util'
+
+/** 绉垎鍟嗗煄灞炴�� */
+export interface PromotionPointProperty {
+ // 甯冨眬绫诲瀷锛氬崟鍒� | 涓夊垪
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
+ // 鍟嗗搧瀛楁
+ fields: {
+ // 鍟嗗搧鍚嶇О
+ name: PromotionPointFieldProperty
+ // 鍟嗗搧绠�浠�
+ introduction: PromotionPointFieldProperty
+ // 鍟嗗搧浠锋牸
+ price: PromotionPointFieldProperty
+ // 甯傚満浠�
+ marketPrice: PromotionPointFieldProperty
+ // 鍟嗗搧閿�閲�
+ salesCount: PromotionPointFieldProperty
+ // 鍟嗗搧搴撳瓨
+ stock: PromotionPointFieldProperty
+ }
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鍥剧墖
+ imgUrl: string
+ }
+ // 鎸夐挳
+ btnBuy: {
+ // 绫诲瀷锛氭枃瀛� | 鍥剧墖
+ type: 'text' | 'img'
+ // 鏂囧瓧
+ text: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樿捣濮嬮鑹�
+ bgBeginColor: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樼粨鏉熼鑹�
+ bgEndColor: string
+ // 鍥剧墖鎸夐挳锛氬浘鐗囧湴鍧�
+ imgUrl: string
+ }
+ // 涓婂渾瑙�
+ borderRadiusTop: number
+ // 涓嬪渾瑙�
+ borderRadiusBottom: number
+ // 闂磋窛
+ space: number
+ // 绉掓潃娲诲姩缂栧彿
+ activityIds: number[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 鍟嗗搧瀛楁
+export interface PromotionPointFieldProperty {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 棰滆壊
+ color: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'PromotionPoint',
+ name: '绉垎鍟嗗煄',
+ icon: 'ep:present',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' }
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '绔嬪嵆鍏戞崲',
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: ''
+ },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<PromotionPointProperty>
diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue b/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue
new file mode 100644
index 0000000..4acd93f
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionPoint/index.vue
@@ -0,0 +1,202 @@
+<template>
+ <div ref="containerRef" :class="`box-content min-h-30px w-full flex flex-row flex-wrap`">
+ <div
+ v-for="(spu, index) in spuList"
+ :key="index"
+ :style="{
+ ...calculateSpace(index),
+ ...calculateWidth(),
+ borderTopLeftRadius: `${property.borderRadiusTop}px`,
+ borderTopRightRadius: `${property.borderRadiusTop}px`,
+ borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+ borderBottomRightRadius: `${property.borderRadiusBottom}px`
+ }"
+ class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+ >
+ <!-- 瑙掓爣 -->
+ <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
+ <el-image :src="property.badge.imgUrl" class="h-26px w-38px" fit="cover" />
+ </div>
+ <!-- 鍟嗗搧灏侀潰鍥� -->
+ <div
+ :class="[
+ 'h-140px',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-140px': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <el-image :src="spu.picUrl" class="h-full w-full" fit="cover" />
+ </div>
+ <div
+ :class="[
+ ' flex flex-col gap-8px p-8px box-border',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <!-- 鍟嗗搧鍚嶇О -->
+ <div
+ v-if="property.fields.name.show"
+ :class="[
+ 'text-14px ',
+ {
+ truncate: property.layoutType !== 'oneColSmallImg',
+ 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ :style="{ color: property.fields.name.color }"
+ >
+ {{ spu.name }}
+ </div>
+ <!-- 鍟嗗搧绠�浠� -->
+ <div
+ v-if="property.fields.introduction.show"
+ :style="{ color: property.fields.introduction.color }"
+ class="truncate text-12px"
+ >
+ {{ spu.introduction }}
+ </div>
+ <div>
+ <!-- 绉垎 -->
+ <span
+ v-if="property.fields.price.show"
+ :style="{ color: property.fields.price.color }"
+ class="text-16px"
+ >
+ {{ spu.point }}绉垎
+ {{ !spu.pointPrice || spu.pointPrice === 0 ? '' : `+${fenToYuan(spu.pointPrice)}鍏僠 }}
+ </span>
+ <!-- 甯傚満浠� -->
+ <span
+ v-if="property.fields.marketPrice.show && spu.marketPrice"
+ :style="{ color: property.fields.marketPrice.color }"
+ class="ml-4px text-10px line-through"
+ >
+ 锟{ fenToYuan(spu.marketPrice) }}
+ </span>
+ </div>
+ <div class="text-12px">
+ <!-- 閿�閲� -->
+ <span
+ v-if="property.fields.salesCount.show"
+ :style="{ color: property.fields.salesCount.color }"
+ >
+ 宸插厬{{ (spu.pointTotalStock || 0) - (spu.pointStock || 0) }}浠�
+ </span>
+ <!-- 搴撳瓨 -->
+ <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
+ 搴撳瓨{{ spu.pointTotalStock || 0 }}
+ </span>
+ </div>
+ </div>
+ <!-- 璐拱鎸夐挳 -->
+ <div class="absolute bottom-8px right-8px">
+ <!-- 鏂囧瓧鎸夐挳 -->
+ <span
+ v-if="property.btnBuy.type === 'text'"
+ :style="{
+ background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
+ }"
+ class="rounded-full p-x-12px p-y-4px text-12px text-white"
+ >
+ {{ property.btnBuy.text }}
+ </span>
+ <!-- 鍥剧墖鎸夐挳 -->
+ <el-image
+ v-else
+ :src="property.btnBuy.imgUrl"
+ class="h-28px w-28px rounded-full"
+ fit="cover"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { PromotionPointProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { PointActivityApi, PointActivityVO, SpuExtension0 } from '@/api/mall/promotion/point'
+import { fenToYuan } from '@/utils'
+
+/** 绉垎鍟嗗煄鍗$墖 */
+defineOptions({ name: 'PromotionPoint' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: PromotionPointProperty }>()
+// 鍟嗗搧鍒楄〃
+const spuList = ref<SpuExtension0[]>([])
+const spuIdList = ref<number[]>([])
+const pointActivityList = ref<PointActivityVO[]>([])
+
+watch(
+ () => props.property.activityIds,
+ async () => {
+ try {
+ // 鏂版坊鍔犵殑绉垎鍟嗗煄缁勪欢锛屾槸娌℃湁娲诲姩ID鐨�
+ const activityIds = props.property.activityIds
+ // 妫�鏌ユ椿鍔↖D鐨勬湁鏁堟��
+ if (Array.isArray(activityIds) && activityIds.length > 0) {
+ // 鑾峰彇绉垎鍟嗗煄娲诲姩璇︽儏鍒楄〃
+ pointActivityList.value = await PointActivityApi.getPointActivityListByIds(activityIds)
+
+ // 鑾峰彇绉垎鍟嗗煄娲诲姩鐨� SPU 璇︽儏鍒楄〃
+ spuList.value = []
+ spuIdList.value = pointActivityList.value.map((activity) => activity.spuId)
+ if (spuIdList.value.length > 0) {
+ spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
+ }
+
+ // 鏇存柊 SPU 鐨勬渶浣庡厬鎹㈢Н鍒嗗拰鎵�闇�鍏戞崲閲戦
+ pointActivityList.value.forEach((activity) => {
+ // 鍖归厤spuId
+ const spu = spuList.value.find((spu) => spu.id === activity.spuId)
+ if (spu) {
+ spu.pointStock = activity.stock
+ spu.pointTotalStock = activity.totalStock
+ spu.point = activity.point
+ spu.pointPrice = activity.price
+ }
+ })
+ }
+ } catch (error) {
+ console.error('鑾峰彇绉垎鍟嗗煄娲诲姩缁嗚妭鎴� SPU 缁嗚妭鏃跺嚭閿�:', error)
+ }
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+
+/**
+ * 璁$畻鍟嗗搧鐨勯棿璺�
+ * @param index 鍟嗗搧绱㈠紩
+ */
+const calculateSpace = (index: number) => {
+ // 鍟嗗搧鐨勫垪鏁�
+ const columns = props.property.layoutType === 'twoCol' ? 2 : 1
+ // 绗竴鍒楁病鏈夊乏杈硅窛
+ const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
+ // 绗竴琛屾病鏈変笂杈硅窛
+ const marginTop = index < columns ? '0' : props.property.space + 'px'
+
+ return { marginLeft, marginTop }
+}
+
+// 瀹瑰櫒
+const containerRef = ref()
+// 璁$畻鍟嗗搧鐨勫搴�
+const calculateWidth = () => {
+ let width = '100%'
+ // 鍙屽垪鏃舵瘡鍒楃殑瀹藉害涓猴細锛堟�诲搴� - 闂磋窛锛�/ 2
+ if (props.property.layoutType === 'twoCol') {
+ width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
+ }
+ return { width }
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue b/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue
new file mode 100644
index 0000000..ea20776
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionPoint/property.vue
@@ -0,0 +1,154 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form :model="formData" label-width="80px">
+ <el-card class="property-group" header="绉垎鍟嗗煄娲诲姩" shadow="never">
+ <PointShowcase v-model="formData.activityIds" />
+ </el-card>
+ <el-card class="property-group" header="鍟嗗搧鏍峰紡" shadow="never">
+ <el-form-item label="甯冨眬" prop="type">
+ <el-radio-group v-model="formData.layoutType">
+ <el-tooltip class="item" content="鍗曞垪澶у浘" placement="bottom">
+ <el-radio-button value="oneColBigImg">
+ <Icon icon="fluent:text-column-one-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍗曞垪灏忓浘" placement="bottom">
+ <el-radio-button value="oneColSmallImg">
+ <Icon icon="fluent:text-column-two-left-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍙屽垪" placement="bottom">
+ <el-radio-button value="twoCol">
+ <Icon icon="fluent:text-column-two-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <!--<el-tooltip class="item" content="涓夊垪" placement="bottom">
+ <el-radio-button value="threeCol">
+ <Icon icon="fluent:text-column-three-24-filled" />
+ </el-radio-button>
+ </el-tooltip>-->
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="fields.name.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.name.color" />
+ <el-checkbox v-model="formData.fields.name.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧绠�浠�" prop="fields.introduction.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.introduction.color" />
+ <el-checkbox v-model="formData.fields.introduction.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧浠锋牸" prop="fields.price.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.price.color" />
+ <el-checkbox v-model="formData.fields.price.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="甯傚満浠�" prop="fields.marketPrice.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.marketPrice.color" />
+ <el-checkbox v-model="formData.fields.marketPrice.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧閿�閲�" prop="fields.salesCount.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.salesCount.color" />
+ <el-checkbox v-model="formData.fields.salesCount.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧搴撳瓨" prop="fields.stock.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.stock.color" />
+ <el-checkbox v-model="formData.fields.stock.show" />
+ </div>
+ </el-form-item>
+ </el-card>
+ <el-card class="property-group" header="瑙掓爣" shadow="never">
+ <el-form-item label="瑙掓爣" prop="badge.show">
+ <el-switch v-model="formData.badge.show" />
+ </el-form-item>
+ <el-form-item v-if="formData.badge.show" label="瑙掓爣" prop="badge.imgUrl">
+ <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+ <template #tip> 寤鸿灏哄锛�36 * 22</template>
+ </UploadImg>
+ </el-form-item>
+ </el-card>
+ <el-card class="property-group" header="鎸夐挳" shadow="never">
+ <el-form-item label="鎸夐挳绫诲瀷" prop="btnBuy.type">
+ <el-radio-group v-model="formData.btnBuy.type">
+ <el-radio-button value="text">鏂囧瓧</el-radio-button>
+ <el-radio-button value="img">鍥剧墖</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <template v-if="formData.btnBuy.type === 'text'">
+ <el-form-item label="鎸夐挳鏂囧瓧" prop="btnBuy.text">
+ <el-input v-model="formData.btnBuy.text" />
+ </el-form-item>
+ <el-form-item label="宸︿晶鑳屾櫙" prop="btnBuy.bgBeginColor">
+ <ColorInput v-model="formData.btnBuy.bgBeginColor" />
+ </el-form-item>
+ <el-form-item label="鍙充晶鑳屾櫙" prop="btnBuy.bgEndColor">
+ <ColorInput v-model="formData.btnBuy.bgEndColor" />
+ </el-form-item>
+ </template>
+ <template v-else>
+ <el-form-item label="鍥剧墖" prop="btnBuy.imgUrl">
+ <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
+ <template #tip> 寤鸿灏哄锛�56 * 56</template>
+ </UploadImg>
+ </el-form-item>
+ </template>
+ </el-card>
+ <el-card class="property-group" header="鍟嗗搧鏍峰紡" shadow="never">
+ <el-form-item label="涓婂渾瑙�" prop="borderRadiusTop">
+ <el-slider
+ v-model="formData.borderRadiusTop"
+ :max="100"
+ :min="0"
+ :show-input-controls="false"
+ input-size="small"
+ show-input
+ />
+ </el-form-item>
+ <el-form-item label="涓嬪渾瑙�" prop="borderRadiusBottom">
+ <el-slider
+ v-model="formData.borderRadiusBottom"
+ :max="100"
+ :min="0"
+ :show-input-controls="false"
+ input-size="small"
+ show-input
+ />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ :show-input-controls="false"
+ input-size="small"
+ show-input
+ />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script lang="ts" setup>
+import { PromotionPointProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import PointShowcase from '@/views/mall/promotion/point/components/PointShowcase.vue'
+
+// 绉掓潃灞炴�ч潰鏉�
+defineOptions({ name: 'PromotionPointProperty' })
+
+const props = defineProps<{ modelValue: PromotionPointProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts b/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts
new file mode 100644
index 0000000..022be92
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/config.ts
@@ -0,0 +1,96 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 绉掓潃灞炴�� */
+export interface PromotionSeckillProperty {
+ // 甯冨眬绫诲瀷锛氬崟鍒� | 涓夊垪
+ layoutType: 'oneColBigImg' | 'oneColSmallImg' | 'twoCol'
+ // 鍟嗗搧瀛楁
+ fields: {
+ // 鍟嗗搧鍚嶇О
+ name: PromotionSeckillFieldProperty
+ // 鍟嗗搧绠�浠�
+ introduction: PromotionSeckillFieldProperty
+ // 鍟嗗搧浠锋牸
+ price: PromotionSeckillFieldProperty
+ // 甯傚満浠�
+ marketPrice: PromotionSeckillFieldProperty
+ // 鍟嗗搧閿�閲�
+ salesCount: PromotionSeckillFieldProperty
+ // 鍟嗗搧搴撳瓨
+ stock: PromotionSeckillFieldProperty
+ }
+ // 瑙掓爣
+ badge: {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 瑙掓爣鍥剧墖
+ imgUrl: string
+ }
+ // 鎸夐挳
+ btnBuy: {
+ // 绫诲瀷锛氭枃瀛� | 鍥剧墖
+ type: 'text' | 'img'
+ // 鏂囧瓧
+ text: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樿捣濮嬮鑹�
+ bgBeginColor: string
+ // 鏂囧瓧鎸夐挳锛氳儗鏅笎鍙樼粨鏉熼鑹�
+ bgEndColor: string
+ // 鍥剧墖鎸夐挳锛氬浘鐗囧湴鍧�
+ imgUrl: string
+ }
+ // 涓婂渾瑙�
+ borderRadiusTop: number
+ // 涓嬪渾瑙�
+ borderRadiusBottom: number
+ // 闂磋窛
+ space: number
+ // 绉掓潃娲诲姩缂栧彿
+ activityIds: number[]
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 鍟嗗搧瀛楁
+export interface PromotionSeckillFieldProperty {
+ // 鏄惁鏄剧ず
+ show: boolean
+ // 棰滆壊
+ color: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'PromotionSeckill',
+ name: '绉掓潃',
+ icon: 'mdi:calendar-time',
+ property: {
+ layoutType: 'oneColBigImg',
+ fields: {
+ name: { show: true, color: '#000' },
+ introduction: { show: true, color: '#999' },
+ price: { show: true, color: '#ff3000' },
+ marketPrice: { show: true, color: '#c4c4c4' },
+ salesCount: { show: true, color: '#c4c4c4' },
+ stock: { show: false, color: '#c4c4c4' }
+ },
+ badge: { show: false, imgUrl: '' },
+ btnBuy: {
+ type: 'text',
+ text: '绔嬪嵆绉掓潃',
+ bgBeginColor: '#FF6000',
+ bgEndColor: '#FE832A',
+ imgUrl: ''
+ },
+ borderRadiusTop: 8,
+ borderRadiusBottom: 8,
+ space: 8,
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<PromotionSeckillProperty>
diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue b/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue
new file mode 100644
index 0000000..3d34a3d
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/index.vue
@@ -0,0 +1,201 @@
+<template>
+ <div :class="`box-content min-h-30px w-full flex flex-row flex-wrap`" ref="containerRef">
+ <div
+ class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
+ :style="{
+ ...calculateSpace(index),
+ ...calculateWidth(),
+ borderTopLeftRadius: `${property.borderRadiusTop}px`,
+ borderTopRightRadius: `${property.borderRadiusTop}px`,
+ borderBottomLeftRadius: `${property.borderRadiusBottom}px`,
+ borderBottomRightRadius: `${property.borderRadiusBottom}px`
+ }"
+ v-for="(spu, index) in spuList"
+ :key="index"
+ >
+ <!-- 瑙掓爣 -->
+ <div v-if="property.badge.show" class="absolute left-0 top-0 z-1 items-center justify-center">
+ <el-image fit="cover" :src="property.badge.imgUrl" class="h-26px w-38px" />
+ </div>
+ <!-- 鍟嗗搧灏侀潰鍥� -->
+ <div
+ :class="[
+ 'h-140px',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-140px': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <el-image fit="cover" class="h-full w-full" :src="spu.picUrl" />
+ </div>
+ <div
+ :class="[
+ ' flex flex-col gap-8px p-8px box-border',
+ {
+ 'w-full': property.layoutType !== 'oneColSmallImg',
+ 'w-[calc(100%-140px-16px)]': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ >
+ <!-- 鍟嗗搧鍚嶇О -->
+ <div
+ v-if="property.fields.name.show"
+ :class="[
+ 'text-14px ',
+ {
+ truncate: property.layoutType !== 'oneColSmallImg',
+ 'overflow-ellipsis line-clamp-2': property.layoutType === 'oneColSmallImg'
+ }
+ ]"
+ :style="{ color: property.fields.name.color }"
+ >
+ {{ spu.name }}
+ </div>
+ <!-- 鍟嗗搧绠�浠� -->
+ <div
+ v-if="property.fields.introduction.show"
+ class="truncate text-12px"
+ :style="{ color: property.fields.introduction.color }"
+ >
+ {{ spu.introduction }}
+ </div>
+ <div>
+ <!-- 浠锋牸 -->
+ <span
+ v-if="property.fields.price.show"
+ class="text-16px"
+ :style="{ color: property.fields.price.color }"
+ >
+ 锟{ fenToYuan(spu.price || Infinity) }}
+ </span>
+ <!-- 甯傚満浠� -->
+ <span
+ v-if="property.fields.marketPrice.show && spu.marketPrice"
+ class="ml-4px text-10px line-through"
+ :style="{ color: property.fields.marketPrice.color }"
+ >锟{ fenToYuan(spu.marketPrice) }}</span
+ >
+ </div>
+ <div class="text-12px">
+ <!-- 閿�閲� -->
+ <span
+ v-if="property.fields.salesCount.show"
+ :style="{ color: property.fields.salesCount.color }"
+ >
+ 宸插敭{{ (spu.salesCount || 0) + (spu.virtualSalesCount || 0) }}浠�
+ </span>
+ <!-- 搴撳瓨 -->
+ <span v-if="property.fields.stock.show" :style="{ color: property.fields.stock.color }">
+ 搴撳瓨{{ spu.stock || 0 }}
+ </span>
+ </div>
+ </div>
+ <!-- 璐拱鎸夐挳 -->
+ <div class="absolute bottom-8px right-8px">
+ <!-- 鏂囧瓧鎸夐挳 -->
+ <span
+ v-if="property.btnBuy.type === 'text'"
+ class="rounded-full p-x-12px p-y-4px text-12px text-white"
+ :style="{
+ background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`
+ }"
+ >
+ {{ property.btnBuy.text }}
+ </span>
+ <!-- 鍥剧墖鎸夐挳 -->
+ <el-image
+ v-else
+ class="h-28px w-28px rounded-full"
+ fit="cover"
+ :src="property.btnBuy.imgUrl"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { PromotionSeckillProperty } from './config'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { fenToYuan } from '@/utils'
+
+/** 绉掓潃鍗$墖 */
+defineOptions({ name: 'PromotionSeckill' })
+// 瀹氫箟灞炴��
+const props = defineProps<{ property: PromotionSeckillProperty }>()
+// 鍟嗗搧鍒楄〃
+const spuList = ref<ProductSpuApi.Spu[]>([])
+const spuIdList = ref<number[]>([])
+const seckillActivityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
+
+watch(
+ () => props.property.activityIds,
+ async () => {
+ try {
+ // 鏂版坊鍔犵殑绉掓潃缁勪欢锛屾槸娌℃湁娲诲姩ID鐨�
+ const activityIds = props.property.activityIds
+ // 妫�鏌ユ椿鍔↖D鐨勬湁鏁堟��
+ if (Array.isArray(activityIds) && activityIds.length > 0) {
+ // 鑾峰彇绉掓潃娲诲姩璇︽儏鍒楄〃
+ seckillActivityList.value =
+ await SeckillActivityApi.getSeckillActivityListByIds(activityIds)
+
+ // 鑾峰彇绉掓潃娲诲姩鐨� SPU 璇︽儏鍒楄〃
+ spuList.value = []
+ spuIdList.value = seckillActivityList.value
+ .map((activity) => activity.spuId)
+ .filter((spuId): spuId is number => typeof spuId === 'number')
+ if (spuIdList.value.length > 0) {
+ spuList.value = await ProductSpuApi.getSpuDetailList(spuIdList.value)
+ }
+
+ // 鏇存柊 SPU 鐨勬渶浣庝环鏍�
+ seckillActivityList.value.forEach((activity) => {
+ // 鍖归厤spuId
+ const spu = spuList.value.find((spu) => spu.id === activity.spuId)
+ if (spu) {
+ // 璧嬪�兼椿鍔ㄤ环鏍硷紝鍝釜鏈�渚垮疁灏辫祴鍊煎摢涓�
+ spu.price = Math.min(activity.seckillPrice || Infinity, spu.price || Infinity)
+ }
+ })
+ }
+ } catch (error) {
+ console.error('鑾峰彇绉掓潃娲诲姩缁嗚妭鎴� SPU 缁嗚妭鏃跺嚭閿�:', error)
+ }
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+)
+
+/**
+ * 璁$畻鍟嗗搧鐨勯棿璺�
+ * @param index 鍟嗗搧绱㈠紩
+ */
+const calculateSpace = (index: number) => {
+ // 鍟嗗搧鐨勫垪鏁�
+ const columns = props.property.layoutType === 'twoCol' ? 2 : 1
+ // 绗竴鍒楁病鏈夊乏杈硅窛
+ const marginLeft = index % columns === 0 ? '0' : props.property.space + 'px'
+ // 绗竴琛屾病鏈変笂杈硅窛
+ const marginTop = index < columns ? '0' : props.property.space + 'px'
+
+ return { marginLeft, marginTop }
+}
+
+// 瀹瑰櫒
+const containerRef = ref()
+// 璁$畻鍟嗗搧鐨勫搴�
+const calculateWidth = () => {
+ let width = '100%'
+ // 鍙屽垪鏃舵瘡鍒楃殑瀹藉害涓猴細锛堟�诲搴� - 闂磋窛锛�/ 2
+ if (props.property.layoutType === 'twoCol') {
+ width = `${(containerRef.value.offsetWidth - props.property.space) / 2}px`
+ }
+ return { width }
+}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue b/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue
new file mode 100644
index 0000000..594c10b
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/PromotionSeckill/property.vue
@@ -0,0 +1,164 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="80px" :model="formData">
+ <el-card header="绉掓潃娲诲姩" class="property-group" shadow="never">
+ <SeckillShowcase v-model="formData.activityIds" />
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="甯冨眬" prop="type">
+ <el-radio-group v-model="formData.layoutType">
+ <el-tooltip class="item" content="鍗曞垪澶у浘" placement="bottom">
+ <el-radio-button value="oneColBigImg">
+ <Icon icon="fluent:text-column-one-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍗曞垪灏忓浘" placement="bottom">
+ <el-radio-button value="oneColSmallImg">
+ <Icon icon="fluent:text-column-two-left-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip class="item" content="鍙屽垪" placement="bottom">
+ <el-radio-button value="twoCol">
+ <Icon icon="fluent:text-column-two-24-filled" />
+ </el-radio-button>
+ </el-tooltip>
+ <!--<el-tooltip class="item" content="涓夊垪" placement="bottom">
+ <el-radio-button value="threeCol">
+ <Icon icon="fluent:text-column-three-24-filled" />
+ </el-radio-button>
+ </el-tooltip>-->
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="fields.name.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.name.color" />
+ <el-checkbox v-model="formData.fields.name.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧绠�浠�" prop="fields.introduction.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.introduction.color" />
+ <el-checkbox v-model="formData.fields.introduction.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧浠锋牸" prop="fields.price.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.price.color" />
+ <el-checkbox v-model="formData.fields.price.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="甯傚満浠�" prop="fields.marketPrice.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.marketPrice.color" />
+ <el-checkbox v-model="formData.fields.marketPrice.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧閿�閲�" prop="fields.salesCount.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.salesCount.color" />
+ <el-checkbox v-model="formData.fields.salesCount.show" />
+ </div>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧搴撳瓨" prop="fields.stock.show">
+ <div class="flex gap-8px">
+ <ColorInput v-model="formData.fields.stock.color" />
+ <el-checkbox v-model="formData.fields.stock.show" />
+ </div>
+ </el-form-item>
+ </el-card>
+ <el-card header="瑙掓爣" class="property-group" shadow="never">
+ <el-form-item label="瑙掓爣" prop="badge.show">
+ <el-switch v-model="formData.badge.show" />
+ </el-form-item>
+ <el-form-item label="瑙掓爣" prop="badge.imgUrl" v-if="formData.badge.show">
+ <UploadImg v-model="formData.badge.imgUrl" height="44px" width="72px">
+ <template #tip> 寤鸿灏哄锛�36 * 22</template>
+ </UploadImg>
+ </el-form-item>
+ </el-card>
+ <el-card header="鎸夐挳" class="property-group" shadow="never">
+ <el-form-item label="鎸夐挳绫诲瀷" prop="btnBuy.type">
+ <el-radio-group v-model="formData.btnBuy.type">
+ <el-radio-button value="text">鏂囧瓧</el-radio-button>
+ <el-radio-button value="img">鍥剧墖</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <template v-if="formData.btnBuy.type === 'text'">
+ <el-form-item label="鎸夐挳鏂囧瓧" prop="btnBuy.text">
+ <el-input v-model="formData.btnBuy.text" />
+ </el-form-item>
+ <el-form-item label="宸︿晶鑳屾櫙" prop="btnBuy.bgBeginColor">
+ <ColorInput v-model="formData.btnBuy.bgBeginColor" />
+ </el-form-item>
+ <el-form-item label="鍙充晶鑳屾櫙" prop="btnBuy.bgEndColor">
+ <ColorInput v-model="formData.btnBuy.bgEndColor" />
+ </el-form-item>
+ </template>
+ <template v-else>
+ <el-form-item label="鍥剧墖" prop="btnBuy.imgUrl">
+ <UploadImg v-model="formData.btnBuy.imgUrl" height="56px" width="56px">
+ <template #tip> 寤鸿灏哄锛�56 * 56</template>
+ </UploadImg>
+ </el-form-item>
+ </template>
+ </el-card>
+ <el-card header="鍟嗗搧鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="涓婂渾瑙�" prop="borderRadiusTop">
+ <el-slider
+ v-model="formData.borderRadiusTop"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="涓嬪渾瑙�" prop="borderRadiusBottom">
+ <el-slider
+ v-model="formData.borderRadiusBottom"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ <el-form-item label="闂撮殧" prop="space">
+ <el-slider
+ v-model="formData.space"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { PromotionSeckillProperty } from './config'
+import { useVModel } from '@vueuse/core'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { CommonStatusEnum } from '@/utils/constants'
+import SeckillShowcase from '@/views/mall/promotion/seckill/components/SeckillShowcase.vue'
+
+// 绉掓潃灞炴�ч潰鏉�
+defineOptions({ name: 'PromotionSeckillProperty' })
+
+const props = defineProps<{ modelValue: PromotionSeckillProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+// 娲诲姩鍒楄〃
+const activityList = ref<SeckillActivityApi.SeckillActivityVO[]>([])
+onMounted(async () => {
+ const { list } = await SeckillActivityApi.getSeckillActivityPage({
+ status: CommonStatusEnum.ENABLE
+ })
+ activityList.value = list
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/SearchBar/config.ts b/src/components/DiyEditor/components/mobile/SearchBar/config.ts
new file mode 100644
index 0000000..ef47b27
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/SearchBar/config.ts
@@ -0,0 +1,43 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鎼滅储妗嗗睘鎬� */
+export interface SearchProperty {
+ height: number // 鎼滅储鏍忛珮搴�
+ showScan: boolean // 鏄剧ず鎵竴鎵�
+ borderRadius: number // 妗嗕綋鏍峰紡
+ placeholder: string // 鍗犱綅鏂囧瓧
+ placeholderPosition: PlaceholderPosition // 鍗犱綅鏂囧瓧浣嶇疆
+ backgroundColor: string // 妗嗕綋棰滆壊
+ textColor: string // 瀛椾綋棰滆壊
+ hotKeywords: string[] // 鐑瘝
+ style: ComponentStyle
+}
+
+// 鏂囧瓧浣嶇疆
+export type PlaceholderPosition = 'left' | 'center'
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'SearchBar',
+ name: '鎼滅储妗�',
+ icon: 'ep:search',
+ property: {
+ height: 28,
+ showScan: false,
+ borderRadius: 0,
+ placeholder: '鎼滅储鍟嗗搧',
+ placeholderPosition: 'left',
+ backgroundColor: 'rgb(238, 238, 238)',
+ textColor: 'rgb(150, 151, 153)',
+ hotKeywords: [],
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ paddingTop: 8,
+ paddingRight: 8,
+ paddingBottom: 8,
+ paddingLeft: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<SearchProperty>
diff --git a/src/components/DiyEditor/components/mobile/SearchBar/index.vue b/src/components/DiyEditor/components/mobile/SearchBar/index.vue
new file mode 100644
index 0000000..9de261a
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/SearchBar/index.vue
@@ -0,0 +1,75 @@
+<template>
+ <div
+ class="search-bar"
+ :style="{
+ color: property.textColor
+ }"
+ >
+ <!-- 鎼滅储妗� -->
+ <div
+ class="inner"
+ :style="{
+ height: `${property.height}px`,
+ background: property.backgroundColor,
+ borderRadius: `${property.borderRadius}px`
+ }"
+ >
+ <div
+ class="placeholder"
+ :style="{
+ justifyContent: property.placeholderPosition
+ }"
+ >
+ <Icon icon="ep:search" />
+ <span>{{ property.placeholder || '鎼滅储鍟嗗搧' }}</span>
+ </div>
+ <div class="right">
+ <!-- 鎼滅储鐑瘝 -->
+ <span v-for="(keyword, index) in property.hotKeywords" :key="index">{{ keyword }}</span>
+ <!-- 鎵竴鎵� -->
+ <Icon icon="ant-design:scan-outlined" v-show="property.showScan" />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { SearchProperty } from './config'
+/** 鎼滅储妗� */
+defineOptions({ name: 'SearchBar' })
+defineProps<{ property: SearchProperty }>()
+</script>
+
+<style scoped lang="scss">
+.search-bar {
+ /* 鎼滅储妗� */
+ .inner {
+ position: relative;
+ display: flex;
+ min-height: 28px;
+ font-size: 14px;
+ align-items: center;
+
+ .placeholder {
+ display: flex;
+ width: 100%;
+ padding: 0 8px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ white-space: nowrap;
+ align-items: center;
+ gap: 2px;
+ }
+
+ .right {
+ position: absolute;
+ right: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/SearchBar/property.vue b/src/components/DiyEditor/components/mobile/SearchBar/property.vue
new file mode 100644
index 0000000..73aeeef
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/SearchBar/property.vue
@@ -0,0 +1,87 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <!-- 琛ㄥ崟 -->
+ <el-form label-width="80px" :model="formData" class="m-t-8px">
+ <el-card header="鎼滅储鐑瘝" class="property-group" shadow="never">
+ <Draggable v-model="formData.hotKeywords" :empty-item="''" :min="0">
+ <template #default="{ index }">
+ <el-input v-model="formData.hotKeywords[index]" placeholder="璇疯緭鍏ョ儹璇�" />
+ </template>
+ </Draggable>
+ </el-card>
+ <el-card header="鎼滅储鏍峰紡" class="property-group" shadow="never">
+ <el-form-item label="妗嗕綋鏍峰紡">
+ <el-radio-group v-model="formData!.borderRadius">
+ <el-tooltip content="鏂瑰舰" placement="top">
+ <el-radio-button :value="0">
+ <Icon icon="tabler:input-search" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip content="鍦嗗舰" placement="top">
+ <el-radio-button :value="10">
+ <Icon icon="iconoir:input-search" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鎻愮ず鏂囧瓧" prop="placeholder">
+ <el-input v-model="formData.placeholder" />
+ </el-form-item>
+ <el-form-item label="鏂囨湰浣嶇疆" prop="placeholderPosition">
+ <el-radio-group v-model="formData!.placeholderPosition">
+ <el-tooltip content="灞呭乏" placement="top">
+ <el-radio-button value="left">
+ <Icon icon="ant-design:align-left-outlined" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip content="灞呬腑" placement="top">
+ <el-radio-button value="center">
+ <Icon icon="ant-design:align-center-outlined" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鎵竴鎵�" prop="showScan">
+ <el-switch v-model="formData!.showScan" />
+ </el-form-item>
+ <el-form-item label="妗嗕綋楂樺害" prop="height">
+ <el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
+ </el-form-item>
+ <el-form-item label="妗嗕綋棰滆壊" prop="backgroundColor">
+ <ColorInput v-model="formData.backgroundColor" />
+ </el-form-item>
+ <el-form-item class="lef" label="鏂囨湰棰滆壊" prop="textColor">
+ <ColorInput v-model="formData.textColor" />
+ </el-form-item>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
+import { isString } from '@/utils/is'
+
+/** 鎼滅储妗嗗睘鎬ч潰鏉� */
+defineOptions({ name: 'SearchProperty' })
+
+const props = defineProps<{ modelValue: SearchProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 鐩戝惉鐑瘝鏁扮粍鍙樺寲
+watch(
+ () => formData.value.hotKeywords,
+ (newVal) => {
+ // 鎵惧埌闈炲瓧绗︿覆椤圭殑绱㈠紩
+ const nonStringIndex = newVal.findIndex((item) => !isString(item))
+ if (nonStringIndex !== -1) {
+ formData.value.hotKeywords[nonStringIndex] = ''
+ }
+ },
+ { deep: true, flush: 'post' }
+)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/TabBar/config.ts b/src/components/DiyEditor/components/mobile/TabBar/config.ts
new file mode 100644
index 0000000..88d706f
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/TabBar/config.ts
@@ -0,0 +1,97 @@
+import { DiyComponent } from '@/components/DiyEditor/util'
+
+/** 搴曢儴瀵艰埅鑿滃崟灞炴�� */
+export interface TabBarProperty {
+ // 閫夐」鍒楄〃
+ items: TabBarItemProperty[]
+ // 涓婚
+ theme: string
+ // 鏍峰紡
+ style: TabBarStyle
+}
+
+// 閫夐」灞炴��
+export interface TabBarItemProperty {
+ // 鏍囩鏂囧瓧
+ text: string
+ // 閾炬帴
+ url: string
+ // 榛樿鍥炬爣閾炬帴
+ iconUrl: string
+ // 閫変腑鐨勫浘鏍囬摼鎺�
+ activeIconUrl: string
+}
+
+// 鏍峰紡
+export interface TabBarStyle {
+ // 鑳屾櫙绫诲瀷
+ bgType: 'color' | 'img'
+ // 鑳屾櫙棰滆壊
+ bgColor: string
+ // 鍥剧墖閾炬帴
+ bgImg: string
+ // 榛樿棰滆壊
+ color: string
+ // 閫変腑鐨勯鑹�
+ activeColor: string
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'TabBar',
+ name: '搴曢儴瀵艰埅',
+ icon: 'fluent:table-bottom-row-16-filled',
+ property: {
+ theme: 'red',
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ color: '#282828',
+ activeColor: '#fc4141'
+ },
+ items: [
+ {
+ text: '棣栭〉',
+ url: '/pages/index/index',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png'
+ },
+ {
+ text: '鍒嗙被',
+ url: '/pages/index/category?id=3',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png'
+ },
+ {
+ text: '璐墿杞�',
+ url: '/pages/index/cart',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png'
+ },
+ {
+ text: '鎴戠殑',
+ url: '/pages/index/user',
+ iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png',
+ activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png'
+ }
+ ]
+ }
+} as DiyComponent<TabBarProperty>
+
+export const THEME_LIST = [
+ { id: 'red', name: '涓浗绾�', icon: 'icon-park-twotone:theme', color: '#d10019' },
+ { id: 'orange', name: '妗旀', icon: 'icon-park-twotone:theme', color: '#f37b1d' },
+ { id: 'gold', name: '鏄庨粍', icon: 'icon-park-twotone:theme', color: '#fbbd08' },
+ { id: 'green', name: '姗勬缁�', icon: 'icon-park-twotone:theme', color: '#8dc63f' },
+ { id: 'cyan', name: '澶╅潚', icon: 'icon-park-twotone:theme', color: '#1cbbb4' },
+ { id: 'blue', name: '娴疯摑', icon: 'icon-park-twotone:theme', color: '#0081ff' },
+ { id: 'purple', name: '濮圭传', icon: 'icon-park-twotone:theme', color: '#6739b6' },
+ { id: 'brightRed', name: '瀚g孩', icon: 'icon-park-twotone:theme', color: '#e54d42' },
+ { id: 'forestGreen', name: '妫豢', icon: 'icon-park-twotone:theme', color: '#39b54a' },
+ { id: 'mauve', name: '鏈ㄦЭ', icon: 'icon-park-twotone:theme', color: '#9c26b0' },
+ { id: 'pink', name: '妗冪矇', icon: 'icon-park-twotone:theme', color: '#e03997' },
+ { id: 'brown', name: '妫曡', icon: 'icon-park-twotone:theme', color: '#a5673f' },
+ { id: 'grey', name: '鐜勭伆', icon: 'icon-park-twotone:theme', color: '#8799a3' },
+ { id: 'gray', name: '鑽夌伆', icon: 'icon-park-twotone:theme', color: '#aaaaaa' },
+ { id: 'black', name: '澧ㄩ粦', icon: 'icon-park-twotone:theme', color: '#333333' }
+]
diff --git a/src/components/DiyEditor/components/mobile/TabBar/index.vue b/src/components/DiyEditor/components/mobile/TabBar/index.vue
new file mode 100644
index 0000000..44ba43c
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/TabBar/index.vue
@@ -0,0 +1,66 @@
+<template>
+ <div class="tab-bar">
+ <div
+ class="tab-bar-bg"
+ :style="{
+ background:
+ property.style.bgType === 'color'
+ ? property.style.bgColor
+ : `url(${property.style.bgImg})`,
+ backgroundSize: '100% 100%',
+ backgroundRepeat: 'no-repeat'
+ }"
+ >
+ <div v-for="(item, index) in property.items" :key="index" class="tab-bar-item">
+ <el-image :src="index === 0 ? item.activeIconUrl : item.iconUrl">
+ <template #error>
+ <div class="h-full w-full flex items-center justify-center">
+ <Icon icon="ep:picture" />
+ </div>
+ </template>
+ </el-image>
+ <span :style="{ color: index === 0 ? property.style.activeColor : property.style.color }">
+ {{ item.text }}
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { TabBarProperty } from './config'
+
+/** 椤甸潰搴曢儴瀵艰埅鏍� */
+defineOptions({ name: 'TabBar' })
+
+defineProps<{ property: TabBarProperty }>()
+</script>
+<style lang="scss" scoped>
+.tab-bar {
+ z-index: 2;
+ width: 100%;
+
+ .tab-bar-bg {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-around;
+ padding: 8px 0;
+
+ .tab-bar-item {
+ display: flex;
+ width: 100%;
+ font-size: 12px;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ :deep(img),
+ .el-icon {
+ width: 26px;
+ height: 26px;
+ border-radius: 4px;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/TabBar/property.vue b/src/components/DiyEditor/components/mobile/TabBar/property.vue
new file mode 100644
index 0000000..e435012
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/TabBar/property.vue
@@ -0,0 +1,103 @@
+<template>
+ <div class="tab-bar">
+ <!-- 琛ㄥ崟 -->
+ <el-form :model="formData" label-width="80px">
+ <el-form-item label="涓婚" prop="theme">
+ <el-select v-model="formData!.theme" @change="handleThemeChange">
+ <el-option
+ v-for="(theme, index) in THEME_LIST"
+ :key="index"
+ :label="theme.name"
+ :value="theme.id"
+ >
+ <template #default>
+ <div class="flex items-center justify-between">
+ <Icon :icon="theme.icon" :color="theme.color" />
+ <span>{{ theme.name }}</span>
+ </div>
+ </template>
+ </el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="榛樿棰滆壊">
+ <ColorInput v-model="formData!.style.color" />
+ </el-form-item>
+ <el-form-item label="閫変腑棰滆壊">
+ <ColorInput v-model="formData!.style.activeColor" />
+ </el-form-item>
+ <el-form-item label="瀵艰埅鑳屾櫙">
+ <el-radio-group v-model="formData!.style.bgType">
+ <el-radio-button value="color">绾壊</el-radio-button>
+ <el-radio-button value="img">鍥剧墖</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="閫夋嫨棰滆壊" v-if="formData!.style.bgType === 'color'">
+ <ColorInput v-model="formData!.style.bgColor" />
+ </el-form-item>
+ <el-form-item label="閫夋嫨鍥剧墖" v-if="formData!.style.bgType === 'img'">
+ <UploadImg v-model="formData!.style.bgImg" width="100%" height="50px" class="min-w-200px">
+ <template #tip> 寤鸿灏哄 375 * 50 </template>
+ </UploadImg>
+ </el-form-item>
+
+ <el-text tag="p">鍥炬爣璁剧疆</el-text>
+ <el-text type="info" size="small"> 鎷栧姩宸︿笂瑙掔殑灏忓渾鐐瑰彲瀵瑰叾鎺掑簭, 鍥炬爣寤鸿灏哄 44*44 </el-text>
+ <Draggable v-model="formData.items" :limit="5">
+ <template #default="{ element }">
+ <div class="m-b-8px flex items-center justify-around">
+ <div class="flex flex-col items-center justify-between">
+ <UploadImg
+ v-model="element.iconUrl"
+ width="40px"
+ height="40px"
+ :show-delete="false"
+ :show-btn-text="false"
+ />
+ <el-text size="small">鏈�変腑</el-text>
+ </div>
+ <div>
+ <UploadImg
+ v-model="element.activeIconUrl"
+ width="40px"
+ height="40px"
+ :show-delete="false"
+ :show-btn-text="false"
+ />
+ <el-text>宸查�変腑</el-text>
+ </div>
+ </div>
+ <el-form-item prop="text" label="鏂囧瓧" label-width="48px" class="m-b-8px!">
+ <el-input v-model="element.text" placeholder="璇疯緭鍏ユ枃瀛�" />
+ </el-form-item>
+ <el-form-item prop="url" label="閾炬帴" label-width="48px" class="m-b-0!">
+ <AppLinkInput v-model="element.url" />
+ </el-form-item>
+ </template>
+ </Draggable>
+ </el-form>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { TabBarProperty, component, THEME_LIST } from './config'
+import { useVModel } from '@vueuse/core'
+// 搴曢儴瀵艰埅鏍�
+defineOptions({ name: 'TabBarProperty' })
+
+const props = defineProps<{ modelValue: TabBarProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 灏嗘暟鎹簱鐨勫�兼洿鏂板埌鍙充晶灞炴�ф爮
+component.property.items = formData.value.items
+
+// 瑕佺殑涓婚
+const handleThemeChange = () => {
+ const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
+ if (theme?.color) {
+ formData.value.style.activeColor = theme.color
+ }
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/DiyEditor/components/mobile/TitleBar/config.ts b/src/components/DiyEditor/components/mobile/TitleBar/config.ts
new file mode 100644
index 0000000..4d2a42e
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/TitleBar/config.ts
@@ -0,0 +1,73 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鏍囬鏍忓睘鎬� */
+export interface TitleBarProperty {
+ // 鑳屾櫙鍥�
+ bgImgUrl: string
+ // 鍋忕Щ
+ marginLeft: number
+ // 鏄剧ず浣嶇疆
+ textAlign: 'left' | 'center'
+ // 涓绘爣棰�
+ title: string
+ // 鍓爣棰�
+ description: string
+ // 鏍囬澶у皬
+ titleSize: number
+ // 鎻忚堪澶у皬
+ descriptionSize: number
+ // 鏍囬绮楃粏
+ titleWeight: number
+ // 鎻忚堪绮楃粏
+ descriptionWeight: number
+ // 鏍囬棰滆壊
+ titleColor: string
+ // 鎻忚堪棰滆壊
+ descriptionColor: string
+ // 楂樺害
+ height: number
+ // 鏌ョ湅鏇村
+ more: {
+ // 鏄惁鏄剧ず鏌ョ湅鏇村
+ show: false
+ // 鏍峰紡閫夋嫨
+ type: 'text' | 'icon' | 'all'
+ // 鑷畾涔夋枃瀛�
+ text: string
+ // 閾炬帴
+ url: string
+ }
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'TitleBar',
+ name: '鏍囬鏍�',
+ icon: 'material-symbols:line-start',
+ property: {
+ title: '涓绘爣棰�',
+ description: '鍓爣棰�',
+ titleSize: 16,
+ descriptionSize: 12,
+ titleWeight: 400,
+ textAlign: 'left',
+ descriptionWeight: 200,
+ titleColor: 'rgba(50, 50, 51, 10)',
+ descriptionColor: 'rgba(150, 151, 153, 10)',
+ marginLeft: 0,
+ height: 40,
+ more: {
+ //鏌ョ湅鏇村
+ show: false,
+ type: 'icon',
+ text: '鏌ョ湅鏇村',
+ url: ''
+ },
+ style: {
+ bgType: 'color',
+ bgColor: '#fff'
+ } as ComponentStyle
+ }
+} as DiyComponent<TitleBarProperty>
diff --git a/src/components/DiyEditor/components/mobile/TitleBar/index.vue b/src/components/DiyEditor/components/mobile/TitleBar/index.vue
new file mode 100644
index 0000000..8c77d62
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/TitleBar/index.vue
@@ -0,0 +1,75 @@
+<template>
+ <div class="title-bar" :style="{ height: `${property.height}px` }">
+ <el-image v-if="property.bgImgUrl" :src="property.bgImgUrl" fit="cover" class="w-full" />
+ <div class="absolute left-0 top-0 w-full h-full flex flex-col justify-center">
+ <!-- 鏍囬 -->
+ <div
+ :style="{
+ fontSize: `${property.titleSize}px`,
+ fontWeight: property.titleWeight,
+ color: property.titleColor,
+ textAlign: property.textAlign,
+ marginLeft: `${property.marginLeft}px`,
+ marginBottom: '4px'
+ }"
+ v-if="property.title"
+ >
+ {{ property.title }}
+ </div>
+ <!-- 鍓爣棰� -->
+ <div
+ :style="{
+ fontSize: `${property.descriptionSize}px`,
+ fontWeight: property.descriptionWeight,
+ color: property.descriptionColor,
+ textAlign: property.textAlign,
+ marginLeft: `${property.marginLeft}px`
+ }"
+ v-if="property.description"
+ >
+ {{ property.description }}
+ </div>
+ </div>
+ <!-- 鏇村 -->
+ <div
+ class="more"
+ v-show="property.more.show"
+ :style="{
+ color: property.descriptionColor
+ }"
+ >
+ <span v-if="property.more.type !== 'icon'"> {{ property.more.text }} </span>
+ <Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { TitleBarProperty } from './config'
+
+/** 鏍囬鏍� */
+defineOptions({ name: 'TitleBar' })
+
+defineProps<{ property: TitleBarProperty }>()
+</script>
+<style scoped lang="scss">
+.title-bar {
+ position: relative;
+ width: 100%;
+ min-height: 20px;
+ box-sizing: border-box;
+
+ /* 鏇村 */
+ .more {
+ position: absolute;
+ top: 0;
+ right: 8px;
+ bottom: 0;
+ display: flex;
+ margin: auto;
+ font-size: 10px;
+ color: #969799;
+ align-items: center;
+ justify-content: center;
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/TitleBar/property.vue b/src/components/DiyEditor/components/mobile/TitleBar/property.vue
new file mode 100644
index 0000000..5eebb15
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/TitleBar/property.vue
@@ -0,0 +1,139 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <el-form label-width="85px" :model="formData" :rules="rules">
+ <el-card header="椋庢牸" class="property-group" shadow="never">
+ <el-form-item label="鑳屾櫙鍥剧墖" prop="bgImgUrl">
+ <UploadImg v-model="formData.bgImgUrl" width="100%" height="40px">
+ <template #tip>寤鸿灏哄 750*80</template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="鏍囬浣嶇疆" prop="textAlign">
+ <el-radio-group v-model="formData!.textAlign">
+ <el-tooltip content="灞呭乏" placement="top">
+ <el-radio-button value="left">
+ <Icon icon="ant-design:align-left-outlined" />
+ </el-radio-button>
+ </el-tooltip>
+ <el-tooltip content="灞呬腑" placement="top">
+ <el-radio-button value="center">
+ <Icon icon="ant-design:align-center-outlined" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍋忕Щ閲�" prop="marginLeft" label-width="70px">
+ <el-slider
+ v-model="formData.marginLeft"
+ :max="100"
+ :min="0"
+ show-input
+ input-size="small"
+ />
+ </el-form-item>
+ <el-form-item label="楂樺害" prop="height" label-width="70px">
+ <el-slider
+ v-model="formData.height"
+ :max="200"
+ :min="20"
+ show-input
+ input-size="small"
+ />
+ </el-form-item>
+ </el-card>
+ <el-card header="涓绘爣棰�" class="property-group" shadow="never">
+ <el-form-item label="鏂囧瓧" prop="title" label-width="40px">
+ <InputWithColor
+ v-model="formData.title"
+ v-model:color="formData.titleColor"
+ show-word-limit
+ maxlength="20"
+ />
+ </el-form-item>
+ <el-form-item label="澶у皬" prop="titleSize" label-width="40px">
+ <el-slider
+ v-model="formData.titleSize"
+ :max="60"
+ :min="10"
+ show-input
+ input-size="small"
+ />
+ </el-form-item>
+ <el-form-item label="绮楃粏" prop="titleWeight" label-width="40px">
+ <el-slider
+ v-model="formData.titleWeight"
+ :min="100"
+ :max="900"
+ :step="100"
+ show-input
+ input-size="small"
+ />
+ </el-form-item>
+ </el-card>
+ <el-card header="鍓爣棰�" class="property-group" shadow="never">
+ <el-form-item label="鏂囧瓧" prop="description" label-width="40px">
+ <InputWithColor
+ v-model="formData.description"
+ v-model:color="formData.descriptionColor"
+ show-word-limit
+ maxlength="50"
+ />
+ </el-form-item>
+ <el-form-item label="澶у皬" prop="descriptionSize" label-width="40px">
+ <el-slider
+ v-model="formData.descriptionSize"
+ :max="60"
+ :min="10"
+ show-input
+ input-size="small"
+ />
+ </el-form-item>
+ <el-form-item label="绮楃粏" prop="descriptionWeight" label-width="40px">
+ <el-slider
+ v-model="formData.descriptionWeight"
+ :min="100"
+ :max="900"
+ :step="100"
+ show-input
+ input-size="small"
+ />
+ </el-form-item>
+ </el-card>
+ <el-card header="鏌ョ湅鏇村" class="property-group" shadow="never">
+ <el-form-item label="鏄惁鏄剧ず" prop="more.show">
+ <el-checkbox v-model="formData.more.show" />
+ </el-form-item>
+ <!-- 鏇村鎸夐挳鐨� 鏍峰紡閫夋嫨 -->
+ <template v-if="formData.more.show">
+ <el-form-item label="鏍峰紡" prop="more.type">
+ <el-radio-group v-model="formData.more.type">
+ <el-radio value="text">鏂囧瓧</el-radio>
+ <el-radio value="icon">鍥炬爣</el-radio>
+ <el-radio value="all">鏂囧瓧+鍥炬爣</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏇村鏂囧瓧" prop="more.text" v-show="formData.more.type !== 'icon'">
+ <el-input v-model="formData.more.text" />
+ </el-form-item>
+ <el-form-item label="璺宠浆閾炬帴" prop="more.url">
+ <AppLinkInput v-model="formData.more.url" />
+ </el-form-item>
+ </template>
+ </el-card>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+<script setup lang="ts">
+import { TitleBarProperty } from './config'
+import { useVModel } from '@vueuse/core'
+// 瀵艰埅鏍忓睘鎬ч潰鏉�
+defineOptions({ name: 'TitleBarProperty' })
+
+const props = defineProps<{ modelValue: TitleBarProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 琛ㄥ崟鏍¢獙
+const rules = {}
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserCard/config.ts b/src/components/DiyEditor/components/mobile/UserCard/config.ts
new file mode 100644
index 0000000..7b33776
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserCard/config.ts
@@ -0,0 +1,21 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鐢ㄦ埛鍗$墖灞炴�� */
+export interface UserCardProperty {
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'UserCard',
+ name: '鐢ㄦ埛鍗$墖',
+ icon: 'mdi:user-card-details',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<UserCardProperty>
diff --git a/src/components/DiyEditor/components/mobile/UserCard/index.vue b/src/components/DiyEditor/components/mobile/UserCard/index.vue
new file mode 100644
index 0000000..14b447c
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserCard/index.vue
@@ -0,0 +1,29 @@
+<template>
+ <div class="flex flex-col">
+ <div class="flex items-center justify-between p-x-18px p-y-24px">
+ <div class="flex flex-1 items-center gap-16px">
+ <el-avatar :size="60">
+ <Icon icon="ep:avatar" :size="60" />
+ </el-avatar>
+ <span class="text-18px font-bold">鑺嬮亾婧愮爜</span>
+ </div>
+ <Icon icon="tdesign:qrcode" :size="20" />
+ </div>
+ <div
+ class="flex items-center justify-between justify-between bg-white p-x-20px p-y-8px text-12px"
+ >
+ <span class="color-#ff690d">鐐瑰嚮缁戝畾鎵嬫満鍙�</span>
+ <span class="rounded-26px bg-#ff6100 p-x-8px p-y-5px color-white">鍘荤粦瀹�</span>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { UserCardProperty } from './config'
+
+/** 鐢ㄦ埛鍗$墖 */
+defineOptions({ name: 'UserCard' })
+// 瀹氫箟灞炴��
+defineProps<{ property: UserCardProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserCard/property.vue b/src/components/DiyEditor/components/mobile/UserCard/property.vue
new file mode 100644
index 0000000..50ecb55
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserCard/property.vue
@@ -0,0 +1,17 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserCardProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 鐢ㄦ埛鍗$墖灞炴�ч潰鏉�
+defineOptions({ name: 'UserCardProperty' })
+
+const props = defineProps<{ modelValue: UserCardProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserCoupon/config.ts b/src/components/DiyEditor/components/mobile/UserCoupon/config.ts
new file mode 100644
index 0000000..92eba9b
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserCoupon/config.ts
@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鐢ㄦ埛鍗″埜灞炴�� */
+export interface UserCouponProperty {
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'UserCoupon',
+ name: '鐢ㄦ埛鍗″埜',
+ icon: 'ep:ticket',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<UserCouponProperty>
diff --git a/src/components/DiyEditor/components/mobile/UserCoupon/index.vue b/src/components/DiyEditor/components/mobile/UserCoupon/index.vue
new file mode 100644
index 0000000..27ad310
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserCoupon/index.vue
@@ -0,0 +1,15 @@
+<template>
+ <el-image
+ src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/couponCardStyle.png"
+ />
+</template>
+<script setup lang="ts">
+import { UserCouponProperty } from './config'
+
+/** 鐢ㄦ埛鍗″埜 */
+defineOptions({ name: 'UserCoupon' })
+// 瀹氫箟灞炴��
+defineProps<{ property: UserCouponProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserCoupon/property.vue b/src/components/DiyEditor/components/mobile/UserCoupon/property.vue
new file mode 100644
index 0000000..221cc90
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserCoupon/property.vue
@@ -0,0 +1,17 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserCouponProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 鐢ㄦ埛鍗″埜灞炴�ч潰鏉�
+defineOptions({ name: 'UserCouponProperty' })
+
+const props = defineProps<{ modelValue: UserCouponProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserOrder/config.ts b/src/components/DiyEditor/components/mobile/UserOrder/config.ts
new file mode 100644
index 0000000..f9c5a6d
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserOrder/config.ts
@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鐢ㄦ埛璁㈠崟灞炴�� */
+export interface UserOrderProperty {
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'UserOrder',
+ name: '鐢ㄦ埛璁㈠崟',
+ icon: 'ep:list',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<UserOrderProperty>
diff --git a/src/components/DiyEditor/components/mobile/UserOrder/index.vue b/src/components/DiyEditor/components/mobile/UserOrder/index.vue
new file mode 100644
index 0000000..450ae54
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserOrder/index.vue
@@ -0,0 +1,13 @@
+<template>
+ <el-image src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/orderCardStyle.png" />
+</template>
+<script setup lang="ts">
+import { UserOrderProperty } from './config'
+
+/** 鐢ㄦ埛璁㈠崟 */
+defineOptions({ name: 'UserOrder' })
+// 瀹氫箟灞炴��
+defineProps<{ property: UserOrderProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserOrder/property.vue b/src/components/DiyEditor/components/mobile/UserOrder/property.vue
new file mode 100644
index 0000000..d315db6
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserOrder/property.vue
@@ -0,0 +1,17 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserOrderProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 鐢ㄦ埛璁㈠崟灞炴�ч潰鏉�
+defineOptions({ name: 'UserOrderProperty' })
+
+const props = defineProps<{ modelValue: UserOrderProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserWallet/config.ts b/src/components/DiyEditor/components/mobile/UserWallet/config.ts
new file mode 100644
index 0000000..4e0955f
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserWallet/config.ts
@@ -0,0 +1,23 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 鐢ㄦ埛璧勪骇灞炴�� */
+export interface UserWalletProperty {
+ // 缁勪欢鏍峰紡
+ style: ComponentStyle
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'UserWallet',
+ name: '鐢ㄦ埛璧勪骇',
+ icon: 'ep:wallet-filled',
+ property: {
+ style: {
+ bgType: 'color',
+ bgColor: '',
+ marginLeft: 8,
+ marginRight: 8,
+ marginBottom: 8
+ } as ComponentStyle
+ }
+} as DiyComponent<UserWalletProperty>
diff --git a/src/components/DiyEditor/components/mobile/UserWallet/index.vue b/src/components/DiyEditor/components/mobile/UserWallet/index.vue
new file mode 100644
index 0000000..0efc937
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserWallet/index.vue
@@ -0,0 +1,15 @@
+<template>
+ <el-image
+ src="https://shopro.sheepjs.com/admin/static/images/shop/decorate/walletCardStyle.png"
+ />
+</template>
+<script setup lang="ts">
+import { UserWalletProperty } from './config'
+
+/** 鐢ㄦ埛璧勪骇 */
+defineOptions({ name: 'UserWallet' })
+// 瀹氫箟灞炴��
+defineProps<{ property: UserWalletProperty }>()
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/UserWallet/property.vue b/src/components/DiyEditor/components/mobile/UserWallet/property.vue
new file mode 100644
index 0000000..e0ac83e
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/UserWallet/property.vue
@@ -0,0 +1,17 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style" />
+</template>
+
+<script setup lang="ts">
+import { UserWalletProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 鐢ㄦ埛璧勪骇灞炴�ч潰鏉�
+defineOptions({ name: 'UserWalletProperty' })
+
+const props = defineProps<{ modelValue: UserWalletProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts b/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts
new file mode 100644
index 0000000..02f0374
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts
@@ -0,0 +1,37 @@
+import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
+
+/** 瑙嗛鎾斁灞炴�� */
+export interface VideoPlayerProperty {
+ // 瑙嗛閾炬帴
+ videoUrl: string
+ // 灏侀潰閾炬帴
+ posterUrl: string
+ // 鏄惁鑷姩鎾斁
+ autoplay: boolean
+ // 缁勪欢鏍峰紡
+ style: VideoPlayerStyle
+}
+
+// 瑙嗛鎾斁鏍峰紡
+export interface VideoPlayerStyle extends ComponentStyle {
+ // 瑙嗛楂樺害
+ height: number
+}
+
+// 瀹氫箟缁勪欢
+export const component = {
+ id: 'VideoPlayer',
+ name: '瑙嗛鎾斁',
+ icon: 'ep:video-play',
+ property: {
+ videoUrl: '',
+ posterUrl: '',
+ autoplay: false,
+ style: {
+ bgType: 'color',
+ bgColor: '#fff',
+ marginBottom: 8,
+ height: 300
+ } as VideoPlayerStyle
+ }
+} as DiyComponent<VideoPlayerProperty>
diff --git a/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue b/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue
new file mode 100644
index 0000000..fa9a914
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue
@@ -0,0 +1,30 @@
+<template>
+ <div class="w-full" :style="{ height: `${property.style.height}px` }">
+ <el-image class="w-full w-full" :src="property.posterUrl" v-if="property.posterUrl" />
+ <video
+ v-else
+ class="w-full w-full"
+ :src="property.videoUrl"
+ :poster="property.posterUrl"
+ :autoplay="property.autoplay"
+ controls
+ ></video>
+ </div>
+</template>
+<script setup lang="ts">
+import { VideoPlayerProperty } from './config'
+
+/** 瑙嗛鎾斁 */
+defineOptions({ name: 'VideoPlayer' })
+
+defineProps<{ property: VideoPlayerProperty }>()
+</script>
+
+<style scoped lang="scss">
+/* 鍥剧墖 */
+img {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+</style>
diff --git a/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue b/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue
new file mode 100644
index 0000000..1c3deec
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue
@@ -0,0 +1,55 @@
+<template>
+ <ComponentContainerProperty v-model="formData.style">
+ <template #style>
+ <el-form-item label="楂樺害" prop="height">
+ <el-slider
+ v-model="formData.style.height"
+ :max="500"
+ :min="100"
+ show-input
+ input-size="small"
+ :show-input-controls="false"
+ />
+ </el-form-item>
+ </template>
+ <el-form label-width="80px" :model="formData">
+ <el-form-item label="涓婁紶瑙嗛" prop="videoUrl">
+ <UploadFile
+ v-model="formData.videoUrl"
+ :file-type="['mp4']"
+ :limit="1"
+ :file-size="100"
+ class="min-w-80px"
+ />
+ </el-form-item>
+ <el-form-item label="涓婁紶灏侀潰" prop="posterUrl">
+ <UploadImg
+ v-model="formData.posterUrl"
+ draggable="false"
+ height="80px"
+ width="100%"
+ class="min-w-80px"
+ >
+ <template #tip> 寤鸿瀹藉害750 </template>
+ </UploadImg>
+ </el-form-item>
+ <el-form-item label="鑷姩鎾斁" prop="autoplay">
+ <el-switch v-model="formData.autoplay" />
+ </el-form-item>
+ </el-form>
+ </ComponentContainerProperty>
+</template>
+
+<script setup lang="ts">
+import { VideoPlayerProperty } from './config'
+import { useVModel } from '@vueuse/core'
+
+// 瑙嗛鎾斁灞炴�ч潰鏉�
+defineOptions({ name: 'VideoPlayerProperty' })
+
+const props = defineProps<{ modelValue: VideoPlayerProperty }>()
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/DiyEditor/components/mobile/index.ts b/src/components/DiyEditor/components/mobile/index.ts
new file mode 100644
index 0000000..c0dc67d
--- /dev/null
+++ b/src/components/DiyEditor/components/mobile/index.ts
@@ -0,0 +1,61 @@
+/*
+ * 缁勪欢娉ㄥ唽
+ *
+ * 缁勪欢瑙勮寖锛�
+ * 1. 姣忎釜瀛愮洰褰曞氨鏄竴涓嫭绔嬬殑缁勪欢锛屾瘡涓洰褰曞寘鎷互涓嬩笁涓枃浠讹細
+ * 2. config.ts锛氱粍浠堕厤缃紝蹇呴�夛紝鐢ㄤ簬瀹氫箟缁勪欢銆佺粍浠堕粯璁ょ殑灞炴�с�佸畾涔夊睘鎬х殑绫诲瀷
+ * 3. index.vue锛氱粍浠跺睍绀猴紝鐢ㄤ簬灞曠ず缁勪欢鐨勬覆鏌撴晥鏋溿�傚彲浠ヤ笉鎻愪緵锛屽 Page锛堥〉闈㈣缃級锛屽彧闇�瑕佸睘鎬ч厤缃〃鍗曞嵆鍙�
+ * 4. property.vue锛氱粍浠跺睘鎬ц〃鍗曪紝鐢ㄤ簬閰嶇疆缁勪欢锛屽繀閫夛紝
+ *
+ * 娉細
+ * 缁勪欢ID浠onfig.ts涓厤缃殑id涓哄噯锛屼笌缁勪欢鐩綍鐨勫悕绉版棤鍏筹紝浣嗚繕鏄缓璁粍浠剁洰褰曠殑鍚嶇О涓庣粍浠禝D淇濇寔涓�鑷�
+ */
+
+// 瀵煎叆缁勪欢鐣岄潰妯″潡
+const viewModules: Record<string, any> = import.meta.glob('./*/*.vue')
+// 瀵煎叆閰嶇疆妯″潡
+const configModules: Record<string, any> = import.meta.glob('./*/config.ts', { eager: true })
+
+// 鐣岄潰妯″潡
+const components = {}
+// 缁勪欢閰嶇疆妯″潡
+const componentConfigs = {}
+
+// 缁勪欢鐣岄潰鐨勭被鍨�
+type ViewType = 'index' | 'property'
+
+/**
+ * 娉ㄥ唽缁勪欢鐨勭晫闈㈡ā鍧�
+ *
+ * @param componentId 缁勪欢ID
+ * @param configPath 閰嶇疆妯″潡鐨勬枃浠惰矾寰�
+ * @param viewType 缁勪欢鐣岄潰鐨勭被鍨�
+ */
+const registerComponentViewModule = (
+ componentId: string,
+ configPath: string,
+ viewType: ViewType
+) => {
+ const viewPath = configPath.replace('config.ts', `${viewType}.vue`)
+ const viewModule = viewModules[viewPath]
+ if (viewModule) {
+ // 瀹氫箟寮傛缁勪欢
+ components[componentId] = defineAsyncComponent(viewModule)
+ }
+}
+
+// 娉ㄥ唽
+Object.keys(configModules).forEach((modulePath: string) => {
+ const component = configModules[modulePath].component
+ const componentId = component?.id
+ if (componentId) {
+ // 娉ㄥ唽缁勪欢
+ componentConfigs[componentId] = component
+ // 娉ㄥ唽棰勮鐣岄潰
+ registerComponentViewModule(componentId, modulePath, 'index')
+ // 娉ㄥ唽灞炴�ч厤缃〃鍗�
+ registerComponentViewModule(`${componentId}Property`, modulePath, 'property')
+ }
+})
+
+export { components, componentConfigs }
diff --git a/src/components/DiyEditor/index.vue b/src/components/DiyEditor/index.vue
new file mode 100644
index 0000000..fa23a4d
--- /dev/null
+++ b/src/components/DiyEditor/index.vue
@@ -0,0 +1,604 @@
+<template>
+ <el-container class="editor">
+ <!-- 椤堕儴锛氬伐鍏锋爮 -->
+ <el-header class="editor-header">
+ <!-- 宸︿晶鎿嶄綔鍖� -->
+ <slot name="toolBarLeft"></slot>
+ <!-- 涓績鎿嶄綔鍖� -->
+ <div class="header-center flex flex-1 items-center justify-center">
+ <span>{{ title }}</span>
+ </div>
+ <!-- 鍙充晶鎿嶄綔鍖� -->
+ <el-button-group class="header-right">
+ <el-tooltip content="閲嶇疆">
+ <el-button @click="handleReset">
+ <Icon :size="24" icon="system-uicons:reset-alt" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip v-if="previewUrl" content="棰勮">
+ <el-button @click="handlePreview">
+ <Icon :size="24" icon="ep:view" />
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="淇濆瓨">
+ <el-button @click="handleSave">
+ <Icon :size="24" icon="ep:check" />
+ </el-button>
+ </el-tooltip>
+ </el-button-group>
+ </el-header>
+
+ <!-- 涓績鍖哄煙 -->
+ <el-container class="editor-container">
+ <!-- 宸︿晶锛氱粍浠跺簱锛圕omponentLibrary锛� -->
+ <ComponentLibrary v-if="libs && libs.length > 0" ref="componentLibrary" :list="libs" />
+ <!-- 涓績锛氳璁″尯鍩燂紙ComponentContainer锛� -->
+ <div class="editor-center page-prop-area" @click="handlePageSelected">
+ <!-- 鎵嬫満椤堕儴 -->
+ <div class="editor-design-top">
+ <!-- 鎵嬫満椤堕儴鐘舵�佹爮 -->
+ <img alt="" class="status-bar" src="@/assets/imgs/diy/statusBar.png" />
+ <!-- 鎵嬫満椤堕儴瀵艰埅鏍� -->
+ <ComponentContainer
+ v-if="showNavigationBar"
+ :active="selectedComponent?.id === navigationBarComponent.id"
+ :component="navigationBarComponent"
+ :show-toolbar="false"
+ class="cursor-pointer!"
+ @click="handleNavigationBarSelected"
+ />
+ </div>
+ <!-- 缁濆瀹氫綅鐨勭粍浠讹細渚嬪 寮圭獥銆佹诞鍔ㄦ寜閽瓑 -->
+ <div
+ v-for="(component, index) in pageComponents"
+ :key="index"
+ @click="handleComponentSelected(component, index)"
+ >
+ <component
+ :is="component.id"
+ v-if="component.position === 'fixed' && selectedComponent?.uid === component.uid"
+ :property="component.property"
+ />
+ </div>
+ <!-- 鎵嬫満椤甸潰缂栬緫鍖哄煙 -->
+ <el-scrollbar
+ :view-style="{
+ backgroundColor: pageConfigComponent.property.backgroundColor,
+ backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
+ }"
+ height="100%"
+ view-class="phone-container"
+ wrap-class="editor-design-center page-prop-area"
+ >
+ <draggable
+ v-model="pageComponents"
+ :animation="200"
+ :force-fallback="false"
+ class="page-prop-area drag-area"
+ filter=".component-toolbar"
+ ghost-class="draggable-ghost"
+ group="component"
+ item-key="index"
+ @change="handleComponentChange"
+ >
+ <template #item="{ element, index }">
+ <ComponentContainer
+ v-if="!element.position || element.position === 'center'"
+ :active="selectedComponentIndex === index"
+ :can-move-down="index < pageComponents.length - 1"
+ :can-move-up="index > 0"
+ :component="element"
+ @click="handleComponentSelected(element, index)"
+ @copy="handleCopyComponent(index)"
+ @delete="handleDeleteComponent(index)"
+ @move="(direction) => handleMoveComponent(index, direction)"
+ />
+ </template>
+ </draggable>
+ </el-scrollbar>
+ <!-- 鎵嬫満搴曢儴瀵艰埅 -->
+ <div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
+ <ComponentContainer
+ :active="selectedComponent?.id === tabBarComponent.id"
+ :component="tabBarComponent"
+ :show-toolbar="false"
+ @click="handleTabBarSelected"
+ />
+ </div>
+ <!-- 鍥哄畾甯冨眬鐨勭粍浠� 鎿嶄綔鎸夐挳鍖� -->
+ <div class="fixed-component-action-group">
+ <el-tag
+ v-if="showPageConfig"
+ :effect="selectedComponent?.uid === pageConfigComponent.uid ? 'dark' : 'plain'"
+ :type="selectedComponent?.uid === pageConfigComponent.uid ? 'primary' : 'info'"
+ size="large"
+ @click="handleComponentSelected(pageConfigComponent)"
+ >
+ <Icon :icon="pageConfigComponent.icon" :size="12" />
+ <span>{{ pageConfigComponent.name }}</span>
+ </el-tag>
+ <template v-for="(component, index) in pageComponents" :key="index">
+ <el-tag
+ v-if="component.position === 'fixed'"
+ :effect="selectedComponent?.uid === component.uid ? 'dark' : 'plain'"
+ :type="selectedComponent?.uid === component.uid ? 'primary' : 'info'"
+ closable
+ size="large"
+ @click="handleComponentSelected(component)"
+ @close="handleDeleteComponent(index)"
+ >
+ <Icon :icon="component.icon" :size="12" />
+ <span>{{ component.name }}</span>
+ </el-tag>
+ </template>
+ </div>
+ </div>
+ <!-- 鍙充晶锛氬睘鎬ч潰鏉匡紙ComponentContainerProperty锛� -->
+ <el-aside v-if="selectedComponent?.property" class="editor-right" width="350px">
+ <el-card
+ body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
+ class="h-full"
+ shadow="never"
+ >
+ <!-- 缁勪欢鍚嶇О -->
+ <template #header>
+ <div class="flex items-center gap-8px">
+ <Icon :icon="selectedComponent?.icon" color="gray" />
+ <span>{{ selectedComponent?.name }}</span>
+ </div>
+ </template>
+ <el-scrollbar
+ class="m-[calc(0px-var(--el-card-padding))]"
+ view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
+ >
+ <component
+ :is="selectedComponent?.id + 'Property'"
+ :key="selectedComponent?.uid || selectedComponent?.id"
+ v-model="selectedComponent.property"
+ />
+ </el-scrollbar>
+ </el-card>
+ </el-aside>
+ </el-container>
+ </el-container>
+
+ <!-- 棰勮寮规 -->
+ <Dialog v-model="previewDialogVisible" title="棰勮" width="700">
+ <div class="flex justify-around">
+ <IFrame
+ :src="previewUrl"
+ class="w-375px border-4px border-rounded-8px border-solid p-2px h-667px!"
+ />
+ <div class="flex flex-col">
+ <el-text>鎵嬫満鎵爜棰勮</el-text>
+ <Qrcode :text="previewUrl" logo="/logo.gif" />
+ </div>
+ </div>
+ </Dialog>
+</template>
+<script lang="ts">
+// 娉ㄥ唽鎵�鏈夌殑缁勪欢
+import { components } from './components/mobile/index'
+
+export default {
+ components: { ...components }
+}
+</script>
+<script lang="ts" setup>
+import draggable from 'vuedraggable'
+import ComponentLibrary from './components/ComponentLibrary.vue'
+import { cloneDeep, includes } from 'lodash-es'
+import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
+import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
+import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
+import { isEmpty, isString } from '@/utils/is'
+import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
+import { componentConfigs } from '@/components/DiyEditor/components/mobile'
+import { array, oneOfType } from 'vue-types'
+import { propTypes } from '@/utils/propTypes'
+
+/** 椤甸潰瑁呬慨璇︽儏椤� */
+defineOptions({ name: 'DiyPageDetail' })
+
+// 宸︿晶缁勪欢搴�
+const componentLibrary = ref()
+// 椤甸潰璁剧疆缁勪欢
+const pageConfigComponent = ref<DiyComponent<any>>(cloneDeep(PAGE_CONFIG_COMPONENT))
+// 椤堕儴瀵艰埅鏍�
+const navigationBarComponent = ref<DiyComponent<any>>(cloneDeep(NAVIGATION_BAR_COMPONENT))
+// 搴曢儴瀵艰埅鑿滃崟
+const tabBarComponent = ref<DiyComponent<any>>(cloneDeep(TAB_BAR_COMPONENT))
+
+// 閫変腑鐨勭粍浠讹紝榛樿閫変腑椤堕儴瀵艰埅鏍�
+const selectedComponent = ref<DiyComponent<any>>()
+// 閫変腑鐨勭粍浠剁储寮�
+const selectedComponentIndex = ref<number>(-1)
+// 缁勪欢鍒楄〃
+const pageComponents = ref<DiyComponent<any>[]>([])
+// 瀹氫箟灞炴��
+const props = defineProps({
+ // 椤甸潰閰嶇疆锛屾敮鎸丣son瀛楃涓�
+ modelValue: oneOfType<string | PageConfig>([String, Object]).isRequired,
+ // 鏍囬
+ title: propTypes.string.def(''),
+ // 缁勪欢搴�
+ libs: array<DiyComponentLibrary>(),
+ // 鏄惁鏄剧ず椤堕儴瀵艰埅鏍�
+ showNavigationBar: propTypes.bool.def(true),
+ // 鏄惁鏄剧ず搴曢儴瀵艰埅鑿滃崟
+ showTabBar: propTypes.bool.def(false),
+ // 鏄惁鏄剧ず椤甸潰閰嶇疆
+ showPageConfig: propTypes.bool.def(true),
+ // 棰勮鍦板潃锛氭彁渚涗簡棰勮鍦板潃锛屾墠浼氭樉绀洪瑙堟寜閽�
+ previewUrl: propTypes.string.def('')
+})
+
+// 鐩戝惉浼犲叆鐨勯〉闈㈤厤缃�
+// 瑙f瀽鍑� pageConfigComponent 椤甸潰鏁翠綋鐨勯厤缃紝navigationBarComponent銆乸ageComponents銆乼abBarComponent 椤甸潰涓娿�佷腑銆佷笅鐨勯厤缃�
+watch(
+ () => props.modelValue,
+ () => {
+ const modelValue =
+ isString(props.modelValue) && !isEmpty(props.modelValue)
+ ? (JSON.parse(props.modelValue) as PageConfig)
+ : props.modelValue
+ pageConfigComponent.value.property =
+ (typeof modelValue !== 'string' && modelValue?.page) || PAGE_CONFIG_COMPONENT.property
+ navigationBarComponent.value.property =
+ (typeof modelValue !== 'string' && modelValue?.navigationBar) ||
+ NAVIGATION_BAR_COMPONENT.property
+ tabBarComponent.value.property =
+ (typeof modelValue !== 'string' && modelValue?.tabBar) || TAB_BAR_COMPONENT.property
+ // 鏌ユ壘瀵瑰簲鐨勯〉闈㈢粍浠�
+ pageComponents.value = ((typeof modelValue !== 'string' && modelValue?.components) || []).map(
+ (item) => {
+ const component = componentConfigs[item.id]
+ return { ...component, property: item.property }
+ }
+ )
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 閫夋嫨缁勪欢淇敼鍏跺睘鎬у悗鏇存柊瀹冪殑閰嶇疆 */
+watch(
+ selectedComponent,
+ (val: any) => {
+ if (!val || selectedComponentIndex.value === -1) {
+ return
+ }
+ // 濡傛灉鏄熀纭�璁剧疆椤碉紝榛樿閫変腑鐨勭储寮曟敼鎴� -1锛屼负浜嗛槻姝㈠垹闄ょ粍浠跺悗鍒囨崲鍒版椤靛鑷存姤閿�
+ // https://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/792
+ if (props.showTabBar) {
+ selectedComponentIndex.value = -1
+ }
+ pageComponents.value[selectedComponentIndex.value] = selectedComponent.value!
+ },
+ { deep: true }
+)
+
+// 淇濆瓨
+const handleSave = () => {
+ // 鍙戦�佷繚瀛橀�氱煡
+ emits('save')
+}
+// 鐩戝惉閰嶇疆淇敼
+const pageConfigChange = () => {
+ const pageConfig = {
+ page: pageConfigComponent.value.property,
+ navigationBar: navigationBarComponent.value.property,
+ tabBar: tabBarComponent.value.property,
+ components: pageComponents.value.map((component) => {
+ // 鍙繚鐣橝PP鏈夌敤鐨勫瓧娈�
+ return { id: component.id, property: component.property }
+ })
+ } as PageConfig
+ if (!props.showTabBar) {
+ delete pageConfig.tabBar
+ }
+ // 鍙戦�佹暟鎹洿鏂伴�氱煡
+ const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
+ emits('update:modelValue', modelValue)
+}
+watch(
+ () => [
+ pageConfigComponent.value.property,
+ navigationBarComponent.value.property,
+ tabBarComponent.value.property,
+ pageComponents.value
+ ],
+ () => {
+ pageConfigChange()
+ },
+ { deep: true }
+)
+// 澶勭悊椤甸潰閫変腑锛氭樉绀哄睘鎬ц〃鍗�
+const handlePageSelected = (event: any) => {
+ if (!props.showPageConfig) return
+
+ // 閰嶇疆浜嗘牱寮� page-prop-area 鐨勫厓绱狅紝鎵嶆樉绀洪〉闈㈣缃�
+ if (includes(event?.target?.classList, 'page-prop-area')) {
+ handleComponentSelected(unref(pageConfigComponent))
+ }
+}
+
+/**
+ * 閫変腑缁勪欢
+ *
+ * @param component 缁勪欢
+ * @param index 缁勪欢鐨勭储寮�
+ */
+const handleComponentSelected = (component: DiyComponent<any>, index: number = -1) => {
+ selectedComponent.value = component
+ selectedComponentIndex.value = index
+}
+
+// 閫変腑椤堕儴瀵艰埅鏍�
+const handleNavigationBarSelected = () => {
+ handleComponentSelected(unref(navigationBarComponent))
+}
+
+// 閫変腑搴曢儴瀵艰埅鑿滃崟
+const handleTabBarSelected = () => {
+ handleComponentSelected(unref(tabBarComponent))
+}
+
+// 缁勪欢鍙樺姩锛堟嫋鎷斤級
+const handleComponentChange = (dragEvent: any) => {
+ // 鏂板锛屽嵆浠庣粍浠跺簱鎷栨嫿娣诲姞缁勪欢
+ if (dragEvent.added) {
+ const { element, newIndex } = dragEvent.added
+ handleComponentSelected(element, newIndex)
+ } else if (dragEvent.moved) {
+ // 鎷栨嫿鎺掑簭
+ const { newIndex } = dragEvent.moved
+ // 淇濇寔閫変腑
+ selectedComponentIndex.value = newIndex
+ }
+}
+
+// 浜ゆ崲缁勪欢
+const swapComponent = (oldIndex: number, newIndex: number) => {
+ ;[pageComponents.value[oldIndex], pageComponents.value[newIndex]] = [
+ pageComponents.value[newIndex],
+ pageComponents.value[oldIndex]
+ ]
+ // 淇濇寔閫変腑
+ selectedComponentIndex.value = newIndex
+}
+
+/** 绉诲姩缁勪欢锛堜笂绉汇�佷笅绉伙級 */
+const handleMoveComponent = (index: number, direction: number) => {
+ const newIndex = index + direction
+ if (newIndex < 0 || newIndex >= pageComponents.value.length) return
+
+ swapComponent(index, newIndex)
+}
+
+/** 澶嶅埗缁勪欢 */
+const handleCopyComponent = (index: number) => {
+ const component = cloneDeep(pageComponents.value[index])
+ component.uid = new Date().getTime()
+ pageComponents.value.splice(index + 1, 0, component)
+}
+
+/**
+ * 鍒犻櫎缁勪欢
+ * @param index 褰撳墠缁勪欢index
+ */
+const handleDeleteComponent = (index: number) => {
+ // 鍒犻櫎缁勪欢
+ pageComponents.value.splice(index, 1)
+ if (index < pageComponents.value.length) {
+ // 1. 涓嶆槸鏈�鍚庝竴涓粍浠舵椂锛屽垹闄ゅ悗閫変腑涓嬮潰鐨勭粍浠�
+ let bottomIndex = index
+ handleComponentSelected(pageComponents.value[bottomIndex], bottomIndex)
+ } else if (pageComponents.value.length > 0) {
+ // 2. 涓嶆槸绗竴涓粍浠舵椂锛屽垹闄ゅ悗閫変腑涓婇潰鐨勭粍浠�
+ let topIndex = index - 1
+ handleComponentSelected(pageComponents.value[topIndex], topIndex)
+ } else {
+ // 3. 缁勪欢鍏ㄩ儴鍒犻櫎涔嬪悗锛屾樉绀洪〉闈㈣缃�
+ handleComponentSelected(unref(pageConfigComponent))
+ }
+}
+
+// 宸ュ叿鏍忔搷浣�
+const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue'])
+
+// 娉ㄥ叆鏃犳劅鍒锋柊椤甸潰鍑芥暟
+const reload = inject<() => void>('reload')
+// 閲嶇疆
+const handleReset = () => {
+ if (reload) reload()
+ emits('reset')
+}
+
+// 棰勮
+const previewDialogVisible = ref(false)
+const handlePreview = () => {
+ previewDialogVisible.value = true
+ emits('preview')
+}
+
+// 璁剧疆榛樿閫変腑鐨勭粍浠�
+const setDefaultSelectedComponent = () => {
+ if (props.showPageConfig) {
+ selectedComponent.value = unref(pageConfigComponent)
+ } else if (props.showNavigationBar) {
+ selectedComponent.value = unref(navigationBarComponent)
+ } else if (props.showTabBar) {
+ selectedComponent.value = unref(tabBarComponent)
+ }
+}
+
+watch(
+ () => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
+ () => setDefaultSelectedComponent()
+)
+
+onMounted(() => setDefaultSelectedComponent())
+</script>
+<style lang="scss" scoped>
+/* 鎵嬫満瀹藉害 */
+$phone-width: 375px;
+$toolbar-height: 42px;
+
+/* 鏍硅妭鐐规牱寮� */
+.editor {
+ display: flex;
+ height: 100%;
+ margin: calc(0px - var(--app-content-padding));
+ flex-direction: column;
+
+ /* 椤堕儴锛氬伐鍏锋爮 */
+ .editor-header {
+ display: flex;
+ height: $toolbar-height;
+ padding: 0;
+ background-color: var(--el-bg-color);
+ border-bottom: solid 1px var(--el-border-color);
+ align-items: center;
+ justify-content: space-between;
+
+ /* 宸ュ叿鏍忥細鍙充晶鎸夐挳 */
+ .header-right {
+ height: 100%;
+
+ .el-button {
+ height: 100%;
+ }
+ }
+
+ /* 闅愯棌宸ュ叿鏍忔寜閽殑杈规 */
+ :deep(.el-radio-button__inner),
+ :deep(.el-button) {
+ border-top: none !important;
+ border-bottom: none !important;
+ border-radius: 0 !important;
+ }
+ }
+
+ /* 涓績鎿嶄綔鍖� */
+ .editor-container {
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) -
+ $toolbar-height
+ );
+
+ /* 鍙充晶灞炴�ч潰鏉� */
+ .editor-right {
+ overflow: hidden;
+ box-shadow: -8px 0 8px -8px rgb(0 0 0 / 12%);
+ flex-shrink: 0;
+
+ /* 灞炴�ч潰鏉块《閮細鍑忓皯鍐呰竟璺� */
+ :deep(.el-card__header) {
+ padding: 8px 16px;
+ }
+
+ /* 灞炴�ч潰鏉垮垎缁� */
+ :deep(.property-group) {
+ margin: 0 -20px;
+
+ &.el-card {
+ border: none;
+ }
+
+ /* 灞炴�у垎缁勫悕绉� */
+ .el-card__header {
+ padding: 8px 32px;
+ background: var(--el-bg-color-page);
+ border: none;
+ }
+
+ .el-card__body {
+ border: none;
+ }
+ }
+ }
+
+ /* 涓績鍖哄煙 */
+ .editor-center {
+ position: relative;
+ display: flex;
+ width: 100%;
+ margin: 16px 0 0;
+ overflow: hidden;
+ background-color: var(--app-content-bg-color);
+ flex: 1 1 0;
+ flex-direction: column;
+ justify-content: center;
+
+ /* 鎵嬫満椤堕儴 */
+ .editor-design-top {
+ display: flex;
+ width: $phone-width;
+ margin: 0 auto;
+ flex-direction: column;
+
+ /* 鎵嬫満椤堕儴鐘舵�佹爮 */
+ .status-bar {
+ width: $phone-width;
+ height: 20px;
+ background-color: #fff;
+ }
+ }
+
+ /* 鎵嬫満搴曢儴瀵艰埅 */
+ .editor-design-bottom {
+ width: $phone-width;
+ margin: 0 auto;
+ }
+
+ /* 鎵嬫満椤甸潰缂栬緫鍖哄煙 */
+ :deep(.editor-design-center) {
+ width: 100%;
+
+ /* 涓讳綋鍐呭 */
+ .phone-container {
+ position: relative;
+ width: $phone-width;
+ height: 100%;
+ margin: 0 auto;
+ background-repeat: no-repeat;
+ background-size: 100% 100%;
+
+ .drag-area {
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+
+ /* 鍥哄畾甯冨眬鐨勭粍浠� 鎿嶄綔鎸夐挳鍖� */
+ .fixed-component-action-group {
+ position: absolute;
+ top: 0;
+ right: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ :deep(.el-tag) {
+ box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
+ border: none;
+
+ .el-tag__content {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+
+ .el-icon {
+ margin-right: 4px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/DiyEditor/util.ts b/src/components/DiyEditor/util.ts
new file mode 100644
index 0000000..b6febb9
--- /dev/null
+++ b/src/components/DiyEditor/util.ts
@@ -0,0 +1,125 @@
+import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config'
+import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
+import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
+
+// 椤甸潰瑁呬慨缁勪欢
+export interface DiyComponent<T> {
+ // 鐢ㄤ簬鍖哄垎鍚屼竴绉嶇粍浠剁殑涓嶅悓瀹炰緥
+ uid?: number
+ // 缁勪欢鍞竴鏍囪瘑
+ id: string
+ // 缁勪欢鍚嶇О
+ name: string
+ // 缁勪欢鍥炬爣
+ icon: string
+ /*
+ 缁勪欢浣嶇疆锛�
+ top: 鍥哄畾浜庢墜鏈洪《閮紝渚嬪 椤堕儴鐨勫鑸爮
+ bottom: 鍥哄畾浜庢墜鏈哄簳閮紝渚嬪 搴曢儴鐨勮彍鍗曞鑸爮
+ center: 浣嶄簬鎵嬫満涓績锛屾瘡涓粍浠跺崰涓�琛岋紝椤哄簭鍚戜笅鎺掑垪
+ 绌猴細鍚宑enter
+ fixed: 鐢辩粍浠惰嚜宸卞喅瀹氫綅缃紝濡傚脊绐椾綅浜庢墜鏈轰腑蹇冦�佹诞鍔ㄦ寜閽竴鑸綅浜庢墜鏈哄彸涓嬭
+ */
+ position?: 'top' | 'bottom' | 'center' | '' | 'fixed'
+ // 缁勪欢灞炴��
+ property: T
+}
+
+// 椤甸潰瑁呬慨缁勪欢搴�
+export interface DiyComponentLibrary {
+ // 缁勪欢搴撳悕绉�
+ name: string
+ // 鏄惁灞曞紑
+ extended: boolean
+ // 缁勪欢鍒楄〃
+ components: string[]
+}
+
+// 缁勪欢鏍峰紡
+export interface ComponentStyle {
+ // 鑳屾櫙绫诲瀷
+ bgType: 'color' | 'img'
+ // 鑳屾櫙棰滆壊
+ bgColor: string
+ // 鑳屾櫙鍥剧墖
+ bgImg: string
+ // 澶栬竟璺�
+ margin: number
+ marginTop: number
+ marginRight: number
+ marginBottom: number
+ marginLeft: number
+ // 鍐呰竟璺�
+ padding: number
+ paddingTop: number
+ paddingRight: number
+ paddingBottom: number
+ paddingLeft: number
+ // 杈规鍦嗚
+ borderRadius: number
+ borderTopLeftRadius: number
+ borderTopRightRadius: number
+ borderBottomRightRadius: number
+ borderBottomLeftRadius: number
+}
+
+// 椤甸潰閰嶇疆
+export interface PageConfig {
+ // 椤甸潰灞炴��
+ page: PageConfigProperty
+ // 椤堕儴瀵艰埅鏍忓睘鎬�
+ navigationBar: NavigationBarProperty
+ // 搴曢儴瀵艰埅鑿滃崟灞炴��
+ tabBar?: TabBarProperty
+ // 椤甸潰缁勪欢鍒楄〃
+ components: PageComponent[]
+}
+// 椤甸潰缁勪欢锛屽彧淇濈暀缁勪欢ID锛岀粍浠跺睘鎬�
+export interface PageComponent extends Pick<DiyComponent<any>, 'id' | 'property'> {}
+
+// 椤甸潰缁勪欢搴�
+export const PAGE_LIBS = [
+ {
+ name: '鍩虹缁勪欢',
+ extended: true,
+ components: [
+ 'SearchBar',
+ 'NoticeBar',
+ 'MenuSwiper',
+ 'MenuGrid',
+ 'MenuList',
+ 'Popover',
+ 'FloatingActionButton'
+ ]
+ },
+ {
+ name: '鍥炬枃缁勪欢',
+ extended: true,
+ components: [
+ 'ImageBar',
+ 'Carousel',
+ 'TitleBar',
+ 'VideoPlayer',
+ 'Divider',
+ 'MagicCube',
+ 'HotZone'
+ ]
+ },
+ { name: '鍟嗗搧缁勪欢', extended: true, components: ['ProductCard', 'ProductList'] },
+ {
+ name: '鐢ㄦ埛缁勪欢',
+ extended: true,
+ components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon']
+ },
+ {
+ name: '钀ラ攢缁勪欢',
+ extended: true,
+ components: [
+ 'PromotionCombination',
+ 'PromotionSeckill',
+ 'PromotionPoint',
+ 'CouponCard',
+ 'PromotionArticle'
+ ]
+ }
+] as DiyComponentLibrary[]
diff --git a/src/components/DocAlert/index.vue b/src/components/DocAlert/index.vue
new file mode 100644
index 0000000..0073266
--- /dev/null
+++ b/src/components/DocAlert/index.vue
@@ -0,0 +1,34 @@
+<template>
+ <el-alert v-if="getEnable()" type="success" show-icon>
+ <template #title>
+ <div @click="goToUrl">{{ '銆�' + title + '銆戞枃妗e湴鍧�锛�' + url }}</div>
+ </template>
+ </el-alert>
+</template>
+<script setup lang="tsx">
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'DocAlert' })
+
+const props = defineProps({
+ title: propTypes.string,
+ url: propTypes.string
+})
+
+/** 璺宠浆 URL 閾炬帴 */
+const goToUrl = () => {
+ window.open(props.url)
+}
+
+/** 鏄惁寮�鍚� */
+const getEnable = () => {
+ return import.meta.env.VITE_APP_DOCALERT_ENABLE !== 'false'
+}
+</script>
+<style scoped>
+.el-alert--success.is-light {
+ margin-bottom: 10px;
+ cursor: pointer;
+ border: 1px solid green;
+}
+</style>
diff --git a/src/components/Draggable/index.vue b/src/components/Draggable/index.vue
new file mode 100644
index 0000000..ae7d37b
--- /dev/null
+++ b/src/components/Draggable/index.vue
@@ -0,0 +1,86 @@
+<template>
+ <el-text type="info" size="small"> 鎷栧姩宸︿笂瑙掔殑灏忓渾鐐瑰彲瀵瑰叾鎺掑簭 </el-text>
+ <VueDraggable
+ :list="formData"
+ :force-fallback="false"
+ :animation="200"
+ handle=".drag-icon"
+ class="m-t-8px"
+ item-key="index"
+ >
+ <template #item="{ element, index }">
+ <div
+ class="mb-4px flex flex-col gap-4px border border-gray-2 border-rounded rounded border-solid p-8px"
+ >
+ <!-- 鎿嶄綔鎸夐挳鍖� -->
+ <div
+ class="m--8px m-b-4px flex flex-row items-center justify-between p-8px"
+ style="background-color: var(--app-content-bg-color)"
+ >
+ <el-tooltip content="鎷栧姩鎺掑簭">
+ <Icon
+ icon="ic:round-drag-indicator"
+ class="drag-icon cursor-move"
+ style="color: #8a909c"
+ />
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎">
+ <Icon
+ icon="ep:delete"
+ class="cursor-pointer text-red-5"
+ v-if="formData.length > min"
+ @click="handleDelete(index)"
+ />
+ </el-tooltip>
+ </div>
+ <!-- 鍐呭鍖� -->
+ <slot :element="element" :index="index"></slot>
+ </div>
+ </template>
+ </VueDraggable>
+ <el-tooltip :disabled="limit < 1" :content="`鏈�澶氭坊鍔�${limit}涓猔">
+ <el-button
+ type="primary"
+ plain
+ class="m-t-4px w-full"
+ :disabled="limit > 0 && formData.length >= limit"
+ @click="handleAdd"
+ >
+ <Icon icon="ep:plus" /><span>娣诲姞</span>
+ </el-button>
+ </el-tooltip>
+</template>
+
+<script setup lang="ts">
+// 鎷栨嫿缁勪欢
+import VueDraggable from 'vuedraggable'
+import { useVModel } from '@vueuse/core'
+import { any, array } from 'vue-types'
+import { propTypes } from '@/utils/propTypes'
+import { cloneDeep } from 'lodash-es'
+
+// 鎷栨嫿缁勪欢灏佽
+defineOptions({ name: 'Draggable' })
+
+// 瀹氫箟灞炴��
+const props = defineProps({
+ // 缁戝畾鍊�
+ modelValue: array<any>().isRequired,
+ // 绌虹殑鍏冪礌锛氱偣鍑绘坊鍔犳寜閽椂锛屽垱寤哄厓绱犲苟娣诲姞鍒板垪琛紱榛樿涓虹┖瀵硅薄
+ emptyItem: any<unknown>().def({}),
+ // 鏁伴噺闄愬埗锛氶粯璁や负0锛岃〃绀轰笉闄愬埗
+ limit: propTypes.number.def(0),
+ // 鏈�灏忔暟閲忥細榛樿涓�1
+ min: propTypes.number.def(1)
+})
+// 瀹氫箟浜嬩欢
+const emit = defineEmits(['update:modelValue'])
+const formData = useVModel(props, 'modelValue', emit)
+
+// 澶勭悊娣诲姞
+const handleAdd = () => formData.value.push(cloneDeep(props.emptyItem || {}))
+// 澶勭悊鍒犻櫎
+const handleDelete = (index: number) => formData.value.splice(index, 1)
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/components/Echart/index.ts b/src/components/Echart/index.ts
new file mode 100644
index 0000000..4822092
--- /dev/null
+++ b/src/components/Echart/index.ts
@@ -0,0 +1,3 @@
+import Echart from './src/Echart.vue'
+
+export { Echart }
diff --git a/src/components/Echart/src/Echart.vue b/src/components/Echart/src/Echart.vue
new file mode 100644
index 0000000..487f20a
--- /dev/null
+++ b/src/components/Echart/src/Echart.vue
@@ -0,0 +1,120 @@
+<script lang="ts" setup>
+import type { EChartsOption } from 'echarts'
+import echarts from '@/plugins/echarts'
+import { debounce } from 'lodash-es'
+import 'echarts-wordcloud'
+import { propTypes } from '@/utils/propTypes'
+import { PropType } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { isString } from '@/utils/is'
+import { useDesign } from '@/hooks/web/useDesign'
+
+import 'echarts/lib/component/markPoint'
+import 'echarts/lib/component/markLine'
+import 'echarts/lib/component/markArea'
+
+defineOptions({ name: 'EChart' })
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('echart')
+
+const appStore = useAppStore()
+
+const props = defineProps({
+ options: {
+ type: Object as PropType<EChartsOption>,
+ required: true
+ },
+ width: propTypes.oneOfType([Number, String]).def(''),
+ height: propTypes.oneOfType([Number, String]).def('500px')
+})
+
+const isDark = computed(() => appStore.getIsDark)
+
+const theme = computed(() => {
+ const echartTheme: boolean | string = unref(isDark) ? true : 'auto'
+
+ return echartTheme
+})
+
+const options = computed(() => {
+ return Object.assign(props.options, {
+ darkMode: unref(theme)
+ })
+})
+
+const elRef = ref<ElRef>()
+
+let echartRef: Nullable<echarts.ECharts> = null
+
+const contentEl = ref<Element>()
+
+const styles = computed(() => {
+ const width = isString(props.width) ? props.width : `${props.width}px`
+ const height = isString(props.height) ? props.height : `${props.height}px`
+
+ return {
+ width,
+ height
+ }
+})
+
+const initChart = () => {
+ if (unref(elRef) && props.options) {
+ echartRef = echarts.init(unref(elRef) as HTMLElement)
+ echartRef?.setOption(unref(options))
+ }
+}
+
+watch(
+ () => options.value,
+ (options) => {
+ if (echartRef) {
+ echartRef?.setOption(options)
+ echartRef?.resize()
+ }
+ },
+ {
+ deep: true
+ }
+)
+
+const resizeHandler = debounce(() => {
+ if (echartRef) {
+ echartRef.resize()
+ }
+}, 100)
+
+const contentResizeHandler = async (e: TransitionEvent) => {
+ if (e.propertyName === 'width') {
+ resizeHandler()
+ }
+}
+
+onMounted(() => {
+ initChart()
+
+ window.addEventListener('resize', resizeHandler)
+
+ contentEl.value = document.getElementsByClassName(`${variables.namespace}-layout-content`)[0]
+ unref(contentEl) &&
+ (unref(contentEl) as Element).addEventListener('transitionend', contentResizeHandler)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', resizeHandler)
+ unref(contentEl) &&
+ (unref(contentEl) as Element).removeEventListener('transitionend', contentResizeHandler)
+})
+
+onActivated(() => {
+ if (echartRef) {
+ echartRef.resize()
+ }
+})
+</script>
+
+<template>
+ <div ref="elRef" :class="[$attrs.class, prefixCls]" :style="styles"></div>
+</template>
diff --git a/src/components/Editor/index.ts b/src/components/Editor/index.ts
new file mode 100644
index 0000000..0f4e056
--- /dev/null
+++ b/src/components/Editor/index.ts
@@ -0,0 +1,8 @@
+import Editor from './src/Editor.vue'
+import { IDomEditor } from '@wangeditor-next/editor'
+
+export interface EditorExpose {
+ getEditorRef: () => Promise<IDomEditor>
+}
+
+export { Editor }
diff --git a/src/components/Editor/src/Editor.vue b/src/components/Editor/src/Editor.vue
new file mode 100644
index 0000000..30146be
--- /dev/null
+++ b/src/components/Editor/src/Editor.vue
@@ -0,0 +1,294 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { Editor, Toolbar } from '@wangeditor-next/editor-for-vue'
+import { i18nChangeLanguage, IDomEditor, IEditorConfig } from '@wangeditor-next/editor'
+import { propTypes } from '@/utils/propTypes'
+import { isNumber } from '@/utils/is'
+import { ElMessage } from 'element-plus'
+import { useLocaleStore } from '@/store/modules/locale'
+import { getRefreshToken, getTenantId } from '@/utils/auth'
+import { getUploadUrl } from '@/components/UploadFile/src/useUpload'
+import merge from 'lodash-es/merge'
+
+defineOptions({ name: 'Editor' })
+
+type InsertFnType = (url: string, alt: string, href: string) => void
+
+const localeStore = useLocaleStore()
+
+const currentLocale = computed(() => localeStore.getCurrentLocale)
+
+i18nChangeLanguage(unref(currentLocale).lang)
+
+const props = defineProps({
+ editorId: propTypes.string.def('wangEditor-1'),
+ height: propTypes.oneOfType([Number, String]).def('500px'),
+ editorConfig: {
+ type: Object as PropType<Partial<IEditorConfig>>,
+ default: () => undefined
+ },
+ readonly: propTypes.bool.def(false),
+ modelValue: propTypes.string.def(''),
+ directory: propTypes.string.def('editor-default')
+})
+
+const emit = defineEmits(['change', 'update:modelValue'])
+
+// 缂栬緫鍣ㄥ疄渚嬶紝蹇呴』鐢� shallowRef
+const editorRef = shallowRef<IDomEditor>()
+
+const valueHtml = ref('')
+
+watch(
+ () => props.modelValue,
+ (val: string) => {
+ if (!val) {
+ val = ''
+ }
+ if (val === unref(valueHtml)) return
+ valueHtml.value = val
+ },
+ {
+ immediate: true
+ }
+)
+
+// 鐩戝惉
+watch(
+ () => valueHtml.value,
+ (val: string) => {
+ emit('update:modelValue', val)
+ }
+)
+watch(
+ () => props.readonly,
+ async (val) => {
+ // 鐗规畩锛氱瓑寰� editorRef 娓叉煋瀹屾垚
+ if (!editorRef.value) {
+ await nextTick()
+ }
+ if (val) {
+ editorRef.value?.disable()
+ } else {
+ editorRef.value?.enable()
+ }
+ }
+)
+
+const handleCreated = (editor: IDomEditor) => {
+ editorRef.value = editor
+}
+
+// 缂栬緫鍣ㄩ厤缃�
+const editorConfig = computed((): IEditorConfig => {
+ return merge(
+ {
+ placeholder: '璇疯緭鍏ュ唴瀹�...',
+ readOnly: props.readonly,
+ customAlert: (s: string, t: string) => {
+ switch (t) {
+ case 'success':
+ ElMessage.success(s)
+ break
+ case 'info':
+ ElMessage.info(s)
+ break
+ case 'warning':
+ ElMessage.warning(s)
+ break
+ case 'error':
+ ElMessage.error(s)
+ break
+ default:
+ ElMessage.info(s)
+ break
+ }
+ },
+ autoFocus: false,
+ scroll: true,
+ EXTEND_CONF: {
+ mentionConfig: {
+ showModal: () => {},
+ hideModal: () => {}
+ }
+ },
+ MENU_CONF: {
+ ['uploadImage']: {
+ server: getUploadUrl(),
+ // 鍗曚釜鏂囦欢鐨勬渶澶т綋绉檺鍒讹紝榛樿涓� 2M
+ maxFileSize: 10 * 1024 * 1024,
+ // 鏈�澶氬彲涓婁紶鍑犱釜鏂囦欢锛岄粯璁や负 100
+ maxNumberOfFiles: 100,
+ // 閫夋嫨鏂囦欢鏃剁殑绫诲瀷闄愬埗锛岄粯璁や负 ['image/*'] 銆傚涓嶆兂闄愬埗锛屽垯璁剧疆涓� []
+ allowedFileTypes: ['image/*'],
+
+ // 鑷畾涔夊鍔� http header
+ headers: {
+ Accept: '*',
+ Authorization: 'Bearer ' + getRefreshToken(), // 浣跨敤 getRefreshToken() 鏂规硶锛岃�屼笉浣跨敤 getAccessToken() 鏂规硶鐨勫師鍥狅細Editor 鏃犳硶鏂逛究鐨勫埛鏂拌闂护鐗�
+ 'tenant-id': getTenantId()
+ },
+
+ // 瓒呮椂鏃堕棿锛岄粯璁や负 10 绉�
+ timeout: 15 * 1000, // 15 绉�
+
+ // form-data fieldName锛屽悗绔帴鍙e弬鏁板悕绉帮紝榛樿鍊紈angeditor-uploaded-image
+ fieldName: 'file',
+ // 闄勫姞鍙傛暟
+ meta: {
+ directory: `${props.directory}-image`
+ },
+ metaWithUrl: false,
+
+ // uppy 閰嶇疆椤�
+ uppyConfig: {
+ onBeforeFileAdded: (newFile: any) => {
+ newFile.id = `${newFile.id}-${Date.now()}`
+ return newFile
+ }
+ },
+
+ // 涓婁紶涔嬪墠瑙﹀彂
+ onBeforeUpload(file: File) {
+ // console.log(file)
+ return file
+ },
+ // 涓婁紶杩涘害鐨勫洖璋冨嚱鏁�
+ onProgress(progress: number) {
+ // progress 鏄� 0-100 鐨勬暟瀛�
+ console.log('progress', progress)
+ },
+ onSuccess(file: File, res: any) {
+ console.log('onSuccess', file, res)
+ },
+ onFailed(file: File, res: any) {
+ alert(res.message)
+ console.log('onFailed', file, res)
+ },
+ onError(file: File, err: any, res: any) {
+ alert(err.message)
+ console.error('onError', file, err, res)
+ },
+ // 鑷畾涔夋彃鍏ュ浘鐗�
+ customInsert(res: any, insertFn: InsertFnType) {
+ insertFn(res.data, 'image', res.data)
+ }
+ },
+ ['uploadVideo']: {
+ server: getUploadUrl(),
+ // 鍗曚釜鏂囦欢鐨勬渶澶т綋绉檺鍒讹紝榛樿涓� 10M
+ maxFileSize: 1024 * 1024 * 1024,
+ // 鏈�澶氬彲涓婁紶鍑犱釜鏂囦欢锛岄粯璁や负 100
+ maxNumberOfFiles: 10,
+ // 閫夋嫨鏂囦欢鏃剁殑绫诲瀷闄愬埗锛岄粯璁や负 ['video/*'] 銆傚涓嶆兂闄愬埗锛屽垯璁剧疆涓� []
+ allowedFileTypes: ['video/*'],
+
+ // 鑷畾涔夊鍔� http header
+ headers: {
+ Accept: '*',
+ Authorization: 'Bearer ' + getRefreshToken(), // 浣跨敤 getRefreshToken() 鏂规硶锛岃�屼笉浣跨敤 getAccessToken() 鏂规硶鐨勫師鍥狅細Editor 鏃犳硶鏂逛究鐨勫埛鏂拌闂护鐗�
+ 'tenant-id': getTenantId()
+ },
+
+ // 瓒呮椂鏃堕棿锛岄粯璁や负 30 绉�
+ timeout: 15 * 1000, // 15 绉�
+
+ // form-data fieldName锛屽悗绔帴鍙e弬鏁板悕绉帮紝榛樿鍊紈angeditor-uploaded-image
+ fieldName: 'file',
+ // 闄勫姞鍙傛暟
+ meta: {
+ directory: `${props.directory}-video`
+ },
+ metaWithUrl: false,
+
+ // uppy 閰嶇疆椤�
+ uppyConfig: {
+ onBeforeFileAdded: (newFile: any) => {
+ newFile.id = `${newFile.id}-${Date.now()}`
+ return newFile
+ }
+ },
+
+ // 涓婁紶涔嬪墠瑙﹀彂
+ onBeforeUpload(file: File) {
+ // console.log(file)
+ return file
+ },
+ // 涓婁紶杩涘害鐨勫洖璋冨嚱鏁�
+ onProgress(progress: number) {
+ // progress 鏄� 0-100 鐨勬暟瀛�
+ console.log('progress', progress)
+ },
+ onSuccess(file: File, res: any) {
+ console.log('onSuccess', file, res)
+ },
+ onFailed(file: File, res: any) {
+ alert(res.message)
+ console.log('onFailed', file, res)
+ },
+ onError(file: File, err: any, res: any) {
+ alert(err.message)
+ console.error('onError', file, err, res)
+ },
+ // 鑷畾涔夋彃鍏ュ浘鐗�
+ customInsert(res: any, insertFn: InsertFnType) {
+ insertFn(res.data, 'mp4', res.data)
+ }
+ }
+ },
+ uploadImgShowBase64: true
+ },
+ props.editorConfig || {}
+ )
+})
+
+const editorStyle = computed(() => {
+ return {
+ height: isNumber(props.height) ? `${props.height}px` : props.height
+ }
+})
+
+// 鍥炶皟鍑芥暟
+const handleChange = (editor: IDomEditor) => {
+ emit('change', editor)
+}
+
+// 缁勪欢閿�姣佹椂锛屽強鏃堕攢姣佺紪杈戝櫒
+onBeforeUnmount(() => {
+ const editor = unref(editorRef.value)
+
+ // 閿�姣侊紝骞剁Щ闄� editor
+ editor?.destroy()
+})
+
+const getEditorRef = async (): Promise<IDomEditor> => {
+ await nextTick()
+ return unref(editorRef.value) as IDomEditor
+}
+
+defineExpose({
+ getEditorRef
+})
+</script>
+
+<template>
+ <div class="border-1 border-solid border-[var(--tags-view-border-color)] z-10">
+ <!-- 宸ュ叿鏍� -->
+ <Toolbar
+ :editor="editorRef"
+ :editorId="editorId"
+ class="border-0 b-b-1 border-solid border-[var(--tags-view-border-color)]"
+ />
+ <!-- 缂栬緫鍣� -->
+ <Editor
+ v-model="valueHtml"
+ :defaultConfig="editorConfig"
+ :editorId="editorId"
+ :style="editorStyle"
+ @on-change="handleChange"
+ @on-created="handleCreated"
+ />
+ </div>
+</template>
+
+<style src="@wangeditor-next/editor/dist/css/style.css"></style>
diff --git a/src/components/Error/index.ts b/src/components/Error/index.ts
new file mode 100644
index 0000000..a52c6f9
--- /dev/null
+++ b/src/components/Error/index.ts
@@ -0,0 +1,3 @@
+import Error from './src/Error.vue'
+
+export { Error }
diff --git a/src/components/Error/src/Error.vue b/src/components/Error/src/Error.vue
new file mode 100644
index 0000000..3fd7a17
--- /dev/null
+++ b/src/components/Error/src/Error.vue
@@ -0,0 +1,58 @@
+<script lang="ts" setup>
+import pageError from '@/assets/svgs/404.svg'
+import networkError from '@/assets/svgs/500.svg'
+import noPermission from '@/assets/svgs/403.svg'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'Error' })
+
+interface ErrorMap {
+ url: string
+ message: string
+ buttonText: string
+}
+
+const { t } = useI18n()
+
+const errorMap: {
+ [key: string]: ErrorMap
+} = {
+ '404': {
+ url: pageError,
+ message: t('error.pageError'),
+ buttonText: t('error.returnToHome')
+ },
+ '500': {
+ url: networkError,
+ message: t('error.networkError'),
+ buttonText: t('error.returnToHome')
+ },
+ '403': {
+ url: noPermission,
+ message: t('error.noPermission'),
+ buttonText: t('error.returnToHome')
+ }
+}
+
+const props = defineProps({
+ type: propTypes.string.validate((v: string) => ['404', '500', '403'].includes(v)).def('404')
+})
+
+const emit = defineEmits(['errorClick'])
+
+const btnClick = () => {
+ emit('errorClick', props.type)
+}
+</script>
+
+<template>
+ <div class="flex justify-center">
+ <div class="text-center">
+ <img :src="errorMap[type].url" alt="" width="350" />
+ <div class="text-14px text-[var(--el-color-info)]">{{ errorMap[type].message }}</div>
+ <div class="mt-20px">
+ <ElButton type="primary" @click="btnClick">{{ errorMap[type].buttonText }}</ElButton>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/src/components/Form/index.ts b/src/components/Form/index.ts
new file mode 100644
index 0000000..484c7a2
--- /dev/null
+++ b/src/components/Form/index.ts
@@ -0,0 +1,15 @@
+import Form from './src/Form.vue'
+import { ElForm } from 'element-plus'
+import { FormSchema, FormSetPropsType } from '@/types/form'
+
+export interface FormExpose {
+ setValues: (data: Recordable) => void
+ setProps: (props: Recordable) => void
+ delSchema: (field: string) => void
+ addSchema: (formSchema: FormSchema, index?: number) => void
+ setSchema: (schemaProps: FormSetPropsType[]) => void
+ formModel: Recordable
+ getElFormRef: () => ComponentRef<typeof ElForm>
+}
+
+export { Form }
diff --git a/src/components/Form/src/Form.vue b/src/components/Form/src/Form.vue
new file mode 100644
index 0000000..3acc10a
--- /dev/null
+++ b/src/components/Form/src/Form.vue
@@ -0,0 +1,307 @@
+<script lang="tsx">
+import { computed, defineComponent, onMounted, PropType, ref, unref, watch } from 'vue'
+import { ElCol, ElForm, ElFormItem, ElRow, ElTooltip } from 'element-plus'
+import { componentMap } from './componentMap'
+import { propTypes } from '@/utils/propTypes'
+import { getSlot } from '@/utils/tsxHelper'
+import {
+ initModel,
+ setComponentProps,
+ setFormItemSlots,
+ setGridProp,
+ setItemComponentSlots,
+ setTextPlaceholder
+} from './helper'
+import { useRenderSelect } from './components/useRenderSelect'
+import { useRenderRadio } from './components/useRenderRadio'
+import { useRenderCheckbox } from './components/useRenderCheckbox'
+import { useDesign } from '@/hooks/web/useDesign'
+import { findIndex } from '@/utils'
+import { set } from 'lodash-es'
+import { FormProps } from './types'
+import { Icon } from '@/components/Icon'
+import { FormSchema, FormSetPropsType } from '@/types/form'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('form')
+
+export default defineComponent({
+ // eslint-disable-next-line vue/no-reserved-component-names
+ name: 'Form',
+ props: {
+ // 鐢熸垚Form鐨勫竷灞�缁撴瀯鏁扮粍
+ schema: {
+ type: Array as PropType<FormSchema[]>,
+ default: () => []
+ },
+ // 鏄惁闇�瑕佹爡鏍煎竷灞�
+ // update by 鑺嬭壙锛氬皢 true 鏀规垚 false锛屽洜涓洪」鐩洿甯哥敤杩欑鏂瑰紡
+ isCol: propTypes.bool.def(false),
+ // 琛ㄥ崟鏁版嵁瀵硅薄
+ model: {
+ type: Object as PropType<Recordable>,
+ default: () => ({})
+ },
+ // 鏄惁鑷姩璁剧疆placeholder
+ autoSetPlaceholder: propTypes.bool.def(true),
+ // 鏄惁鑷畾涔夊唴瀹�
+ isCustom: propTypes.bool.def(false),
+ // 琛ㄥ崟label瀹藉害
+ labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
+ // 鏄惁 loading 鏁版嵁涓� add by 鑺嬭壙
+ vLoading: propTypes.bool.def(false)
+ },
+ emits: ['register'],
+ setup(props, { slots, expose, emit }) {
+ // element form 瀹炰緥
+ const elFormRef = ref<ComponentRef<typeof ElForm>>()
+
+ // useForm浼犲叆鐨刾rops
+ const outsideProps = ref<FormProps>({})
+
+ const mergeProps = ref<FormProps>({})
+
+ const getProps = computed(() => {
+ const propsObj = { ...props }
+ Object.assign(propsObj, unref(mergeProps))
+ return propsObj
+ })
+
+ // 琛ㄥ崟鏁版嵁
+ const formModel = ref<Recordable>({})
+
+ onMounted(() => {
+ emit('register', unref(elFormRef)?.$parent, unref(elFormRef))
+ })
+
+ // 瀵硅〃鍗曡祴鍊�
+ const setValues = (data: Recordable = {}) => {
+ formModel.value = Object.assign(unref(formModel), data)
+ }
+
+ const setProps = (props: FormProps = {}) => {
+ mergeProps.value = Object.assign(unref(mergeProps), props)
+ outsideProps.value = props
+ }
+
+ const delSchema = (field: string) => {
+ const { schema } = unref(getProps)
+
+ const index = findIndex(schema, (v: FormSchema) => v.field === field)
+ if (index > -1) {
+ schema.splice(index, 1)
+ }
+ }
+
+ const addSchema = (formSchema: FormSchema, index?: number) => {
+ const { schema } = unref(getProps)
+ if (index !== void 0) {
+ schema.splice(index, 0, formSchema)
+ return
+ }
+ schema.push(formSchema)
+ }
+
+ const setSchema = (schemaProps: FormSetPropsType[]) => {
+ const { schema } = unref(getProps)
+ for (const v of schema) {
+ for (const item of schemaProps) {
+ if (v.field === item.field) {
+ set(v, item.path, item.value)
+ }
+ }
+ }
+ }
+
+ const getElFormRef = (): ComponentRef<typeof ElForm> => {
+ return unref(elFormRef) as ComponentRef<typeof ElForm>
+ }
+
+ expose({
+ setValues,
+ formModel,
+ setProps,
+ delSchema,
+ addSchema,
+ setSchema,
+ getElFormRef
+ })
+
+ // 鐩戝惉琛ㄥ崟缁撴瀯鍖栨暟缁勶紝閲嶆柊鐢熸垚formModel
+ watch(
+ () => unref(getProps).schema,
+ (schema = []) => {
+ formModel.value = initModel(schema, unref(formModel))
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+ )
+
+ // 娓叉煋鍖呰9鏍囩锛屾槸鍚︿娇鐢ㄦ爡鏍煎竷灞�
+ const renderWrap = () => {
+ const { isCol } = unref(getProps)
+ const content = isCol ? (
+ <ElRow gutter={20}>{renderFormItemWrap()}</ElRow>
+ ) : (
+ renderFormItemWrap()
+ )
+ return content
+ }
+
+ // 鏄惁瑕佹覆鏌揺l-col
+ const renderFormItemWrap = () => {
+ // hidden灞炴�ц〃绀洪殣钘忥紝涓嶅仛娓叉煋
+ const { schema = [], isCol } = unref(getProps)
+
+ return schema
+ .filter((v) => !v.hidden)
+ .map((item) => {
+ // 濡傛灉鏄� Divider 缁勪欢锛岄渶瑕佽嚜宸卞崰鐢ㄤ竴琛�
+ const isDivider = item.component === 'Divider'
+ const Com = componentMap['Divider'] as ReturnType<typeof defineComponent>
+ return isDivider ? (
+ <Com {...{ contentPosition: 'left', ...item.componentProps }}>{item?.label}</Com>
+ ) : isCol ? (
+ // 濡傛灉闇�瑕佹爡鏍硷紝闇�瑕佸寘瑁� ElCol
+ <ElCol {...setGridProp(item.colProps)}>{renderFormItem(item)}</ElCol>
+ ) : (
+ renderFormItem(item)
+ )
+ })
+ }
+
+ // 娓叉煋formItem
+ const renderFormItem = (item: FormSchema) => {
+ // 鍗曠嫭缁欏彧鏈塷ptions灞炴�х殑缁勪欢鍋氬垽鏂�
+ const notRenderOptions = ['SelectV2', 'Cascader', 'Transfer']
+ const slotsMap: Recordable = {
+ ...setItemComponentSlots(slots, item?.componentProps?.slots, item.field)
+ }
+ if (
+ item?.component !== 'SelectV2' &&
+ item?.component !== 'Cascader' &&
+ item?.componentProps?.options
+ ) {
+ slotsMap.default = () => renderOptions(item)
+ }
+
+ const formItemSlots: Recordable = setFormItemSlots(slots, item.field)
+ // 濡傛灉鏈� labelMessage锛岃嚜鍔ㄤ娇鐢ㄦ彃妲芥覆鏌�
+ if (item?.labelMessage) {
+ formItemSlots.label = () => {
+ return (
+ <>
+ <span>{item.label}</span>
+ <ElTooltip placement="right" raw-content>
+ {{
+ content: () => <span v-dompurify-html={item.labelMessage}></span>,
+ default: () => (
+ <Icon
+ icon="ep:warning"
+ size={16}
+ color="var(--el-color-primary)"
+ class="relative top-1px ml-2px"
+ ></Icon>
+ )
+ }}
+ </ElTooltip>
+ </>
+ )
+ }
+ }
+ return (
+ <ElFormItem {...(item.formItemProps || {})} prop={item.field} label={item.label || ''}>
+ {{
+ ...formItemSlots,
+ default: () => {
+ const Com = componentMap[item.component as string] as ReturnType<
+ typeof defineComponent
+ >
+
+ const { autoSetPlaceholder } = unref(getProps)
+
+ return slots[item.field] ? (
+ getSlot(slots, item.field, formModel.value)
+ ) : (
+ <Com
+ vModel={formModel.value[item.field]}
+ {...(autoSetPlaceholder && setTextPlaceholder(item))}
+ {...setComponentProps(item)}
+ style={item.componentProps?.style}
+ {...(notRenderOptions.includes(item?.component as string) &&
+ item?.componentProps?.options
+ ? { options: item?.componentProps?.options || [] }
+ : {})}
+ >
+ {{ ...slotsMap }}
+ </Com>
+ )
+ }
+ }}
+ </ElFormItem>
+ )
+ }
+
+ // 娓叉煋options
+ const renderOptions = (item: FormSchema) => {
+ switch (item.component) {
+ case 'Select':
+ case 'SelectV2':
+ const { renderSelectOptions } = useRenderSelect(slots)
+ return renderSelectOptions(item)
+ case 'Radio':
+ case 'RadioButton':
+ const { renderRadioOptions } = useRenderRadio()
+ return renderRadioOptions(item)
+ case 'Checkbox':
+ case 'CheckboxButton':
+ const { renderCheckboxOptions } = useRenderCheckbox()
+ return renderCheckboxOptions(item)
+ default:
+ break
+ }
+ }
+
+ // 杩囨护浼犲叆Form缁勪欢鐨勫睘鎬�
+ const getFormBindValue = () => {
+ // 閬垮厤鍦ㄦ爣绛句笂鍑虹幇澶氫綑鐨勫睘鎬�
+ const delKeys = ['schema', 'isCol', 'autoSetPlaceholder', 'isCustom', 'model']
+ const props = { ...unref(getProps) }
+ for (const key in props) {
+ if (delKeys.indexOf(key) !== -1) {
+ delete props[key]
+ }
+ }
+ return props
+ }
+
+ return () => (
+ <ElForm
+ ref={elFormRef}
+ {...getFormBindValue()}
+ model={props.isCustom ? props.model : formModel}
+ class={prefixCls}
+ v-loading={props.vLoading}
+ >
+ {{
+ // 濡傛灉闇�瑕佽嚜瀹氫箟锛屽氨浠�涔堥兘涓嶆覆鏌擄紝鑰屾槸鎻愪緵榛樿鎻掓Ы
+ default: () => {
+ const { isCustom } = unref(getProps)
+ return isCustom ? getSlot(slots, 'default') : renderWrap()
+ }
+ }}
+ </ElForm>
+ )
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+.#{$elNamespace}-form.#{$namespace}-form .#{$elNamespace}-row {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+}
+</style>
diff --git a/src/components/Form/src/componentMap.ts b/src/components/Form/src/componentMap.ts
new file mode 100644
index 0000000..5af9b40
--- /dev/null
+++ b/src/components/Form/src/componentMap.ts
@@ -0,0 +1,55 @@
+import type { Component } from 'vue'
+import {
+ ElCascader,
+ ElCheckboxGroup,
+ ElColorPicker,
+ ElDatePicker,
+ ElInput,
+ ElInputNumber,
+ ElRadioGroup,
+ ElRate,
+ ElSelect,
+ ElSelectV2,
+ ElTreeSelect,
+ ElSlider,
+ ElSwitch,
+ ElTimePicker,
+ ElTimeSelect,
+ ElTransfer,
+ ElAutocomplete,
+ ElDivider
+} from 'element-plus'
+import { InputPassword } from '@/components/InputPassword'
+import { Editor } from '@/components/Editor'
+import { UploadImg, UploadImgs, UploadFile } from '@/components/UploadFile'
+import { ComponentName } from '@/types/components'
+
+const componentMap: Recordable<Component, ComponentName> = {
+ Radio: ElRadioGroup,
+ Checkbox: ElCheckboxGroup,
+ CheckboxButton: ElCheckboxGroup,
+ Input: ElInput,
+ Autocomplete: ElAutocomplete,
+ InputNumber: ElInputNumber,
+ Select: ElSelect,
+ Cascader: ElCascader,
+ Switch: ElSwitch,
+ Slider: ElSlider,
+ TimePicker: ElTimePicker,
+ DatePicker: ElDatePicker,
+ Rate: ElRate,
+ ColorPicker: ElColorPicker,
+ Transfer: ElTransfer,
+ Divider: ElDivider,
+ TimeSelect: ElTimeSelect,
+ SelectV2: ElSelectV2,
+ TreeSelect: ElTreeSelect,
+ RadioButton: ElRadioGroup,
+ InputPassword: InputPassword,
+ Editor: Editor,
+ UploadImg: UploadImg,
+ UploadImgs: UploadImgs,
+ UploadFile: UploadFile
+}
+
+export { componentMap }
diff --git a/src/components/Form/src/components/useRenderCheckbox.tsx b/src/components/Form/src/components/useRenderCheckbox.tsx
new file mode 100644
index 0000000..e151839
--- /dev/null
+++ b/src/components/Form/src/components/useRenderCheckbox.tsx
@@ -0,0 +1,26 @@
+import { FormSchema } from '@/types/form'
+import { ElCheckbox, ElCheckboxButton } from 'element-plus'
+import { defineComponent } from 'vue'
+
+export const useRenderCheckbox = () => {
+ const renderCheckboxOptions = (item: FormSchema) => {
+ // 濡傛灉鏈夊埆鍚嶏紝灏卞彇鍒悕
+ const labelAlias = item?.componentProps?.optionsAlias?.labelField
+ const valueAlias = item?.componentProps?.optionsAlias?.valueField
+ const Com = (item.component === 'Checkbox' ? ElCheckbox : ElCheckboxButton) as ReturnType<
+ typeof defineComponent
+ >
+ return item?.componentProps?.options?.map((option) => {
+ const { ...other } = option
+ return (
+ <Com {...other} label={option[valueAlias || 'value']}>
+ {option[labelAlias || 'label']}
+ </Com>
+ )
+ })
+ }
+
+ return {
+ renderCheckboxOptions
+ }
+}
diff --git a/src/components/Form/src/components/useRenderRadio.tsx b/src/components/Form/src/components/useRenderRadio.tsx
new file mode 100644
index 0000000..d1005ca
--- /dev/null
+++ b/src/components/Form/src/components/useRenderRadio.tsx
@@ -0,0 +1,26 @@
+import { FormSchema } from '@/types/form'
+import { ElRadio, ElRadioButton } from 'element-plus'
+import { defineComponent } from 'vue'
+
+export const useRenderRadio = () => {
+ const renderRadioOptions = (item: FormSchema) => {
+ // 濡傛灉鏈夊埆鍚嶏紝灏卞彇鍒悕
+ const labelAlias = item?.componentProps?.optionsAlias?.labelField
+ const valueAlias = item?.componentProps?.optionsAlias?.valueField
+ const Com = (item.component === 'Radio' ? ElRadio : ElRadioButton) as ReturnType<
+ typeof defineComponent
+ >
+ return item?.componentProps?.options?.map((option) => {
+ const { ...other } = option
+ return (
+ <Com {...other} label={option[valueAlias || 'value']}>
+ {option[labelAlias || 'label']}
+ </Com>
+ )
+ })
+ }
+
+ return {
+ renderRadioOptions
+ }
+}
diff --git a/src/components/Form/src/components/useRenderSelect.tsx b/src/components/Form/src/components/useRenderSelect.tsx
new file mode 100644
index 0000000..59b72e6
--- /dev/null
+++ b/src/components/Form/src/components/useRenderSelect.tsx
@@ -0,0 +1,57 @@
+import { FormSchema } from '@/types/form'
+import { ComponentOptions } from '@/types/components'
+import { ElOption, ElOptionGroup } from 'element-plus'
+import { getSlot } from '@/utils/tsxHelper'
+import { Slots } from 'vue'
+
+export const useRenderSelect = (slots: Slots) => {
+ // 娓叉煋 select options
+ const renderSelectOptions = (item: FormSchema) => {
+ // 濡傛灉鏈夊埆鍚嶏紝灏卞彇鍒悕
+ const labelAlias = item?.componentProps?.optionsAlias?.labelField
+ return item?.componentProps?.options?.map((option) => {
+ if (option?.options?.length) {
+ return (
+ <ElOptionGroup label={option[labelAlias || 'label']}>
+ {() => {
+ return option?.options?.map((v) => {
+ return renderSelectOptionItem(item, v)
+ })
+ }}
+ </ElOptionGroup>
+ )
+ } else {
+ return renderSelectOptionItem(item, option)
+ }
+ })
+ }
+
+ // 娓叉煋 select option item
+ const renderSelectOptionItem = (item: FormSchema, option: ComponentOptions) => {
+ // 濡傛灉鏈夊埆鍚嶏紝灏卞彇鍒悕
+ const labelAlias = item?.componentProps?.optionsAlias?.labelField
+ const valueAlias = item?.componentProps?.optionsAlias?.valueField
+
+ const { label, value, ...other } = option
+
+ return (
+ <ElOption
+ {...other}
+ label={labelAlias ? option[labelAlias] : label}
+ value={valueAlias ? option[valueAlias] : value}
+ >
+ {{
+ default: () =>
+ // option 鎻掓Ы鍚嶈鍒欙紝{field}-option
+ item?.componentProps?.optionsSlot
+ ? getSlot(slots, `${item.field}-option`, { item: option })
+ : undefined
+ }}
+ </ElOption>
+ )
+ }
+
+ return {
+ renderSelectOptions
+ }
+}
diff --git a/src/components/Form/src/helper.ts b/src/components/Form/src/helper.ts
new file mode 100644
index 0000000..cdfc8ca
--- /dev/null
+++ b/src/components/Form/src/helper.ts
@@ -0,0 +1,148 @@
+import type { Slots } from 'vue'
+import { getSlot } from '@/utils/tsxHelper'
+import { PlaceholderModel } from './types'
+import { FormSchema } from '@/types/form'
+import { ColProps } from '@/types/components'
+
+/**
+ *
+ * @param schema 瀵瑰簲缁勪欢鏁版嵁
+ * @returns 杩斿洖鎻愮ず淇℃伅瀵硅薄
+ * @description 鐢ㄤ簬鑷姩璁剧疆placeholder
+ */
+export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => {
+ const { t } = useI18n()
+ const textMap = ['Input', 'Autocomplete', 'InputNumber', 'InputPassword']
+ const selectMap = ['Select', 'SelectV2', 'TimePicker', 'DatePicker', 'TimeSelect', 'TimeSelect']
+ if (textMap.includes(schema?.component as string)) {
+ return {
+ placeholder: t('common.inputText') + schema.label
+ }
+ }
+ if (selectMap.includes(schema?.component as string)) {
+ // 涓�浜涜寖鍥撮�夋嫨鍣�
+ const twoTextMap = ['datetimerange', 'daterange', 'monthrange', 'datetimerange', 'daterange']
+ if (
+ twoTextMap.includes(
+ (schema?.componentProps?.type || schema?.componentProps?.isRange) as string
+ )
+ ) {
+ return {
+ startPlaceholder: t('common.startTimeText'),
+ endPlaceholder: t('common.endTimeText'),
+ rangeSeparator: '-'
+ }
+ } else {
+ return {
+ placeholder: t('common.selectText') + schema.label
+ }
+ }
+ }
+ return {}
+}
+
+/**
+ *
+ * @param col 鍐呯疆鏍呮牸
+ * @returns 杩斿洖鏍呮牸灞炴��
+ * @description 鍚堝苟浼犲叆杩涙潵鐨勬爡鏍煎睘鎬�
+ */
+export const setGridProp = (col: ColProps = {}): ColProps => {
+ const colProps: ColProps = {
+ // 濡傛灉鏈塻pan锛屼唬琛ㄧ敤鎴蜂紭鍏堢骇鏇撮珮锛屾墍浠ヤ笉闇�瑕侀粯璁ゆ爡鏍�
+ ...(col.span
+ ? {}
+ : {
+ xs: 24,
+ sm: 12,
+ md: 12,
+ lg: 12,
+ xl: 12
+ }),
+ ...col
+ }
+ return colProps
+}
+
+/**
+ *
+ * @param item 浼犲叆鐨勭粍浠跺睘鎬�
+ * @returns 榛樿娣诲姞 clearable 灞炴��
+ */
+export const setComponentProps = (item: FormSchema): Recordable => {
+ const notNeedClearable = ['ColorPicker']
+ const componentProps: Recordable = notNeedClearable.includes(item.component as string)
+ ? { ...item.componentProps }
+ : {
+ clearable: true,
+ ...item.componentProps
+ }
+ // 闇�瑕佸垹闄ら澶栫殑灞炴��
+ delete componentProps?.slots
+ return componentProps
+}
+
+/**
+ *
+ * @param slots 鎻掓Ы
+ * @param slotsProps 鎻掓Ы灞炴��
+ * @param field 瀛楁鍚�
+ */
+export const setItemComponentSlots = (
+ slots: Slots,
+ slotsProps: Recordable = {},
+ field: string
+): Recordable => {
+ const slotObj: Recordable = {}
+ for (const key in slotsProps) {
+ if (slotsProps[key]) {
+ // 鐢变簬缁勪欢鏈夊彲鑳介噸澶嶏紝闇�瑕佹湁涓�涓敮涓�鐨勫墠缂�
+ slotObj[key] = (data: Recordable) => {
+ return getSlot(slots, `${field}-${key}`, data)
+ }
+ }
+ }
+ return slotObj
+}
+
+/**
+ *
+ * @param schema Form琛ㄥ崟缁撴瀯鍖栨暟缁�
+ * @param formModel FormModel
+ * @returns FormModel
+ * @description 鐢熸垚瀵瑰簲鐨刦ormModel
+ */
+export const initModel = (schema: FormSchema[], formModel: Recordable) => {
+ const model: Recordable = { ...formModel }
+ schema.map((v) => {
+ // 濡傛灉鏄痟idden锛屽氨鍒犻櫎瀵瑰簲鐨勫��
+ if (v.hidden) {
+ delete model[v.field]
+ } else if (v.component && v.component !== 'Divider') {
+ const hasField = Reflect.has(model, v.field)
+ // 濡傛灉鍏堝墠宸茬粡鏈夊�煎瓨鍦紝鍒欎笉杩涜閲嶆柊璧嬪�硷紝鑰屾槸閲囩敤鐜版湁鐨勫��
+ model[v.field] = hasField ? model[v.field] : v.value !== void 0 ? v.value : ''
+ }
+ })
+ return model
+}
+
+/**
+ * @param slots 鎻掓Ы
+ * @param field 瀛楁鍚�
+ * @returns 杩斿洖FormIiem鎻掓Ы
+ */
+export const setFormItemSlots = (slots: Slots, field: string): Recordable => {
+ const slotObj: Recordable = {}
+ if (slots[`${field}-error`]) {
+ slotObj['error'] = (data: Recordable) => {
+ return getSlot(slots, `${field}-error`, data)
+ }
+ }
+ if (slots[`${field}-label`]) {
+ slotObj['label'] = (data: Recordable) => {
+ return getSlot(slots, `${field}-label`, data)
+ }
+ }
+ return slotObj
+}
diff --git a/src/components/Form/src/types.ts b/src/components/Form/src/types.ts
new file mode 100644
index 0000000..dcd01e7
--- /dev/null
+++ b/src/components/Form/src/types.ts
@@ -0,0 +1,17 @@
+import { FormSchema } from '@/types/form'
+
+export interface PlaceholderModel {
+ placeholder?: string
+ startPlaceholder?: string
+ endPlaceholder?: string
+ rangeSeparator?: string
+}
+
+export type FormProps = {
+ schema?: FormSchema[]
+ isCol?: boolean
+ model?: Recordable
+ autoSetPlaceholder?: boolean
+ isCustom?: boolean
+ labelWidth?: string | number
+} & Recordable
diff --git a/src/components/FormCreate/index.ts b/src/components/FormCreate/index.ts
new file mode 100644
index 0000000..9d32778
--- /dev/null
+++ b/src/components/FormCreate/index.ts
@@ -0,0 +1,4 @@
+import { useFormCreateDesigner } from './src/useFormCreateDesigner'
+import { useApiSelect } from './src/components/useApiSelect'
+
+export { useFormCreateDesigner, useApiSelect }
diff --git a/src/components/FormCreate/src/components/DictSelect.vue b/src/components/FormCreate/src/components/DictSelect.vue
new file mode 100644
index 0000000..204746d
--- /dev/null
+++ b/src/components/FormCreate/src/components/DictSelect.vue
@@ -0,0 +1,59 @@
+<!-- 鏁版嵁瀛楀吀 Select 閫夋嫨鍣� -->
+<template>
+ <el-select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
+ <el-option
+ v-for="(dict, index) in getDictOptions"
+ :key="index"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ <el-radio-group v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
+ <el-radio v-for="(dict, index) in getDictOptions" :key="index" :value="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ <el-checkbox-group v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
+ <el-checkbox
+ v-for="(dict, index) in getDictOptions"
+ :key="index"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-checkbox-group>
+</template>
+
+<script lang="ts" setup>
+import { getBoolDictOptions, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'DictSelect' })
+
+const attrs = useAttrs()
+
+// 鎺ュ彈鐖剁粍浠跺弬鏁�
+interface Props {
+ dictType: string // 瀛楀吀绫诲瀷
+ valueType?: 'str' | 'int' | 'bool' // 瀛楀吀鍊肩被鍨�
+ selectType?: 'select' | 'radio' | 'checkbox' // 閫夋嫨鍣ㄧ被鍨嬶紝涓嬫媺妗� select銆佸閫夋 checkbox銆佸崟閫夋 radio
+ formCreateInject?: any
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ valueType: 'str',
+ selectType: 'select'
+})
+
+// 鑾峰緱瀛楀吀閰嶇疆
+const getDictOptions = computed(() => {
+ switch (props.valueType) {
+ case 'str':
+ return getStrDictOptions(props.dictType)
+ case 'int':
+ return getIntDictOptions(props.dictType)
+ case 'bool':
+ return getBoolDictOptions(props.dictType)
+ default:
+ return []
+ }
+})
+</script>
diff --git a/src/components/FormCreate/src/components/useApiSelect.tsx b/src/components/FormCreate/src/components/useApiSelect.tsx
new file mode 100644
index 0000000..89a7e8d
--- /dev/null
+++ b/src/components/FormCreate/src/components/useApiSelect.tsx
@@ -0,0 +1,270 @@
+import request from '@/config/axios'
+import { isEmpty } from '@/utils/is'
+import { ApiSelectProps } from '@/components/FormCreate/src/type'
+import { jsonParse } from '@/utils'
+
+export const useApiSelect = (option: ApiSelectProps) => {
+ return defineComponent({
+ name: option.name,
+ props: {
+ // 閫夐」鏍囩
+ labelField: {
+ type: String,
+ default: () => option.labelField ?? 'label'
+ },
+ // 閫夐」鐨勫��
+ valueField: {
+ type: String,
+ default: () => option.valueField ?? 'value'
+ },
+ // api 鎺ュ彛
+ url: {
+ type: String,
+ default: () => option.url ?? ''
+ },
+ // 璇锋眰绫诲瀷
+ method: {
+ type: String,
+ default: 'GET'
+ },
+ // 閫夐」瑙f瀽鍑芥暟
+ parseFunc: {
+ type: String,
+ default: ''
+ },
+ // 璇锋眰鍙傛暟
+ data: {
+ type: String,
+ default: ''
+ },
+ // 閫夋嫨鍣ㄧ被鍨嬶紝涓嬫媺妗� select銆佸閫夋 checkbox銆佸崟閫夋 radio
+ selectType: {
+ type: String,
+ default: 'select'
+ },
+ // 鏄惁澶氶��
+ multiple: {
+ type: Boolean,
+ default: false
+ },
+ // 鏄惁杩滅▼鎼滅储
+ remote: {
+ type: Boolean,
+ default: false
+ },
+ // 杩滅▼鎼滅储鏃舵惡甯︾殑鍙傛暟
+ remoteField: {
+ type: String,
+ default: 'label'
+ },
+ // 杩斿洖鍊肩被鍨嬶紙鐢ㄤ簬閮ㄩ棬閫夋嫨鍣ㄧ瓑锛夛細id 杩斿洖 ID锛宯ame 杩斿洖鍚嶇О
+ returnType: {
+ type: String,
+ default: 'id'
+ }
+ },
+ setup(props) {
+ const attrs = useAttrs()
+ const options = ref<any[]>([]) // 涓嬫媺鏁版嵁
+ const loading = ref(false) // 鏄惁姝e湪浠庤繙绋嬭幏鍙栨暟鎹�
+ const queryParam = ref<any>() // 褰撳墠杈撳叆鐨勫��
+ const getOptions = async () => {
+ options.value = []
+ // 鎺ュ彛閫夋嫨鍣�
+ if (isEmpty(props.url)) {
+ return
+ }
+
+ switch (props.method) {
+ case 'GET':
+ let url: string = props.url
+ if (props.remote) {
+ if (queryParam.value != undefined) {
+ if (url.includes('?')) {
+ url = `${url}&${props.remoteField}=${queryParam.value}`
+ } else {
+ url = `${url}?${props.remoteField}=${queryParam.value}`
+ }
+ }
+ }
+ parseOptions(await request.get({ url: url }))
+ break
+ case 'POST':
+ const data: any = jsonParse(props.data)
+ if (props.remote) {
+ data[props.remoteField] = queryParam.value
+ }
+ parseOptions(await request.post({ url: props.url, data: data }))
+ break
+ }
+ }
+
+ function parseOptions(data: any) {
+ // 鎯呭喌涓�锛氬鏋滄湁鑷畾涔夎В鏋愬嚱鏁颁紭鍏堜娇鐢ㄨ嚜瀹氫箟瑙f瀽
+ if (!isEmpty(props.parseFunc)) {
+ options.value = parseFunc()?.(data)
+ return
+ }
+ // 鎯呭喌浜岋細杩斿洖鐨勭洿鎺ユ槸涓�涓垪琛�
+ if (Array.isArray(data)) {
+ parseOptions0(data)
+ return
+ }
+ // 鎯呭喌浜岋細杩斿洖鐨勬槸鍒嗛〉鏁版嵁,灏濊瘯璇诲彇 list
+ data = data.list
+ if (!!data && Array.isArray(data)) {
+ parseOptions0(data)
+ return
+ }
+ // 鎯呭喌涓夛細涓嶆槸 yudao-vue-pro 鏍囧噯杩斿洖
+ console.warn(
+ `鎺ュ彛[${props.url}] 杩斿洖缁撴灉涓嶆槸 yudao-vue-pro 鏍囧噯杩斿洖寤鸿閲囩敤鑷畾涔夎В鏋愬嚱鏁板鐞哷
+ )
+ }
+
+ function parseOptions0(data: any[]) {
+ if (Array.isArray(data)) {
+ options.value = data.map((item: any) => {
+ const label = parseExpression(item, props.labelField)
+ let value = parseExpression(item, props.valueField)
+
+ // 鏍规嵁 returnType 鍐冲畾杩斿洖鍊�
+ // 濡傛灉璁剧疆浜� returnType 涓� 'name'锛屽垯杩斿洖 label 浣滀负 value
+ if (props.returnType === 'name') {
+ value = label
+ }
+
+ return {
+ label: label,
+ value: value
+ }
+ })
+ return
+ }
+ console.warn(`鎺ュ彛[${props.url}] 杩斿洖缁撴灉涓嶆槸涓�涓暟缁刞)
+ }
+
+ function parseFunc() {
+ let parse: any = null
+ if (!!props.parseFunc) {
+ // 瑙f瀽瀛楃涓插嚱鏁�
+ parse = new Function(`return ${props.parseFunc}`)()
+ }
+ return parse
+ }
+
+ function parseExpression(data: any, template: string) {
+ // 妫�娴嬫槸鍚︿娇鐢ㄤ簡琛ㄨ揪寮�
+ if (template.indexOf('${') === -1) {
+ return data[template]
+ }
+ // 姝e垯琛ㄨ揪寮忓尮閰嶆ā鏉垮瓧绗︿覆涓殑 ${...}
+ const pattern = /\$\{([^}]*)}/g
+ // 浣跨敤replace鍑芥暟閰嶅悎姝e垯琛ㄨ揪寮忓拰鍥炶皟鍑芥暟鏉ヨ繘琛屾浛鎹�
+ return template.replace(pattern, (_, expr) => {
+ // expr 鏄尮閰嶅埌鐨� ${} 鍐呯殑琛ㄨ揪寮忥紙杩欓噷鏄睘鎬у悕锛夛紝浠� data 涓幏鍙栧搴旂殑鍊�
+ const result = data[expr.trim()] // 鍘婚櫎鍓嶅悗绌虹櫧锛屼互闃茬敤鎴疯緭鍏ュ甫绌烘牸鐨勫睘鎬у悕
+ if (!result) {
+ console.warn(
+ `鎺ュ彛閫夋嫨鍣ㄩ�夐」妯$増[${template}][${expr.trim()}] 瑙f瀽鍊煎け璐ョ粨鏋滀负[${result}], 璇锋鏌ュ睘鎬у悕绉版槸鍚﹀瓨鍦ㄤ簬鎺ュ彛杩斿洖鍊间腑,瀛樺湪鍒欏拷鐣ユ鏉★紒锛侊紒`
+ )
+ }
+ return result
+ })
+ }
+
+ const remoteMethod = async (query: any) => {
+ if (!query) {
+ return
+ }
+ loading.value = true
+ try {
+ queryParam.value = query
+ await getOptions()
+ } finally {
+ loading.value = false
+ }
+ }
+
+ onMounted(async () => {
+ await getOptions()
+ })
+
+ const buildSelect = () => {
+ if (props.multiple) {
+ // fix锛氬鍐欐姝ユ槸涓轰簡瑙e喅 multiple 灞炴�ч棶棰�
+ return (
+ <el-select
+ class="w-1/1"
+ multiple
+ loading={loading.value}
+ {...attrs}
+ remote={props.remote}
+ {...(props.remote && { remoteMethod: remoteMethod })}
+ >
+ {options.value.map((item, index) => (
+ <el-option key={index} label={item.label} value={item.value} />
+ ))}
+ </el-select>
+ )
+ }
+ return (
+ <el-select
+ class="w-1/1"
+ loading={loading.value}
+ {...attrs}
+ remote={props.remote}
+ {...(props.remote && { remoteMethod: remoteMethod })}
+ >
+ {options.value.map((item, index) => (
+ <el-option key={index} label={item.label} value={item.value} />
+ ))}
+ </el-select>
+ )
+ }
+ const buildCheckbox = () => {
+ if (isEmpty(options.value)) {
+ options.value = [
+ { label: '閫夐」1', value: '閫夐」1' },
+ { label: '閫夐」2', value: '閫夐」2' }
+ ]
+ }
+ return (
+ <el-checkbox-group class="w-1/1" {...attrs}>
+ {options.value.map((item, index) => (
+ <el-checkbox key={index} label={item.label} value={item.value} />
+ ))}
+ </el-checkbox-group>
+ )
+ }
+ const buildRadio = () => {
+ if (isEmpty(options.value)) {
+ options.value = [
+ { label: '閫夐」1', value: '閫夐」1' },
+ { label: '閫夐」2', value: '閫夐」2' }
+ ]
+ }
+ return (
+ <el-radio-group class="w-1/1" {...attrs}>
+ {options.value.map((item, index) => (
+ <el-radio key={index} value={item.value}>
+ {item.label}
+ </el-radio>
+ ))}
+ </el-radio-group>
+ )
+ }
+ return () => (
+ <>
+ {props.selectType === 'select'
+ ? buildSelect()
+ : props.selectType === 'radio'
+ ? buildRadio()
+ : props.selectType === 'checkbox'
+ ? buildCheckbox()
+ : buildSelect()}
+ </>
+ )
+ }
+ })
+}
diff --git a/src/components/FormCreate/src/config/index.ts b/src/components/FormCreate/src/config/index.ts
new file mode 100644
index 0000000..b1e2dde
--- /dev/null
+++ b/src/components/FormCreate/src/config/index.ts
@@ -0,0 +1,15 @@
+import { useUploadFileRule } from './useUploadFileRule'
+import { useUploadImgRule } from './useUploadImgRule'
+import { useUploadImgsRule } from './useUploadImgsRule'
+import { useDictSelectRule } from './useDictSelectRule'
+import { useEditorRule } from './useEditorRule'
+import { useSelectRule } from './useSelectRule'
+
+export {
+ useUploadFileRule,
+ useUploadImgRule,
+ useUploadImgsRule,
+ useDictSelectRule,
+ useEditorRule,
+ useSelectRule
+}
diff --git a/src/components/FormCreate/src/config/selectRule.ts b/src/components/FormCreate/src/config/selectRule.ts
new file mode 100644
index 0000000..a6f3841
--- /dev/null
+++ b/src/components/FormCreate/src/config/selectRule.ts
@@ -0,0 +1,181 @@
+const selectRule = [
+ {
+ type: 'select',
+ field: 'selectType',
+ title: '閫夋嫨鍣ㄧ被鍨�',
+ value: 'select',
+ options: [
+ { label: '涓嬫媺妗�', value: 'select' },
+ { label: '鍗曢�夋', value: 'radio' },
+ { label: '澶氶�夋', value: 'checkbox' }
+ ],
+ // 鍙傝�� https://www.form-create.com/v3/guide/control 缁勪欢鑱斿姩锛屽崟閫夋鍜屽閫夋涓嶉渶瑕佸閫夊睘鎬�
+ control: [
+ {
+ value: 'select',
+ condition: '==',
+ method: 'hidden',
+ rule: [
+ 'multiple',
+ 'clearable',
+ 'collapseTags',
+ 'multipleLimit',
+ 'allowCreate',
+ 'filterable',
+ 'noMatchText',
+ 'remote',
+ 'remoteMethod',
+ 'reserveKeyword',
+ 'defaultFirstOption',
+ 'automaticDropdown'
+ ]
+ }
+ ]
+ },
+ {
+ type: 'switch',
+ field: 'filterable',
+ title: '鏄惁鍙悳绱�'
+ },
+ { type: 'switch', field: 'multiple', title: '鏄惁澶氶��' },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '鏄惁绂佺敤'
+ },
+ { type: 'switch', field: 'clearable', title: '鏄惁鍙互娓呯┖閫夐」' },
+ {
+ type: 'switch',
+ field: 'collapseTags',
+ title: '澶氶�夋椂鏄惁灏嗛�変腑鍊兼寜鏂囧瓧鐨勫舰寮忓睍绀�'
+ },
+ {
+ type: 'inputNumber',
+ field: 'multipleLimit',
+ title: '澶氶�夋椂鐢ㄦ埛鏈�澶氬彲浠ラ�夋嫨鐨勯」鐩暟锛屼负 0 鍒欎笉闄愬埗',
+ props: { min: 0 }
+ },
+ {
+ type: 'input',
+ field: 'autocomplete',
+ title: 'autocomplete 灞炴��'
+ },
+ { type: 'input', field: 'placeholder', title: '鍗犱綅绗�' },
+ { type: 'switch', field: 'allowCreate', title: '鏄惁鍏佽鐢ㄦ埛鍒涘缓鏂版潯鐩�' },
+ {
+ type: 'input',
+ field: 'noMatchText',
+ title: '鎼滅储鏉′欢鏃犲尮閰嶆椂鏄剧ず鐨勬枃瀛�'
+ },
+ { type: 'input', field: 'noDataText', title: '閫夐」涓虹┖鏃舵樉绀虹殑鏂囧瓧' },
+ {
+ type: 'switch',
+ field: 'reserveKeyword',
+ title: '澶氶�変笖鍙悳绱㈡椂锛屾槸鍚﹀湪閫変腑涓�涓�夐」鍚庝繚鐣欏綋鍓嶇殑鎼滅储鍏抽敭璇�'
+ },
+ {
+ type: 'switch',
+ field: 'defaultFirstOption',
+ title: '鍦ㄨ緭鍏ユ鎸変笅鍥炶溅锛岄�夋嫨绗竴涓尮閰嶉」'
+ },
+ {
+ type: 'switch',
+ field: 'popperAppendToBody',
+ title: '鏄惁灏嗗脊鍑烘鎻掑叆鑷� body 鍏冪礌',
+ value: true
+ },
+ {
+ type: 'switch',
+ field: 'automaticDropdown',
+ title: '瀵逛簬涓嶅彲鎼滅储鐨� Select锛屾槸鍚﹀湪杈撳叆妗嗚幏寰楃劍鐐瑰悗鑷姩寮瑰嚭閫夐」鑿滃崟'
+ }
+]
+
+const apiSelectRule = [
+ {
+ type: 'input',
+ field: 'url',
+ title: 'url 鍦板潃',
+ props: {
+ placeholder: '/system/user/simple-list'
+ }
+ },
+ {
+ type: 'select',
+ field: 'method',
+ title: '璇锋眰绫诲瀷',
+ value: 'GET',
+ options: [
+ { label: 'GET', value: 'GET' },
+ { label: 'POST', value: 'POST' }
+ ],
+ control: [
+ {
+ value: 'GET',
+ condition: '!=',
+ method: 'hidden',
+ rule: [
+ {
+ type: 'input',
+ field: 'data',
+ title: '璇锋眰鍙傛暟 JSON 鏍煎紡',
+ props: {
+ autosize: true,
+ type: 'textarea',
+ placeholder: '{"type": 1}'
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type: 'input',
+ field: 'labelField',
+ title: 'label 灞炴��',
+ info: '鍙互浣跨敤 el 琛ㄨ揪寮忥細${灞炴�锛屾潵瀹炵幇澶嶆潅鏁版嵁缁勫悎銆傚锛�${nickname}-${id}',
+ props: {
+ placeholder: 'nickname'
+ }
+ },
+ {
+ type: 'input',
+ field: 'valueField',
+ title: 'value 灞炴��',
+ info: '鍙互浣跨敤 el 琛ㄨ揪寮忥細${灞炴�锛屾潵瀹炵幇澶嶆潅鏁版嵁缁勫悎銆傚锛�${nickname}-${id}',
+ props: {
+ placeholder: 'id'
+ }
+ },
+ {
+ type: 'input',
+ field: 'parseFunc',
+ title: '閫夐」瑙f瀽鍑芥暟',
+ info: `data 涓烘帴鍙h繑鍥炲��,闇�瑕佸啓涓�涓尶鍚嶅嚱鏁拌В鏋愯繑鍥炲�间负閫夋嫨鍣� options 鍒楄〃
+ (data: any)=>{ label: string; value: any }[]`,
+ props: {
+ autosize: true,
+ rows: { minRows: 2, maxRows: 6 },
+ type: 'textarea',
+ placeholder: `
+ function (data) {
+ console.log(data)
+ return data.list.map(item=> ({label: item.nickname,value: item.id}))
+ }`
+ }
+ },
+ {
+ type: 'switch',
+ field: 'remote',
+ info: '鏄惁鍙悳绱�',
+ title: '鍏朵腑鐨勯�夐」鏄惁浠庢湇鍔″櫒杩滅▼鍔犺浇'
+ },
+ {
+ type: 'input',
+ field: 'remoteField',
+ title: '璇锋眰鍙傛暟',
+ info: '杩滅▼璇锋眰鏃惰姹傛惡甯︾殑鍙傛暟鍚嶇О锛屽锛歯ame'
+ }
+]
+
+export { selectRule, apiSelectRule }
diff --git a/src/components/FormCreate/src/config/useDictSelectRule.ts b/src/components/FormCreate/src/config/useDictSelectRule.ts
new file mode 100644
index 0000000..f232f48
--- /dev/null
+++ b/src/components/FormCreate/src/config/useDictSelectRule.ts
@@ -0,0 +1,64 @@
+import { generateUUID } from '@/utils'
+import * as DictDataApi from '@/api/system/dict/dict.type'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+import { selectRule } from '@/components/FormCreate/src/config/selectRule'
+import { cloneDeep } from 'lodash-es'
+
+/**
+ * 瀛楀吀閫夋嫨鍣ㄨ鍒欙紝濡傛灉瑙勫垯浣跨敤鍒板姩鎬佹暟鎹垯闇�瑕佸崟鐙厤缃笉鑳戒娇鐢� useSelectRule
+ */
+export const useDictSelectRule = () => {
+ const label = '瀛楀吀閫夋嫨鍣�'
+ const name = 'DictSelect'
+ const rules = cloneDeep(selectRule)
+ const dictOptions = ref<{ label: string; value: string }[]>([]) // 瀛楀吀绫诲瀷涓嬫媺鏁版嵁
+ onMounted(async () => {
+ const data = await DictDataApi.getSimpleDictTypeList()
+ if (!data || data.length === 0) {
+ return
+ }
+ dictOptions.value =
+ data?.map((item: DictDataApi.DictTypeVO) => ({
+ label: item.name,
+ value: item.type
+ })) ?? []
+ })
+ return {
+ icon: 'icon-doc-text',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: generateUUID(),
+ title: label,
+ info: '',
+ $required: false
+ }
+ },
+ props(_, { t }) {
+ return localeProps(t, name + '.props', [
+ makeRequiredRule(),
+ {
+ type: 'select',
+ field: 'dictType',
+ title: '瀛楀吀绫诲瀷',
+ value: '',
+ options: dictOptions.value
+ },
+ {
+ type: 'select',
+ field: 'valueType',
+ title: '瀛楀吀鍊肩被鍨�',
+ value: 'str',
+ options: [
+ { label: '鏁板瓧', value: 'int' },
+ { label: '瀛楃涓�', value: 'str' },
+ { label: '甯冨皵鍊�', value: 'bool' }
+ ]
+ },
+ ...rules
+ ])
+ }
+ }
+}
diff --git a/src/components/FormCreate/src/config/useEditorRule.ts b/src/components/FormCreate/src/config/useEditorRule.ts
new file mode 100644
index 0000000..ac6d9ac
--- /dev/null
+++ b/src/components/FormCreate/src/config/useEditorRule.ts
@@ -0,0 +1,32 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useEditorRule = () => {
+ const label = '瀵屾枃鏈�'
+ const name = 'Editor'
+ return {
+ icon: 'icon-editor',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: generateUUID(),
+ title: label,
+ info: '',
+ $required: false
+ }
+ },
+ props(_, { t }) {
+ return localeProps(t, name + '.props', [
+ makeRequiredRule(),
+ {
+ type: 'input',
+ field: 'height',
+ title: '楂樺害'
+ },
+ { type: 'switch', field: 'readonly', title: '鏄惁鍙' }
+ ])
+ }
+ }
+}
diff --git a/src/components/FormCreate/src/config/useSelectRule.ts b/src/components/FormCreate/src/config/useSelectRule.ts
new file mode 100644
index 0000000..e1d77fb
--- /dev/null
+++ b/src/components/FormCreate/src/config/useSelectRule.ts
@@ -0,0 +1,37 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+import { selectRule } from '@/components/FormCreate/src/config/selectRule'
+import { SelectRuleOption } from '@/components/FormCreate/src/type'
+import { cloneDeep } from 'lodash-es'
+
+/**
+ * 閫氱敤閫夋嫨鍣ㄨ鍒� hook
+ *
+ * @param option 瑙勫垯閰嶇疆
+ */
+export const useSelectRule = (option: SelectRuleOption) => {
+ const label = option.label
+ const name = option.name
+ const rules = cloneDeep(selectRule)
+ return {
+ icon: option.icon,
+ label,
+ name,
+ event: option.event,
+ rule() {
+ return {
+ type: name,
+ field: generateUUID(),
+ title: label,
+ info: '',
+ $required: false
+ }
+ },
+ props(_, { t }) {
+ if (!option.props) {
+ option.props = []
+ }
+ return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...rules])
+ }
+ }
+}
diff --git a/src/components/FormCreate/src/config/useUploadFileRule.ts b/src/components/FormCreate/src/config/useUploadFileRule.ts
new file mode 100644
index 0000000..a1ea85e
--- /dev/null
+++ b/src/components/FormCreate/src/config/useUploadFileRule.ts
@@ -0,0 +1,80 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUploadFileRule = () => {
+ const label = '鏂囦欢涓婁紶'
+ const name = 'UploadFile'
+ return {
+ icon: 'icon-upload',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: generateUUID(),
+ title: label,
+ info: '',
+ $required: false
+ }
+ },
+ props(_, { t }) {
+ return localeProps(t, name + '.props', [
+ makeRequiredRule(),
+ {
+ type: 'select',
+ field: 'fileType',
+ title: '鏂囦欢绫诲瀷',
+ value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
+ options: [
+ { label: 'doc', value: 'doc' },
+ { label: 'xls', value: 'xls' },
+ { label: 'ppt', value: 'ppt' },
+ { label: 'txt', value: 'txt' },
+ { label: 'pdf', value: 'pdf' }
+ ],
+ props: {
+ multiple: true
+ }
+ },
+ {
+ type: 'switch',
+ field: 'autoUpload',
+ title: '鏄惁鍦ㄩ�夊彇鏂囦欢鍚庣珛鍗宠繘琛屼笂浼�',
+ value: true
+ },
+ {
+ type: 'switch',
+ field: 'drag',
+ title: '鎷栨嫿涓婁紶',
+ value: false
+ },
+ {
+ type: 'switch',
+ field: 'isShowTip',
+ title: '鏄惁鏄剧ず鎻愮ず',
+ value: true
+ },
+ {
+ type: 'inputNumber',
+ field: 'fileSize',
+ title: '澶у皬闄愬埗(MB)',
+ value: 5,
+ props: { min: 0 }
+ },
+ {
+ type: 'inputNumber',
+ field: 'limit',
+ title: '鏁伴噺闄愬埗',
+ value: 5,
+ props: { min: 0 }
+ },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '鏄惁绂佺敤',
+ value: false
+ }
+ ])
+ }
+ }
+}
diff --git a/src/components/FormCreate/src/config/useUploadImgRule.ts b/src/components/FormCreate/src/config/useUploadImgRule.ts
new file mode 100644
index 0000000..546cf9d
--- /dev/null
+++ b/src/components/FormCreate/src/config/useUploadImgRule.ts
@@ -0,0 +1,89 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUploadImgRule = () => {
+ const label = '鍗曞浘涓婁紶'
+ const name = 'UploadImg'
+ return {
+ icon: 'icon-upload',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: generateUUID(),
+ title: label,
+ info: '',
+ $required: false
+ }
+ },
+ props(_, { t }) {
+ return localeProps(t, name + '.props', [
+ makeRequiredRule(),
+ {
+ type: 'switch',
+ field: 'drag',
+ title: '鎷栨嫿涓婁紶',
+ value: false
+ },
+ {
+ type: 'select',
+ field: 'fileType',
+ title: '鍥剧墖绫诲瀷闄愬埗',
+ value: ['image/jpeg', 'image/png', 'image/gif'],
+ options: [
+ { label: 'image/apng', value: 'image/apng' },
+ { label: 'image/bmp', value: 'image/bmp' },
+ { label: 'image/gif', value: 'image/gif' },
+ { label: 'image/jpeg', value: 'image/jpeg' },
+ { label: 'image/pjpeg', value: 'image/pjpeg' },
+ { label: 'image/svg+xml', value: 'image/svg+xml' },
+ { label: 'image/tiff', value: 'image/tiff' },
+ { label: 'image/webp', value: 'image/webp' },
+ { label: 'image/x-icon', value: 'image/x-icon' }
+ ],
+ props: {
+ multiple: true
+ }
+ },
+ {
+ type: 'inputNumber',
+ field: 'fileSize',
+ title: '澶у皬闄愬埗(MB)',
+ value: 5,
+ props: { min: 0 }
+ },
+ {
+ type: 'input',
+ field: 'height',
+ title: '缁勪欢楂樺害',
+ value: '150px'
+ },
+ {
+ type: 'input',
+ field: 'width',
+ title: '缁勪欢瀹藉害',
+ value: '150px'
+ },
+ {
+ type: 'input',
+ field: 'borderradius',
+ title: '缁勪欢杈规鍦嗚',
+ value: '8px'
+ },
+ {
+ type: 'switch',
+ field: 'disabled',
+ title: '鏄惁鏄剧ず鍒犻櫎鎸夐挳',
+ value: true
+ },
+ {
+ type: 'switch',
+ field: 'showBtnText',
+ title: '鏄惁鏄剧ず鎸夐挳鏂囧瓧',
+ value: true
+ }
+ ])
+ }
+ }
+}
diff --git a/src/components/FormCreate/src/config/useUploadImgsRule.ts b/src/components/FormCreate/src/config/useUploadImgsRule.ts
new file mode 100644
index 0000000..0bf2378
--- /dev/null
+++ b/src/components/FormCreate/src/config/useUploadImgsRule.ts
@@ -0,0 +1,84 @@
+import { generateUUID } from '@/utils'
+import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils'
+
+export const useUploadImgsRule = () => {
+ const label = '澶氬浘涓婁紶'
+ const name = 'UploadImgs'
+ return {
+ icon: 'icon-upload',
+ label,
+ name,
+ rule() {
+ return {
+ type: name,
+ field: generateUUID(),
+ title: label,
+ info: '',
+ $required: false
+ }
+ },
+ props(_, { t }) {
+ return localeProps(t, name + '.props', [
+ makeRequiredRule(),
+ {
+ type: 'switch',
+ field: 'drag',
+ title: '鎷栨嫿涓婁紶',
+ value: false
+ },
+ {
+ type: 'select',
+ field: 'fileType',
+ title: '鍥剧墖绫诲瀷闄愬埗',
+ value: ['image/jpeg', 'image/png', 'image/gif'],
+ options: [
+ { label: 'image/apng', value: 'image/apng' },
+ { label: 'image/bmp', value: 'image/bmp' },
+ { label: 'image/gif', value: 'image/gif' },
+ { label: 'image/jpeg', value: 'image/jpeg' },
+ { label: 'image/pjpeg', value: 'image/pjpeg' },
+ { label: 'image/svg+xml', value: 'image/svg+xml' },
+ { label: 'image/tiff', value: 'image/tiff' },
+ { label: 'image/webp', value: 'image/webp' },
+ { label: 'image/x-icon', value: 'image/x-icon' }
+ ],
+ props: {
+ multiple: true
+ }
+ },
+ {
+ type: 'inputNumber',
+ field: 'fileSize',
+ title: '澶у皬闄愬埗(MB)',
+ value: 5,
+ props: { min: 0 }
+ },
+ {
+ type: 'inputNumber',
+ field: 'limit',
+ title: '鏁伴噺闄愬埗',
+ value: 5,
+ props: { min: 0 }
+ },
+ {
+ type: 'input',
+ field: 'height',
+ title: '缁勪欢楂樺害',
+ value: '150px'
+ },
+ {
+ type: 'input',
+ field: 'width',
+ title: '缁勪欢瀹藉害',
+ value: '150px'
+ },
+ {
+ type: 'input',
+ field: 'borderradius',
+ title: '缁勪欢杈规鍦嗚',
+ value: '8px'
+ }
+ ])
+ }
+ }
+}
diff --git a/src/components/FormCreate/src/type/index.ts b/src/components/FormCreate/src/type/index.ts
new file mode 100644
index 0000000..4a096b9
--- /dev/null
+++ b/src/components/FormCreate/src/type/index.ts
@@ -0,0 +1,31 @@
+// 宸︿晶鎷栨嫿鎸夐挳
+export interface MenuItem {
+ label: string
+ name: string
+ icon: string
+}
+
+// 宸︿晶鎷栨嫿鎸夐挳鍒嗙被
+export interface Menu {
+ title: string
+ name: string
+ list: MenuItem[]
+}
+
+// 閫氱敤涓嬫媺缁勪欢 Props 绫诲瀷
+export interface ApiSelectProps {
+ name: string // 缁勪欢鍚嶇О
+ labelField?: string // 閫夐」鏍囩
+ valueField?: string // 閫夐」鐨勫��
+ url?: string // url 鎺ュ彛
+ isDict?: boolean // 鏄惁瀛楀吀閫夋嫨鍣�
+}
+
+// 閫夋嫨缁勪欢瑙勫垯閰嶇疆绫诲瀷
+export interface SelectRuleOption {
+ label: string // label 鍚嶇О
+ name: string // 缁勪欢鍚嶇О
+ icon: string // 缁勪欢鍥炬爣
+ props?: any[] // 缁勪欢瑙勫垯
+ event?: any[] // 浜嬩欢閰嶇疆
+}
diff --git a/src/components/FormCreate/src/useFormCreateDesigner.ts b/src/components/FormCreate/src/useFormCreateDesigner.ts
new file mode 100644
index 0000000..4e87e43
--- /dev/null
+++ b/src/components/FormCreate/src/useFormCreateDesigner.ts
@@ -0,0 +1,165 @@
+import {
+ useDictSelectRule,
+ useEditorRule,
+ useSelectRule,
+ useUploadFileRule,
+ useUploadImgRule,
+ useUploadImgsRule
+} from './config'
+import { Ref } from 'vue'
+import { Menu } from '@/components/FormCreate/src/type'
+import { apiSelectRule } from '@/components/FormCreate/src/config/selectRule'
+import { generateUUID } from '@/utils'
+
+/**
+ * 琛ㄥ崟璁捐鍣ㄥ寮� hook
+ * 鏂板
+ * - 鏂囦欢涓婁紶
+ * - 鍗曞浘涓婁紶
+ * - 澶氬浘涓婁紶
+ * - 瀛楀吀閫夋嫨鍣�
+ * - 鐢ㄦ埛閫夋嫨鍣�
+ * - 閮ㄩ棬閫夋嫨鍣�
+ * - 瀵屾枃鏈�
+ */
+export const useFormCreateDesigner = async (designer: Ref) => {
+ const editorRule = useEditorRule()
+ const uploadFileRule = useUploadFileRule()
+ const uploadImgRule = useUploadImgRule()
+ const uploadImgsRule = useUploadImgsRule()
+
+ /**
+ * 鏋勫缓琛ㄥ崟缁勪欢
+ */
+ const buildFormComponents = () => {
+ // 绉婚櫎鑷甫鐨勪笂浼犵粍浠惰鍒欙紝浣跨敤 uploadFileRule銆乽ploadImgRule銆乽ploadImgsRule 鏇夸唬
+ designer.value?.removeMenuItem('upload')
+ // 绉婚櫎鑷甫鐨勫瘜鏂囨湰缁勪欢瑙勫垯锛屼娇鐢� editorRule 鏇夸唬
+ designer.value?.removeMenuItem('fcEditor')
+ const components = [editorRule, uploadFileRule, uploadImgRule, uploadImgsRule]
+ components.forEach((component) => {
+ // 鎻掑叆缁勪欢瑙勫垯
+ designer.value?.addComponent(component)
+ // 鎻掑叆鎷栨嫿鎸夐挳鍒� `main` 鍒嗙被涓�
+ designer.value?.appendMenuItem('main', {
+ icon: component.icon,
+ name: component.name,
+ label: component.label
+ })
+ })
+ }
+
+ const userSelectRule = useSelectRule({
+ name: 'UserSelect',
+ label: '鐢ㄦ埛閫夋嫨鍣�',
+ icon: 'icon-user-o'
+ })
+ const deptSelectRule = useSelectRule({
+ name: 'DeptSelect',
+ label: '閮ㄩ棬閫夋嫨鍣�',
+ icon: 'icon-address-card-o',
+ props: [
+ {
+ type: 'select',
+ field: 'returnType',
+ title: '杩斿洖鍊肩被鍨�',
+ value: 'id',
+ options: [
+ { label: '閮ㄩ棬缂栧彿', value: 'id' },
+ { label: '閮ㄩ棬鍚嶇О', value: 'name' }
+ ]
+ }
+ ]
+ })
+ const dictSelectRule = useDictSelectRule()
+ const apiSelectRule0 = useSelectRule({
+ name: 'ApiSelect',
+ label: '鎺ュ彛閫夋嫨鍣�',
+ icon: 'icon-server',
+ props: [...apiSelectRule],
+ event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus']
+ })
+
+ /**
+ * 鏋勫缓绯荤粺瀛楁鑿滃崟
+ */
+ const buildSystemMenu = () => {
+ // 绉婚櫎鑷甫鐨勪笅鎷夐�夋嫨鍣ㄧ粍浠讹紝浣跨敤 currencySelectRule 鏇夸唬
+ // designer.value?.removeMenuItem('select')
+ // designer.value?.removeMenuItem('radio')
+ // designer.value?.removeMenuItem('checkbox')
+ const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0]
+ const menu: Menu = {
+ name: 'system',
+ title: '绯荤粺瀛楁',
+ list: components.map((component) => {
+ // 鎻掑叆缁勪欢瑙勫垯
+ designer.value?.addComponent(component)
+ // 鎻掑叆鎷栨嫿鎸夐挳鍒� `system` 鍒嗙被涓�
+ return {
+ icon: component.icon,
+ name: component.name,
+ label: component.label
+ }
+ })
+ }
+ designer.value?.addMenu(menu)
+ }
+
+ /**
+ * 淇閲嶅鐨勫瓧娈� ID 闂
+ * 褰撳鍒剁粍浠舵椂锛岃嚜鍔ㄤ负鏂扮粍浠剁敓鎴愭柊鐨勫瓧娈� ID
+ *
+ * 瀵瑰簲 issue锛歨ttps://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICM22X
+ */
+ const fixDuplicateFields = () => {
+ // 鑾峰彇褰撳墠鎵�鏈夎鍒�
+ const rules = designer.value?.getRule() || []
+ const fieldIds = new Set<string>()
+ let hasChanges = false
+
+ // 閬嶅巻鎵�鏈夎鍒欙紝妫�娴嬪苟淇閲嶅鐨勫瓧娈� ID
+ rules.forEach((rule: any) => {
+ if (rule.field) {
+ if (fieldIds.has(rule.field)) {
+ // 鍙戠幇閲嶅锛岀敓鎴愭柊鐨処D
+ const oldField = rule.field
+ const newField = generateUUID()
+ console.log(`[FormCreate] 妫�娴嬪埌閲嶅瀛楁ID: ${oldField}, 宸茶嚜鍔ㄦ洿鏂颁负: ${newField}`)
+ rule.field = newField
+ hasChanges = true
+ } else {
+ fieldIds.add(rule.field)
+ }
+ }
+ })
+
+ // 濡傛灉鏈夐噸澶嶅瓧娈佃淇锛屾洿鏂拌璁″櫒
+ if (hasChanges) {
+ designer.value?.setRule(rules)
+ }
+
+ return hasChanges
+ }
+
+ onMounted(async () => {
+ await nextTick()
+ buildFormComponents()
+ buildSystemMenu()
+
+ // 鐩戝惉璁捐鍣ㄥ唴瀹瑰彉鍖栵紝鑷姩淇閲嶅瀛楁ID
+ let isFixing = false // 闃叉鏃犻檺寰幆
+ watch(
+ () => designer.value?.getRule(),
+ async () => {
+ if (!isFixing) {
+ isFixing = true
+ await nextTick()
+ fixDuplicateFields()
+ isFixing = false
+ }
+ },
+ { deep: true }
+ )
+ })
+}
diff --git a/src/components/FormCreate/src/utils/index.ts b/src/components/FormCreate/src/utils/index.ts
new file mode 100644
index 0000000..a2b3e67
--- /dev/null
+++ b/src/components/FormCreate/src/utils/index.ts
@@ -0,0 +1,61 @@
+export function makeRequiredRule() {
+ return {
+ type: 'Required',
+ field: 'formCreate$required',
+ title: '鏄惁蹇呭~'
+ }
+}
+
+export const localeProps = (t, prefix, rules) => {
+ return rules.map((rule) => {
+ if (rule.field === 'formCreate$required') {
+ rule.title = t('props.required') || rule.title
+ } else if (rule.field && rule.field !== '_optionType') {
+ rule.title = t('components.' + prefix + '.' + rule.field) || rule.title
+ }
+ return rule
+ })
+}
+
+/**
+ * 瑙f瀽琛ㄥ崟缁勪欢鐨� field, title 绛夊瓧娈碉紙閫掑綊锛屽鏋滅粍浠跺寘鍚瓙缁勪欢锛�
+ *
+ * @param rule 缁勪欢鐨勭敓鎴愯鍒� https://www.form-create.com/v3/guide/rule
+ * @param fields 瑙f瀽鍚庤〃鍗曠粍浠跺瓧娈�
+ * @param parentTitle 濡傛灉鏄瓙琛ㄥ崟锛屽瓙琛ㄥ崟鐨勬爣棰橈紝榛樿涓虹┖
+ */
+export const parseFormFields = (
+ rule: Record<string, any>,
+ fields: Array<Record<string, any>> = [],
+ parentTitle: string = ''
+) => {
+ const { type, field, $required, title: tempTitle, children } = rule
+ if (field && tempTitle) {
+ let title = tempTitle
+ if (parentTitle) {
+ title = `${parentTitle}.${tempTitle}`
+ }
+ let required = false
+ if ($required) {
+ required = true
+ }
+ fields.push({
+ field,
+ title,
+ type,
+ required
+ })
+ // TODO 瀛愯〃鍗� 闇�瑕佸鐞嗗瓙琛ㄥ崟瀛楁
+ // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+ // // 瑙f瀽瀛愯〃鍗曠殑瀛楁
+ // rule.props.rule.forEach((item) => {
+ // parseFields(item, fieldsPermission, title)
+ // })
+ // }
+ }
+ if (children && Array.isArray(children)) {
+ children.forEach((rule) => {
+ parseFormFields(rule, fields)
+ })
+ }
+}
diff --git a/src/components/Highlight/index.ts b/src/components/Highlight/index.ts
new file mode 100644
index 0000000..3e2d9ed
--- /dev/null
+++ b/src/components/Highlight/index.ts
@@ -0,0 +1,3 @@
+import Highlight from './src/Highlight.vue'
+
+export { Highlight }
diff --git a/src/components/Highlight/src/Highlight.vue b/src/components/Highlight/src/Highlight.vue
new file mode 100644
index 0000000..ef923a9
--- /dev/null
+++ b/src/components/Highlight/src/Highlight.vue
@@ -0,0 +1,65 @@
+<script lang="tsx">
+import { defineComponent, PropType, computed, h, unref } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+export default defineComponent({
+ name: 'Highlight',
+ props: {
+ tag: propTypes.string.def('span'),
+ keys: {
+ type: Array as PropType<string[]>,
+ default: () => []
+ },
+ color: propTypes.string.def('var(--el-color-primary)')
+ },
+ emits: ['click'],
+ setup(props, { emit, slots }) {
+ const keyNodes = computed(() => {
+ return props.keys.map((key) => {
+ return h(
+ 'span',
+ {
+ onClick: () => {
+ emit('click', key)
+ },
+ style: {
+ color: props.color,
+ cursor: 'pointer'
+ }
+ },
+ key
+ )
+ })
+ })
+
+ const parseText = (text: string) => {
+ props.keys.forEach((key, index) => {
+ const regexp = new RegExp(key, 'g')
+ text = text.replace(regexp, `{{${index}}}`)
+ })
+ return text.split(/{{|}}/)
+ }
+
+ const renderText = () => {
+ if (!slots?.default) return null
+ const node = slots?.default()[0].children
+
+ if (!node) {
+ return slots?.default()[0]
+ }
+
+ const textArray = parseText(node as string)
+ const regexp = /^[0-9]*$/
+ const nodes = textArray.map((t) => {
+ if (regexp.test(t)) {
+ return unref(keyNodes)[t] || t
+ }
+ return t
+ })
+ return h(props.tag, nodes)
+ }
+
+ return () => renderText()
+ }
+})
+</script>
diff --git a/src/components/IFrame/index.ts b/src/components/IFrame/index.ts
new file mode 100644
index 0000000..9f8cf24
--- /dev/null
+++ b/src/components/IFrame/index.ts
@@ -0,0 +1,3 @@
+import IFrame from './src/IFrame.vue'
+
+export { IFrame }
diff --git a/src/components/IFrame/src/IFrame.vue b/src/components/IFrame/src/IFrame.vue
new file mode 100644
index 0000000..64ffc0e
--- /dev/null
+++ b/src/components/IFrame/src/IFrame.vue
@@ -0,0 +1,47 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'IFrame' })
+
+const props = defineProps({
+ src: propTypes.string.def('')
+})
+const loading = ref(true)
+const frameRef = ref<HTMLElement | null>(null)
+const init = () => {
+ nextTick(() => {
+ loading.value = true
+ if (!frameRef.value) return
+ frameRef.value.onload = () => {
+ loading.value = false
+ }
+ })
+}
+onMounted(() => {
+ init()
+})
+watch(
+ () => props.src,
+ () => {
+ init()
+ }
+)
+</script>
+<template>
+ <div
+ v-loading="loading"
+ class="w-full h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
+ >
+ <iframe
+ ref="frameRef"
+ :src="props.src"
+ frameborder="0"
+ scrolling="auto"
+ height="100%"
+ width="100%"
+ allowfullscreen="true"
+ webkitallowfullscreen="true"
+ mozallowfullscreen="true"
+ ></iframe>
+ </div>
+</template>
diff --git a/src/components/Icon/index.ts b/src/components/Icon/index.ts
new file mode 100644
index 0000000..33d1de3
--- /dev/null
+++ b/src/components/Icon/index.ts
@@ -0,0 +1,4 @@
+import Icon from './src/Icon.vue'
+import IconSelect from './src/IconSelect.vue'
+
+export { Icon, IconSelect }
diff --git a/src/components/Icon/src/Icon.vue b/src/components/Icon/src/Icon.vue
new file mode 100644
index 0000000..7e2ec94
--- /dev/null
+++ b/src/components/Icon/src/Icon.vue
@@ -0,0 +1,86 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import Iconify from '@purge-icons/generated'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'Icon' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('icon')
+
+const props = defineProps({
+ // icon name
+ icon: propTypes.string,
+ // icon color
+ color: propTypes.string,
+ // icon size
+ size: propTypes.number.def(16),
+ // icon svg class
+ svgClass: propTypes.string.def('')
+})
+
+const elRef = ref<ElRef>(null)
+
+const isLocal = computed(() => props.icon?.startsWith('svg-icon:'))
+
+const symbolId = computed(() => {
+ return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
+})
+
+const getIconifyStyle = computed(() => {
+ const { color, size } = props
+ return {
+ fontSize: `${size}px`,
+ height: '1em',
+ color
+ }
+})
+
+const getSvgClass = computed(() => {
+ const { svgClass } = props
+ return `iconify ${svgClass}`
+})
+
+const updateIcon = async (icon: string) => {
+ if (unref(isLocal)) return
+
+ const el = unref(elRef)
+ if (!el) return
+
+ await nextTick()
+
+ if (!icon) return
+
+ const svg = Iconify.renderSVG(icon, {})
+ if (svg) {
+ el.textContent = ''
+ el.appendChild(svg)
+ } else {
+ const span = document.createElement('span')
+ span.className = 'iconify'
+ span.dataset.icon = icon
+ el.textContent = ''
+ el.appendChild(span)
+ }
+}
+
+watch(
+ () => props.icon,
+ (icon: string) => {
+ updateIcon(icon)
+ }
+)
+</script>
+
+<template>
+ <ElIcon :class="prefixCls" :color="color" :size="size">
+ <svg v-if="isLocal" :class="getSvgClass">
+ <use :xlink:href="symbolId" />
+ </svg>
+
+ <span v-else ref="elRef" :class="$attrs.class" :style="getIconifyStyle">
+ <span :class="getSvgClass" :data-icon="symbolId"></span>
+ </span>
+ </ElIcon>
+</template>
diff --git a/src/components/Icon/src/IconSelect.vue b/src/components/Icon/src/IconSelect.vue
new file mode 100644
index 0000000..76cc6d5
--- /dev/null
+++ b/src/components/Icon/src/IconSelect.vue
@@ -0,0 +1,239 @@
+<script lang="ts" setup>
+import { CSSProperties } from 'vue'
+import { cloneDeep } from 'lodash-es'
+import { IconJson } from '@/components/Icon/src/data'
+
+defineOptions({ name: 'IconSelect' })
+
+type ParameterCSSProperties = (item?: string) => CSSProperties | undefined
+
+const props = defineProps({
+ modelValue: {
+ require: false,
+ type: String
+ },
+ clearable: {
+ require: false,
+ type: Boolean
+ }
+})
+const emit = defineEmits<{ (e: 'update:modelValue', v: string) }>()
+
+const visible = ref(false)
+const inputValue = toRef(props, 'modelValue')
+const iconList = ref(IconJson)
+const icon = ref('add-location')
+const currentActiveType = ref('ep:')
+// 娣辨嫹璐濆浘鏍囨暟鎹紝鍓嶇鍋氭悳绱�
+const copyIconList = cloneDeep(iconList.value)
+
+const pageSize = ref(96)
+const currentPage = ref(1)
+
+// 鎼滅储鏉′欢
+const filterValue = ref('')
+
+const tabsList = [
+ {
+ label: 'Element Plus',
+ name: 'ep:'
+ },
+ {
+ label: 'Font Awesome 4',
+ name: 'fa:'
+ },
+ {
+ label: 'Font Awesome 5 Solid',
+ name: 'fa-solid:'
+ }
+]
+
+const pageList = computed(() => {
+ if (currentPage.value === 1) {
+ return copyIconList[currentActiveType.value]
+ ?.filter((v) => v.includes(filterValue.value))
+ .slice(currentPage.value - 1, pageSize.value)
+ } else {
+ return copyIconList[currentActiveType.value]
+ ?.filter((v) => v.includes(filterValue.value))
+ .slice(
+ pageSize.value * (currentPage.value - 1),
+ pageSize.value * (currentPage.value - 1) + pageSize.value
+ )
+ }
+})
+const iconCount = computed(() => {
+ return copyIconList[currentActiveType.value] == undefined
+ ? 0
+ : copyIconList[currentActiveType.value].length
+})
+
+const iconItemStyle = computed((): ParameterCSSProperties => {
+ return (item) => {
+ if (inputValue.value === currentActiveType.value + item) {
+ return {
+ borderColor: 'var(--el-color-primary)',
+ color: 'var(--el-color-primary)'
+ }
+ }
+ }
+})
+
+function handleClick({ props }) {
+ currentPage.value = 1
+ currentActiveType.value = props.name
+ emit('update:modelValue', currentActiveType.value + iconList.value[currentActiveType.value][0])
+ icon.value = iconList.value[currentActiveType.value][0]
+}
+
+function onChangeIcon(item) {
+ icon.value = item
+ emit('update:modelValue', currentActiveType.value + item)
+ visible.value = false
+}
+
+function onCurrentChange(page) {
+ currentPage.value = page
+}
+
+function clearIcon() {
+ icon.value = ''
+ emit('update:modelValue', '')
+ visible.value = false
+}
+
+watch(
+ () => {
+ return props.modelValue
+ },
+ () => {
+ if (props.modelValue && props.modelValue.indexOf(':') >= 0) {
+ currentActiveType.value = props.modelValue.substring(0, props.modelValue.indexOf(':') + 1)
+ icon.value = props.modelValue.substring(props.modelValue.indexOf(':') + 1)
+ }
+ }
+)
+watch(
+ () => {
+ return filterValue.value
+ },
+ () => {
+ currentPage.value = 1
+ }
+)
+</script>
+
+<template>
+ <div class="selector">
+ <ElInput v-model="inputValue" @click="visible = !visible" :clearable="props.clearable" @clear="clearIcon">
+ <template #append>
+ <ElPopover
+ :popper-options="{
+ placement: 'auto'
+ }"
+ :visible="visible"
+ :width="355"
+ popper-class="pure-popper"
+ trigger="click"
+ >
+ <template #reference>
+ <div
+ class="h-32px w-40px flex cursor-pointer items-center justify-center"
+ @click="visible = !visible"
+ >
+ <Icon :icon="currentActiveType + icon" />
+ </div>
+ </template>
+
+ <ElInput v-model="filterValue" class="p-2" clearable placeholder="鎼滅储鍥炬爣" />
+ <ElDivider border-style="dashed" />
+
+ <ElTabs v-model="currentActiveType" @tab-click="handleClick">
+ <ElTabPane
+ v-for="(pane, index) in tabsList"
+ :key="index"
+ :label="pane.label"
+ :name="pane.name"
+ >
+ <ElDivider border-style="dashed" class="tab-divider" />
+ <ElScrollbar height="220px">
+ <ul class="ml-2 flex flex-wrap">
+ <li
+ v-for="(item, key) in pageList"
+ :key="key"
+ :style="iconItemStyle(item)"
+ :title="item"
+ class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
+ @click="onChangeIcon(item)"
+ >
+ <Icon :icon="currentActiveType + item" />
+ </li>
+ </ul>
+ </ElScrollbar>
+ </ElTabPane>
+ </ElTabs>
+ <ElDivider border-style="dashed" />
+
+ <ElPagination
+ :current-page="currentPage"
+ :page-size="pageSize"
+ :total="iconCount"
+ background
+ class="h-10 flex items-center justify-center"
+ layout="prev, pager, next"
+ size="small"
+ @current-change="onCurrentChange"
+ />
+ </ElPopover>
+ </template>
+ </ElInput>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+.el-divider--horizontal {
+ margin: 1px auto !important;
+}
+
+.tab-divider.el-divider--horizontal {
+ margin: 0 !important;
+}
+
+.icon-item {
+ &:hover {
+ color: var(--el-color-primary);
+ border-color: var(--el-color-primary);
+ transform: scaleX(1.05);
+ transition: all 0.4s;
+ }
+}
+
+:deep(.el-tabs__nav-next) {
+ font-size: 15px;
+ line-height: 32px;
+ box-shadow: -5px 0 5px -6px #ccc;
+}
+
+:deep(.el-tabs__nav-prev) {
+ font-size: 15px;
+ line-height: 32px;
+ box-shadow: 5px 0 5px -6px #ccc;
+}
+
+:deep(.el-input-group__append) {
+ padding: 0;
+}
+
+:deep(.el-tabs__item) {
+ height: 30px;
+ font-size: 12px;
+ font-weight: normal;
+ line-height: 30px;
+}
+
+:deep(.el-tabs__header),
+:deep(.el-tabs__nav-wrap) {
+ position: static;
+ margin: 0;
+}
+</style>
diff --git a/src/components/Icon/src/data.ts b/src/components/Icon/src/data.ts
new file mode 100644
index 0000000..2a4ed5a
--- /dev/null
+++ b/src/components/Icon/src/data.ts
@@ -0,0 +1,1961 @@
+export const IconJson = {
+ 'ep:': [
+ 'add-location',
+ 'aim',
+ 'alarm-clock',
+ 'apple',
+ 'arrow-down',
+ 'arrow-down-bold',
+ 'arrow-left',
+ 'arrow-left-bold',
+ 'arrow-right',
+ 'arrow-right-bold',
+ 'arrow-up',
+ 'arrow-up-bold',
+ 'avatar',
+ 'back',
+ 'baseball',
+ 'basketball',
+ 'bell',
+ 'bell-filled',
+ 'bicycle',
+ 'bottom',
+ 'bottom-left',
+ 'bottom-right',
+ 'bowl',
+ 'box',
+ 'briefcase',
+ 'brush',
+ 'brush-filled',
+ 'burger',
+ 'calendar',
+ 'camera',
+ 'camera-filled',
+ 'caret-bottom',
+ 'caret-left',
+ 'caret-right',
+ 'caret-top',
+ 'cellphone',
+ 'chat-dot-round',
+ 'chat-dot-square',
+ 'chat-line-round',
+ 'chat-line-square',
+ 'chat-round',
+ 'chat-square',
+ 'check',
+ 'checked',
+ 'cherry',
+ 'chicken',
+ 'circle-check',
+ 'circle-check-filled',
+ 'circle-close',
+ 'circle-close-filled',
+ 'circle-plus',
+ 'circle-plus-filled',
+ 'clock',
+ 'close',
+ 'close-bold',
+ 'cloudy',
+ 'coffee',
+ 'coffee-cup',
+ 'coin',
+ 'cold-drink',
+ 'collection',
+ 'collection-tag',
+ 'comment',
+ 'compass',
+ 'connection',
+ 'coordinate',
+ 'copy-document',
+ 'cpu',
+ 'credit-card',
+ 'crop',
+ 'd-arrow-left',
+ 'd-arrow-right',
+ 'd-caret',
+ 'data-analysis',
+ 'data-board',
+ 'data-line',
+ 'delete',
+ 'delete-filled',
+ 'delete-location',
+ 'dessert',
+ 'discount',
+ 'dish',
+ 'dish-dot',
+ 'document',
+ 'document-add',
+ 'document-checked',
+ 'document-copy',
+ 'document-delete',
+ 'document-remove',
+ 'download',
+ 'drizzling',
+ 'edit',
+ 'edit-pen',
+ 'eleme',
+ 'eleme-filled',
+ 'expand',
+ 'failed',
+ 'female',
+ 'files',
+ 'film',
+ 'filter',
+ 'finished',
+ 'first-aid-kit',
+ 'flag',
+ 'fold',
+ 'folder',
+ 'folder-add',
+ 'folder-checked',
+ 'folder-delete',
+ 'folder-opened',
+ 'folder-remove',
+ 'food',
+ 'football',
+ 'fork-spoon',
+ 'fries',
+ 'full-screen',
+ 'goblet',
+ 'goblet-full',
+ 'goblet-square',
+ 'goblet-square-full',
+ 'goods',
+ 'goods-filled',
+ 'grape',
+ 'grid',
+ 'guide',
+ 'headset',
+ 'help',
+ 'help-filled',
+ 'histogram',
+ 'home-filled',
+ 'hot-water',
+ 'house',
+ 'ice-cream',
+ 'ice-cream-round',
+ 'ice-cream-square',
+ 'ice-drink',
+ 'ice-tea',
+ 'info-filled',
+ 'iphone',
+ 'key',
+ 'knife-fork',
+ 'lightning',
+ 'link',
+ 'list',
+ 'loading',
+ 'location',
+ 'location-filled',
+ 'location-information',
+ 'lock',
+ 'lollipop',
+ 'magic-stick',
+ 'magnet',
+ 'male',
+ 'management',
+ 'map-location',
+ 'medal',
+ 'menu',
+ 'message',
+ 'message-box',
+ 'mic',
+ 'microphone',
+ 'milk-tea',
+ 'minus',
+ 'money',
+ 'monitor',
+ 'moon',
+ 'moon-night',
+ 'more',
+ 'more-filled',
+ 'mostly-cloudy',
+ 'mouse',
+ 'mug',
+ 'mute',
+ 'mute-notification',
+ 'no-smoking',
+ 'notebook',
+ 'notification',
+ 'odometer',
+ 'office-building',
+ 'open',
+ 'operation',
+ 'opportunity',
+ 'orange',
+ 'paperclip',
+ 'partly-cloudy',
+ 'pear',
+ 'phone',
+ 'phone-filled',
+ 'picture',
+ 'picture-filled',
+ 'picture-rounded',
+ 'pie-chart',
+ 'place',
+ 'platform',
+ 'plus',
+ 'pointer',
+ 'position',
+ 'postcard',
+ 'pouring',
+ 'present',
+ 'price-tag',
+ 'printer',
+ 'promotion',
+ 'question-filled',
+ 'rank',
+ 'reading',
+ 'reading-lamp',
+ 'refresh',
+ 'refresh-left',
+ 'refresh-right',
+ 'refrigerator',
+ 'remove',
+ 'remove-filled',
+ 'right',
+ 'scale-to-original',
+ 'school',
+ 'scissor',
+ 'search',
+ 'select',
+ 'sell',
+ 'semi-select',
+ 'service',
+ 'set-up',
+ 'setting',
+ 'share',
+ 'ship',
+ 'shop',
+ 'shopping-bag',
+ 'shopping-cart',
+ 'shopping-cart-full',
+ 'smoking',
+ 'soccer',
+ 'sold-out',
+ 'sort',
+ 'sort-down',
+ 'sort-up',
+ 'stamp',
+ 'star',
+ 'star-filled',
+ 'stopwatch',
+ 'success-filled',
+ 'sugar',
+ 'suitcase',
+ 'sunny',
+ 'sunrise',
+ 'sunset',
+ 'switch',
+ 'switch-button',
+ 'takeaway-box',
+ 'ticket',
+ 'tickets',
+ 'timer',
+ 'toilet-paper',
+ 'tools',
+ 'top',
+ 'top-left',
+ 'top-right',
+ 'trend-charts',
+ 'trophy',
+ 'turn-off',
+ 'umbrella',
+ 'unlock',
+ 'upload',
+ 'upload-filled',
+ 'user',
+ 'user-filled',
+ 'van',
+ 'video-camera',
+ 'video-camera-filled',
+ 'video-pause',
+ 'video-play',
+ 'view',
+ 'wallet',
+ 'wallet-filled',
+ 'warning',
+ 'warning-filled',
+ 'watch',
+ 'watermelon',
+ 'wind-power',
+ 'zoom-in',
+ 'zoom-out'
+ ],
+ 'fa:': [
+ '500px',
+ 'address-book',
+ 'address-book-o',
+ 'address-card',
+ 'address-card-o',
+ 'adjust',
+ 'adn',
+ 'align-center',
+ 'align-justify',
+ 'align-left',
+ 'amazon',
+ 'ambulance',
+ 'american-sign-language-interpreting',
+ 'anchor',
+ 'android',
+ 'angellist',
+ 'angle-double-left',
+ 'angle-double-up',
+ 'angle-down',
+ 'angle-left',
+ 'angle-up',
+ 'apple',
+ 'archive',
+ 'area-chart',
+ 'arrow-circle-left',
+ 'arrow-circle-o-left',
+ 'arrow-circle-o-up',
+ 'arrow-circle-up',
+ 'arrow-left',
+ 'arrow-up',
+ 'arrows',
+ 'arrows-alt',
+ 'arrows-h',
+ 'arrows-v',
+ 'assistive-listening-systems',
+ 'asterisk',
+ 'at',
+ 'audio-description',
+ 'automobile',
+ 'backward',
+ 'balance-scale',
+ 'ban',
+ 'bandcamp',
+ 'bank',
+ 'bar-chart',
+ 'barcode',
+ 'bars',
+ 'bath',
+ 'battery',
+ 'battery-0',
+ 'battery-1',
+ 'battery-2',
+ 'battery-3',
+ 'bed',
+ 'beer',
+ 'behance',
+ 'behance-square',
+ 'bell',
+ 'bell-o',
+ 'bell-slash',
+ 'bell-slash-o',
+ 'bicycle',
+ 'binoculars',
+ 'birthday-cake',
+ 'bitbucket',
+ 'bitbucket-square',
+ 'bitcoin',
+ 'black-tie',
+ 'blind',
+ 'bluetooth',
+ 'bluetooth-b',
+ 'bold',
+ 'bolt',
+ 'bomb',
+ 'book',
+ 'bookmark',
+ 'bookmark-o',
+ 'braille',
+ 'briefcase',
+ 'bug',
+ 'building',
+ 'building-o',
+ 'bullhorn',
+ 'bullseye',
+ 'bus',
+ 'buysellads',
+ 'cab',
+ 'calculator',
+ 'calendar',
+ 'calendar-check-o',
+ 'calendar-minus-o',
+ 'calendar-o',
+ 'calendar-plus-o',
+ 'calendar-times-o',
+ 'camera',
+ 'camera-retro',
+ 'caret-down',
+ 'caret-left',
+ 'caret-square-o-left',
+ 'caret-square-o-up',
+ 'caret-up',
+ 'cart-arrow-down',
+ 'cart-plus',
+ 'cc',
+ 'cc-amex',
+ 'cc-diners-club',
+ 'cc-discover',
+ 'cc-jcb',
+ 'cc-mastercard',
+ 'cc-paypal',
+ 'cc-stripe',
+ 'cc-visa',
+ 'certificate',
+ 'chain',
+ 'chain-broken',
+ 'check',
+ 'check-circle',
+ 'check-circle-o',
+ 'check-square',
+ 'check-square-o',
+ 'chevron-circle-left',
+ 'chevron-circle-up',
+ 'chevron-down',
+ 'chevron-left',
+ 'chevron-up',
+ 'child',
+ 'chrome',
+ 'circle',
+ 'circle-o',
+ 'circle-o-notch',
+ 'circle-thin',
+ 'clipboard',
+ 'clock-o',
+ 'clone',
+ 'close',
+ 'cloud',
+ 'cloud-download',
+ 'cloud-upload',
+ 'cny',
+ 'code',
+ 'code-fork',
+ 'codepen',
+ 'codiepie',
+ 'coffee',
+ 'cog',
+ 'cogs',
+ 'columns',
+ 'comment',
+ 'comment-o',
+ 'commenting',
+ 'commenting-o',
+ 'comments',
+ 'comments-o',
+ 'compass',
+ 'compress',
+ 'connectdevelop',
+ 'contao',
+ 'copy',
+ 'copyright',
+ 'creative-commons',
+ 'credit-card',
+ 'credit-card-alt',
+ 'crop',
+ 'crosshairs',
+ 'css3',
+ 'cube',
+ 'cubes',
+ 'cut',
+ 'cutlery',
+ 'dashboard',
+ 'dashcube',
+ 'database',
+ 'deaf',
+ 'dedent',
+ 'delicious',
+ 'desktop',
+ 'deviantart',
+ 'diamond',
+ 'digg',
+ 'dollar',
+ 'dot-circle-o',
+ 'download',
+ 'dribbble',
+ 'drivers-license',
+ 'drivers-license-o',
+ 'dropbox',
+ 'drupal',
+ 'edge',
+ 'edit',
+ 'eercast',
+ 'eject',
+ 'ellipsis-h',
+ 'ellipsis-v',
+ 'empire',
+ 'envelope',
+ 'envelope-o',
+ 'envelope-open',
+ 'envelope-open-o',
+ 'envelope-square',
+ 'envira',
+ 'eraser',
+ 'etsy',
+ 'eur',
+ 'exchange',
+ 'exclamation',
+ 'exclamation-circle',
+ 'exclamation-triangle',
+ 'expand',
+ 'expeditedssl',
+ 'external-link',
+ 'external-link-square',
+ 'eye',
+ 'eye-slash',
+ 'eyedropper',
+ 'fa',
+ 'facebook',
+ 'facebook-official',
+ 'facebook-square',
+ 'fast-backward',
+ 'fax',
+ 'feed',
+ 'female',
+ 'fighter-jet',
+ 'file',
+ 'file-archive-o',
+ 'file-audio-o',
+ 'file-code-o',
+ 'file-excel-o',
+ 'file-image-o',
+ 'file-movie-o',
+ 'file-o',
+ 'file-pdf-o',
+ 'file-powerpoint-o',
+ 'file-text',
+ 'file-text-o',
+ 'file-word-o',
+ 'film',
+ 'filter',
+ 'fire',
+ 'fire-extinguisher',
+ 'firefox',
+ 'first-order',
+ 'flag',
+ 'flag-checkered',
+ 'flag-o',
+ 'flask',
+ 'flickr',
+ 'floppy-o',
+ 'folder',
+ 'folder-o',
+ 'folder-open',
+ 'folder-open-o',
+ 'font',
+ 'fonticons',
+ 'fort-awesome',
+ 'forumbee',
+ 'foursquare',
+ 'free-code-camp',
+ 'frown-o',
+ 'futbol-o',
+ 'gamepad',
+ 'gavel',
+ 'gbp',
+ 'genderless',
+ 'get-pocket',
+ 'gg',
+ 'gg-circle',
+ 'gift',
+ 'git',
+ 'git-square',
+ 'github',
+ 'github-alt',
+ 'github-square',
+ 'gitlab',
+ 'gittip',
+ 'glass',
+ 'glide',
+ 'glide-g',
+ 'globe',
+ 'google',
+ 'google-plus',
+ 'google-plus-circle',
+ 'google-plus-square',
+ 'google-wallet',
+ 'graduation-cap',
+ 'grav',
+ 'group',
+ 'h-square',
+ 'hacker-news',
+ 'hand-grab-o',
+ 'hand-lizard-o',
+ 'hand-o-left',
+ 'hand-o-up',
+ 'hand-paper-o',
+ 'hand-peace-o',
+ 'hand-pointer-o',
+ 'hand-scissors-o',
+ 'hand-spock-o',
+ 'handshake-o',
+ 'hashtag',
+ 'hdd-o',
+ 'header',
+ 'headphones',
+ 'heart',
+ 'heart-o',
+ 'heartbeat',
+ 'history',
+ 'home',
+ 'hospital-o',
+ 'hourglass',
+ 'hourglass-1',
+ 'hourglass-2',
+ 'hourglass-3',
+ 'hourglass-o',
+ 'houzz',
+ 'html5',
+ 'i-cursor',
+ 'id-badge',
+ 'ils',
+ 'image',
+ 'imdb',
+ 'inbox',
+ 'indent',
+ 'industry',
+ 'info',
+ 'info-circle',
+ 'inr',
+ 'instagram',
+ 'internet-explorer',
+ 'intersex',
+ 'ioxhost',
+ 'italic',
+ 'joomla',
+ 'jsfiddle',
+ 'key',
+ 'keyboard-o',
+ 'krw',
+ 'language',
+ 'laptop',
+ 'lastfm',
+ 'lastfm-square',
+ 'leaf',
+ 'leanpub',
+ 'lemon-o',
+ 'level-up',
+ 'life-bouy',
+ 'lightbulb-o',
+ 'line-chart',
+ 'linkedin',
+ 'linkedin-square',
+ 'linode',
+ 'linux',
+ 'list',
+ 'list-alt',
+ 'list-ol',
+ 'list-ul',
+ 'location-arrow',
+ 'lock',
+ 'long-arrow-left',
+ 'long-arrow-up',
+ 'low-vision',
+ 'magic',
+ 'magnet',
+ 'mail-forward',
+ 'mail-reply',
+ 'mail-reply-all',
+ 'male',
+ 'map',
+ 'map-marker',
+ 'map-o',
+ 'map-pin',
+ 'map-signs',
+ 'mars',
+ 'mars-double',
+ 'mars-stroke',
+ 'mars-stroke-h',
+ 'mars-stroke-v',
+ 'maxcdn',
+ 'meanpath',
+ 'medium',
+ 'medkit',
+ 'meetup',
+ 'meh-o',
+ 'mercury',
+ 'microchip',
+ 'microphone',
+ 'microphone-slash',
+ 'minus',
+ 'minus-circle',
+ 'minus-square',
+ 'minus-square-o',
+ 'mixcloud',
+ 'mobile',
+ 'modx',
+ 'money',
+ 'moon-o',
+ 'motorcycle',
+ 'mouse-pointer',
+ 'music',
+ 'neuter',
+ 'newspaper-o',
+ 'object-group',
+ 'object-ungroup',
+ 'odnoklassniki',
+ 'odnoklassniki-square',
+ 'opencart',
+ 'openid',
+ 'opera',
+ 'optin-monster',
+ 'pagelines',
+ 'paint-brush',
+ 'paper-plane',
+ 'paper-plane-o',
+ 'paperclip',
+ 'paragraph',
+ 'pause',
+ 'pause-circle',
+ 'pause-circle-o',
+ 'paw',
+ 'paypal',
+ 'pencil',
+ 'pencil-square',
+ 'percent',
+ 'phone',
+ 'phone-square',
+ 'pie-chart',
+ 'pied-piper',
+ 'pied-piper-alt',
+ 'pied-piper-pp',
+ 'pinterest',
+ 'pinterest-p',
+ 'pinterest-square',
+ 'plane',
+ 'play',
+ 'play-circle',
+ 'play-circle-o',
+ 'plug',
+ 'plus',
+ 'plus-circle',
+ 'plus-square',
+ 'plus-square-o',
+ 'podcast',
+ 'power-off',
+ 'print',
+ 'product-hunt',
+ 'puzzle-piece',
+ 'qq',
+ 'qrcode',
+ 'question',
+ 'question-circle',
+ 'question-circle-o',
+ 'quora',
+ 'quote-left',
+ 'quote-right',
+ 'ra',
+ 'random',
+ 'ravelry',
+ 'recycle',
+ 'reddit',
+ 'reddit-alien',
+ 'reddit-square',
+ 'refresh',
+ 'registered',
+ 'renren',
+ 'repeat',
+ 'retweet',
+ 'road',
+ 'rocket',
+ 'rotate-left',
+ 'rouble',
+ 'rss-square',
+ 'safari',
+ 'scribd',
+ 'search',
+ 'search-minus',
+ 'search-plus',
+ 'sellsy',
+ 'server',
+ 'share-alt',
+ 'share-alt-square',
+ 'share-square',
+ 'share-square-o',
+ 'shield',
+ 'ship',
+ 'shirtsinbulk',
+ 'shopping-bag',
+ 'shopping-basket',
+ 'shopping-cart',
+ 'shower',
+ 'sign-in',
+ 'sign-language',
+ 'sign-out',
+ 'signal',
+ 'simplybuilt',
+ 'sitemap',
+ 'skyatlas',
+ 'skype',
+ 'slack',
+ 'sliders',
+ 'slideshare',
+ 'smile-o',
+ 'snapchat',
+ 'snapchat-ghost',
+ 'snapchat-square',
+ 'snowflake-o',
+ 'sort',
+ 'sort-alpha-asc',
+ 'sort-alpha-desc',
+ 'sort-amount-asc',
+ 'sort-amount-desc',
+ 'sort-asc',
+ 'sort-numeric-asc',
+ 'sort-numeric-desc',
+ 'soundcloud',
+ 'space-shuttle',
+ 'spinner',
+ 'spoon',
+ 'spotify',
+ 'square',
+ 'square-o',
+ 'stack-exchange',
+ 'stack-overflow',
+ 'star',
+ 'star-half',
+ 'star-half-empty',
+ 'star-o',
+ 'steam',
+ 'steam-square',
+ 'step-backward',
+ 'stethoscope',
+ 'sticky-note',
+ 'sticky-note-o',
+ 'stop',
+ 'stop-circle',
+ 'stop-circle-o',
+ 'street-view',
+ 'strikethrough',
+ 'stumbleupon',
+ 'stumbleupon-circle',
+ 'subscript',
+ 'subway',
+ 'suitcase',
+ 'sun-o',
+ 'superpowers',
+ 'superscript',
+ 'table',
+ 'tablet',
+ 'tag',
+ 'tags',
+ 'tasks',
+ 'telegram',
+ 'television',
+ 'tencent-weibo',
+ 'terminal',
+ 'text-height',
+ 'text-width',
+ 'th',
+ 'th-large',
+ 'th-list',
+ 'themeisle',
+ 'thermometer',
+ 'thermometer-0',
+ 'thermometer-1',
+ 'thermometer-2',
+ 'thermometer-3',
+ 'thumb-tack',
+ 'thumbs-down',
+ 'thumbs-o-up',
+ 'thumbs-up',
+ 'ticket',
+ 'times-circle',
+ 'times-circle-o',
+ 'times-rectangle',
+ 'times-rectangle-o',
+ 'tint',
+ 'toggle-off',
+ 'toggle-on',
+ 'trademark',
+ 'train',
+ 'transgender-alt',
+ 'trash',
+ 'trash-o',
+ 'tree',
+ 'trello',
+ 'tripadvisor',
+ 'trophy',
+ 'truck',
+ 'try',
+ 'tty',
+ 'tumblr',
+ 'tumblr-square',
+ 'twitch',
+ 'twitter',
+ 'twitter-square',
+ 'umbrella',
+ 'underline',
+ 'universal-access',
+ 'unlock',
+ 'unlock-alt',
+ 'upload',
+ 'usb',
+ 'user',
+ 'user-circle',
+ 'user-circle-o',
+ 'user-md',
+ 'user-o',
+ 'user-plus',
+ 'user-secret',
+ 'user-times',
+ 'venus',
+ 'venus-double',
+ 'venus-mars',
+ 'viacoin',
+ 'viadeo',
+ 'viadeo-square',
+ 'video-camera',
+ 'vimeo',
+ 'vimeo-square',
+ 'vine',
+ 'vk',
+ 'volume-control-phone',
+ 'volume-down',
+ 'volume-off',
+ 'volume-up',
+ 'wechat',
+ 'weibo',
+ 'whatsapp',
+ 'wheelchair',
+ 'wheelchair-alt',
+ 'wifi',
+ 'wikipedia-w',
+ 'window-maximize',
+ 'window-minimize',
+ 'window-restore',
+ 'windows',
+ 'wordpress',
+ 'wpbeginner',
+ 'wpexplorer',
+ 'wpforms',
+ 'wrench',
+ 'xing',
+ 'xing-square',
+ 'y-combinator',
+ 'yahoo',
+ 'yelp',
+ 'yoast',
+ 'youtube',
+ 'youtube-play',
+ 'youtube-square'
+ ],
+ 'fa-solid:': [
+ 'abacus',
+ 'ad',
+ 'address-book',
+ 'address-card',
+ 'adjust',
+ 'air-freshener',
+ 'align-center',
+ 'align-justify',
+ 'align-left',
+ 'align-right',
+ 'allergies',
+ 'ambulance',
+ 'american-sign-language-interpreting',
+ 'anchor',
+ 'angle-double-down',
+ 'angle-double-left',
+ 'angle-double-right',
+ 'angle-double-up',
+ 'angle-down',
+ 'angle-left',
+ 'angle-right',
+ 'angle-up',
+ 'angry',
+ 'ankh',
+ 'apple-alt',
+ 'archive',
+ 'archway',
+ 'arrow-alt-circle-down',
+ 'arrow-alt-circle-left',
+ 'arrow-alt-circle-right',
+ 'arrow-alt-circle-up',
+ 'arrow-circle-down',
+ 'arrow-circle-left',
+ 'arrow-circle-right',
+ 'arrow-circle-up',
+ 'arrow-down',
+ 'arrow-left',
+ 'arrow-right',
+ 'arrow-up',
+ 'arrows-alt',
+ 'arrows-alt-h',
+ 'arrows-alt-v',
+ 'assistive-listening-systems',
+ 'asterisk',
+ 'at',
+ 'atlas',
+ 'atom',
+ 'audio-description',
+ 'award',
+ 'baby',
+ 'baby-carriage',
+ 'backspace',
+ 'backward',
+ 'bacon',
+ 'bacteria',
+ 'bacterium',
+ 'bahai',
+ 'balance-scale',
+ 'balance-scale-left',
+ 'balance-scale-right',
+ 'ban',
+ 'band-aid',
+ 'barcode',
+ 'bars',
+ 'baseball-ball',
+ 'basketball-ball',
+ 'bath',
+ 'battery-empty',
+ 'battery-full',
+ 'battery-half',
+ 'battery-quarter',
+ 'battery-three-quarters',
+ 'bed',
+ 'beer',
+ 'bell',
+ 'bell-slash',
+ 'bezier-curve',
+ 'bible',
+ 'bicycle',
+ 'biking',
+ 'binoculars',
+ 'biohazard',
+ 'birthday-cake',
+ 'blender',
+ 'blender-phone',
+ 'blind',
+ 'blog',
+ 'bold',
+ 'bolt',
+ 'bomb',
+ 'bone',
+ 'bong',
+ 'book',
+ 'book-dead',
+ 'book-medical',
+ 'book-open',
+ 'book-reader',
+ 'bookmark',
+ 'border-all',
+ 'border-none',
+ 'border-style',
+ 'bowling-ball',
+ 'box',
+ 'box-open',
+ 'box-tissue',
+ 'boxes',
+ 'braille',
+ 'brain',
+ 'bread-slice',
+ 'briefcase',
+ 'briefcase-medical',
+ 'broadcast-tower',
+ 'broom',
+ 'brush',
+ 'bug',
+ 'building',
+ 'bullhorn',
+ 'bullseye',
+ 'burn',
+ 'bus',
+ 'bus-alt',
+ 'business-time',
+ 'calculator',
+ 'calculator-alt',
+ 'calendar',
+ 'calendar-alt',
+ 'calendar-check',
+ 'calendar-day',
+ 'calendar-minus',
+ 'calendar-plus',
+ 'calendar-times',
+ 'calendar-week',
+ 'camera',
+ 'camera-retro',
+ 'campground',
+ 'candy-cane',
+ 'cannabis',
+ 'capsules',
+ 'car',
+ 'car-alt',
+ 'car-battery',
+ 'car-crash',
+ 'car-side',
+ 'caravan',
+ 'caret-down',
+ 'caret-left',
+ 'caret-right',
+ 'caret-square-down',
+ 'caret-square-left',
+ 'caret-square-right',
+ 'caret-square-up',
+ 'caret-up',
+ 'carrot',
+ 'cart-arrow-down',
+ 'cart-plus',
+ 'cash-register',
+ 'cat',
+ 'certificate',
+ 'chair',
+ 'chalkboard',
+ 'chalkboard-teacher',
+ 'charging-station',
+ 'chart-area',
+ 'chart-bar',
+ 'chart-line',
+ 'chart-pie',
+ 'check',
+ 'check-circle',
+ 'check-double',
+ 'check-square',
+ 'cheese',
+ 'chess',
+ 'chess-bishop',
+ 'chess-board',
+ 'chess-king',
+ 'chess-knight',
+ 'chess-pawn',
+ 'chess-queen',
+ 'chess-rook',
+ 'chevron-circle-down',
+ 'chevron-circle-left',
+ 'chevron-circle-right',
+ 'chevron-circle-up',
+ 'chevron-down',
+ 'chevron-left',
+ 'chevron-right',
+ 'chevron-up',
+ 'child',
+ 'church',
+ 'circle',
+ 'circle-notch',
+ 'city',
+ 'clinic-medical',
+ 'clipboard',
+ 'clipboard-check',
+ 'clipboard-list',
+ 'clock',
+ 'clone',
+ 'closed-captioning',
+ 'cloud',
+ 'cloud-download-alt',
+ 'cloud-meatball',
+ 'cloud-moon',
+ 'cloud-moon-rain',
+ 'cloud-rain',
+ 'cloud-showers-heavy',
+ 'cloud-sun',
+ 'cloud-sun-rain',
+ 'cloud-upload-alt',
+ 'cocktail',
+ 'code',
+ 'code-branch',
+ 'coffee',
+ 'cog',
+ 'cogs',
+ 'coins',
+ 'columns',
+ 'comment',
+ 'comment-alt',
+ 'comment-dollar',
+ 'comment-dots',
+ 'comment-medical',
+ 'comment-slash',
+ 'comments',
+ 'comments-dollar',
+ 'compact-disc',
+ 'compass',
+ 'compress',
+ 'compress-alt',
+ 'compress-arrows-alt',
+ 'concierge-bell',
+ 'cookie',
+ 'cookie-bite',
+ 'copy',
+ 'copyright',
+ 'couch',
+ 'credit-card',
+ 'crop',
+ 'crop-alt',
+ 'cross',
+ 'crosshairs',
+ 'crow',
+ 'crown',
+ 'crutch',
+ 'cube',
+ 'cubes',
+ 'cut',
+ 'database',
+ 'deaf',
+ 'democrat',
+ 'desktop',
+ 'dharmachakra',
+ 'diagnoses',
+ 'dice',
+ 'dice-d20',
+ 'dice-d6',
+ 'dice-five',
+ 'dice-four',
+ 'dice-one',
+ 'dice-six',
+ 'dice-three',
+ 'dice-two',
+ 'digital-tachograph',
+ 'directions',
+ 'disease',
+ 'divide',
+ 'dizzy',
+ 'dna',
+ 'dog',
+ 'dollar-sign',
+ 'dolly',
+ 'dolly-flatbed',
+ 'donate',
+ 'door-closed',
+ 'door-open',
+ 'dot-circle',
+ 'dove',
+ 'download',
+ 'drafting-compass',
+ 'dragon',
+ 'draw-polygon',
+ 'drum',
+ 'drum-steelpan',
+ 'drumstick-bite',
+ 'dumbbell',
+ 'dumpster',
+ 'dumpster-fire',
+ 'dungeon',
+ 'edit',
+ 'egg',
+ 'eject',
+ 'ellipsis-h',
+ 'ellipsis-v',
+ 'empty-set',
+ 'envelope',
+ 'envelope-open',
+ 'envelope-open-text',
+ 'envelope-square',
+ 'equals',
+ 'eraser',
+ 'ethernet',
+ 'euro-sign',
+ 'exchange-alt',
+ 'exclamation',
+ 'exclamation-circle',
+ 'exclamation-triangle',
+ 'expand',
+ 'expand-alt',
+ 'expand-arrows-alt',
+ 'external-link-alt',
+ 'external-link-square-alt',
+ 'eye',
+ 'eye-dropper',
+ 'eye-slash',
+ 'fan',
+ 'fast-backward',
+ 'fast-forward',
+ 'faucet',
+ 'fax',
+ 'feather',
+ 'feather-alt',
+ 'female',
+ 'fighter-jet',
+ 'file',
+ 'file-alt',
+ 'file-archive',
+ 'file-audio',
+ 'file-code',
+ 'file-contract',
+ 'file-csv',
+ 'file-download',
+ 'file-excel',
+ 'file-export',
+ 'file-image',
+ 'file-import',
+ 'file-invoice',
+ 'file-invoice-dollar',
+ 'file-medical',
+ 'file-medical-alt',
+ 'file-pdf',
+ 'file-powerpoint',
+ 'file-prescription',
+ 'file-signature',
+ 'file-upload',
+ 'file-video',
+ 'file-word',
+ 'fill',
+ 'fill-drip',
+ 'film',
+ 'filter',
+ 'fingerprint',
+ 'fire',
+ 'fire-alt',
+ 'fire-extinguisher',
+ 'first-aid',
+ 'fish',
+ 'fist-raised',
+ 'flag',
+ 'flag-checkered',
+ 'flag-usa',
+ 'flask',
+ 'flushed',
+ 'folder',
+ 'folder-minus',
+ 'folder-open',
+ 'folder-plus',
+ 'font',
+ 'football-ball',
+ 'forward',
+ 'frog',
+ 'frown',
+ 'frown-open',
+ 'function',
+ 'funnel-dollar',
+ 'futbol',
+ 'gamepad',
+ 'gas-pump',
+ 'gavel',
+ 'gem',
+ 'genderless',
+ 'ghost',
+ 'gift',
+ 'gifts',
+ 'glass-cheers',
+ 'glass-martini',
+ 'glass-martini-alt',
+ 'glass-whiskey',
+ 'glasses',
+ 'globe',
+ 'globe-africa',
+ 'globe-americas',
+ 'globe-asia',
+ 'globe-europe',
+ 'golf-ball',
+ 'gopuram',
+ 'graduation-cap',
+ 'greater-than',
+ 'greater-than-equal',
+ 'grimace',
+ 'grin',
+ 'grin-alt',
+ 'grin-beam',
+ 'grin-beam-sweat',
+ 'grin-hearts',
+ 'grin-squint',
+ 'grin-squint-tears',
+ 'grin-stars',
+ 'grin-tears',
+ 'grin-tongue',
+ 'grin-tongue-squint',
+ 'grin-tongue-wink',
+ 'grin-wink',
+ 'grip-horizontal',
+ 'grip-lines',
+ 'grip-lines-vertical',
+ 'grip-vertical',
+ 'guitar',
+ 'h-square',
+ 'hamburger',
+ 'hammer',
+ 'hamsa',
+ 'hand-holding',
+ 'hand-holding-heart',
+ 'hand-holding-medical',
+ 'hand-holding-usd',
+ 'hand-holding-water',
+ 'hand-lizard',
+ 'hand-middle-finger',
+ 'hand-paper',
+ 'hand-peace',
+ 'hand-point-down',
+ 'hand-point-left',
+ 'hand-point-right',
+ 'hand-point-up',
+ 'hand-pointer',
+ 'hand-rock',
+ 'hand-scissors',
+ 'hand-sparkles',
+ 'hand-spock',
+ 'hands',
+ 'hands-helping',
+ 'hands-wash',
+ 'handshake',
+ 'handshake-alt-slash',
+ 'handshake-slash',
+ 'hanukiah',
+ 'hard-hat',
+ 'hashtag',
+ 'hat-cowboy',
+ 'hat-cowboy-side',
+ 'hat-wizard',
+ 'hdd',
+ 'head-side-cough',
+ 'head-side-cough-slash',
+ 'head-side-mask',
+ 'head-side-virus',
+ 'heading',
+ 'headphones',
+ 'headphones-alt',
+ 'headset',
+ 'heart',
+ 'heart-broken',
+ 'heartbeat',
+ 'helicopter',
+ 'highlighter',
+ 'hiking',
+ 'hippo',
+ 'history',
+ 'hockey-puck',
+ 'holly-berry',
+ 'home',
+ 'horse',
+ 'horse-head',
+ 'hospital',
+ 'hospital-alt',
+ 'hospital-symbol',
+ 'hospital-user',
+ 'hot-tub',
+ 'hotdog',
+ 'hotel',
+ 'hourglass',
+ 'hourglass-end',
+ 'hourglass-half',
+ 'hourglass-start',
+ 'house-damage',
+ 'house-user',
+ 'hryvnia',
+ 'i-cursor',
+ 'ice-cream',
+ 'icicles',
+ 'icons',
+ 'id-badge',
+ 'id-card',
+ 'id-card-alt',
+ 'igloo',
+ 'image',
+ 'images',
+ 'inbox',
+ 'indent',
+ 'industry',
+ 'infinity',
+ 'info',
+ 'info-circle',
+ 'integral',
+ 'intersection',
+ 'italic',
+ 'jedi',
+ 'joint',
+ 'journal-whills',
+ 'kaaba',
+ 'key',
+ 'keyboard',
+ 'khanda',
+ 'kiss',
+ 'kiss-beam',
+ 'kiss-wink-heart',
+ 'kiwi-bird',
+ 'lambda',
+ 'landmark',
+ 'language',
+ 'laptop',
+ 'laptop-code',
+ 'laptop-house',
+ 'laptop-medical',
+ 'laugh',
+ 'laugh-beam',
+ 'laugh-squint',
+ 'laugh-wink',
+ 'layer-group',
+ 'leaf',
+ 'lemon',
+ 'less-than',
+ 'less-than-equal',
+ 'level-down-alt',
+ 'level-up-alt',
+ 'life-ring',
+ 'lightbulb',
+ 'link',
+ 'lira-sign',
+ 'list',
+ 'list-alt',
+ 'list-ol',
+ 'list-ul',
+ 'location-arrow',
+ 'lock',
+ 'lock-open',
+ 'long-arrow-alt-down',
+ 'long-arrow-alt-left',
+ 'long-arrow-alt-right',
+ 'long-arrow-alt-up',
+ 'low-vision',
+ 'luggage-cart',
+ 'lungs',
+ 'lungs-virus',
+ 'magic',
+ 'magnet',
+ 'mail-bulk',
+ 'male',
+ 'map',
+ 'map-marked',
+ 'map-marked-alt',
+ 'map-marker',
+ 'map-marker-alt',
+ 'map-pin',
+ 'map-signs',
+ 'marker',
+ 'mars',
+ 'mars-double',
+ 'mars-stroke',
+ 'mars-stroke-h',
+ 'mars-stroke-v',
+ 'mask',
+ 'medal',
+ 'medkit',
+ 'meh',
+ 'meh-blank',
+ 'meh-rolling-eyes',
+ 'memory',
+ 'menorah',
+ 'mercury',
+ 'meteor',
+ 'microchip',
+ 'microphone',
+ 'microphone-alt',
+ 'microphone-alt-slash',
+ 'microphone-slash',
+ 'microscope',
+ 'minus',
+ 'minus-circle',
+ 'minus-square',
+ 'mitten',
+ 'mobile',
+ 'mobile-alt',
+ 'money-bill',
+ 'money-bill-alt',
+ 'money-bill-wave',
+ 'money-bill-wave-alt',
+ 'money-check',
+ 'money-check-alt',
+ 'monument',
+ 'moon',
+ 'mortar-pestle',
+ 'mosque',
+ 'motorcycle',
+ 'mountain',
+ 'mouse',
+ 'mouse-pointer',
+ 'mug-hot',
+ 'music',
+ 'network-wired',
+ 'neuter',
+ 'newspaper',
+ 'not-equal',
+ 'notes-medical',
+ 'object-group',
+ 'object-ungroup',
+ 'oil-can',
+ 'om',
+ 'omega',
+ 'otter',
+ 'outdent',
+ 'pager',
+ 'paint-brush',
+ 'paint-roller',
+ 'palette',
+ 'pallet',
+ 'paper-plane',
+ 'paperclip',
+ 'parachute-box',
+ 'paragraph',
+ 'parking',
+ 'passport',
+ 'pastafarianism',
+ 'paste',
+ 'pause',
+ 'pause-circle',
+ 'paw',
+ 'peace',
+ 'pen',
+ 'pen-alt',
+ 'pen-fancy',
+ 'pen-nib',
+ 'pen-square',
+ 'pencil-alt',
+ 'pencil-ruler',
+ 'people-arrows',
+ 'people-carry',
+ 'pepper-hot',
+ 'percent',
+ 'percentage',
+ 'person-booth',
+ 'phone',
+ 'phone-alt',
+ 'phone-slash',
+ 'phone-square',
+ 'phone-square-alt',
+ 'phone-volume',
+ 'photo-video',
+ 'pi',
+ 'piggy-bank',
+ 'pills',
+ 'pizza-slice',
+ 'place-of-worship',
+ 'plane',
+ 'plane-arrival',
+ 'plane-departure',
+ 'plane-slash',
+ 'play',
+ 'play-circle',
+ 'plug',
+ 'plus',
+ 'plus-circle',
+ 'plus-square',
+ 'podcast',
+ 'poll',
+ 'poll-h',
+ 'poo',
+ 'poo-storm',
+ 'poop',
+ 'portrait',
+ 'pound-sign',
+ 'power-off',
+ 'pray',
+ 'praying-hands',
+ 'prescription',
+ 'prescription-bottle',
+ 'prescription-bottle-alt',
+ 'print',
+ 'procedures',
+ 'project-diagram',
+ 'pump-medical',
+ 'pump-soap',
+ 'puzzle-piece',
+ 'qrcode',
+ 'question',
+ 'question-circle',
+ 'quidditch',
+ 'quote-left',
+ 'quote-right',
+ 'quran',
+ 'radiation',
+ 'radiation-alt',
+ 'rainbow',
+ 'random',
+ 'receipt',
+ 'record-vinyl',
+ 'recycle',
+ 'redo',
+ 'redo-alt',
+ 'registered',
+ 'remove-format',
+ 'reply',
+ 'reply-all',
+ 'republican',
+ 'restroom',
+ 'retweet',
+ 'ribbon',
+ 'ring',
+ 'road',
+ 'robot',
+ 'rocket',
+ 'route',
+ 'rss',
+ 'rss-square',
+ 'ruble-sign',
+ 'ruler',
+ 'ruler-combined',
+ 'ruler-horizontal',
+ 'ruler-vertical',
+ 'running',
+ 'rupee-sign',
+ 'sad-cry',
+ 'sad-tear',
+ 'satellite',
+ 'satellite-dish',
+ 'save',
+ 'school',
+ 'screwdriver',
+ 'scroll',
+ 'sd-card',
+ 'search',
+ 'search-dollar',
+ 'search-location',
+ 'search-minus',
+ 'search-plus',
+ 'seedling',
+ 'server',
+ 'shapes',
+ 'share',
+ 'share-alt',
+ 'share-alt-square',
+ 'share-square',
+ 'shekel-sign',
+ 'shield-alt',
+ 'shield-virus',
+ 'ship',
+ 'shipping-fast',
+ 'shoe-prints',
+ 'shopping-bag',
+ 'shopping-basket',
+ 'shopping-cart',
+ 'shower',
+ 'shuttle-van',
+ 'sigma',
+ 'sign',
+ 'sign-in-alt',
+ 'sign-language',
+ 'sign-out-alt',
+ 'signal',
+ 'signal-alt',
+ 'signal-alt-slash',
+ 'signal-slash',
+ 'signature',
+ 'sim-card',
+ 'sink',
+ 'sitemap',
+ 'skating',
+ 'skiing',
+ 'skiing-nordic',
+ 'skull',
+ 'skull-crossbones',
+ 'slash',
+ 'sleigh',
+ 'sliders-h',
+ 'smile',
+ 'smile-beam',
+ 'smile-wink',
+ 'smog',
+ 'smoking',
+ 'smoking-ban',
+ 'sms',
+ 'snowboarding',
+ 'snowflake',
+ 'snowman',
+ 'snowplow',
+ 'soap',
+ 'socks',
+ 'solar-panel',
+ 'sort',
+ 'sort-alpha-down',
+ 'sort-alpha-down-alt',
+ 'sort-alpha-up',
+ 'sort-alpha-up-alt',
+ 'sort-amount-down',
+ 'sort-amount-down-alt',
+ 'sort-amount-up',
+ 'sort-amount-up-alt',
+ 'sort-down',
+ 'sort-numeric-down',
+ 'sort-numeric-down-alt',
+ 'sort-numeric-up',
+ 'sort-numeric-up-alt',
+ 'sort-up',
+ 'spa',
+ 'space-shuttle',
+ 'spell-check',
+ 'spider',
+ 'spinner',
+ 'splotch',
+ 'spray-can',
+ 'square',
+ 'square-full',
+ 'square-root',
+ 'square-root-alt',
+ 'stamp',
+ 'star',
+ 'star-and-crescent',
+ 'star-half',
+ 'star-half-alt',
+ 'star-of-david',
+ 'star-of-life',
+ 'step-backward',
+ 'step-forward',
+ 'stethoscope',
+ 'sticky-note',
+ 'stop',
+ 'stop-circle',
+ 'stopwatch',
+ 'stopwatch-20',
+ 'store',
+ 'store-alt',
+ 'store-alt-slash',
+ 'store-slash',
+ 'stream',
+ 'street-view',
+ 'strikethrough',
+ 'stroopwafel',
+ 'subscript',
+ 'subway',
+ 'suitcase',
+ 'suitcase-rolling',
+ 'sun',
+ 'superscript',
+ 'surprise',
+ 'swatchbook',
+ 'swimmer',
+ 'swimming-pool',
+ 'synagogue',
+ 'sync',
+ 'sync-alt',
+ 'syringe',
+ 'table',
+ 'table-tennis',
+ 'tablet',
+ 'tablet-alt',
+ 'tablets',
+ 'tachometer-alt',
+ 'tag',
+ 'tags',
+ 'tally',
+ 'tape',
+ 'tasks',
+ 'taxi',
+ 'teeth',
+ 'teeth-open',
+ 'temperature-high',
+ 'temperature-low',
+ 'tenge',
+ 'terminal',
+ 'text-height',
+ 'text-width',
+ 'th',
+ 'th-large',
+ 'th-list',
+ 'theater-masks',
+ 'thermometer',
+ 'thermometer-empty',
+ 'thermometer-full',
+ 'thermometer-half',
+ 'thermometer-quarter',
+ 'thermometer-three-quarters',
+ 'theta',
+ 'thumbs-down',
+ 'thumbs-up',
+ 'thumbtack',
+ 'ticket-alt',
+ 'tilde',
+ 'times',
+ 'times-circle',
+ 'tint',
+ 'tint-slash',
+ 'tired',
+ 'toggle-off',
+ 'toggle-on',
+ 'toilet',
+ 'toilet-paper',
+ 'toilet-paper-slash',
+ 'toolbox',
+ 'tools',
+ 'tooth',
+ 'torah',
+ 'torii-gate',
+ 'tractor',
+ 'trademark',
+ 'traffic-light',
+ 'trailer',
+ 'train',
+ 'tram',
+ 'transgender',
+ 'transgender-alt',
+ 'trash',
+ 'trash-alt',
+ 'trash-restore',
+ 'trash-restore-alt',
+ 'tree',
+ 'trophy',
+ 'truck',
+ 'truck-loading',
+ 'truck-monster',
+ 'truck-moving',
+ 'truck-pickup',
+ 'tshirt',
+ 'tty',
+ 'tv',
+ 'umbrella',
+ 'umbrella-beach',
+ 'underline',
+ 'undo',
+ 'undo-alt',
+ 'union',
+ 'universal-access',
+ 'university',
+ 'unlink',
+ 'unlock',
+ 'unlock-alt',
+ 'upload',
+ 'user',
+ 'user-alt',
+ 'user-alt-slash',
+ 'user-astronaut',
+ 'user-check',
+ 'user-circle',
+ 'user-clock',
+ 'user-cog',
+ 'user-edit',
+ 'user-friends',
+ 'user-graduate',
+ 'user-injured',
+ 'user-lock',
+ 'user-md',
+ 'user-minus',
+ 'user-ninja',
+ 'user-nurse',
+ 'user-plus',
+ 'user-secret',
+ 'user-shield',
+ 'user-slash',
+ 'user-tag',
+ 'user-tie',
+ 'user-times',
+ 'users',
+ 'users-cog',
+ 'users-slash',
+ 'utensil-spoon',
+ 'utensils',
+ 'value-absolute',
+ 'vector-square',
+ 'venus',
+ 'venus-double',
+ 'venus-mars',
+ 'vest',
+ 'vest-patches',
+ 'vial',
+ 'vials',
+ 'video',
+ 'video-slash',
+ 'vihara',
+ 'virus',
+ 'virus-slash',
+ 'viruses',
+ 'voicemail',
+ 'volleyball-ball',
+ 'volume',
+ 'volume-down',
+ 'volume-mute',
+ 'volume-off',
+ 'volume-slash',
+ 'volume-up',
+ 'vote-yea',
+ 'vr-cardboard',
+ 'walking',
+ 'wallet',
+ 'warehouse',
+ 'water',
+ 'wave-square',
+ 'weight',
+ 'weight-hanging',
+ 'wheelchair',
+ 'wifi',
+ 'wifi-slash',
+ 'wind',
+ 'window-close',
+ 'window-maximize',
+ 'window-minimize',
+ 'window-restore',
+ 'wine-bottle',
+ 'wine-glass',
+ 'wine-glass-alt',
+ 'won-sign',
+ 'wrench',
+ 'x-ray',
+ 'yen-sign',
+ 'yin-yang'
+ ]
+}
diff --git a/src/components/ImageViewer/index.ts b/src/components/ImageViewer/index.ts
new file mode 100644
index 0000000..35764d6
--- /dev/null
+++ b/src/components/ImageViewer/index.ts
@@ -0,0 +1,33 @@
+import ImageViewer from './src/ImageViewer.vue'
+import { isClient } from '@/utils/is'
+import { createVNode, render, VNode } from 'vue'
+import { ImageViewerProps } from './src/types'
+
+let instance: Nullable<VNode> = null
+
+export function createImageViewer(options: ImageViewerProps) {
+ if (!isClient) return
+ const {
+ urlList,
+ initialIndex = 0,
+ infinite = true,
+ hideOnClickModal = false,
+ teleported = false,
+ zIndex = 2000,
+ show = true
+ } = options
+
+ const propsData: Partial<ImageViewerProps> = {}
+ const container = document.createElement('div')
+ propsData.urlList = urlList
+ propsData.initialIndex = initialIndex
+ propsData.infinite = infinite
+ propsData.hideOnClickModal = hideOnClickModal
+ propsData.teleported = teleported
+ propsData.zIndex = zIndex
+ propsData.show = show
+
+ document.body.appendChild(container)
+ instance = createVNode(ImageViewer, propsData)
+ render(instance, container)
+}
diff --git a/src/components/ImageViewer/src/ImageViewer.vue b/src/components/ImageViewer/src/ImageViewer.vue
new file mode 100644
index 0000000..c84d06b
--- /dev/null
+++ b/src/components/ImageViewer/src/ImageViewer.vue
@@ -0,0 +1,35 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'ImageViewer' })
+
+const props = defineProps({
+ urlList: {
+ type: Array as PropType<string[]>,
+ default: (): string[] => []
+ },
+ zIndex: propTypes.number.def(200),
+ initialIndex: propTypes.number.def(0),
+ infinite: propTypes.bool.def(true),
+ hideOnClickModal: propTypes.bool.def(false),
+ teleported: propTypes.bool.def(false),
+ show: propTypes.bool.def(false)
+})
+
+const getBindValue = computed(() => {
+ const propsData: Recordable = { ...props }
+ delete propsData.show
+ return propsData
+})
+
+const show = ref(props.show)
+
+const close = () => {
+ show.value = false
+}
+</script>
+
+<template>
+ <ElImageViewer v-if="show" v-bind="getBindValue" @close="close" />
+</template>
diff --git a/src/components/ImageViewer/src/types.ts b/src/components/ImageViewer/src/types.ts
new file mode 100644
index 0000000..2fff4c0
--- /dev/null
+++ b/src/components/ImageViewer/src/types.ts
@@ -0,0 +1,9 @@
+export interface ImageViewerProps {
+ urlList?: string[]
+ zIndex?: number
+ initialIndex?: number
+ infinite?: boolean
+ hideOnClickModal?: boolean
+ teleported?: boolean
+ show?: boolean
+}
diff --git a/src/components/Infotip/index.ts b/src/components/Infotip/index.ts
new file mode 100644
index 0000000..413fa5f
--- /dev/null
+++ b/src/components/Infotip/index.ts
@@ -0,0 +1,3 @@
+import Infotip from './src/Infotip.vue'
+
+export { Infotip }
diff --git a/src/components/Infotip/src/Infotip.vue b/src/components/Infotip/src/Infotip.vue
new file mode 100644
index 0000000..0afd692
--- /dev/null
+++ b/src/components/Infotip/src/Infotip.vue
@@ -0,0 +1,54 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { useDesign } from '@/hooks/web/useDesign'
+import { propTypes } from '@/utils/propTypes'
+import { TipSchema } from '@/types/infoTip'
+
+defineOptions({ name: 'InfoTip' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('infotip')
+
+defineProps({
+ title: propTypes.string.def(''),
+ schema: {
+ type: Array as PropType<Array<string | TipSchema>>,
+ required: true,
+ default: () => []
+ },
+ showIndex: propTypes.bool.def(true),
+ highlightColor: propTypes.string.def('var(--el-color-primary)')
+})
+
+const emit = defineEmits(['click'])
+
+const keyClick = (key: string) => {
+ emit('click', key)
+}
+</script>
+
+<template>
+ <div
+ :class="[
+ prefixCls,
+ 'p-20px mb-20px border-1px border-solid border-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)]'
+ ]"
+ >
+ <div v-if="title" :class="[`${prefixCls}__header`, 'flex items-center']">
+ <Icon :size="22" color="var(--el-color-primary)" icon="ep:warning-filled" />
+ <span :class="[`${prefixCls}__title`, 'pl-5px text-16px font-bold']">{{ title }}</span>
+ </div>
+ <div :class="`${prefixCls}__content`">
+ <p v-for="(item, $index) in schema" :key="$index" class="mt-15px text-14px">
+ <Highlight
+ :color="highlightColor"
+ :keys="typeof item === 'string' ? [] : item.keys"
+ @click="keyClick"
+ >
+ {{ showIndex ? `${$index + 1}銆乣 : '' }}{{ typeof item === 'string' ? item : item.label }}
+ </Highlight>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/src/components/InputPassword/index.ts b/src/components/InputPassword/index.ts
new file mode 100644
index 0000000..1dcc38e
--- /dev/null
+++ b/src/components/InputPassword/index.ts
@@ -0,0 +1,3 @@
+import InputPassword from './src/InputPassword.vue'
+
+export { InputPassword }
diff --git a/src/components/InputPassword/src/InputPassword.vue b/src/components/InputPassword/src/InputPassword.vue
new file mode 100644
index 0000000..b8c93e7
--- /dev/null
+++ b/src/components/InputPassword/src/InputPassword.vue
@@ -0,0 +1,152 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { useConfigGlobal } from '@/hooks/web/useConfigGlobal'
+import type { ZxcvbnResult } from '@zxcvbn-ts/core'
+import { zxcvbn } from '@zxcvbn-ts/core'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'InputPassword' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('input-password')
+
+const props = defineProps({
+ // 鏄惁鏄剧ず瀵嗙爜寮哄害
+ strength: propTypes.bool.def(false),
+ modelValue: propTypes.string.def('')
+})
+
+watch(
+ () => props.modelValue,
+ (val: string) => {
+ if (val === unref(valueRef)) return
+ valueRef.value = val
+ }
+)
+
+const { configGlobal } = useConfigGlobal()
+
+const emit = defineEmits(['update:modelValue'])
+
+// 璁剧疆input鐨則ype灞炴��
+const textType = ref<'password' | 'text'>('password')
+
+const changeTextType = () => {
+ textType.value = unref(textType) === 'text' ? 'password' : 'text'
+}
+
+// 杈撳叆妗嗙殑鍊�
+const valueRef = ref(props.modelValue)
+
+// 鐩戝惉
+watch(
+ () => valueRef.value,
+ (val: string) => {
+ emit('update:modelValue', val)
+ }
+)
+
+// 鑾峰彇瀵嗙爜寮哄害
+const getPasswordStrength = computed(() => {
+ const value = unref(valueRef)
+ const zxcvbnRef = zxcvbn(unref(valueRef)) as ZxcvbnResult
+ return value ? zxcvbnRef.score : -1
+})
+
+const getIconName = computed(() => (unref(textType) === 'password' ? 'ep:hide' : 'ep:view'))
+</script>
+
+<template>
+ <div :class="[prefixCls, `${prefixCls}--${configGlobal?.size}`]">
+ <ElInput v-model="valueRef" :type="textType" v-bind="$attrs">
+ <template #suffix>
+ <Icon :icon="getIconName" class="el-input__icon cursor-pointer" @click="changeTextType" />
+ </template>
+ </ElInput>
+ <div
+ v-if="strength"
+ :class="`${prefixCls}__bar`"
+ class="relative mb-6px ml-auto mr-auto mt-10px h-6px"
+ >
+ <div :class="`${prefixCls}__bar--fill`" :data-score="getPasswordStrength"></div>
+ </div>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-input-password;
+
+.#{$prefix-cls} {
+ :deep(.#{$elNamespace}-input__clear) {
+ margin-left: 5px;
+ }
+
+ &__bar {
+ background-color: var(--el-text-color-disabled);
+ border-radius: var(--el-border-radius-base);
+
+ &::before,
+ &::after {
+ position: absolute;
+ z-index: 10;
+ display: block;
+ width: 20%;
+ height: inherit;
+ background-color: transparent;
+ border-color: var(--el-color-white);
+ border-style: solid;
+ border-width: 0 5px;
+ content: '';
+ }
+
+ &::before {
+ left: 20%;
+ }
+
+ &::after {
+ right: 20%;
+ }
+
+ &--fill {
+ position: absolute;
+ width: 0;
+ height: inherit;
+ background-color: transparent;
+ border-radius: inherit;
+ transition:
+ width 0.5s ease-in-out,
+ background 0.25s;
+
+ &[data-score='0'] {
+ width: 20%;
+ background-color: var(--el-color-danger);
+ }
+
+ &[data-score='1'] {
+ width: 40%;
+ background-color: var(--el-color-danger);
+ }
+
+ &[data-score='2'] {
+ width: 60%;
+ background-color: var(--el-color-warning);
+ }
+
+ &[data-score='3'] {
+ width: 80%;
+ background-color: var(--el-color-success);
+ }
+
+ &[data-score='4'] {
+ width: 100%;
+ background-color: var(--el-color-success);
+ }
+ }
+ }
+
+ &--mini > &__bar {
+ border-radius: var(--el-border-radius-small);
+ }
+}
+</style>
diff --git a/src/components/InputWithColor/index.vue b/src/components/InputWithColor/index.vue
new file mode 100644
index 0000000..1311a55
--- /dev/null
+++ b/src/components/InputWithColor/index.vue
@@ -0,0 +1,35 @@
+<template>
+ <el-input v-model="modelValue" v-bind="$attrs">
+ <template #append>
+ <el-color-picker v-model="color" :predefine="PREDEFINE_COLORS" />
+ </template>
+ </el-input>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { PREDEFINE_COLORS } from '@/utils/color'
+import { useVModels } from '@vueuse/core'
+
+/**
+ * 甯﹂鑹查�夋嫨鍣ㄨ緭鍏ユ
+ */
+defineOptions({ name: 'InputWithColor' })
+
+const props = defineProps({
+ modelValue: propTypes.string.def('').isRequired,
+ color: propTypes.string.def('').isRequired
+})
+const emit = defineEmits(['update:modelValue', 'update:color'])
+const { modelValue, color } = useVModels(props, emit)
+</script>
+<style scoped lang="scss">
+:deep(.el-input-group__append) {
+ padding: 0;
+ .el-color-picker__trigger {
+ padding: 0;
+ border-left: none;
+ border-radius: 0 var(--el-input-border-radius) var(--el-input-border-radius) 0;
+ }
+}
+</style>
diff --git a/src/components/JsonEditor/index.ts b/src/components/JsonEditor/index.ts
new file mode 100644
index 0000000..037b449
--- /dev/null
+++ b/src/components/JsonEditor/index.ts
@@ -0,0 +1,3 @@
+import JsonEditor from './src/JsonEditor.vue'
+
+export { JsonEditor }
diff --git a/src/components/JsonEditor/src/JsonEditor.vue b/src/components/JsonEditor/src/JsonEditor.vue
new file mode 100644
index 0000000..f3789ee
--- /dev/null
+++ b/src/components/JsonEditor/src/JsonEditor.vue
@@ -0,0 +1,126 @@
+<template>
+ <div ref="jsonEditorContainer" class="json-editor" :style="{ height }"></div>
+</template>
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
+import JSONEditor, { JSONEditorMode, JSONEditorOptions } from 'jsoneditor'
+import 'jsoneditor/dist/jsoneditor.min.css'
+import { JsonEditorEmits, JsonEditorExpose, JsonEditorProps } from '../types'
+
+/** 鍩轰簬 https://github.com/josdejong/jsoneditor 浜屾灏佽缁勪欢锛屾彁渚� JSON 缂栬緫鍣ㄥ姛鑳姐�� */
+defineOptions({ name: 'JsonEditor' })
+
+const props = withDefaults(defineProps<JsonEditorProps>(), {
+ mode: 'view' as JSONEditorMode,
+ height: '400px',
+ showModeSelection: false,
+ showNavigationBar: false,
+ showStatusBar: false,
+ showMainMenuBar: true
+})
+
+const emits = defineEmits<JsonEditorEmits>()
+const jsonObj = useVModel(props, 'modelValue', emits) as Ref<any>
+const jsonEditorContainer = ref<HTMLElement | null>(null)
+let jsonEditor: JSONEditor | null = null
+
+// 璁剧疆榛樿鍊�
+const height = props.height
+
+// 鍒濆鍖朖SONEditor
+const initJsonEditor = () => {
+ if (!jsonEditorContainer.value) return
+
+ // 鍚堝苟榛樿閰嶇疆鍜岀敤鎴疯嚜瀹氫箟閰嶇疆
+ const options: JSONEditorOptions = {
+ mode: props.mode,
+ modes: props.showModeSelection
+ ? (['tree', 'code', 'form', 'text', 'view', 'preview'] as JSONEditorMode[])
+ : undefined,
+ navigationBar: props.showNavigationBar,
+ statusBar: props.showStatusBar,
+ mainMenuBar: props.showMainMenuBar,
+ onChange: () => {
+ jsonObj.value = jsonEditor?.get()
+ emits('change', jsonEditor?.get())
+ },
+ onValidationError: (errors: any) => {
+ emits('error', errors)
+ },
+ ...props.options
+ } as JSONEditorOptions
+
+ // 鍒涘缓JSONEditor瀹炰緥
+ jsonEditor = new JSONEditor(jsonEditorContainer.value, options)
+
+ // 璁剧疆鍒濆鍊�
+ if (jsonObj.value) {
+ jsonEditor.set(jsonObj.value)
+ }
+
+ if (props.mode === 'view') {
+ jsonEditor?.expandAll() // 榛樿灞曞紑鍏ㄩ儴
+ }
+}
+
+// 鐩戝惉鏁版嵁鍙樺寲
+watch(
+ () => jsonObj.value,
+ (newValue) => {
+ if (!jsonEditor) return
+
+ try {
+ // 闃叉鏃犻檺寰幆鏇存柊
+ const currentJson = jsonEditor.get()
+ if (JSON.stringify(currentJson) !== JSON.stringify(newValue)) {
+ jsonEditor.update(newValue)
+ }
+ } catch (error) {
+ console.error('JSON鏇存柊澶辫触:', error)
+ }
+ },
+ { deep: true }
+)
+
+// 鐩戝惉妯″紡鍙樺寲
+watch(
+ () => props.mode,
+ (newMode) => {
+ if (!jsonEditor) return
+ try {
+ jsonEditor.setMode(newMode)
+ } catch (error) {
+ console.error('鍒囨崲妯″紡澶辫触:', error)
+ }
+ }
+)
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ initJsonEditor()
+})
+
+onBeforeUnmount(() => {
+ if (jsonEditor) {
+ jsonEditor.destroy()
+ jsonEditor = null
+ }
+})
+
+// 鏆撮湶鏂规硶
+defineExpose<JsonEditorExpose>({
+ // 鑾峰彇缂栬緫鍣ㄥ疄渚嬶紝浠ヤ究鍙互璋冪敤鏇村JSONEditor鐨勫師鐢熸柟娉�
+ getEditor: () => jsonEditor
+})
+</script>
+
+<style lang="scss" scoped>
+/* 闅愯棌 Ace 缂栬緫鍣ㄧ殑 powered by ace 鏍囪 */
+:deep(.jsoneditor-menu) {
+ /* 闅愯棌 powered by ace 鏍囪 */
+ .jsoneditor-poweredBy {
+ display: none !important;
+ }
+}
+</style>
diff --git a/src/components/JsonEditor/types/index.ts b/src/components/JsonEditor/types/index.ts
new file mode 100644
index 0000000..b7dadd7
--- /dev/null
+++ b/src/components/JsonEditor/types/index.ts
@@ -0,0 +1,80 @@
+import { JSONEditorOptions, JSONEditorMode } from 'jsoneditor'
+
+export interface JsonEditorProps {
+ /**
+ * JSON鏁版嵁锛屾敮鎸佸弻鍚戠粦瀹�
+ */
+ modelValue: any
+
+ /**
+ * 缂栬緫鍣ㄦā寮�
+ * @default 'tree'
+ */
+ mode?: JSONEditorMode
+
+ /**
+ * 缂栬緫鍣ㄩ珮搴�
+ * @default '400px'
+ */
+ height?: string
+
+ /**
+ * 鏄惁鏄剧ず妯″紡閫夋嫨涓嬫媺鑿滃崟
+ * @default false
+ */
+ showModeSelection?: boolean
+
+ /**
+ * 鏄惁鏄剧ず瀵艰埅鏍�
+ * @default false
+ */
+ showNavigationBar?: boolean
+
+ /**
+ * 鏄惁鏄剧ず鐘舵�佹爮
+ * @default true
+ */
+ showStatusBar?: boolean
+
+ /**
+ * 鏄惁鏄剧ず涓昏彍鍗曟爮
+ * @default true
+ */
+ showMainMenuBar?: boolean
+
+ /**
+ * JSONEditor閰嶇疆閫夐」
+ * @see https://github.com/josdejong/jsoneditor/blob/develop/docs/api.md
+ */
+ options?: Partial<JSONEditorOptions>
+}
+
+/**
+ * JsonEditor缁勪欢瑙﹀彂鐨勪簨浠�
+ */
+export interface JsonEditorEmits {
+ /**
+ * 鏁版嵁鏇存柊鏃惰Е鍙�
+ */
+ (e: 'update:modelValue', value: any): void
+
+ /**
+ * 鏁版嵁鍙樺寲鏃惰Е鍙�
+ */
+ (e: 'change', value: any): void
+
+ /**
+ * 楠岃瘉閿欒鏃惰Е鍙�
+ */
+ (e: 'error', errors: any): void
+}
+
+/**
+ * JsonEditor缁勪欢鏆撮湶鐨勬柟娉�
+ */
+export interface JsonEditorExpose {
+ /**
+ * 鑾峰彇鍘熷鐨凧SONEditor瀹炰緥
+ */
+ getEditor: () => any
+}
diff --git a/src/components/MagicCubeEditor/index.vue b/src/components/MagicCubeEditor/index.vue
new file mode 100644
index 0000000..6af4ca4
--- /dev/null
+++ b/src/components/MagicCubeEditor/index.vue
@@ -0,0 +1,270 @@
+<template>
+ <div class="relative">
+ <table class="cube-table">
+ <!-- 搴曞眰锛氶瓟鏂圭煩闃� -->
+ <tbody>
+ <tr v-for="(rowCubes, row) in cubes" :key="row">
+ <td
+ v-for="(cube, col) in rowCubes"
+ :key="col"
+ :class="['cube', { active: cube.active }]"
+ :style="{
+ width: `${cubeSize}px`,
+ height: `${cubeSize}px`
+ }"
+ @click="handleCubeClick(row, col)"
+ @mouseenter="handleCellHover(row, col)"
+ >
+ <Icon icon="ep-plus" />
+ </td>
+ </tr>
+ </tbody>
+ <!-- 椤跺眰锛氱儹鍖� -->
+ <div
+ v-for="(hotArea, index) in hotAreas"
+ :key="index"
+ class="hot-area"
+ :style="{
+ top: `${cubeSize * hotArea.top}px`,
+ left: `${cubeSize * hotArea.left}px`,
+ height: `${cubeSize * hotArea.height}px`,
+ width: `${cubeSize * hotArea.width}px`
+ }"
+ @click="handleHotAreaSelected(hotArea, index)"
+ @mouseover="exitHotAreaSelectMode"
+ >
+ <!-- 鍙充笂瑙掔儹鍖哄垹闄ゆ寜閽� -->
+ <div
+ v-if="selectedHotAreaIndex === index && hotArea.width && hotArea.height"
+ class="btn-delete"
+ @click="handleDeleteHotArea(index)"
+ >
+ <Icon icon="ep:circle-close-filled" />
+ </div>
+ <span v-if="hotArea.width">{{ `${hotArea.width}脳${hotArea.height}` }}</span>
+ </div>
+ </table>
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import * as vueTypes from 'vue-types'
+import { Point, Rect, isContains, isOverlap, createRect } from './util'
+
+// 榄旀柟缂栬緫鍣�
+// 鏈変袱閮ㄥ垎缁勬垚锛�
+// 1. 榄旀柟鐭╅樀锛氫綅浜庡簳灞傦紝鐢辨柟鍧楃粍浠剁殑浜岀淮琛ㄦ牸锛岀敤浜庡垱寤虹儹鍖�
+// 鎿嶄綔鏂规硶锛�
+// 1.1 鐐瑰嚮鍏朵腑涓�涓柟鍧楀氨浼氳繘鍏ョ儹鍖洪�夋嫨妯″紡
+// 1.2 鍐嶆鐐瑰嚮鍙﹀涓�涓柟鍧楁椂锛岀粨鏉熺儹鍖洪�夋嫨妯″紡
+// 1.3 鍦ㄤ袱涓柟鍧椾腑闂寸殑鍖哄煙鍒涘缓鐑尯
+// 濡傛灉涓ゆ鐐瑰嚮鐨勯兘鏄悓涓�鏂瑰潡锛屽氨鍙垱寤轰竴涓牸瀛愮殑鐑尯
+// 2. 鐑尯锛氫綅浜庨《灞傦紝閲囩敤缁濆瀹氫綅锛岃鐩栧湪榄旀柟鐭╅樀涓婇潰銆�
+defineOptions({ name: 'MagicCubeEditor' })
+
+/**
+ * 鏂瑰潡
+ * @property active 鏄惁婵�娲�
+ */
+type Cube = Point & { active: boolean }
+
+// 瀹氫箟灞炴��
+const props = defineProps({
+ // 鐑尯鍒楄〃
+ modelValue: vueTypes.array<any>().isRequired,
+ // 琛屾暟锛岄粯璁� 4 琛�
+ rows: propTypes.number.def(4),
+ // 鍒楁暟锛岄粯璁� 4 鍒�
+ cols: propTypes.number.def(4),
+ // 鏂瑰潡澶у皬锛屽崟浣峱x锛岄粯璁�75px
+ cubeSize: propTypes.number.def(75)
+})
+
+// 榄旀柟鐭╅樀锛氭墍鏈夌殑鏂瑰潡
+const cubes = ref<Cube[][]>([])
+// 鐩戝惉琛屾暟銆佸垪鏁板彉鍖�
+watch(
+ () => [props.rows, props.cols],
+ () => {
+ // 娓呯┖榄旀柟
+ cubes.value = []
+ if (!props.rows || !props.cols) return
+
+ // 鍒濆鍖栭瓟鏂�
+ for (let row = 0; row < props.rows; row++) {
+ cubes.value[row] = []
+ for (let col = 0; col < props.cols; col++) {
+ cubes.value[row].push({ x: col, y: row, active: false })
+ }
+ }
+ },
+ { immediate: true }
+)
+
+// 鐑尯鍒楄〃
+const hotAreas = ref<Rect[]>([])
+// 鍒濆鍖栫儹鍖�
+watch(
+ () => props.modelValue,
+ () => (hotAreas.value = props.modelValue || []),
+ { immediate: true }
+)
+
+// 鐑尯璧峰鏂瑰潡
+const hotAreaBeginCube = ref<Cube>()
+// 鏄惁寮�鍚簡鐑尯閫夋嫨妯″紡
+const isHotAreaSelectMode = () => !!hotAreaBeginCube.value
+/**
+ * 澶勭悊榧犳爣鐐瑰嚮鏂瑰潡
+ *
+ * @param currentRow 褰撳墠琛屽彿
+ * @param currentCol 褰撳墠鍒楀彿
+ */
+const handleCubeClick = (currentRow: number, currentCol: number) => {
+ const currentCube = cubes.value[currentRow][currentCol]
+ // 鎯呭喌1锛氳繘鍏ョ儹鍖洪�夋嫨妯″紡
+ if (!isHotAreaSelectMode()) {
+ hotAreaBeginCube.value = currentCube
+ hotAreaBeginCube.value.active = true
+ return
+ }
+
+ // 鎯呭喌2锛氱粨鏉熺儹鍖洪�夋嫨妯″紡
+ hotAreas.value.push(createRect(hotAreaBeginCube.value!, currentCube))
+ // 缁撴潫鐑尯閫夋嫨妯″紡
+ exitHotAreaSelectMode()
+ // 鍒涘缓鍚庡氨閫変腑鐑尯
+ let hotAreaIndex = hotAreas.value.length - 1
+ handleHotAreaSelected(hotAreas.value[hotAreaIndex], hotAreaIndex)
+ // 鍙戦�佺儹鍖哄彉鍔ㄩ�氱煡
+ emitUpdateModelValue()
+}
+/**
+ * 澶勭悊榧犳爣缁忚繃鏂瑰潡
+ *
+ * @param currentRow 褰撳墠琛屽彿
+ * @param currentCol 褰撳墠鍒楀彿
+ */
+const handleCellHover = (currentRow: number, currentCol: number) => {
+ // 褰撳墠娌℃湁杩涘叆鐑尯閫夋嫨妯″紡
+ if (!isHotAreaSelectMode()) return
+
+ // 褰撳墠宸查�夌殑鍖哄煙
+ const currentSelectedArea = createRect(
+ hotAreaBeginCube.value!,
+ cubes.value[currentRow][currentCol]
+ )
+ // 鐑尯涓嶅厑璁搁噸鍙�
+ for (const hotArea of hotAreas.value) {
+ // 妫�鏌ユ槸鍚﹂噸鍙�
+ if (isOverlap(hotArea, currentSelectedArea)) {
+ // 缁撴潫鐑尯閫夋嫨妯″紡
+ exitHotAreaSelectMode()
+
+ return
+ }
+ }
+
+ // 婵�娲婚�変腑鍖哄煙鍐呴儴鐨勬柟鍧�
+ eachCube((_, __, cube) => {
+ cube.active = isContains(currentSelectedArea, cube)
+ })
+}
+/**
+ * 澶勭悊鐑尯鍒犻櫎
+ *
+ * @param index 鐑尯绱㈠紩
+ */
+const handleDeleteHotArea = (index: number) => {
+ hotAreas.value.splice(index, 1)
+ // 缁撴潫鐑尯閫夋嫨妯″紡
+ exitHotAreaSelectMode()
+ // 鍙戦�佺儹鍖哄彉鍔ㄩ�氱煡
+ emitUpdateModelValue()
+}
+
+// 鍙戦�佹ā鍨嬫洿鏂�
+const emit = defineEmits(['update:modelValue', 'hotAreaSelected'])
+// 鍙戦�佺儹鍖哄彉鍔ㄩ�氱煡
+const emitUpdateModelValue = () => emit('update:modelValue', hotAreas)
+
+// 鐑尯閫変腑
+const selectedHotAreaIndex = ref(0)
+const handleHotAreaSelected = (hotArea: Rect, index: number) => {
+ selectedHotAreaIndex.value = index
+ emit('hotAreaSelected', hotArea, index)
+}
+
+/**
+ * 缁撴潫鐑尯閫夋嫨妯″紡
+ */
+function exitHotAreaSelectMode() {
+ // 绉婚櫎鏂瑰潡婵�娲绘爣璁�
+ eachCube((_, __, cube) => {
+ if (cube.active) {
+ cube.active = false
+ }
+ })
+
+ // 娓呴櫎璧风偣
+ hotAreaBeginCube.value = undefined
+}
+
+/**
+ * 杩唬榄旀柟鐭╅樀
+ * @param callback 鍥炶皟
+ */
+const eachCube = (callback: (x: number, y: number, cube: Cube) => void) => {
+ for (let x = 0; x < cubes.value.length; x++) {
+ for (let y = 0; y < cubes.value[x].length; y++) {
+ callback(x, y, cubes.value[x][y])
+ }
+ }
+}
+</script>
+<style lang="scss" scoped>
+.cube-table {
+ position: relative;
+ border-spacing: 0;
+ border-collapse: collapse;
+
+ .cube {
+ border: 1px solid var(--el-border-color);
+ text-align: center;
+ color: var(--el-text-color-secondary);
+ cursor: pointer;
+ box-sizing: border-box;
+ &.active {
+ background: var(--el-color-primary-light-9);
+ }
+ }
+
+ .hot-area {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--el-color-primary);
+ background: var(--el-color-primary-light-8);
+ color: var(--el-color-primary);
+ box-sizing: border-box;
+ border-spacing: 0;
+ border-collapse: collapse;
+ cursor: pointer;
+
+ .btn-delete {
+ z-index: 1;
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ height: 16px;
+ width: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ background-color: #fff;
+ }
+ }
+}
+</style>
diff --git a/src/components/MagicCubeEditor/util.ts b/src/components/MagicCubeEditor/util.ts
new file mode 100644
index 0000000..e7c6465
--- /dev/null
+++ b/src/components/MagicCubeEditor/util.ts
@@ -0,0 +1,72 @@
+// 鍧愭爣鐐�
+export interface Point {
+ x: number
+ y: number
+}
+
+// 鐭╁舰
+export interface Rect {
+ // 宸︿笂瑙� X 杞村潗鏍�
+ left: number
+ // 宸︿笂瑙� Y 杞村潗鏍�
+ top: number
+ // 鍙充笅瑙� X 杞村潗鏍�
+ right: number
+ // 鍙充笅瑙� Y 杞村潗鏍�
+ bottom: number
+ // 鐭╁舰瀹藉害
+ width: number
+ // 鐭╁舰楂樺害
+ height: number
+}
+
+/**
+ * 鍒ゆ柇涓や釜鐭╁舰鏄惁閲嶅彔
+ * @param a 鐭╁舰 A
+ * @param b 鐭╁舰 B
+ */
+export const isOverlap = (a: Rect, b: Rect): boolean => {
+ return (
+ a.left < b.left + b.width &&
+ a.left + a.width > b.left &&
+ a.top < b.top + b.height &&
+ a.height + a.top > b.top
+ )
+}
+/**
+ * 妫�鏌ュ潗鏍囩偣鏄惁鍦ㄧ煩褰㈠唴
+ * @param hotArea 鐭╁舰
+ * @param point 鍧愭爣
+ */
+export const isContains = (hotArea: Rect, point: Point): boolean => {
+ return (
+ point.x >= hotArea.left &&
+ point.x < hotArea.right &&
+ point.y >= hotArea.top &&
+ point.y < hotArea.bottom
+ )
+}
+
+/**
+ * 鍦ㄤ袱涓潗鏍囩偣涓棿锛屽垱寤轰竴涓煩褰�
+ *
+ * 瀛樺湪浠ヤ笅鎯呭喌锛�
+ * 1. 涓や釜鍧愭爣鐐规槸鍚屼竴涓綅缃紝鍙崰涓�涓綅缃殑姝f柟褰紝瀹介珮閮戒负 1
+ * 2. X 杞村潗鏍囩浉鍚岋紝鍙崰涓�琛岀殑鐭╁舰锛岄珮搴︿负 1
+ * 3. Y 杞村潗鏍囩浉鍚岋紝鍙崰涓�鍒楃殑鐭╁舰锛屽搴︿负 1
+ * 4. 澶氳澶氬垪鐨勭煩褰�
+ *
+ * @param a 鍧愭爣鐐逛竴
+ * @param b 鍧愭爣鐐逛簩
+ */
+export const createRect = (a: Point, b: Point): Rect => {
+ // 璁$畻鐭╁舰鐨勮寖鍥�
+ const [left, left2] = [a.x, b.x].sort()
+ const [top, top2] = [a.y, b.y].sort()
+ const right = left2 + 1
+ const bottom = top2 + 1
+ const height = bottom - top
+ const width = right - left
+
+ return { left, right, top, bottom, height, width }
+}
diff --git a/src/components/Map/index.vue b/src/components/Map/index.vue
new file mode 100644
index 0000000..7b9ae1c
--- /dev/null
+++ b/src/components/Map/index.vue
@@ -0,0 +1,268 @@
+<!-- 鍦板浘缁勪欢锛氬熀浜庣櫨搴﹀湴鍥綠L瀹炵幇 -->
+<!-- TODO @super锛氳繕瀛樺湪涓や釜娌¤В鍐崇殑灏廱ug,涓�涓槸淇敼鎵嬪姩瀹氫綅鏃朵竴娆″姞杞� 涓嶇煡閬撲负浣曞畾浣嶇偣鍦ㄥ湴鍥惧乏涓婅 璋冧簡鍗婂ぉ娌¤В鍐� 绗簩涓槸妫�绱㈠湴鍧�纭畾瀹氫綅鐨勫姛鑳藉弬鐓х櫨搴︾殑鏂囨。娌′篃鎼炲ソ 鍥炲ご鍐嶈В鍐充竴涓� -->
+<template>
+ <div v-if="props.isWrite">
+ <el-form ref="form" label-width="120px">
+ <el-form-item label="瀹氫綅浣嶇疆:">
+ <el-select
+ class="w-full"
+ v-model="state.address"
+ clearable
+ filterable
+ remote
+ reserve-keyword
+ placeholder="鍙緭鍏ュ湴鍧�鏌ヨ缁忕含搴�"
+ :remote-method="autoSearch"
+ @change="handleAddressSelect"
+ :loading="state.loading"
+ >
+ <el-option
+ v-for="item in state.mapAddrOptions"
+ :key="item.value"
+ :label="item.name"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧鍦板浘:">
+ <!-- TODO @super锛氳繖閲岀湅鐪� unocss 鍝� -->
+ <div id="bdMap" class="mapContainer"></div>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div v-else>
+ <el-descriptions :column="2" border :labelStyle="{ 'font-weight': 'bold' }">
+ <el-descriptions-item label="璁惧浣嶇疆:">{{ state.address }}</el-descriptions-item>
+ </el-descriptions>
+ <div id="bdMap" class="mapContainer"></div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, onMounted } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+// 鎵╁睍 Window 鎺ュ彛浠ュ寘鍚櫨搴﹀湴鍥� GL API
+declare global {
+ interface Window {
+ BMapGL: any
+ initBaiduMap: () => void
+ }
+}
+
+const emits = defineEmits(['locateChange', 'update:center'])
+const state = reactive({
+ lonLat: '', // 缁忓害,绾害
+ address: '',
+ loading: false,
+ latitude: '', // 绾害
+ longitude: '', // 缁忓害
+ map: null as any, // 鍦板浘瀵硅薄
+ mapAddrOptions: [] as any[],
+ mapMarker: null as any, // 鏍囪瀵硅薄
+ geocoder: null as any,
+ autoComplete: null as any,
+ tips: [] // 鎼滅储鎻愮ず
+})
+
+const props = defineProps({
+ clickMap: propTypes.bool.def(false),
+ isWrite: propTypes.bool.def(false),
+ center: propTypes.string.def('')
+})
+
+/** 鍔犺浇鐧惧害鍦板浘 */
+const loadMap = () => {
+ state.address = ''
+ state.latitude = ''
+ state.longitude = ''
+
+ // 鍒涘缓鐧惧害鍦板浘 API 鑴氭湰锛屽姩鎬佸姞杞�
+ const script = document.createElement('script')
+ script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${
+ import.meta.env.VITE_BAIDU_MAP_KEY
+ }&callback=initBaiduMap`
+ document.body.appendChild(script)
+
+ // 瀹氫箟鍏ㄥ眬鍥炶皟鍑芥暟
+ window.initBaiduMap = () => {
+ initMap()
+ initGeocoder()
+ initAutoComplete()
+
+ // TODO @super锛氳繖閲屽姞涓�琛屾敞閲�
+ if (props.clickMap) {
+ state.map.addEventListener('click', (e: any) => {
+ console.log(e)
+ const point = e.latlng
+ console.log(point)
+ state.lonLat = point.lng + ',' + point.lat
+ console.log(state.lonLat)
+ regeoCode(state.lonLat)
+ })
+ }
+
+ // TODO @super锛氳繖閲屽姞涓�琛屾敞閲�
+ if (props.center) {
+ regeoCode(props.center)
+ }
+ }
+}
+
+/** 鍒濆鍖栧湴鍥� */
+const initMap = () => {
+ const mapId = 'bdMap'
+ state.map = new window.BMapGL.Map(mapId)
+ // TODO @super锛氳繖涓槸榛樿鐨勫搰锛�
+ state.map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11)
+ state.map.enableScrollWheelZoom()
+ state.map.disableDoubleClickZoom()
+
+ // 娣诲姞鍦板浘鎺т欢
+ state.map.addControl(new window.BMapGL.NavigationControl())
+ state.map.addControl(new window.BMapGL.ScaleControl())
+ state.map.addControl(new window.BMapGL.ZoomControl())
+}
+
+/** 鍒濆鍖栧湴鐞嗙紪鐮佸櫒 */
+const initGeocoder = () => {
+ state.geocoder = new window.BMapGL.Geocoder()
+}
+
+/** 鍒濆鍖栬嚜鍔ㄥ畬鎴� */
+const initAutoComplete = () => {
+ state.autoComplete = new window.BMapGL.Autocomplete({
+ input: 'searchInput',
+ location: state.map
+ })
+}
+
+/**
+ * 鎼滅储鍦板潃
+ * @param queryValue 鎼滅储鍏抽敭璇�
+ */
+const autoSearch = (queryValue: string) => {
+ if (!queryValue) {
+ state.mapAddrOptions = []
+ return
+ }
+
+ state.loading = true
+
+ // 浣跨敤鐧惧害鍦板浘鍦扮偣妫�绱㈡湇鍔�
+ const localSearch = new window.BMapGL.LocalSearch(state.map, {
+ onSearchComplete: (results: any) => {
+ state.loading = false
+ const temp: any[] = []
+
+ if (results && results.getPoi) {
+ const pois = results.getPoi()
+ pois.forEach((p: any) => {
+ const point = p.point
+ if (point && point.lng && point.lat) {
+ temp.push({
+ name: p.title,
+ value: point.lng + ',' + point.lat
+ })
+ }
+ })
+ }
+
+ state.mapAddrOptions = temp
+ }
+ })
+
+ localSearch.search(queryValue)
+}
+
+/**
+ * 澶勭悊鍦板潃閫夋嫨
+ * @param value 閫変腑鐨勫湴鍧�鍊�
+ */
+const handleAddressSelect = (value: string) => {
+ if (value) {
+ regeoCode(value)
+ }
+}
+
+/**
+ * 娣诲姞鏍囪鐐�
+ * @param lnglat 缁忕含搴︽暟缁�
+ */
+// TODO @super锛氭嫾鍐欙紱灏介噺涓嶈鏈� idea 缁胯壊鎻愰啋鍝�
+const setMarker = (lnglat: any) => {
+ if (!lnglat) return
+
+ // 濡傛灉鐐规爣璁板凡瀛樺湪鍒欏厛绉婚櫎鍘熺偣
+ if (state.mapMarker !== null) {
+ state.map.removeOverlay(state.mapMarker)
+ state.lonLat = ''
+ }
+
+ // 鍒涘缓鏂扮殑鏍囪鐐�
+ const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
+ state.mapMarker = new window.BMapGL.Marker(point)
+
+ // 娣诲姞鐐规爣璁板埌鍦板浘
+ state.map.addOverlay(state.mapMarker)
+ state.map.centerAndZoom(point, 16)
+}
+
+/**
+ * 缁忕含搴﹁浆鍖栦负鍦板潃銆佹坊鍔犳爣璁扮偣
+ * @param lonLat 缁忓害,绾害瀛楃涓�
+ */
+// TODO @super锛氭嫾鍐欙紱灏介噺涓嶈鏈� idea 缁胯壊鎻愰啋鍝�
+const regeoCode = (lonLat: string) => {
+ if (!lonLat) return
+
+ // TODO @super锛氭嫾鍐欙紱灏介噺涓嶈鏈� idea 缁胯壊鎻愰啋鍝�
+ const lnglat = lonLat.split(',')
+ if (lnglat.length !== 2) return
+
+ state.longitude = lnglat[0]
+ state.latitude = lnglat[1]
+
+ // 閫氱煡鐖剁粍浠朵綅缃彉鏇�
+ emits('locateChange', lnglat)
+ emits('update:center', lonLat)
+
+ // 鍏堝皢鍦板浘涓績鐐硅缃埌鐩爣浣嶇疆
+ const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
+ state.map.centerAndZoom(point, 16)
+
+ // 鍐嶈缃爣璁板苟鑾峰彇鍦板潃
+ setMarker(lnglat)
+ getAddress(lnglat)
+}
+
+// TODO @super锛歭nglat 鎷煎啓
+/**
+ * 鏍规嵁缁忕含搴﹁幏鍙栧湴鍧�淇℃伅
+ *
+ * @param lnglat 缁忕含搴︽暟缁�
+ */
+const getAddress = (lnglat: any) => {
+ const point = new window.BMapGL.Point(lnglat[0], lnglat[1])
+
+ state.geocoder.getLocation(point, (result: any) => {
+ if (result && result.address) {
+ state.address = result.address
+ }
+ })
+}
+
+/** 鏄惧紡鏆撮湶鏂规硶锛屼娇鍏跺彲浠ヨ鐖剁粍浠惰闂� */
+defineExpose({ regeoCode })
+
+onMounted(() => {
+ loadMap()
+})
+</script>
+
+<style scoped>
+.mapContainer {
+ width: 100%;
+ height: 400px;
+}
+</style>
diff --git a/src/components/MarkdownView/index.vue b/src/components/MarkdownView/index.vue
new file mode 100644
index 0000000..86fc939
--- /dev/null
+++ b/src/components/MarkdownView/index.vue
@@ -0,0 +1,204 @@
+<template>
+ <div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
+</template>
+
+<script setup lang="ts">
+import { useClipboard } from '@vueuse/core'
+import MarkdownIt from 'markdown-it'
+import 'highlight.js/styles/vs2015.min.css'
+import hljs from 'highlight.js'
+
+// 瀹氫箟缁勪欢灞炴��
+const props = defineProps({
+ content: {
+ type: String,
+ required: true
+ }
+})
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { copy } = useClipboard({ legacy: true }) // 鍒濆鍖� copy 鍒扮矘璐存澘
+const contentRef = ref()
+
+const md = new MarkdownIt({
+ highlight: function (str, lang) {
+ if (lang && hljs.getLanguage(lang)) {
+ try {
+ const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">澶嶅埗</div>`
+ return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
+ } catch (__) {}
+ }
+ return ``
+ }
+})
+
+/** 娓叉煋 markdown */
+const renderedMarkdown = computed(() => {
+ return md.render(props.content)
+})
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 娣诲姞 copy 鐩戝惉
+ contentRef.value.addEventListener('click', (e: any) => {
+ if (e.target.id === 'copy') {
+ copy(e.target?.dataset?.copy)
+ message.success('澶嶅埗鎴愬姛!')
+ }
+ })
+})
+</script>
+
+<style lang="scss">
+.markdown-view {
+ font-family: PingFang SC;
+ font-size: 0.95rem;
+ font-weight: 400;
+ line-height: 1.6rem;
+ letter-spacing: 0em;
+ text-align: left;
+ color: #3b3e55;
+ max-width: 100%;
+
+ pre {
+ position: relative;
+ }
+
+ pre code.hljs {
+ width: auto;
+ }
+
+ code.hljs {
+ border-radius: 6px;
+ padding-top: 20px;
+ width: auto;
+ @media screen and (min-width: 1536px) {
+ width: 960px;
+ }
+
+ @media screen and (max-width: 1536px) and (min-width: 1024px) {
+ width: calc(100vw - 400px - 64px - 32px * 2);
+ }
+
+ @media screen and (max-width: 1024px) and (min-width: 768px) {
+ width: calc(100vw - 32px * 2);
+ }
+
+ @media screen and (max-width: 768px) {
+ width: calc(100vw - 16px * 2);
+ }
+ }
+
+ p,
+ code.hljs {
+ margin-bottom: 16px;
+ }
+
+ p {
+ //margin-bottom: 1rem !important;
+ margin: 0;
+ margin-bottom: 3px;
+ }
+
+ /* 鏍囬閫氱敤鏍煎紡 */
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--color-G900);
+ margin: 24px 0 8px;
+ font-weight: 600;
+ }
+
+ h1 {
+ font-size: 22px;
+ line-height: 32px;
+ }
+
+ h2 {
+ font-size: 20px;
+ line-height: 30px;
+ }
+
+ h3 {
+ font-size: 18px;
+ line-height: 28px;
+ }
+
+ h4 {
+ font-size: 16px;
+ line-height: 26px;
+ }
+
+ h5 {
+ font-size: 16px;
+ line-height: 24px;
+ }
+
+ h6 {
+ font-size: 16px;
+ line-height: 24px;
+ }
+
+ /* 鍒楄〃锛堟湁搴忥紝鏃犲簭锛� */
+ ul,
+ ol {
+ margin: 0 0 8px 0;
+ padding: 0;
+ font-size: 16px;
+ line-height: 24px;
+ color: #3b3e55; // var(--color-CG600);
+ }
+
+ li {
+ margin: 4px 0 0 20px;
+ margin-bottom: 1rem;
+ }
+
+ ol > li {
+ list-style-type: decimal;
+ margin-bottom: 1rem;
+ // 琛ㄨ揪寮�,淇鏈夊簭鍒楄〃搴忓彿灞曠ず涓嶅叏鐨勯棶棰�
+ // &:nth-child(n + 10) {
+ // margin-left: 30px;
+ // }
+
+ // &:nth-child(n + 100) {
+ // margin-left: 30px;
+ // }
+ }
+
+ ul > li {
+ list-style-type: disc;
+ font-size: 16px;
+ line-height: 24px;
+ margin-right: 11px;
+ margin-bottom: 1rem;
+ color: #3b3e55; // var(--color-G900);
+ }
+
+ ol ul,
+ ol ul > li,
+ ul ul,
+ ul ul li {
+ // list-style: circle;
+ font-size: 16px;
+ list-style: none;
+ margin-left: 6px;
+ margin-bottom: 1rem;
+ }
+
+ ul ul ul,
+ ul ul ul li,
+ ol ol,
+ ol ol > li,
+ ol ul ul,
+ ol ul ul > li,
+ ul ol,
+ ul ol > li {
+ list-style: square;
+ }
+}
+</style>
diff --git a/src/components/OperateLogV2/index.ts b/src/components/OperateLogV2/index.ts
new file mode 100644
index 0000000..f69c222
--- /dev/null
+++ b/src/components/OperateLogV2/index.ts
@@ -0,0 +1,3 @@
+import OperateLogV2 from './src/OperateLogV2.vue'
+
+export { OperateLogV2 }
diff --git a/src/components/OperateLogV2/src/OperateLogV2.vue b/src/components/OperateLogV2/src/OperateLogV2.vue
new file mode 100644
index 0000000..6acc1cc
--- /dev/null
+++ b/src/components/OperateLogV2/src/OperateLogV2.vue
@@ -0,0 +1,105 @@
+<!-- 鏌愪釜璁板綍鐨勬搷浣滄棩蹇楀垪琛紝鐩墠涓昏鐢ㄤ簬 CRM 瀹㈡埛銆佸晢鏈虹瓑璇︽儏鐣岄潰 -->
+<template>
+ <div class="pt-20px">
+ <el-timeline>
+ <el-timeline-item
+ v-for="(log, index) in logList"
+ :key="index"
+ :timestamp="formatDate(log.createTime)"
+ placement="top"
+ >
+ <div class="el-timeline-right-content">
+ <el-tag class="mr-10px" type="success">{{ log.userName }}</el-tag>
+ {{ log.action }}
+ </div>
+ <template #dot>
+ <span :style="{ backgroundColor: getUserTypeColor(log.userType) }" class="dot-node-style">
+ {{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
+ </span>
+ </template>
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { OperateLogVO } from '@/api/system/operatelog'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
+import { ElTag } from 'element-plus'
+
+defineOptions({ name: 'OperateLogV2' })
+
+interface Props {
+ logList: OperateLogVO[] // 鎿嶄綔鏃ュ織鍒楄〃
+}
+
+withDefaults(defineProps<Props>(), {
+ logList: () => []
+})
+
+/** 鑾峰緱 userType 棰滆壊 */
+const getUserTypeColor = (type: number) => {
+ const dict = getDictObj(DICT_TYPE.USER_TYPE, type)
+ switch (dict?.colorType) {
+ case 'success':
+ return '#67C23A'
+ case 'info':
+ return '#909399'
+ case 'warning':
+ return '#E6A23C'
+ case 'danger':
+ return '#F56C6C'
+ }
+ return '#409EFF'
+}
+</script>
+
+<style lang="scss" scoped>
+// 鏃堕棿绾挎牱寮忚皟鏁�
+:deep(.el-timeline) {
+ margin: 10px 0 0 110px;
+
+ .el-timeline-item__wrapper {
+ position: relative;
+ top: -20px;
+
+ .el-timeline-item__timestamp {
+ position: absolute !important;
+ top: 10px;
+ left: -150px;
+ }
+ }
+
+ .el-timeline-right-content {
+ display: flex;
+ align-items: center;
+ min-height: 30px;
+ padding: 10px;
+ background-color: #fff;
+
+ &::before {
+ position: absolute;
+ top: 10px;
+ left: 13px; /* 灏嗕吉鍏冪礌姘村钩灞呬腑 */
+ border-color: transparent #fff transparent transparent; /* 灏栬棰滆壊锛屽乏渚ф湞鍚� */
+ border-style: solid;
+ border-width: 8px; /* 璋冩暣灏栬澶у皬 */
+ content: ''; /* 蹇呴』璁剧疆 content 灞炴�� */
+ }
+ }
+}
+
+.dot-node-style {
+ position: absolute;
+ left: -5px;
+ display: flex;
+ width: 20px;
+ height: 20px;
+ font-size: 10px;
+ color: #fff;
+ border-radius: 50%;
+ justify-content: center;
+ align-items: center;
+}
+</style>
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
new file mode 100644
index 0000000..6bb00b3
--- /dev/null
+++ b/src/components/Pagination/index.vue
@@ -0,0 +1,87 @@
+<!-- 鍩轰簬 ruoyi-vue3 鐨� Pagination 閲嶆瀯锛屾牳蹇冩槸绠�鍖栨棤鐢ㄧ殑灞炴�э紝骞朵娇鐢� ts 閲嶅啓 -->
+<template>
+ <el-pagination
+ v-show="total > 0"
+ v-model:current-page="currentPage"
+ v-model:page-size="pageSize"
+ :background="true"
+ :page-sizes="[10, 20, 30, 50, 100]"
+ :pager-count="pagerCount"
+ :total="total"
+ :small="isSmall"
+ class="float-right mb-15px mt-15px"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+</template>
+<script lang="ts" setup>
+import { computed, watchEffect } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+
+defineOptions({ name: 'Pagination' })
+
+// 姝ゅ瑙e喅浜嗗綋鍏ㄥ眬size涓簊mall鐨勬椂鍊欏垎椤电粍浠舵牱寮忓お澶х殑闂
+const appStore = useAppStore()
+const layoutCurrentSize = computed(() => appStore.currentSize)
+const isSmall = ref<boolean>(layoutCurrentSize.value === 'small')
+watchEffect(() => {
+ isSmall.value = layoutCurrentSize.value === 'small'
+})
+
+const props = defineProps({
+ // 鎬绘潯鐩暟
+ total: {
+ required: true,
+ type: Number
+ },
+ // 褰撳墠椤垫暟锛歱ageNo
+ page: {
+ type: Number,
+ default: 1
+ },
+ // 姣忛〉鏄剧ず鏉$洰涓暟锛歱ageSize
+ limit: {
+ type: Number,
+ default: 20
+ },
+ // 璁剧疆鏈�澶ч〉鐮佹寜閽暟銆� 椤电爜鎸夐挳鐨勬暟閲忥紝褰撴�婚〉鏁拌秴杩囪鍊兼椂浼氭姌鍙�
+ // 绉诲姩绔〉鐮佹寜閽殑鏁伴噺绔粯璁ゅ�� 5
+ pagerCount: {
+ type: Number,
+ default: document.body.clientWidth < 992 ? 5 : 7
+ }
+})
+
+const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
+const currentPage = computed({
+ get() {
+ return props.page
+ },
+ set(val) {
+ // 瑙﹀彂 update:page 浜嬩欢锛屾洿鏂� limit 灞炴�э紝浠庤�屾洿鏂� pageNo
+ emit('update:page', val)
+ }
+})
+const pageSize = computed({
+ get() {
+ return props.limit
+ },
+ set(val) {
+ // 瑙﹀彂 update:limit 浜嬩欢锛屾洿鏂� limit 灞炴�э紝浠庤�屾洿鏂� pageSize
+ emit('update:limit', val)
+ }
+})
+const handleSizeChange = (val) => {
+ // 濡傛灉淇敼鍚庤秴杩囨渶澶ч〉闈紝寮哄埗璺宠浆鍒扮 1 椤�
+ if (currentPage.value * val > props.total) {
+ currentPage.value = 1
+ }
+ // 瑙﹀彂 pagination 浜嬩欢锛岄噸鏂板姞杞藉垪琛�
+ emit('pagination', { page: currentPage.value, limit: val })
+}
+const handleCurrentChange = (val) => {
+ // 瑙﹀彂 pagination 浜嬩欢锛岄噸鏂板姞杞藉垪琛�
+ emit('pagination', { page: val, limit: pageSize.value })
+}
+</script>
diff --git a/src/components/Qrcode/index.ts b/src/components/Qrcode/index.ts
new file mode 100644
index 0000000..ce46161
--- /dev/null
+++ b/src/components/Qrcode/index.ts
@@ -0,0 +1,3 @@
+import Qrcode from './src/Qrcode.vue'
+
+export { Qrcode }
diff --git a/src/components/Qrcode/src/Qrcode.vue b/src/components/Qrcode/src/Qrcode.vue
new file mode 100644
index 0000000..f0ce7b7
--- /dev/null
+++ b/src/components/Qrcode/src/Qrcode.vue
@@ -0,0 +1,253 @@
+<script lang="ts" setup>
+import { computed, nextTick, PropType, ref, unref, watch } from 'vue'
+import QRCode, { QRCodeRenderersOptions } from 'qrcode'
+import { cloneDeep } from 'lodash-es'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+import { isString } from '@/utils/is'
+import { QrcodeLogo } from '@/types/qrcode'
+
+defineOptions({ name: 'Qrcode' })
+
+const props = defineProps({
+ // img 鎴栬�� canvas,img涓嶆敮鎸乴ogo宓屽
+ tag: propTypes.string.validate((v: string) => ['canvas', 'img'].includes(v)).def('canvas'),
+ // 浜岀淮鐮佸唴瀹�
+ text: {
+ type: [String, Array] as PropType<string | Recordable[]>,
+ default: null
+ },
+ // qrcode.js閰嶇疆椤�
+ options: {
+ type: Object as PropType<QRCodeRenderersOptions>,
+ default: () => ({})
+ },
+ // 瀹藉害
+ width: propTypes.number.def(200),
+ // logo
+ logo: {
+ type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
+ default: ''
+ },
+ // 鏄惁杩囨湡
+ disabled: propTypes.bool.def(false),
+ // 杩囨湡鎻愮ず鍐呭
+ disabledText: propTypes.string.def('')
+})
+
+const emit = defineEmits(['done', 'click', 'disabled-click'])
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('qrcode')
+
+const { toCanvas, toDataURL } = QRCode
+
+const loading = ref(true)
+
+const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null)
+
+const renderText = computed(() => String(props.text))
+
+const wrapStyle = computed(() => {
+ return {
+ width: props.width + 'px',
+ height: props.width + 'px'
+ }
+})
+
+const initQrcode = async () => {
+ await nextTick()
+ const options = cloneDeep(props.options || {})
+ if (props.tag === 'canvas') {
+ // 瀹归敊鐜囷紝榛樿瀵瑰唴瀹瑰皯鐨勪簩缁寸爜閲囩敤楂樺閿欑巼锛屽唴瀹瑰鐨勪簩缁寸爜閲囩敤浣庡閿欑巼
+ options.errorCorrectionLevel =
+ options.errorCorrectionLevel || getErrorCorrectionLevel(unref(renderText))
+ const _width: number = await getOriginWidth(unref(renderText), options)
+ options.scale = props.width === 0 ? undefined : (props.width / _width) * 4
+ const canvasRef: HTMLCanvasElement | any = await toCanvas(
+ unref(wrapRef) as HTMLCanvasElement,
+ unref(renderText),
+ options
+ )
+ if (props.logo) {
+ const url = await createLogoCode(canvasRef)
+ emit('done', url)
+ loading.value = false
+ } else {
+ emit('done', canvasRef.toDataURL())
+ loading.value = false
+ }
+ } else {
+ const url = await toDataURL(renderText.value, {
+ errorCorrectionLevel: 'H',
+ width: props.width,
+ ...options
+ })
+ ;(unref(wrapRef) as HTMLImageElement).src = url
+ emit('done', url)
+ loading.value = false
+ }
+}
+
+watch(
+ () => renderText.value,
+ (val) => {
+ if (!val) return
+ initQrcode()
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+const createLogoCode = (canvasRef: HTMLCanvasElement) => {
+ const canvasWidth = canvasRef.width
+ const logoOptions: QrcodeLogo = Object.assign(
+ {
+ logoSize: 0.15,
+ bgColor: '#ffffff',
+ borderSize: 0.05,
+ crossOrigin: 'anonymous',
+ borderRadius: 8,
+ logoRadius: 0
+ },
+ isString(props.logo) ? {} : props.logo
+ )
+ const {
+ logoSize = 0.15,
+ bgColor = '#ffffff',
+ borderSize = 0.05,
+ crossOrigin = 'anonymous',
+ borderRadius = 8,
+ logoRadius = 0
+ } = logoOptions
+ const logoSrc = isString(props.logo) ? props.logo : props.logo.src
+ const logoWidth = canvasWidth * logoSize
+ const logoXY = (canvasWidth * (1 - logoSize)) / 2
+ const logoBgWidth = canvasWidth * (logoSize + borderSize)
+ const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2
+
+ const ctx = canvasRef.getContext('2d')
+ if (!ctx) return
+
+ // logo 搴曡壊
+ canvasRoundRect(ctx)(logoBgXY, logoBgXY, logoBgWidth, logoBgWidth, borderRadius)
+ ctx.fillStyle = bgColor
+ ctx.fill()
+
+ // logo
+ const image = new Image()
+ if (crossOrigin || logoRadius) {
+ image.setAttribute('crossOrigin', crossOrigin)
+ }
+ ;(image as any).src = logoSrc
+
+ // 浣跨敤image缁樺埗鍙互閬垮厤鏌愪簺璺ㄥ煙鎯呭喌
+ const drawLogoWithImage = (image: HTMLImageElement) => {
+ ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
+ }
+
+ // 浣跨敤canvas缁樺埗浠ヨ幏寰楁洿澶氱殑鍔熻兘
+ const drawLogoWithCanvas = (image: HTMLImageElement) => {
+ const canvasImage = document.createElement('canvas')
+ canvasImage.width = logoXY + logoWidth
+ canvasImage.height = logoXY + logoWidth
+ const imageCanvas = canvasImage.getContext('2d')
+ if (!imageCanvas || !ctx) return
+ imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth)
+
+ canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius)
+ if (!ctx) return
+ const fillStyle = ctx.createPattern(canvasImage, 'no-repeat')
+ if (fillStyle) {
+ ctx.fillStyle = fillStyle
+ ctx.fill()
+ }
+ }
+
+ // 灏� logo缁樺埗鍒� canvas涓�
+ return new Promise((resolve: any) => {
+ image.onload = () => {
+ logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image)
+ resolve(canvasRef.toDataURL())
+ }
+ })
+}
+
+// 寰楀埌鍘烸rCode鐨勫ぇ灏忥紝浠ヤ究缂╂斁寰楀埌姝g‘鐨凲rCode澶у皬
+const getOriginWidth = async (content: string, options: QRCodeRenderersOptions) => {
+ const _canvas = document.createElement('canvas')
+ await toCanvas(_canvas, content, options)
+ return _canvas.width
+}
+
+// 瀵逛簬鍐呭灏戠殑QrCode锛屽澶у閿欑巼
+const getErrorCorrectionLevel = (content: string) => {
+ if (content.length > 36) {
+ return 'M'
+ } else if (content.length > 16) {
+ return 'Q'
+ } else {
+ return 'H'
+ }
+}
+
+// copy鏉ョ殑鏂规硶锛岀敤浜庣粯鍒跺渾瑙�
+const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
+ return (x: number, y: number, w: number, h: number, r: number) => {
+ const minSize = Math.min(w, h)
+ if (r > minSize / 2) {
+ r = minSize / 2
+ }
+ ctx.beginPath()
+ ctx.moveTo(x + r, y)
+ ctx.arcTo(x + w, y, x + w, y + h, r)
+ ctx.arcTo(x + w, y + h, x, y + h, r)
+ ctx.arcTo(x, y + h, x, y, r)
+ ctx.arcTo(x, y, x + w, y, r)
+ ctx.closePath()
+ return ctx
+ }
+}
+
+const clickCode = () => {
+ emit('click')
+}
+
+const disabledClick = () => {
+ emit('disabled-click')
+}
+</script>
+
+<template>
+ <div v-loading="loading" :class="[prefixCls, 'relative inline-block']" :style="wrapStyle">
+ <component :is="tag" ref="wrapRef" @click="clickCode" />
+ <div
+ v-if="disabled"
+ :class="`${prefixCls}--disabled`"
+ class="absolute left-0 top-0 h-full w-full flex items-center justify-center"
+ @click="disabledClick"
+ >
+ <div class="absolute left-[50%] top-[50%] font-bold">
+ <Icon :size="30" color="var(--el-color-primary)" icon="ep:refresh-right" />
+ <div>{{ disabledText }}</div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-qrcode;
+
+.#{$prefix-cls} {
+ &--disabled {
+ background: rgb(255 255 255 / 95%);
+
+ & > div {
+ transform: translate(-50%, -50%);
+ }
+ }
+}
+</style>
diff --git a/src/components/RouterSearch/index.vue b/src/components/RouterSearch/index.vue
new file mode 100644
index 0000000..52425ec
--- /dev/null
+++ b/src/components/RouterSearch/index.vue
@@ -0,0 +1,121 @@
+<template>
+ <ElDialog v-if="isModal" v-model="showSearch" :show-close="false" title="鑿滃崟鎼滅储">
+ <el-select
+ filterable
+ :reserve-keyword="false"
+ remote
+ placeholder="璇疯緭鍏ヨ彍鍗曞唴瀹�"
+ :remote-method="remoteMethod"
+ style="width: 100%"
+ @change="handleChange"
+ >
+ <el-option
+ v-for="item in options"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </ElDialog>
+ <div v-else class="custom-hover" @click.stop="showTopSearch = !showTopSearch">
+ <Icon icon="ep:search" :color="color"/>
+ <el-select
+ @click.stop
+ filterable
+ :reserve-keyword="false"
+ remote
+ placeholder="璇疯緭鍏ヨ彍鍗曞唴瀹�"
+ :remote-method="remoteMethod"
+ class="overflow-hidden transition-all-600"
+ :class="showTopSearch ? '!w-220px ml2' : '!w-0'"
+ @change="handleChange"
+ >
+ <el-option
+ v-for="item in options"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+defineProps({
+ isModal: {
+ type: Boolean,
+ default: true
+ },
+ color: propTypes.string.def('')
+})
+
+const router = useRouter() // 璺敱瀵硅薄
+const showSearch = ref(false) // 鏄惁鏄剧ず寮规
+const showTopSearch = ref(false) // 鏄惁鏄剧ず椤堕儴鎼滅储妗�
+const value: Ref = ref('') // 鐢ㄦ埛杈撳叆鐨勫��
+
+const routers = router.getRoutes() // 璺敱瀵硅薄
+const options = computed(() => {
+ // 鎻愮ず閫夐」
+ if (!value.value) {
+ return []
+ }
+ const list = routers.filter((item: any) => {
+ if (item.meta.title?.indexOf(value.value) > -1 || item.path.indexOf(value.value) > -1) {
+ return true
+ }
+ })
+ return list.map((item) => {
+ return {
+ label: `${item.meta.title}${item.path}`,
+ value: item.path
+ }
+ })
+})
+
+function remoteMethod(data) {
+ // 杩欓噷鍙互鎵ц鐩稿簲鐨勬搷浣滐紙渚嬪鎵撳紑鎼滅储妗嗙瓑锛�
+ value.value = data
+}
+
+function handleChange(path) {
+ router.push({ path })
+ hiddenSearch()
+ hiddenTopSearch()
+}
+
+function hiddenSearch() {
+ showSearch.value = false
+}
+
+function hiddenTopSearch() {
+ showTopSearch.value = false
+}
+
+onMounted(() => {
+ window.addEventListener('keydown', listenKey)
+ window.addEventListener('click', hiddenTopSearch)
+})
+
+onUnmounted(() => {
+ window.removeEventListener('keydown', listenKey)
+ window.removeEventListener('click', hiddenTopSearch)
+})
+
+// 鐩戝惉 ctrl + k
+function listenKey(event) {
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
+ // 闃绘瑙﹀彂娴忚鍣ㄩ粯璁や簨浠�
+ event.preventDefault()
+ showSearch.value = !showSearch.value
+ // 杩欓噷鍙互鎵ц鐩稿簲鐨勬搷浣滐紙渚嬪鎵撳紑鎼滅储妗嗙瓑锛�
+ }
+}
+
+defineExpose({
+ openSearch: () => {
+ showSearch.value = true
+ }
+})
+</script>
diff --git a/src/components/Search/index.ts b/src/components/Search/index.ts
new file mode 100644
index 0000000..fcc6f16
--- /dev/null
+++ b/src/components/Search/index.ts
@@ -0,0 +1,3 @@
+import Search from './src/Search.vue'
+
+export { Search }
diff --git a/src/components/Search/src/Search.vue b/src/components/Search/src/Search.vue
new file mode 100644
index 0000000..3218a63
--- /dev/null
+++ b/src/components/Search/src/Search.vue
@@ -0,0 +1,157 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+import { useForm } from '@/hooks/web/useForm'
+import { findIndex } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+import { FormSchema } from '@/types/form'
+
+defineOptions({ name: 'Search' })
+
+const { t } = useI18n()
+
+const props = defineProps({
+ // 鐢熸垚Form鐨勫竷灞�缁撴瀯鏁扮粍
+ schema: {
+ type: Array as PropType<FormSchema[]>,
+ default: () => []
+ },
+ // 鏄惁闇�瑕佹爡鏍煎竷灞�
+ isCol: propTypes.bool.def(false),
+ // 琛ㄥ崟label瀹藉害
+ labelWidth: propTypes.oneOfType([String, Number]).def('auto'),
+ // 鎿嶄綔鎸夐挳椋庢牸浣嶇疆
+ layout: propTypes.string.validate((v: string) => ['inline', 'bottom'].includes(v)).def('inline'),
+ // 搴曢儴鎸夐挳鐨勫榻愭柟寮�
+ buttomPosition: propTypes.string
+ .validate((v: string) => ['left', 'center', 'right'].includes(v))
+ .def('center'),
+ showSearch: propTypes.bool.def(true),
+ showReset: propTypes.bool.def(true),
+ // 鏄惁鏄剧ず浼哥缉
+ expand: propTypes.bool.def(false),
+ // 浼哥缉鐨勭晫闄愬瓧娈�
+ expandField: propTypes.string.def(''),
+ inline: propTypes.bool.def(true),
+ model: {
+ type: Object as PropType<Recordable>,
+ default: () => ({})
+ }
+})
+
+const emit = defineEmits(['search', 'reset'])
+
+const visible = ref(true)
+
+const newSchema = computed(() => {
+ let schema: FormSchema[] = cloneDeep(props.schema)
+ if (props.expand && props.expandField && !unref(visible)) {
+ const index = findIndex(schema, (v: FormSchema) => v.field === props.expandField)
+ if (index > -1) {
+ const length = schema.length
+ schema.splice(index + 1, length)
+ }
+ }
+ if (props.layout === 'inline') {
+ schema = schema.concat([
+ {
+ field: 'action',
+ formItemProps: {
+ labelWidth: '0px'
+ }
+ }
+ ])
+ }
+ return schema
+})
+
+const { register, elFormRef, methods } = useForm({
+ model: props.model || {}
+})
+
+const search = async () => {
+ await unref(elFormRef)?.validate(async (isValid) => {
+ if (isValid) {
+ const { getFormData } = methods
+ const model = await getFormData()
+ emit('search', model)
+ }
+ })
+}
+
+const reset = async () => {
+ unref(elFormRef)?.resetFields()
+ const { getFormData } = methods
+ const model = await getFormData()
+ emit('reset', model)
+}
+
+const bottonButtonStyle = computed(() => {
+ return {
+ textAlign: props.buttomPosition as unknown as 'left' | 'center' | 'right'
+ }
+})
+
+const setVisible = () => {
+ unref(elFormRef)?.resetFields()
+ visible.value = !unref(visible)
+}
+</script>
+
+<template>
+ <!-- update by 鑺嬭壙锛歝lass="-mb-15px" 鐢ㄤ簬闄嶄綆鍜� ContentWrap 缁勪欢鐨勫簳閮ㄨ窛绂伙紝閬垮厤绌洪殭杩囧ぇ -->
+ <Form
+ :inline="inline"
+ :is-col="isCol"
+ :is-custom="false"
+ :label-width="labelWidth"
+ :schema="newSchema"
+ class="-mb-15px"
+ hide-required-asterisk
+ @register="register"
+ >
+ <template #action>
+ <div v-if="layout === 'inline'">
+ <!-- update by 鑺嬭壙锛氬幓闄ゆ悳绱㈢殑 type="primary"锛岄鑹插彉娣′竴鐐� -->
+ <ElButton v-if="showSearch" @click="search">
+ <Icon class="mr-5px" icon="ep:search" />
+ {{ t('common.query') }}
+ </ElButton>
+ <!-- update by 鑺嬭壙锛氬皢 icon="ep:refresh-right" 淇敼鎴� icon="ep:refresh"锛屽拰 ruoyi-vue 鎼滅储淇濇寔涓�鑷� -->
+ <ElButton v-if="showReset" @click="reset">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ {{ t('common.reset') }}
+ </ElButton>
+ <ElButton v-if="expand" text @click="setVisible">
+ {{ t(visible ? 'common.shrink' : 'common.expand') }}
+ <Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
+ </ElButton>
+ <!-- add by 鑺嬭壙锛氳ˉ鍏呭湪鎼滅储鍚庣殑鎸夐挳 -->
+ <slot name="actionMore"></slot>
+ </div>
+ </template>
+ <template v-for="name in Object.keys($slots)" :key="name" #[name]>
+ <slot :name="name"></slot>
+ </template>
+ </Form>
+
+ <template v-if="layout === 'bottom'">
+ <div :style="bottonButtonStyle">
+ <ElButton v-if="showSearch" type="primary" @click="search">
+ <Icon class="mr-5px" icon="ep:search" />
+ {{ t('common.query') }}
+ </ElButton>
+ <ElButton v-if="showReset" @click="reset">
+ <Icon class="mr-5px" icon="ep:refresh-right" />
+ {{ t('common.reset') }}
+ </ElButton>
+ <ElButton v-if="expand" text @click="setVisible">
+ {{ t(visible ? 'common.shrink' : 'common.expand') }}
+ <Icon :icon="visible ? 'ep:arrow-up' : 'ep:arrow-down'" />
+ </ElButton>
+ <!-- add by 鑺嬭壙锛氳ˉ鍏呭湪鎼滅储鍚庣殑鎸夐挳 -->
+ <slot name="actionMore"></slot>
+ </div>
+ </template>
+</template>
diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue
new file mode 100644
index 0000000..78c5130
--- /dev/null
+++ b/src/components/ShortcutDateRangePicker/index.vue
@@ -0,0 +1,84 @@
+<template>
+ <div class="flex flex-row items-center gap-2">
+ <el-radio-group v-model="shortcutDays" @change="handleShortcutDaysChange">
+ <el-radio-button :value="1">鏄ㄥぉ</el-radio-button>
+ <el-radio-button :value="7">鏈�杩�7澶�</el-radio-button>
+ <el-radio-button :value="30">鏈�杩�30澶�</el-radio-button>
+ </el-radio-group>
+ <el-date-picker
+ v-model="times"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ :shortcuts="shortcuts"
+ class="!w-240px"
+ @change="emitDateRangePicker"
+ />
+ <slot></slot>
+ </div>
+</template>
+<script lang="ts" setup>
+import dayjs from 'dayjs'
+import * as DateUtil from '@/utils/formatTime'
+
+/** 蹇嵎鏃ユ湡鑼冨洿閫夋嫨缁勪欢 */
+defineOptions({ name: 'ShortcutDateRangePicker' })
+
+const shortcutDays = ref(7) // 鏃ユ湡蹇嵎澶╂暟锛堝崟閫夋寜閽粍锛�, 榛樿7澶�
+const times = ref<[string, string]>(['', '']) // 鏃堕棿鑼冨洿鍙傛暟
+defineExpose({ times }) // 鏆撮湶鏃堕棿鑼冨洿鍙傛暟
+/** 鏃ユ湡蹇嵎閫夋嫨 */
+const shortcuts = [
+ {
+ text: '鏄ㄥぉ',
+ value: () => DateUtil.getDayRange(new Date(), -1)
+ },
+ {
+ text: '鏈�杩�7澶�',
+ value: () => DateUtil.getLast7Days()
+ },
+ {
+ text: '鏈湀',
+ value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')]
+ },
+ {
+ text: '鏈�杩�30澶�',
+ value: () => DateUtil.getLast30Days()
+ },
+ {
+ text: '鏈�杩�1骞�',
+ value: () => DateUtil.getLast1Year()
+ }
+]
+
+/** 璁剧疆鏃堕棿鑼冨洿 */
+function setTimes() {
+ const beginDate = dayjs().subtract(shortcutDays.value, 'd')
+ const yesterday = dayjs().subtract(1, 'd')
+ times.value = DateUtil.getDateRange(beginDate, yesterday)
+}
+
+/** 蹇嵎鏃ユ湡鍗曢�夋寜閽�変腑 */
+const handleShortcutDaysChange = async () => {
+ // 璁剧疆鏃堕棿鑼冨洿
+ setTimes()
+ // 鍙戦�佹椂闂磋寖鍥撮�変腑浜嬩欢
+ await emitDateRangePicker()
+}
+
+/** 瑙﹀彂浜嬩欢锛氭椂闂磋寖鍥撮�変腑 */
+const emits = defineEmits<{
+ (e: 'change', times: [dayjs.ConfigType, dayjs.ConfigType]): void
+}>()
+/** 瑙﹀彂鏃堕棿鑼冨洿閫変腑浜嬩欢 */
+const emitDateRangePicker = async () => {
+ emits('change', times.value)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ handleShortcutDaysChange()
+})
+</script>
diff --git a/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue b/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
new file mode 100644
index 0000000..ae7495e
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/NodeHandler.vue
@@ -0,0 +1,308 @@
+<template>
+ <div class="node-handler-wrapper">
+ <div class="node-handler">
+ <el-popover
+ trigger="hover"
+ v-model:visible="popoverShow"
+ placement="right-start"
+ width="auto"
+ v-if="!readonly"
+ >
+ <div class="handler-item-wrapper">
+ <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)">
+ <div class="approve handler-item-icon">
+ <span class="iconfont icon-approve icon-size"></span>
+ </div>
+ <div class="handler-item-text">瀹℃壒浜�</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.TRANSACTOR_NODE)">
+ <div class="transactor handler-item-icon">
+ <span class="iconfont icon-transactor icon-size"></span>
+ </div>
+ <div class="handler-item-text">鍔炵悊浜�</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)">
+ <div class="handler-item-icon copy">
+ <span class="iconfont icon-size icon-copy"></span>
+ </div>
+ <div class="handler-item-text">鎶勯��</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.CONDITION_BRANCH_NODE)">
+ <div class="handler-item-icon condition">
+ <span class="iconfont icon-size icon-exclusive"></span>
+ </div>
+ <div class="handler-item-text">鏉′欢鍒嗘敮</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.PARALLEL_BRANCH_NODE)">
+ <div class="handler-item-icon parallel">
+ <span class="iconfont icon-size icon-parallel"></span>
+ </div>
+ <div class="handler-item-text">骞惰鍒嗘敮</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)">
+ <div class="handler-item-icon inclusive">
+ <span class="iconfont icon-size icon-inclusive"></span>
+ </div>
+ <div class="handler-item-text">鍖呭鍒嗘敮</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.DELAY_TIMER_NODE)">
+ <div class="handler-item-icon delay">
+ <span class="iconfont icon-size icon-delay"></span>
+ </div>
+ <div class="handler-item-text">寤惰繜鍣�</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.ROUTER_BRANCH_NODE)">
+ <div class="handler-item-icon router">
+ <span class="iconfont icon-size icon-router"></span>
+ </div>
+ <div class="handler-item-text">璺敱鍒嗘敮</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)">
+ <div class="handler-item-icon trigger">
+ <span class="iconfont icon-size icon-trigger"></span>
+ </div>
+ <div class="handler-item-text">瑙﹀彂鍣�</div>
+ </div>
+ <div class="handler-item" @click="addNode(NodeType.CHILD_PROCESS_NODE)">
+ <div class="handler-item-icon child-process">
+ <span class="iconfont icon-size icon-child-process"></span>
+ </div>
+ <div class="handler-item-text">瀛愭祦绋�</div>
+ </div>
+ </div>
+ <template #reference>
+ <div class="add-icon"><Icon icon="ep:plus" /></div>
+ </template>
+ </el-popover>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import {
+ ApproveMethodType,
+ AssignEmptyHandlerType,
+ AssignStartUserHandlerType,
+ ConditionType,
+ NODE_DEFAULT_NAME,
+ NodeType,
+ RejectHandlerType,
+ SimpleFlowNode,
+ DEFAULT_CONDITION_GROUP_VALUE
+} from './consts'
+import { generateUUID } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({
+ name: 'NodeHandler'
+})
+
+const popoverShow = ref(false)
+const props = defineProps({
+ childNode: {
+ type: Object as () => SimpleFlowNode,
+ default: null
+ },
+ currentNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+const emits = defineEmits(['update:childNode'])
+
+const readonly = inject<Boolean>('readonly') // 鏄惁鍙
+
+const addNode = (type: number) => {
+ popoverShow.value = false
+ if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) {
+ const id = 'Activity_' + generateUUID()
+ const data: SimpleFlowNode = {
+ id: id,
+ name: NODE_DEFAULT_NAME.get(type) as string,
+ showText: '',
+ type: type,
+ approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
+ // 瓒呮椂澶勭悊
+ rejectHandler: {
+ type: RejectHandlerType.FINISH_PROCESS
+ },
+ timeoutHandler: {
+ enable: false
+ },
+ assignEmptyHandler: {
+ type: AssignEmptyHandlerType.APPROVE
+ },
+ assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
+ childNode: props.childNode,
+ taskCreateListener: {
+ enable: false
+ },
+ taskAssignListener: {
+ enable: false
+ },
+ taskCompleteListener: {
+ enable: false
+ }
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.COPY_TASK_NODE) {
+ const data: SimpleFlowNode = {
+ id: 'Activity_' + generateUUID(),
+ name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string,
+ showText: '',
+ type: NodeType.COPY_TASK_NODE,
+ childNode: props.childNode
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.CONDITION_BRANCH_NODE) {
+ const data: SimpleFlowNode = {
+ name: '鏉′欢鍒嗘敮',
+ type: NodeType.CONDITION_BRANCH_NODE,
+ id: 'GateWay_' + generateUUID(),
+ childNode: props.childNode,
+ conditionNodes: [
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '鏉′欢1',
+ showText: '',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionSetting: {
+ defaultFlow: false,
+ conditionType: ConditionType.RULE,
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+ }
+ },
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '鍏跺畠鎯呭喌',
+ showText: '鏈弧瓒冲叾瀹冩潯浠舵椂锛屽皢杩涘叆姝ゅ垎鏀�',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionSetting: {
+ defaultFlow: true
+ }
+ }
+ ]
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.PARALLEL_BRANCH_NODE) {
+ const data: SimpleFlowNode = {
+ name: '骞惰鍒嗘敮',
+ type: NodeType.PARALLEL_BRANCH_NODE,
+ id: 'GateWay_' + generateUUID(),
+ childNode: props.childNode,
+ conditionNodes: [
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '骞惰1',
+ showText: '鏃犻渶閰嶇疆鏉′欢鍚屾椂鎵ц',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined
+ },
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '骞惰2',
+ showText: '鏃犻渶閰嶇疆鏉′欢鍚屾椂鎵ц',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined
+ }
+ ]
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.INCLUSIVE_BRANCH_NODE) {
+ const data: SimpleFlowNode = {
+ name: '鍖呭鍒嗘敮',
+ type: NodeType.INCLUSIVE_BRANCH_NODE,
+ id: 'GateWay_' + generateUUID(),
+ childNode: props.childNode,
+ conditionNodes: [
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '鍖呭鏉′欢1',
+ showText: '',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionSetting: {
+ defaultFlow: false,
+ conditionType: ConditionType.RULE,
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+ }
+ },
+ {
+ id: 'Flow_' + generateUUID(),
+ name: '鍏跺畠鎯呭喌',
+ showText: '鏈弧瓒冲叾瀹冩潯浠舵椂锛屽皢杩涘叆姝ゅ垎鏀�',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionSetting: {
+ defaultFlow: true
+ }
+ }
+ ]
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.DELAY_TIMER_NODE) {
+ const data: SimpleFlowNode = {
+ id: 'Activity_' + generateUUID(),
+ name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string,
+ showText: '',
+ type: NodeType.DELAY_TIMER_NODE,
+ childNode: props.childNode
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.ROUTER_BRANCH_NODE) {
+ const data: SimpleFlowNode = {
+ id: 'GateWay_' + generateUUID(),
+ name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string,
+ showText: '',
+ type: NodeType.ROUTER_BRANCH_NODE,
+ childNode: props.childNode
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.TRIGGER_NODE) {
+ const data: SimpleFlowNode = {
+ id: 'Activity_' + generateUUID(),
+ name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string,
+ showText: '',
+ type: NodeType.TRIGGER_NODE,
+ childNode: props.childNode
+ }
+ emits('update:childNode', data)
+ }
+ if (type === NodeType.CHILD_PROCESS_NODE) {
+ const data: SimpleFlowNode = {
+ id: 'Activity_' + generateUUID(),
+ name: NODE_DEFAULT_NAME.get(NodeType.CHILD_PROCESS_NODE) as string,
+ showText: '',
+ type: NodeType.CHILD_PROCESS_NODE,
+ childNode: props.childNode,
+ childProcessSetting: {
+ calledProcessDefinitionKey: '',
+ calledProcessDefinitionName: '',
+ async: false,
+ skipStartUserNode: false,
+ startUserSetting: {
+ type: 1
+ },
+ timeoutSetting: {
+ enable: false
+ },
+ multiInstanceSetting: {
+ enable: false
+ }
+ }
+ }
+ emits('update:childNode', data)
+ }
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue b/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
new file mode 100644
index 0000000..dddeda6
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/ProcessNodeTree.vue
@@ -0,0 +1,150 @@
+<template>
+ <!-- 鍙戣捣浜鸿妭鐐� -->
+ <StartUserNode
+ v-if="currentNode && currentNode.type === NodeType.START_USER_NODE"
+ :flow-node="currentNode"
+ />
+ <!-- 瀹℃壒鑺傜偣 -->
+ <UserTaskNode
+ v-if="
+ currentNode &&
+ (currentNode.type === NodeType.USER_TASK_NODE ||
+ currentNode.type === NodeType.TRANSACTOR_NODE)
+ "
+ :flow-node="currentNode"
+ @update:flow-node="handleModelValueUpdate"
+ @find:parent-node="findFromParentNode"
+ />
+ <!-- 鎶勯�佽妭鐐� -->
+ <CopyTaskNode
+ v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE"
+ :flow-node="currentNode"
+ @update:flow-node="handleModelValueUpdate"
+ />
+ <!-- 鏉′欢鑺傜偣 -->
+ <ExclusiveNode
+ v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE"
+ :flow-node="currentNode"
+ @update:model-value="handleModelValueUpdate"
+ @find:parent-node="findFromParentNode"
+ />
+ <!-- 骞惰鑺傜偣 -->
+ <ParallelNode
+ v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE"
+ :flow-node="currentNode"
+ @update:model-value="handleModelValueUpdate"
+ @find:parent-node="findFromParentNode"
+ />
+ <!-- 鍖呭鍒嗘敮鑺傜偣 -->
+ <InclusiveNode
+ v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE"
+ :flow-node="currentNode"
+ @update:model-value="handleModelValueUpdate"
+ @find:parent-node="findFromParentNode"
+ />
+ <!-- 寤惰繜鍣ㄨ妭鐐� -->
+ <DelayTimerNode
+ v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE"
+ :flow-node="currentNode"
+ @update:flow-node="handleModelValueUpdate"
+ />
+ <!-- 璺敱鍒嗘敮鑺傜偣 -->
+ <RouterNode
+ v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE"
+ :flow-node="currentNode"
+ @update:flow-node="handleModelValueUpdate"
+ />
+ <!-- 瑙﹀彂鍣ㄨ妭鐐� -->
+ <TriggerNode
+ v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE"
+ :flow-node="currentNode"
+ @update:flow-node="handleModelValueUpdate"
+ />
+ <!-- 瀛愭祦绋嬭妭鐐� -->
+ <ChildProcessNode
+ v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE"
+ :flow-node="currentNode"
+ @update:flow-node="handleModelValueUpdate"
+ />
+ <!-- 閫掑綊鏄剧ず瀛╁瓙鑺傜偣 -->
+ <ProcessNodeTree
+ v-if="currentNode && currentNode.childNode"
+ v-model:flow-node="currentNode.childNode"
+ :parent-node="currentNode"
+ @find:recursive-find-parent-node="recursiveFindParentNode"
+ />
+
+ <!-- 缁撴潫鑺傜偣 -->
+ <EndEventNode
+ v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE"
+ :flow-node="currentNode"
+ />
+</template>
+<script setup lang="ts">
+import StartUserNode from './nodes/StartUserNode.vue'
+import EndEventNode from './nodes/EndEventNode.vue'
+import UserTaskNode from './nodes/UserTaskNode.vue'
+import CopyTaskNode from './nodes/CopyTaskNode.vue'
+import ExclusiveNode from './nodes/ExclusiveNode.vue'
+import ParallelNode from './nodes/ParallelNode.vue'
+import InclusiveNode from './nodes/InclusiveNode.vue'
+import DelayTimerNode from './nodes/DelayTimerNode.vue'
+import RouterNode from './nodes/RouterNode.vue'
+import TriggerNode from './nodes/TriggerNode.vue'
+import ChildProcessNode from './nodes/ChildProcessNode.vue'
+import { SimpleFlowNode, NodeType } from './consts'
+import { useWatchNode } from './node'
+defineOptions({
+ name: 'ProcessNodeTree'
+})
+const props = defineProps({
+ parentNode: {
+ type: Object as () => SimpleFlowNode,
+ default: () => null
+ },
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ default: () => null
+ }
+})
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+ 'find:recursiveFindParentNode': [
+ nodeList: SimpleFlowNode[],
+ curentNode: SimpleFlowNode,
+ nodeType: number
+ ]
+}>()
+
+const currentNode = useWatchNode(props)
+
+// 鐢ㄤ簬鍒犻櫎鑺傜偣
+const handleModelValueUpdate = (updateValue) => {
+ emits('update:flowNode', updateValue)
+}
+
+const findFromParentNode = (nodeList: SimpleFlowNode[], nodeType: number) => {
+ emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
+}
+
+// 閫掑綊浠庣埗鑺傜偣涓煡璇㈠尮閰嶇殑鑺傜偣
+const recursiveFindParentNode = (
+ nodeList: SimpleFlowNode[],
+ findNode: SimpleFlowNode,
+ nodeType: number
+) => {
+ if (!findNode) {
+ return
+ }
+ if (findNode.type === NodeType.START_USER_NODE) {
+ nodeList.push(findNode)
+ return
+ }
+
+ if (findNode.type === nodeType) {
+ nodeList.push(findNode)
+ }
+ emits('find:recursiveFindParentNode', nodeList, props.parentNode, nodeType)
+}
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
new file mode 100644
index 0000000..b940d6f
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue
@@ -0,0 +1,237 @@
+<template>
+ <div v-loading="loading" class="overflow-auto">
+ <SimpleProcessModel
+ ref="simpleProcessModelRef"
+ v-if="processNodeTree"
+ :flow-node="processNodeTree"
+ :readonly="false"
+ @save="saveSimpleFlowModel"
+ />
+ <Dialog v-model="errorDialogVisible" title="淇濆瓨澶辫触" width="400" :fullscreen="false">
+ <div class="mb-2">浠ヤ笅鑺傜偣鍐呭涓嶅畬鍠勶紝璇蜂慨鏀瑰悗淇濆瓨</div>
+ <div
+ class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
+ v-for="(item, index) in errorNodes"
+ :key="index"
+ >
+ {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+ </div>
+ <template #footer>
+ <el-button type="primary" @click="errorDialogVisible = false">鐭ラ亾浜�</el-button>
+ </template>
+ </Dialog>
+ </div>
+</template>
+
+<script setup lang="ts">
+import SimpleProcessModel from './SimpleProcessModel.vue'
+import { SimpleFlowNode, NodeType, NodeId, NODE_DEFAULT_TEXT } from './consts'
+import { getForm } from '@/api/bpm/form'
+import { handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import { BpmModelFormType } from '@/utils/constants'
+
+defineOptions({
+ name: 'SimpleProcessDesigner'
+})
+
+const emits = defineEmits(['success']) // 淇濆瓨鎴愬姛浜嬩欢
+
+const props = defineProps({
+ modelName: {
+ type: String,
+ required: false
+ },
+ // 娴佺▼琛ㄥ崟 ID
+ modelFormId: {
+ type: Number,
+ required: false,
+ default: undefined,
+ },
+ // 琛ㄥ崟绫诲瀷
+ modelFormType: {
+ type: Number,
+ required: false,
+ default: BpmModelFormType.NORMAL,
+ },
+ // 鍙彂璧锋祦绋嬬殑浜哄憳缂栧彿
+ startUserIds: {
+ type: Array,
+ required: false
+ },
+ // 鍙彂璧锋祦绋嬬殑閮ㄩ棬缂栧彿
+ startDeptIds: {
+ type: Array,
+ required: false
+ }
+})
+
+const processData = inject('processData') as Ref
+const loading = ref(false)
+const formFields = ref<string[]>([])
+const formType = ref(props.modelFormType);
+
+// 鐩戝惉 modelFormType 鍙樺寲
+watch(
+ () => props.modelFormType,
+ (newVal) => {
+ formType.value = newVal;
+ },
+);
+
+// 鐩戝惉 modelFormId 鍙樺寲
+watch(
+ () => props.modelFormId,
+ async (newVal) => {
+ if (newVal) {
+ const form = await getForm(newVal);
+ formFields.value = form?.fields;
+ } else {
+ // 濡傛灉 modelFormId 涓虹┖锛屾竻绌鸿〃鍗曞瓧娈�
+ formFields.value = [];
+ }
+ },
+ { immediate: true },
+);
+
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 瑙掕壊鍒楄〃
+const postOptions = ref<PostApi.PostVO[]>([]) // 宀椾綅鍒楄〃
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const deptOptions = ref<DeptApi.DeptVO[]>([]) // 閮ㄩ棬鍒楄〃
+const deptTreeOptions = ref()
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 鐢ㄦ埛缁勫垪琛�
+
+provide('formFields', formFields)
+provide('formType', formType)
+provide('roleList', roleOptions)
+provide('postList', postOptions)
+provide('userList', userOptions)
+provide('deptList', deptOptions)
+provide('userGroupList', userGroupOptions)
+provide('deptTree', deptTreeOptions)
+provide('startUserIds', props.startUserIds)
+provide('startDeptIds', props.startDeptIds)
+provide('tasks', [])
+provide('processInstance', {})
+
+
+const message = useMessage() // 鍥介檯鍖�
+const processNodeTree = ref<SimpleFlowNode | undefined>()
+provide('processNodeTree', processNodeTree)
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+
+// 娣诲姞鏇存柊妯″瀷鐨勬柟娉�
+const updateModel = () => {
+ if (!processNodeTree.value) {
+ processNodeTree.value = {
+ name: '鍙戣捣浜�',
+ type: NodeType.START_USER_NODE,
+ id: NodeId.START_USER_NODE_ID,
+ childNode: {
+ id: NodeId.END_EVENT_NODE_ID,
+ name: '缁撴潫',
+ type: NodeType.END_EVENT_NODE
+ }
+ }
+ // 鍒濆鍖栨椂涔熻Е鍙戜竴娆′繚瀛�
+ saveSimpleFlowModel(processNodeTree.value)
+ }
+}
+
+const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
+ if (!simpleModelNode) {
+ return
+ }
+
+ try {
+ processData.value = simpleModelNode
+ emits('success', simpleModelNode)
+ } catch (error) {
+ console.error('淇濆瓨澶辫触:', error)
+ }
+}
+
+// 鏍¢獙鑺傜偣璁剧疆銆� 鏆傛椂浠� showText 涓虹┖ 鏈妭鐐归敊璇厤缃�
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+ if (node) {
+ const { type, showText, conditionNodes } = node
+ if (type == NodeType.END_EVENT_NODE) {
+ return
+ }
+ if (type == NodeType.START_USER_NODE) {
+ // 鍙戣捣浜鸿妭鐐规殏鏃朵笉鐢ㄦ牎楠岋紝鐩存帴鏍¢獙瀛╁瓙鑺傜偣
+ validateNode(node.childNode, errorNodes)
+ }
+
+ if (
+ type === NodeType.USER_TASK_NODE ||
+ type === NodeType.COPY_TASK_NODE ||
+ type === NodeType.CONDITION_NODE
+ ) {
+ if (!showText) {
+ errorNodes.push(node)
+ }
+ validateNode(node.childNode, errorNodes)
+ }
+
+ if (
+ type == NodeType.CONDITION_BRANCH_NODE ||
+ type == NodeType.PARALLEL_BRANCH_NODE ||
+ type == NodeType.INCLUSIVE_BRANCH_NODE
+ ) {
+ // 鍒嗘敮鑺傜偣
+ // 1. 鍏堟牎楠屽悇涓垎鏀�
+ conditionNodes?.forEach((item) => {
+ validateNode(item, errorNodes)
+ })
+ // 2. 鏍¢獙瀛╁瓙鑺傜偣
+ validateNode(node.childNode, errorNodes)
+ }
+ }
+}
+
+onMounted(async () => {
+ try {
+ loading.value = true
+ // // 鑾峰彇琛ㄥ崟瀛楁
+ // if (props.modelId) {
+ // const bpmnModel = await getModel(props.modelId)
+ // if (bpmnModel) {
+ // formType.value = bpmnModel.formType
+ // if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) {
+ // const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
+ // formFields.value = bpmnForm?.fields
+ // }
+ // }
+ // }
+ // 鑾峰緱瑙掕壊鍒楄〃
+ roleOptions.value = await RoleApi.getSimpleRoleList()
+ // 鑾峰緱宀椾綅鍒楄〃
+ postOptions.value = await PostApi.getSimplePostList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 鑾峰緱閮ㄩ棬鍒楄〃
+ deptOptions.value = await DeptApi.getSimpleDeptList()
+ deptTreeOptions.value = handleTree(deptOptions.value as DeptApi.DeptVO[], 'id')
+ // 鑾峰彇鐢ㄦ埛缁勫垪琛�
+ userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+ // 鍔犺浇娴佺▼鏁版嵁
+ if (processData.value) {
+ processNodeTree.value = processData?.value
+ } else {
+ updateModel()
+ }
+ } finally {
+ loading.value = false
+ }
+})
+
+const simpleProcessModelRef = ref()
+
+defineExpose({})
+</script>
diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
new file mode 100644
index 0000000..a8a0ac6
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue
@@ -0,0 +1,265 @@
+<template>
+ <div class="simple-process-model-container position-relative">
+ <div class="position-absolute top-0px right-0px bg-#fff z-index-button-group">
+ <el-row type="flex" justify="end">
+ <el-button-group key="scale-control" size="default">
+ <el-button v-if="!readonly" size="default" @click="exportJson">
+ <Icon icon="ep:download" /> 瀵煎嚭
+ </el-button>
+ <el-button v-if="!readonly" size="default" @click="importJson">
+ <Icon icon="ep:upload" />瀵煎叆
+ </el-button>
+ <!-- 鐢ㄤ簬鎵撳紑鏈湴鏂囦欢-->
+ <input
+ v-if="!readonly"
+ type="file"
+ id="files"
+ ref="refFile"
+ style="display: none"
+ accept=".json"
+ @change="importLocalFile"
+ />
+ <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
+ <el-button size="default" :plain="true" :icon="ZoomOut" @click="zoomOut()" />
+ <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
+ <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
+ <el-button size="default" @click="resetPosition">閲嶇疆</el-button>
+ </el-button-group>
+ </el-row>
+ </div>
+ <div
+ class="simple-process-model"
+ :style="`transform: translate(${currentX}px, ${currentY}px) scale(${scaleValue / 100});`"
+ @mousedown="startDrag"
+ @mousemove="onDrag"
+ @mouseup="stopDrag"
+ @mouseleave="stopDrag"
+ @mouseenter="setGrabCursor"
+ >
+ <ProcessNodeTree v-if="processNodeTree" v-model:flow-node="processNodeTree" />
+ </div>
+ </div>
+ <Dialog v-model="errorDialogVisible" title="淇濆瓨澶辫触" width="400" :fullscreen="false">
+ <div class="mb-2">浠ヤ笅鑺傜偣鍐呭涓嶅畬鍠勶紝璇蜂慨鏀瑰悗淇濆瓨</div>
+ <div
+ class="mb-3 b-rounded-1 bg-gray-100 p-2 line-height-normal"
+ v-for="(item, index) in errorNodes"
+ :key="index"
+ >
+ {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }}
+ </div>
+ <template #footer>
+ <el-button type="primary" @click="errorDialogVisible = false">鐭ラ亾浜�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import ProcessNodeTree from './ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
+import { useWatchNode } from './node'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { isString } from '@/utils/is'
+import download from '@/utils/download'
+
+defineOptions({
+ name: 'SimpleProcessModel'
+})
+
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ },
+ readonly: {
+ type: Boolean,
+ required: false,
+ default: true
+ }
+})
+
+const emits = defineEmits<{
+ save: [node: SimpleFlowNode | undefined]
+}>()
+
+const processNodeTree = useWatchNode(props)
+
+provide('readonly', props.readonly)
+
+// TODO 鍙紭鍖栵細鎷栨嫿鏈夌偣鍗¢】
+/** 鎷栨嫿銆佹斁澶х缉灏忕瓑鎿嶄綔 */
+let scaleValue = ref(100)
+const MAX_SCALE_VALUE = 200
+const MIN_SCALE_VALUE = 50
+const isDragging = ref(false)
+const startX = ref(0)
+const startY = ref(0)
+const currentX = ref(0)
+const currentY = ref(0)
+const initialX = ref(0)
+const initialY = ref(0)
+
+const setGrabCursor = () => {
+ document.body.style.cursor = 'grab'
+}
+
+const resetCursor = () => {
+ document.body.style.cursor = 'default'
+}
+
+const startDrag = (e: MouseEvent) => {
+ isDragging.value = true
+ startX.value = e.clientX - currentX.value
+ startY.value = e.clientY - currentY.value
+ setGrabCursor() // 璁剧疆灏忔墜鍏夋爣
+}
+
+const onDrag = (e: MouseEvent) => {
+ if (!isDragging.value) return
+ e.preventDefault() // 绂佺敤鏂囨湰閫夋嫨
+
+ // 浣跨敤 requestAnimationFrame 浼樺寲鎬ц兘
+ requestAnimationFrame(() => {
+ currentX.value = e.clientX - startX.value
+ currentY.value = e.clientY - startY.value
+ })
+}
+
+const stopDrag = () => {
+ isDragging.value = false
+ resetCursor() // 閲嶇疆鍏夋爣
+}
+
+const zoomIn = () => {
+ if (scaleValue.value == MAX_SCALE_VALUE) {
+ return
+ }
+ scaleValue.value += 10
+}
+
+const zoomOut = () => {
+ if (scaleValue.value == MIN_SCALE_VALUE) {
+ return
+ }
+ scaleValue.value -= 10
+}
+
+const processReZoom = () => {
+ scaleValue.value = 100
+}
+
+const resetPosition = () => {
+ currentX.value = initialX.value
+ currentY.value = initialY.value
+}
+
+/** 鏍¢獙鑺傜偣璁剧疆 */
+const errorDialogVisible = ref(false)
+let errorNodes: SimpleFlowNode[] = []
+
+const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
+ if (node) {
+ const { type, showText, conditionNodes } = node
+ if (type == NodeType.END_EVENT_NODE) {
+ return
+ }
+ if (type == NodeType.START_USER_NODE) {
+ // 鍙戣捣浜鸿妭鐐规殏鏃朵笉鐢ㄦ牎楠岋紝鐩存帴鏍¢獙瀛╁瓙鑺傜偣
+ validateNode(node.childNode, errorNodes)
+ }
+
+ if (
+ type === NodeType.USER_TASK_NODE ||
+ type === NodeType.COPY_TASK_NODE ||
+ type === NodeType.CONDITION_NODE
+ ) {
+ if (!showText) {
+ errorNodes.push(node)
+ }
+ validateNode(node.childNode, errorNodes)
+ }
+
+ if (
+ type == NodeType.CONDITION_BRANCH_NODE ||
+ type == NodeType.PARALLEL_BRANCH_NODE ||
+ type == NodeType.INCLUSIVE_BRANCH_NODE
+ ) {
+ // 鍒嗘敮鑺傜偣
+ // 1. 鍏堟牎楠屽悇涓垎鏀�
+ conditionNodes?.forEach((item) => {
+ validateNode(item, errorNodes)
+ })
+ // 2. 鏍¢獙瀛╁瓙鑺傜偣
+ validateNode(node.childNode, errorNodes)
+ }
+ }
+}
+
+/** 鑾峰彇褰撳墠娴佺▼鏁版嵁 */
+const getCurrentFlowData = async () => {
+ try {
+ errorNodes = []
+ validateNode(processNodeTree.value, errorNodes)
+ if (errorNodes.length > 0) {
+ errorDialogVisible.value = true
+ return undefined
+ }
+ return processNodeTree.value
+ } catch (error) {
+ console.error('鑾峰彇娴佺▼鏁版嵁澶辫触:', error)
+ return undefined
+ }
+}
+
+defineExpose({
+ getCurrentFlowData
+})
+
+/** 瀵煎嚭 JSON */
+const exportJson = () => {
+ download.json(new Blob([JSON.stringify(processNodeTree.value)]), 'model.json')
+}
+
+/** 瀵煎叆 JSON */
+const refFile = ref()
+const importJson = () => {
+ refFile.value.click()
+}
+const importLocalFile = () => {
+ const file = refFile.value.files[0]
+ const reader = new FileReader()
+ reader.readAsText(file)
+ reader.onload = function () {
+ if (isString(this.result)) {
+ processNodeTree.value = JSON.parse(this.result)
+ emits('save', processNodeTree.value)
+ }
+ }
+}
+
+// 鍦ㄧ粍浠跺垵濮嬪寲鏃惰褰曞垵濮嬩綅缃�
+onMounted(() => {
+ initialX.value = currentX.value
+ initialY.value = currentY.value
+})
+</script>
+
+<style lang="scss" scoped>
+.simple-process-model-container {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ overflow: hidden;
+ user-select: none; // 绂佺敤鏂囨湰閫夋嫨
+}
+
+.simple-process-model {
+ position: relative; // 纭繚鐩稿瀹氫綅
+ min-width: 100%; // 纭繚瀹藉害涓�100%
+ min-height: 100%; // 纭繚楂樺害涓�100%
+}
+
+.z-index-button-group {
+ z-index: 10;
+}
+</style>
diff --git a/src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue b/src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue
new file mode 100644
index 0000000..26cd43f
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/SimpleProcessViewer.vue
@@ -0,0 +1,47 @@
+<template>
+ <SimpleProcessModel :flow-node="simpleModel" :readonly="true" />
+</template>
+
+<script setup lang="ts">
+import { useWatchNode } from './node'
+import { SimpleFlowNode } from './consts'
+
+defineOptions({
+ name: 'SimpleProcessViewer'
+})
+
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ },
+ // 娴佺▼浠诲姟
+ tasks: {
+ type: Array,
+ default: () => [] as any[]
+ },
+ // 娴佺▼瀹炰緥
+ processInstance: {
+ type: Object,
+ default: () => undefined
+ }
+})
+const approveTasks = ref<any[]>(props.tasks)
+const currentProcessInstance = ref(props.processInstance)
+const simpleModel = useWatchNode(props)
+watch(
+ () => props.tasks,
+ (newValue) => {
+ approveTasks.value = newValue
+ }
+)
+watch(
+ () => props.processInstance,
+ (newValue) => {
+ currentProcessInstance.value = newValue
+ }
+)
+
+provide('tasks', approveTasks)
+provide('processInstance', currentProcessInstance)
+</script>
diff --git a/src/components/SimpleProcessDesignerV2/src/consts.ts b/src/components/SimpleProcessDesignerV2/src/consts.ts
new file mode 100644
index 0000000..6475009
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/consts.ts
@@ -0,0 +1,902 @@
+// @ts-ignore
+import { DictDataVO } from '@/api/system/dict/types'
+import { TaskStatusEnum } from '@/api/bpm/task'
+/**
+ * 鑺傜偣绫诲瀷
+ */
+export enum NodeType {
+ /**
+ * 缁撴潫鑺傜偣
+ */
+ END_EVENT_NODE = 1,
+ /**
+ * 鍙戣捣浜鸿妭鐐�
+ */
+ START_USER_NODE = 10,
+ /**
+ * 瀹℃壒浜鸿妭鐐�
+ */
+ USER_TASK_NODE = 11,
+
+ /**
+ * 鎶勯�佷汉鑺傜偣
+ */
+ COPY_TASK_NODE = 12,
+
+ /**
+ * 鍔炵悊浜鸿妭鐐�
+ */
+ TRANSACTOR_NODE = 13,
+
+ /**
+ * 寤惰繜鍣ㄨ妭鐐�
+ */
+ DELAY_TIMER_NODE = 14,
+
+ /**
+ * 瑙﹀彂鍣ㄨ妭鐐�
+ */
+ TRIGGER_NODE = 15,
+
+ /**
+ * 瀛愭祦绋嬭妭鐐�
+ */
+ CHILD_PROCESS_NODE = 20,
+
+ /**
+ * 鏉′欢鑺傜偣
+ */
+ CONDITION_NODE = 50,
+ /**
+ * 鏉′欢鍒嗘敮鑺傜偣 (瀵瑰簲鎺掍粬缃戝叧)
+ */
+ CONDITION_BRANCH_NODE = 51,
+ /**
+ * 骞惰鍒嗘敮鑺傜偣 (瀵瑰簲骞惰缃戝叧)
+ */
+ PARALLEL_BRANCH_NODE = 52,
+
+ /**
+ * 鍖呭鍒嗘敮鑺傜偣 (瀵瑰簲鍖呭缃戝叧)
+ */
+ INCLUSIVE_BRANCH_NODE = 53,
+ /**
+ * 璺敱鍒嗘敮鑺傜偣
+ */
+ ROUTER_BRANCH_NODE = 54
+}
+
+export enum NodeId {
+ /**
+ * 鍙戣捣浜鸿妭鐐� Id
+ */
+ START_USER_NODE_ID = 'StartUserNode',
+
+ /**
+ * 鍙戣捣浜鸿妭鐐� Id
+ */
+ END_EVENT_NODE_ID = 'EndEvent'
+}
+
+/**
+ * 鑺傜偣缁撴瀯瀹氫箟
+ */
+export interface SimpleFlowNode {
+ id: string
+ type: NodeType
+ name: string
+ showText?: string
+ // 瀛╁瓙鑺傜偣
+ childNode?: SimpleFlowNode
+ // 鏉′欢鑺傜偣
+ conditionNodes?: SimpleFlowNode[]
+ // 瀹℃壒绫诲瀷
+ approveType?: ApproveType
+ // 鍊欓�変汉绛栫暐
+ candidateStrategy?: number
+ // 鍊欓�変汉鍙傛暟
+ candidateParam?: string
+ // 澶氫汉瀹℃壒鏂瑰紡
+ approveMethod?: ApproveMethodType
+ //閫氳繃姣斾緥
+ approveRatio?: number
+ // 瀹℃壒鎸夐挳璁剧疆
+ buttonsSetting?: any[]
+ // 琛ㄥ崟鏉冮檺
+ fieldsPermission?: Array<Record<string, any>>
+ // 瀹℃壒浠诲姟瓒呮椂澶勭悊
+ timeoutHandler?: TimeoutHandler
+ // 瀹℃壒浠诲姟鎷掔粷澶勭悊
+ rejectHandler?: RejectHandler
+ // 瀹℃壒浜轰负绌虹殑澶勭悊
+ assignEmptyHandler?: AssignEmptyHandler
+ // 瀹℃壒鑺傜偣鐨勫鎵逛汉涓庡彂璧蜂汉鐩稿悓鏃讹紝瀵瑰簲鐨勫鐞嗙被鍨�
+ assignStartUserHandlerType?: number
+ // 鍒涘缓浠诲姟鐩戝惉鍣�
+ taskCreateListener?: ListenerHandler
+ // 鍒涘缓浠诲姟鐩戝惉鍣�
+ taskAssignListener?: ListenerHandler
+ // 鍒涘缓浠诲姟鐩戝惉鍣�
+ taskCompleteListener?: ListenerHandler
+ // 鏉′欢璁剧疆
+ conditionSetting?: ConditionSetting
+ // 娲诲姩鐨勭姸鎬侊紝鐢ㄤ簬鍓嶇鑺傜偣鐘舵�佸睍绀�
+ activityStatus?: TaskStatusEnum
+ // 寤惰繜璁剧疆
+ delaySetting?: DelaySetting
+ // 璺敱鍒嗘敮
+ routerGroups?: RouterSetting[]
+ defaultFlowId?: string
+ // 绛惧悕
+ signEnable?: boolean
+ // 瀹℃壒鎰忚
+ reasonRequire?: boolean
+ // 璺宠繃琛ㄨ揪寮�
+ skipExpression?: string
+ // 瑙﹀彂鍣ㄨ缃�
+ triggerSetting?: TriggerSetting
+ // 瀛愭祦绋�
+ childProcessSetting?: ChildProcessSetting
+}
+// 鍊欓�変汉绛栫暐鏋氫妇 锛� 鐢ㄤ簬瀹℃壒鑺傜偣銆傛妱閫佽妭鐐� )
+export enum CandidateStrategy {
+ /**
+ * 鎸囧畾瑙掕壊
+ */
+ ROLE = 10,
+ /**
+ * 閮ㄩ棬鎴愬憳
+ */
+ DEPT_MEMBER = 20,
+ /**
+ * 閮ㄩ棬鐨勮礋璐d汉
+ */
+ DEPT_LEADER = 21,
+ /**
+ * 杩炵画澶氱骇閮ㄩ棬鐨勮礋璐d汉
+ */
+ MULTI_LEVEL_DEPT_LEADER = 23,
+ /**
+ * 鎸囧畾宀椾綅
+ */
+ POST = 22,
+ /**
+ * 鎸囧畾鐢ㄦ埛
+ */
+ USER = 30,
+ /**
+ * 瀹℃壒浜鸿嚜閫�
+ */
+ APPROVE_USER_SELECT = 34,
+ /**
+ * 鍙戣捣浜鸿嚜閫�
+ */
+ START_USER_SELECT = 35,
+ /**
+ * 鍙戣捣浜鸿嚜宸�
+ */
+ START_USER = 36,
+ /**
+ * 鍙戣捣浜洪儴闂ㄨ礋璐d汉
+ */
+ START_USER_DEPT_LEADER = 37,
+ /**
+ * 鍙戣捣浜鸿繛缁绾ч儴闂ㄧ殑璐熻矗浜�
+ */
+ START_USER_MULTI_LEVEL_DEPT_LEADER = 38,
+ /**
+ * 鎸囧畾鐢ㄦ埛缁�
+ */
+ USER_GROUP = 40,
+ /**
+ * 琛ㄥ崟鍐呯敤鎴峰瓧娈�
+ */
+ FORM_USER = 50,
+ /**
+ * 琛ㄥ崟鍐呴儴闂ㄨ礋璐d汉
+ */
+ FORM_DEPT_LEADER = 51,
+ /**
+ * 娴佺▼琛ㄨ揪寮�
+ */
+ EXPRESSION = 60
+}
+
+// 澶氫汉瀹℃壒鏂瑰紡绫诲瀷鏋氫妇 锛� 鐢ㄤ簬瀹℃壒鑺傜偣 锛�
+export enum ApproveMethodType {
+ /**
+ * 闅忔満鎸戦�変竴浜哄鎵�
+ */
+ RANDOM_SELECT_ONE_APPROVE = 1,
+
+ /**
+ * 澶氫汉浼氱(鎸夐�氳繃姣斾緥)
+ */
+ APPROVE_BY_RATIO = 2,
+
+ /**
+ * 澶氫汉鎴栫(閫氳繃鍙渶涓�浜猴紝鎷掔粷鍙渶涓�浜�)
+ */
+ ANY_APPROVE = 3,
+ /**
+ * 澶氫汉渚濇瀹℃壒
+ */
+ SEQUENTIAL_APPROVE = 4
+}
+
+/**
+ * 瀹℃壒鎷掔粷缁撴瀯瀹氫箟
+ */
+export type RejectHandler = {
+ // 瀹℃壒鎷掔粷绫诲瀷
+ type: RejectHandlerType
+ // 閫�鍥炶妭鐐� Id
+ returnNodeId?: string
+}
+
+/**
+ * 瀹℃壒瓒呮椂缁撴瀯瀹氫箟
+ */
+export type TimeoutHandler = {
+ // 鏄惁寮�鍚秴鏃跺鐞�
+ enable: boolean
+ // 瓒呮椂鎵ц鐨勫姩浣�
+ type?: number
+ // 瓒呮椂鏃堕棿璁剧疆
+ timeDuration?: string
+ // 鎵ц鍔ㄤ綔鏄嚜鍔ㄦ彁閱�, 鏈�澶ф彁閱掓鏁�
+ maxRemindCount?: number
+}
+
+/**
+ * 瀹℃壒浜轰负绌虹殑缁撴瀯瀹氫箟
+ */
+export type AssignEmptyHandler = {
+ // 瀹℃壒浜轰负绌虹殑澶勭悊绫诲瀷
+ type: AssignEmptyHandlerType
+ // 鎸囧畾鐢ㄦ埛鐨勭紪鍙锋暟缁�
+ userIds?: number[]
+}
+
+/**
+ * 鐩戝惉鍣ㄧ殑缁撴瀯瀹氫箟
+ */
+export type ListenerHandler = {
+ enable: boolean
+ path?: string
+ header?: HttpRequestParam[]
+ body?: HttpRequestParam[]
+}
+export type HttpRequestParam = {
+ key: string
+ type: number
+ value: string
+}
+export enum BpmHttpRequestParamTypeEnum {
+ /**
+ * 鍥哄畾鍊�
+ */
+ FIXED_VALUE = 1,
+ /**
+ * 琛ㄥ崟
+ */
+ FROM_FORM = 2
+}
+export const BPM_HTTP_REQUEST_PARAM_TYPES = [
+ {
+ value: 1,
+ label: '鍥哄畾鍊�'
+ },
+ {
+ value: 2,
+ label: '琛ㄥ崟'
+ }
+]
+
+// 瀹℃壒鎷掔粷绫诲瀷鏋氫妇
+export enum RejectHandlerType {
+ /**
+ * 缁撴潫娴佺▼
+ */
+ FINISH_PROCESS = 1,
+ /**
+ * 椹冲洖鍒版寚瀹氳妭鐐�
+ */
+ RETURN_USER_TASK = 2
+}
+// 鐢ㄦ埛浠诲姟瓒呮椂澶勭悊绫诲瀷鏋氫妇
+export enum TimeoutHandlerType {
+ /**
+ * 鑷姩鎻愰啋
+ */
+ REMINDER = 1,
+ /**
+ * 鑷姩鍚屾剰
+ */
+ APPROVE = 2,
+ /**
+ * 鑷姩鎷掔粷
+ */
+ REJECT = 3
+}
+// 鐢ㄦ埛浠诲姟鐨勫鎵逛汉涓虹┖鏃讹紝澶勭悊绫诲瀷鏋氫妇
+export enum AssignEmptyHandlerType {
+ /**
+ * 鑷姩閫氳繃
+ */
+ APPROVE = 1,
+ /**
+ * 鑷姩鎷掔粷
+ */
+ REJECT = 2,
+ /**
+ * 鎸囧畾浜哄憳瀹℃壒
+ */
+ ASSIGN_USER,
+ /**
+ * 杞氦缁欐祦绋嬬鐞嗗憳
+ */
+ ASSIGN_ADMIN = 4
+}
+// 鐢ㄦ埛浠诲姟鐨勫鎵逛汉涓庡彂璧蜂汉鐩稿悓鏃讹紝澶勭悊绫诲瀷鏋氫妇
+export enum AssignStartUserHandlerType {
+ /**
+ * 鐢卞彂璧蜂汉瀵硅嚜宸卞鎵�
+ */
+ START_USER_AUDIT = 1,
+ /**
+ * 鑷姩璺宠繃銆愬弬鑰冮涔︺�戯細1锛夊鏋滃綋鍓嶈妭鐐硅繕鏈夊叾浠栧鎵逛汉锛屽垯浜ょ敱鍏朵粬瀹℃壒浜鸿繘琛屽鎵癸紱2锛夊鏋滃綋鍓嶈妭鐐规病鏈夊叾浠栧鎵逛汉锛屽垯璇ヨ妭鐐硅嚜鍔ㄩ�氳繃
+ */
+ SKIP = 2,
+ /**
+ * 杞氦缁欓儴闂ㄨ礋璐d汉瀹℃壒
+ */
+ ASSIGN_DEPT_LEADER = 3
+}
+
+// 鐢ㄦ埛浠诲姟鐨勫鎵圭被鍨嬨�� 銆愬弬鑰冮涔︺��
+export enum ApproveType {
+ /**
+ * 浜哄伐瀹℃壒
+ */
+ USER = 1,
+ /**
+ * 鑷姩閫氳繃
+ */
+ AUTO_APPROVE = 2,
+ /**
+ * 鑷姩鎷掔粷
+ */
+ AUTO_REJECT = 3
+}
+
+// 鏃堕棿鍗曚綅鏋氫妇
+export enum TimeUnitType {
+ /**
+ * 鍒嗛挓
+ */
+ MINUTE = 1,
+ /**
+ * 灏忔椂
+ */
+ HOUR = 2,
+ /**
+ * 澶�
+ */
+ DAY = 3
+}
+
+/**
+ * 鏉′欢鑺傜偣璁剧疆缁撴瀯瀹氫箟锛岀敤浜庢潯浠惰妭鐐�
+ */
+export type ConditionSetting = {
+ // 鏉′欢绫诲瀷
+ conditionType?: ConditionType
+ // 鏉′欢琛ㄨ揪寮�
+ conditionExpression?: string
+ // 鏉′欢缁�
+ conditionGroups?: ConditionGroup
+ // 鏄惁榛樿鐨勬潯浠�
+ defaultFlow?: boolean
+}
+
+// 鏉′欢閰嶇疆绫诲瀷 锛� 鐢ㄤ簬鏉′欢鑺傜偣閰嶇疆 锛�
+export enum ConditionType {
+ /**
+ * 鏉′欢琛ㄨ揪寮�
+ */
+ EXPRESSION = 1,
+
+ /**
+ * 鏉′欢瑙勫垯
+ */
+ RULE = 2
+}
+/**
+ * 琛ㄥ崟鏉冮檺鐨勬灇涓�
+ */
+export enum FieldPermissionType {
+ /**
+ * 鍙
+ */
+ READ = '1',
+ /**
+ * 缂栬緫
+ */
+ WRITE = '2',
+ /**
+ * 闅愯棌
+ */
+ NONE = '3'
+}
+/**
+ * 鎿嶄綔鎸夐挳鏉冮檺缁撴瀯瀹氫箟
+ */
+export type ButtonSetting = {
+ id: OperationButtonType
+ displayName: string
+ enable: boolean
+}
+
+// 鎿嶄綔鎸夐挳绫诲瀷鏋氫妇 (鐢ㄤ簬瀹℃壒鑺傜偣)
+export enum OperationButtonType {
+ /**
+ * 閫氳繃
+ */
+ APPROVE = 1,
+ /**
+ * 鎷掔粷
+ */
+ REJECT = 2,
+ /**
+ * 杞姙
+ */
+ TRANSFER = 3,
+ /**
+ * 濮旀淳
+ */
+ DELEGATE = 4,
+ /**
+ * 鍔犵
+ */
+ ADD_SIGN = 5,
+ /**
+ * 閫�鍥�
+ */
+ RETURN = 6,
+ /**
+ * 鎶勯��
+ */
+ COPY = 7
+}
+
+/**
+ * 鏉′欢瑙勫垯缁撴瀯瀹氫箟
+ */
+export type ConditionRule = {
+ opCode: string
+ leftSide: string
+ rightSide: string
+}
+
+/**
+ * 鏉′欢缁勭粨鏋勫畾涔�
+ */
+export type ConditionGroup = {
+ // 鏉′欢缁勭殑閫昏緫鍏崇郴鏄惁涓轰笖
+ and: boolean
+ // 鏉′欢鏁扮粍
+ conditions: Condition[]
+}
+/**
+ * 鏉′欢缁勯粯璁ゅ��
+ */
+export const DEFAULT_CONDITION_GROUP_VALUE = {
+ and: true,
+ conditions: [
+ {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ ]
+ }
+ ]
+}
+
+/**
+ * 鏉′欢缁撴瀯瀹氫箟
+ */
+export type Condition = {
+ // 鏉′欢瑙勫垯鐨勯�昏緫鍏崇郴鏄惁涓轰笖
+ and: boolean
+ rules: ConditionRule[]
+}
+
+export const NODE_DEFAULT_TEXT = new Map<number, string>()
+NODE_DEFAULT_TEXT.set(NodeType.USER_TASK_NODE, '璇烽厤缃鎵逛汉')
+NODE_DEFAULT_TEXT.set(NodeType.COPY_TASK_NODE, '璇烽厤缃妱閫佷汉')
+NODE_DEFAULT_TEXT.set(NodeType.CONDITION_NODE, '璇疯缃潯浠�')
+NODE_DEFAULT_TEXT.set(NodeType.START_USER_NODE, '璇疯缃彂璧蜂汉')
+NODE_DEFAULT_TEXT.set(NodeType.DELAY_TIMER_NODE, '璇疯缃欢杩熷櫒')
+NODE_DEFAULT_TEXT.set(NodeType.ROUTER_BRANCH_NODE, '璇疯缃矾鐢辫妭鐐�')
+NODE_DEFAULT_TEXT.set(NodeType.TRIGGER_NODE, '璇疯缃Е鍙戝櫒')
+NODE_DEFAULT_TEXT.set(NodeType.TRANSACTOR_NODE, '璇疯缃姙鐞嗕汉')
+NODE_DEFAULT_TEXT.set(NodeType.CHILD_PROCESS_NODE, '璇疯缃瓙娴佺▼')
+
+export const NODE_DEFAULT_NAME = new Map<number, string>()
+NODE_DEFAULT_NAME.set(NodeType.USER_TASK_NODE, '瀹℃壒浜�')
+NODE_DEFAULT_NAME.set(NodeType.COPY_TASK_NODE, '鎶勯�佷汉')
+NODE_DEFAULT_NAME.set(NodeType.CONDITION_NODE, '鏉′欢')
+NODE_DEFAULT_NAME.set(NodeType.START_USER_NODE, '鍙戣捣浜�')
+NODE_DEFAULT_NAME.set(NodeType.DELAY_TIMER_NODE, '寤惰繜鍣�')
+NODE_DEFAULT_NAME.set(NodeType.ROUTER_BRANCH_NODE, '璺敱鍒嗘敮')
+NODE_DEFAULT_NAME.set(NodeType.TRIGGER_NODE, '瑙﹀彂鍣�')
+NODE_DEFAULT_NAME.set(NodeType.TRANSACTOR_NODE, '鍔炵悊浜�')
+NODE_DEFAULT_NAME.set(NodeType.CHILD_PROCESS_NODE, '瀛愭祦绋�')
+
+// 鍊欓�変汉绛栫暐銆傛殏鏃朵笉浠庡瓧鍏镐腑鍙栥�� 鍚庣画鍙兘璋冩暣銆傛帶鍒舵樉绀洪『搴�
+export const CANDIDATE_STRATEGY: DictDataVO[] = [
+ { label: '鎸囧畾鎴愬憳', value: CandidateStrategy.USER },
+ { label: '鎸囧畾瑙掕壊', value: CandidateStrategy.ROLE },
+ { label: '鎸囧畾宀椾綅', value: CandidateStrategy.POST },
+ { label: '閮ㄩ棬鎴愬憳', value: CandidateStrategy.DEPT_MEMBER },
+ { label: '閮ㄩ棬璐熻矗浜�', value: CandidateStrategy.DEPT_LEADER },
+ { label: '杩炵画澶氱骇閮ㄩ棬璐熻矗浜�', value: CandidateStrategy.MULTI_LEVEL_DEPT_LEADER },
+ { label: '鍙戣捣浜鸿嚜閫�', value: CandidateStrategy.START_USER_SELECT },
+ { label: '瀹℃壒浜鸿嚜閫�', value: CandidateStrategy.APPROVE_USER_SELECT },
+ { label: '鍙戣捣浜烘湰浜�', value: CandidateStrategy.START_USER },
+ { label: '鍙戣捣浜洪儴闂ㄨ礋璐d汉', value: CandidateStrategy.START_USER_DEPT_LEADER },
+ { label: '鍙戣捣浜鸿繛缁儴闂ㄨ礋璐d汉', value: CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER },
+ { label: '鐢ㄦ埛缁�', value: CandidateStrategy.USER_GROUP },
+ { label: '琛ㄥ崟鍐呯敤鎴峰瓧娈�', value: CandidateStrategy.FORM_USER },
+ { label: '琛ㄥ崟鍐呴儴闂ㄨ礋璐d汉', value: CandidateStrategy.FORM_DEPT_LEADER },
+ { label: '娴佺▼琛ㄨ揪寮�', value: CandidateStrategy.EXPRESSION }
+]
+// 瀹℃壒鑺傜偣 鐨勫鎵圭被鍨�
+export const APPROVE_TYPE: DictDataVO[] = [
+ { label: '浜哄伐瀹℃壒', value: ApproveType.USER },
+ { label: '鑷姩閫氳繃', value: ApproveType.AUTO_APPROVE },
+ { label: '鑷姩鎷掔粷', value: ApproveType.AUTO_REJECT }
+]
+
+export const APPROVE_METHODS: DictDataVO[] = [
+ { label: '鎸夐『搴忎緷娆″鎵�', value: ApproveMethodType.SEQUENTIAL_APPROVE },
+ { label: '浼氱锛堝彲鍚屾椂瀹℃壒锛岃嚦灏� % 浜哄繀椤诲鎵归�氳繃锛�', value: ApproveMethodType.APPROVE_BY_RATIO },
+ { label: '鎴栫(鍙悓鏃跺鎵癸紝鏈変竴浜洪�氳繃鍗冲彲)', value: ApproveMethodType.ANY_APPROVE },
+ { label: '闅忔満鎸戦�変竴浜哄鎵�', value: ApproveMethodType.RANDOM_SELECT_ONE_APPROVE }
+]
+
+export const CONDITION_CONFIG_TYPES: DictDataVO[] = [
+ { label: '鏉′欢瑙勫垯', value: ConditionType.RULE },
+ { label: '鏉′欢琛ㄨ揪寮�', value: ConditionType.EXPRESSION }
+]
+
+// 鏃堕棿鍗曚綅绫诲瀷
+export const TIME_UNIT_TYPES: DictDataVO[] = [
+ { label: '鍒嗛挓', value: TimeUnitType.MINUTE },
+ { label: '灏忔椂', value: TimeUnitType.HOUR },
+ { label: '澶�', value: TimeUnitType.DAY }
+]
+// 瓒呮椂澶勭悊鎵ц鍔ㄤ綔绫诲瀷
+export const TIMEOUT_HANDLER_TYPES: DictDataVO[] = [
+ { label: '鑷姩鎻愰啋', value: 1 },
+ { label: '鑷姩鍚屾剰', value: 2 },
+ { label: '鑷姩鎷掔粷', value: 3 }
+]
+export const REJECT_HANDLER_TYPES: DictDataVO[] = [
+ { label: '缁堟娴佺▼', value: RejectHandlerType.FINISH_PROCESS },
+ { label: '椹冲洖鍒版寚瀹氳妭鐐�', value: RejectHandlerType.RETURN_USER_TASK }
+ // { label: '缁撴潫浠诲姟', value: RejectHandlerType.FINISH_TASK }
+]
+export const ASSIGN_EMPTY_HANDLER_TYPES: DictDataVO[] = [
+ { label: '鑷姩閫氳繃', value: 1 },
+ { label: '鑷姩鎷掔粷', value: 2 },
+ { label: '鎸囧畾鎴愬憳瀹℃壒', value: 3 },
+ { label: '杞氦缁欐祦绋嬬鐞嗗憳', value: 4 }
+]
+export const ASSIGN_START_USER_HANDLER_TYPES: DictDataVO[] = [
+ { label: '鐢卞彂璧蜂汉瀵硅嚜宸卞鎵�', value: 1 },
+ { label: '鑷姩璺宠繃', value: 2 },
+ { label: '杞氦缁欓儴闂ㄨ礋璐d汉瀹℃壒', value: 3 }
+]
+
+// 姣旇緝杩愮畻绗�
+export const COMPARISON_OPERATORS: DictDataVO = [
+ {
+ value: '==',
+ label: '绛変簬'
+ },
+ {
+ value: '!=',
+ label: '涓嶇瓑浜�'
+ },
+ {
+ value: '>',
+ label: '澶т簬'
+ },
+ {
+ value: '>=',
+ label: '澶т簬绛変簬'
+ },
+ {
+ value: '<',
+ label: '灏忎簬'
+ },
+ {
+ value: '<=',
+ label: '灏忎簬绛変簬'
+ }
+]
+// 瀹℃壒鎿嶄綔鎸夐挳鍚嶇О
+export const OPERATION_BUTTON_NAME = new Map<number, string>()
+OPERATION_BUTTON_NAME.set(OperationButtonType.APPROVE, '閫氳繃')
+OPERATION_BUTTON_NAME.set(OperationButtonType.REJECT, '鎷掔粷')
+OPERATION_BUTTON_NAME.set(OperationButtonType.TRANSFER, '杞姙')
+OPERATION_BUTTON_NAME.set(OperationButtonType.DELEGATE, '濮旀淳')
+OPERATION_BUTTON_NAME.set(OperationButtonType.ADD_SIGN, '鍔犵')
+OPERATION_BUTTON_NAME.set(OperationButtonType.RETURN, '閫�鍥�')
+OPERATION_BUTTON_NAME.set(OperationButtonType.COPY, '鎶勯��')
+
+// 榛樿鐨勬寜閽潈闄愯缃�
+export const DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '閫氳繃', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '鎷掔粷', enable: true },
+ { id: OperationButtonType.TRANSFER, displayName: '杞姙', enable: true },
+ { id: OperationButtonType.DELEGATE, displayName: '濮旀淳', enable: true },
+ { id: OperationButtonType.ADD_SIGN, displayName: '鍔犵', enable: true },
+ { id: OperationButtonType.RETURN, displayName: '閫�鍥�', enable: true }
+]
+
+// 鍔炵悊浜洪粯璁ょ殑鎸夐挳鏉冮檺璁剧疆
+export const TRANSACTOR_DEFAULT_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '鍔炵悊', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '鎷掔粷', enable: false },
+ { id: OperationButtonType.TRANSFER, displayName: '杞姙', enable: false },
+ { id: OperationButtonType.DELEGATE, displayName: '濮旀淳', enable: false },
+ { id: OperationButtonType.ADD_SIGN, displayName: '鍔犵', enable: false },
+ { id: OperationButtonType.RETURN, displayName: '閫�鍥�', enable: false }
+]
+
+// 鍙戣捣浜虹殑鎸夐挳鏉冮檺銆傛殏鏃跺畾姝伙紝涓嶅彲浠ョ紪杈�
+export const START_USER_BUTTON_SETTING: ButtonSetting[] = [
+ { id: OperationButtonType.APPROVE, displayName: '鎻愪氦', enable: true },
+ { id: OperationButtonType.REJECT, displayName: '鎷掔粷', enable: false },
+ { id: OperationButtonType.TRANSFER, displayName: '杞姙', enable: false },
+ { id: OperationButtonType.DELEGATE, displayName: '濮旀淳', enable: false },
+ { id: OperationButtonType.ADD_SIGN, displayName: '鍔犵', enable: false },
+ { id: OperationButtonType.RETURN, displayName: '閫�鍥�', enable: false }
+]
+
+export const MULTI_LEVEL_DEPT: DictDataVO = [
+ { label: '绗� 1 绾ч儴闂�', value: 1 },
+ { label: '绗� 2 绾ч儴闂�', value: 2 },
+ { label: '绗� 3 绾ч儴闂�', value: 3 },
+ { label: '绗� 4 绾ч儴闂�', value: 4 },
+ { label: '绗� 5 绾ч儴闂�', value: 5 },
+ { label: '绗� 6 绾ч儴闂�', value: 6 },
+ { label: '绗� 7 绾ч儴闂�', value: 7 },
+ { label: '绗� 8 绾ч儴闂�', value: 8 },
+ { label: '绗� 9 绾ч儴闂�', value: 9 },
+ { label: '绗� 10 绾ч儴闂�', value: 10 },
+ { label: '绗� 11 绾ч儴闂�', value: 11 },
+ { label: '绗� 12 绾ч儴闂�', value: 12 },
+ { label: '绗� 13 绾ч儴闂�', value: 13 },
+ { label: '绗� 14 绾ч儴闂�', value: 14 },
+ { label: '绗� 15 绾ч儴闂�', value: 15 }
+]
+
+/**
+ * 娴佺▼瀹炰緥鐨勫彉閲忔灇涓�
+ */
+export enum ProcessVariableEnum {
+ /**
+ * 鍙戣捣鐢ㄦ埛 ID
+ */
+ START_USER_ID = 'PROCESS_START_USER_ID',
+ /**
+ * 鍙戣捣鏃堕棿
+ */
+ START_TIME = 'PROCESS_START_TIME',
+ /**
+ * 娴佺▼瀹氫箟鍚嶇О
+ */
+ PROCESS_DEFINITION_NAME = 'PROCESS_DEFINITION_NAME'
+}
+
+/**
+ * 寤惰繜璁剧疆
+ */
+export type DelaySetting = {
+ // 寤惰繜绫诲瀷
+ delayType: number
+ // 寤惰繜鏃堕棿琛ㄨ揪寮�
+ delayTime: string
+}
+/**
+ * 寤惰繜绫诲瀷
+ */
+export enum DelayTypeEnum {
+ /**
+ * 鍥哄畾鏃堕暱
+ */
+ FIXED_TIME_DURATION = 1,
+ /**
+ * 鍥哄畾鏃ユ湡鏃堕棿
+ */
+ FIXED_DATE_TIME = 2
+}
+export const DELAY_TYPE = [
+ { label: '鍥哄畾鏃堕暱', value: DelayTypeEnum.FIXED_TIME_DURATION },
+ { label: '鍥哄畾鏃ユ湡', value: DelayTypeEnum.FIXED_DATE_TIME }
+]
+
+/**
+ * 璺敱鍒嗘敮缁撴瀯瀹氫箟
+ */
+export type RouterSetting = {
+ nodeId: string
+ conditionType: ConditionType
+ conditionExpression: string
+ conditionGroups: ConditionGroup
+}
+
+// ==================== 瑙﹀彂鍣ㄧ浉鍏冲畾涔� ====================
+/**
+ * 瑙﹀彂鍣ㄨ妭鐐圭粨鏋勫畾涔�
+ */
+export type TriggerSetting = {
+ type: TriggerTypeEnum
+ httpRequestSetting?: HttpRequestTriggerSetting
+ formSettings?: FormTriggerSetting[]
+}
+
+/**
+ * 瑙﹀彂鍣ㄧ被鍨嬫灇涓�
+ */
+export enum TriggerTypeEnum {
+ /**
+ * 鍙戦�� HTTP 璇锋眰瑙﹀彂鍣�
+ */
+ HTTP_REQUEST = 1,
+ /**
+ * 鎺ユ敹 HTTP 鍥炶皟璇锋眰瑙﹀彂鍣�
+ */
+ HTTP_CALLBACK = 2,
+ /**
+ * 琛ㄥ崟鏁版嵁鏇存柊瑙﹀彂鍣�
+ */
+ FORM_UPDATE = 10,
+ /**
+ * 琛ㄥ崟鏁版嵁鍒犻櫎瑙﹀彂鍣�
+ */
+ FORM_DELETE = 11
+}
+
+/**
+ * HTTP 璇锋眰瑙﹀彂鍣ㄧ粨鏋勫畾涔�
+ */
+export type HttpRequestTriggerSetting = {
+ // 璇锋眰 URL
+ url: string
+ // 璇锋眰澶村弬鏁拌缃�
+ header?: HttpRequestParam[]
+ // 璇锋眰浣撳弬鏁拌缃�
+ body?: HttpRequestParam[]
+ // 璇锋眰鍝嶅簲璁剧疆
+ response?: Record<string, string>[]
+}
+
+/**
+ * 娴佺▼琛ㄥ崟瑙﹀彂鍣ㄩ厤缃粨鏋勫畾涔�
+ */
+export type FormTriggerSetting = {
+ // 鏉′欢绫诲瀷
+ conditionType?: ConditionType
+ // 鏉′欢琛ㄨ揪寮�
+ conditionExpression?: string
+ // 鏉′欢缁�
+ conditionGroups?: ConditionGroup
+ // 鏇存柊琛ㄥ崟瀛楁閰嶇疆
+ updateFormFields?: Record<string, any>
+ // 鍒犻櫎琛ㄥ崟瀛楁閰嶇疆
+ deleteFields?: string[]
+}
+
+export const TRIGGER_TYPES: DictDataVO[] = [
+ { label: '鍙戦�� HTTP 璇锋眰', value: TriggerTypeEnum.HTTP_REQUEST },
+ { label: '鎺ユ敹 HTTP 鍥炶皟', value: TriggerTypeEnum.HTTP_CALLBACK },
+ { label: '淇敼琛ㄥ崟鏁版嵁', value: TriggerTypeEnum.FORM_UPDATE },
+ { label: '鍒犻櫎琛ㄥ崟鏁版嵁', value: TriggerTypeEnum.FORM_DELETE }
+]
+
+/**
+ * 瀛愭祦绋嬭妭鐐圭粨鏋勫畾涔�
+ */
+export type ChildProcessSetting = {
+ calledProcessDefinitionKey: string
+ calledProcessDefinitionName: string
+ async: boolean
+ inVariables?: IOParameter[]
+ outVariables?: IOParameter[]
+ skipStartUserNode: boolean
+ startUserSetting: StartUserSetting
+ timeoutSetting: TimeoutSetting
+ multiInstanceSetting: MultiInstanceSetting
+}
+export type IOParameter = {
+ source: string
+ target: string
+}
+export type StartUserSetting = {
+ type: ChildProcessStartUserTypeEnum
+ formField?: string
+ emptyType?: ChildProcessStartUserEmptyTypeEnum
+}
+export type TimeoutSetting = {
+ enable: boolean
+ type?: DelayTypeEnum
+ timeExpression?: string
+}
+export type MultiInstanceSetting = {
+ enable: boolean
+ sequential?: boolean
+ approveRatio?: number
+ sourceType?: ChildProcessMultiInstanceSourceTypeEnum
+ source?: string
+}
+export enum ChildProcessStartUserTypeEnum {
+ /**
+ * 鍚屼富娴佺▼鍙戣捣浜�
+ */
+ MAIN_PROCESS_START_USER = 1,
+ /**
+ * 琛ㄥ崟
+ */
+ FROM_FORM = 2
+}
+export const CHILD_PROCESS_START_USER_TYPE = [
+ { label: '鍚屼富娴佺▼鍙戣捣浜�', value: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER },
+ { label: '琛ㄥ崟', value: ChildProcessStartUserTypeEnum.FROM_FORM }
+]
+export enum ChildProcessStartUserEmptyTypeEnum {
+ /**
+ * 鍚屼富娴佺▼鍙戣捣浜�
+ */
+ MAIN_PROCESS_START_USER = 1,
+ /**
+ * 瀛愭祦绋嬬鐞嗗憳
+ */
+ CHILD_PROCESS_ADMIN = 2,
+ /**
+ * 涓绘祦绋嬬鐞嗗憳
+ */
+ MAIN_PROCESS_ADMIN = 3
+}
+export const CHILD_PROCESS_START_USER_EMPTY_TYPE = [
+ { label: '鍚屼富娴佺▼鍙戣捣浜�', value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER },
+ { label: '瀛愭祦绋嬬鐞嗗憳', value: ChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN },
+ { label: '涓绘祦绋嬬鐞嗗憳', value: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN }
+]
+export enum ChildProcessMultiInstanceSourceTypeEnum {
+ /**
+ * 鍥哄畾鏁伴噺
+ */
+ FIXED_QUANTITY = 1,
+ /**
+ * 鏁板瓧琛ㄥ崟
+ */
+ NUMBER_FORM = 2,
+ /**
+ * 澶氶�夎〃鍗�
+ */
+ MULTIPLE_FORM = 3
+}
+export const CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = [
+ { label: '鍥哄畾鏁伴噺', value: ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY },
+ { label: '鏁板瓧琛ㄥ崟', value: ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM },
+ { label: '澶氶�夎〃鍗�', value: ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM }
+]
diff --git a/src/components/SimpleProcessDesignerV2/src/index.ts b/src/components/SimpleProcessDesignerV2/src/index.ts
new file mode 100644
index 0000000..88de07f
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/index.ts
@@ -0,0 +1,5 @@
+import SimpleProcessDesigner from './SimpleProcessDesigner.vue'
+import SimpleProcessViewer from './SimpleProcessViewer.vue'
+import '../theme/simple-process-designer.scss'
+
+export { SimpleProcessDesigner, SimpleProcessViewer}
diff --git a/src/components/SimpleProcessDesignerV2/src/node.ts b/src/components/SimpleProcessDesignerV2/src/node.ts
new file mode 100644
index 0000000..73534f6
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/node.ts
@@ -0,0 +1,617 @@
+import { TaskStatusEnum } from '@/api/bpm/task'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import {
+ SimpleFlowNode,
+ CandidateStrategy,
+ NodeType,
+ ApproveMethodType,
+ RejectHandlerType,
+ NODE_DEFAULT_NAME,
+ AssignStartUserHandlerType,
+ AssignEmptyHandlerType,
+ FieldPermissionType,
+ HttpRequestParam,
+ ProcessVariableEnum,
+ ConditionType,
+ ConditionGroup,
+ COMPARISON_OPERATORS
+} from './consts'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+
+export function useWatchNode(props: { flowNode: SimpleFlowNode }): Ref<SimpleFlowNode> {
+ const node = ref<SimpleFlowNode>(props.flowNode)
+ watch(
+ () => props.flowNode,
+ (newValue) => {
+ node.value = newValue
+ }
+ )
+ return node
+}
+
+// 瑙f瀽 formCreate 鎵�鏈夎〃鍗曞瓧娈�, 骞惰繑鍥�
+const parseFormCreateFields = (formFields?: string[]) => {
+ const result: Array<Record<string, any>> = []
+ if (formFields) {
+ formFields.forEach((fieldStr: string) => {
+ parseFormFields(JSON.parse(fieldStr), result)
+ })
+ }
+ return result
+}
+
+/**
+ * @description 琛ㄥ崟鏁版嵁鏉冮檺閰嶇疆锛岀敤浜庡彂璧蜂汉鑺傜偣 銆佸鎵硅妭鐐广�佹妱閫佽妭鐐�
+ */
+export function useFormFieldsPermission(defaultPermission: FieldPermissionType) {
+ // 瀛楁鏉冮檺閰嶇疆. 闇�瑕佹湁 field, title, permissioin 灞炴��
+ const fieldsPermissionConfig = ref<Array<Record<string, any>>>([])
+
+ const formType = inject<Ref<number | undefined>>('formType', ref()) // 琛ㄥ崟绫诲瀷
+
+ const formFields = inject<Ref<string[]>>('formFields', ref([])) // 娴佺▼琛ㄥ崟瀛楁
+
+ const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
+ nodeFormFields = toRaw(nodeFormFields)
+ if (!nodeFormFields || nodeFormFields.length === 0) {
+ fieldsPermissionConfig.value = getDefaultFieldsPermission(unref(formFields))
+ } else {
+ fieldsPermissionConfig.value = mergeFieldsPermission(nodeFormFields, unref(formFields))
+ }
+ }
+ // 鍚堝苟宸茬粡璁剧疆鐨勮〃鍗曞瓧娈垫潈闄愶紝褰撳墠娴佺▼琛ㄥ崟瀛楁 (鍙兘鏂板锛屾垨鍒犻櫎浜嗗瓧娈�)
+ const mergeFieldsPermission = (
+ formFieldsPermisson: Array<Record<string, string>>,
+ formFields?: string[]
+ ) => {
+ let mergedFieldsPermission: Array<Record<string, any>> = []
+ if (formFields) {
+ mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => {
+ const found = formFieldsPermisson.find(
+ (fieldPermission) => fieldPermission.field == item.field
+ )
+ return {
+ field: item.field,
+ title: item.title,
+ permission: found ? found.permission : defaultPermission
+ }
+ })
+ }
+ return mergedFieldsPermission
+ }
+
+ // 榛樿鐨勮〃鍗曟潈闄愶細 鑾峰彇琛ㄥ崟鐨勬墍鏈夊瓧娈碉紝璁剧疆瀛楁榛樿鏉冮檺涓哄彧璇�
+ const getDefaultFieldsPermission = (formFields?: string[]) => {
+ let defaultFieldsPermission: Array<Record<string, any>> = []
+ if (formFields) {
+ defaultFieldsPermission = parseFormCreateFields(formFields).map((item) => {
+ return {
+ field: item.field,
+ title: item.title,
+ permission: defaultPermission
+ }
+ })
+ }
+ return defaultFieldsPermission
+ }
+
+ // 鑾峰彇琛ㄥ崟鐨勬墍鏈夊瓧娈碉紝浣滀负涓嬫媺妗嗛�夐」
+ const formFieldOptions = parseFormCreateFields(unref(formFields))
+
+ return {
+ formType,
+ fieldsPermissionConfig,
+ formFieldOptions,
+ getNodeConfigFormFields
+ }
+}
+
+/**
+ * @description 鑾峰彇娴佺▼琛ㄥ崟鐨勫瓧娈�
+ */
+export function useFormFields() {
+ const formFields = inject<Ref<string[]>>('formFields', ref([])) // 娴佺▼琛ㄥ崟瀛楁
+ return parseFormCreateFields(unref(formFields))
+}
+
+// TODO @鑺嬭壙锛氬悗缁渶瑕佹妸鍚勭绫讳技 useFormFieldsPermission 鐨勯�昏緫锛屾娊鎴愪竴涓�氱敤鏂规硶銆�
+/**
+ * @description 鑾峰彇娴佺▼琛ㄥ崟鐨勫瓧娈靛拰鍙戣捣浜哄瓧娈�
+ */
+export function useFormFieldsAndStartUser() {
+ const injectFormFields = inject<Ref<string[]>>('formFields', ref([])) // 娴佺▼琛ㄥ崟瀛楁
+ const formFields = parseFormCreateFields(unref(injectFormFields))
+ // 娣诲姞鍙戣捣浜�
+ formFields.unshift({
+ field: ProcessVariableEnum.START_USER_ID,
+ title: '鍙戣捣浜�',
+ required: true
+ })
+ return formFields
+}
+
+export type UserTaskFormType = {
+ candidateStrategy: CandidateStrategy
+ approveMethod: ApproveMethodType
+ roleIds?: number[] // 瑙掕壊
+ deptIds?: number[] // 閮ㄩ棬
+ deptLevel?: number // 閮ㄩ棬灞傜骇
+ userIds?: number[] // 鐢ㄦ埛
+ userGroups?: number[] // 鐢ㄦ埛缁�
+ postIds?: number[] // 宀椾綅
+ expression?: string // 娴佺▼琛ㄨ揪寮�
+ formUser?: string // 琛ㄥ崟鍐呯敤鎴峰瓧娈�
+ formDept?: string // 琛ㄥ崟鍐呴儴闂ㄥ瓧娈�
+ approveRatio?: number
+ rejectHandlerType?: RejectHandlerType
+ returnNodeId?: string
+ timeoutHandlerEnable?: boolean
+ timeoutHandlerType?: number
+ assignEmptyHandlerType?: AssignEmptyHandlerType
+ assignEmptyHandlerUserIds?: number[]
+ assignStartUserHandlerType?: AssignStartUserHandlerType
+ timeDuration?: number
+ maxRemindCount?: number
+ buttonsSetting: any[]
+ taskCreateListenerEnable?: boolean
+ taskCreateListenerPath?: string
+ taskCreateListener?: {
+ header: HttpRequestParam[]
+ body: HttpRequestParam[]
+ }
+ taskAssignListenerEnable?: boolean
+ taskAssignListenerPath?: string
+ taskAssignListener?: {
+ header: HttpRequestParam[]
+ body: HttpRequestParam[]
+ }
+ taskCompleteListenerEnable?: boolean
+ taskCompleteListenerPath?: string
+ taskCompleteListener?: {
+ header: HttpRequestParam[]
+ body: HttpRequestParam[]
+ }
+ signEnable: boolean
+ reasonRequire: boolean
+ skipExpression?: string
+}
+
+export type CopyTaskFormType = {
+ candidateStrategy: CandidateStrategy
+ roleIds?: number[] // 瑙掕壊
+ deptIds?: number[] // 閮ㄩ棬
+ deptLevel?: number // 閮ㄩ棬灞傜骇
+ userIds?: number[] // 鐢ㄦ埛
+ userGroups?: number[] // 鐢ㄦ埛缁�
+ postIds?: number[] // 宀椾綅
+ formUser?: string // 琛ㄥ崟鍐呯敤鎴峰瓧娈�
+ formDept?: string // 琛ㄥ崟鍐呴儴闂ㄥ瓧娈�
+ expression?: string // 娴佺▼琛ㄨ揪寮�
+}
+
+/**
+ * @description 鑺傜偣琛ㄥ崟鏁版嵁銆� 鐢ㄤ簬瀹℃壒鑺傜偣銆佹妱閫佽妭鐐�
+ */
+export function useNodeForm(nodeType: NodeType) {
+ const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList', ref([])) // 瑙掕壊鍒楄〃
+ const postOptions = inject<Ref<PostApi.PostVO[]>>('postList', ref([])) // 宀椾綅鍒楄〃
+ const userOptions = inject<Ref<UserApi.UserVO[]>>('userList', ref([])) // 鐢ㄦ埛鍒楄〃
+ const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList', ref([])) // 閮ㄩ棬鍒楄〃
+ const userGroupOptions = inject<Ref<UserGroupApi.UserGroupVO[]>>('userGroupList', ref([])) // 鐢ㄦ埛缁勫垪琛�
+ const deptTreeOptions = inject('deptTree', ref()) // 閮ㄩ棬鏍�
+ const formFields = inject<Ref<string[]>>('formFields', ref([])) // 娴佺▼琛ㄥ崟瀛楁
+ const configForm = ref<UserTaskFormType | CopyTaskFormType>()
+ if (nodeType === NodeType.USER_TASK_NODE || nodeType === NodeType.TRANSACTOR_NODE) {
+ configForm.value = {
+ candidateStrategy: CandidateStrategy.USER,
+ approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE,
+ approveRatio: 100,
+ rejectHandlerType: RejectHandlerType.FINISH_PROCESS,
+ assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
+ returnNodeId: '',
+ timeoutHandlerEnable: false,
+ timeoutHandlerType: 1,
+ timeDuration: 6, // 榛樿 6灏忔椂
+ maxRemindCount: 1, // 榛樿 鎻愰啋 1娆�
+ buttonsSetting: []
+ }
+ } else {
+ configForm.value = {
+ candidateStrategy: CandidateStrategy.USER
+ }
+ }
+
+ const getShowText = (): string => {
+ let showText = ''
+ // 鎸囧畾鎴愬憳
+ if (configForm.value?.candidateStrategy === CandidateStrategy.USER) {
+ if (configForm.value?.userIds!.length > 0) {
+ const candidateNames: string[] = []
+ userOptions?.value.forEach((item) => {
+ if (configForm.value?.userIds!.includes(item.id)) {
+ candidateNames.push(item.nickname)
+ }
+ })
+ showText = `鎸囧畾鎴愬憳锛�${candidateNames.join(',')}`
+ }
+ }
+ // 鎸囧畾瑙掕壊
+ if (configForm.value?.candidateStrategy === CandidateStrategy.ROLE) {
+ if (configForm.value.roleIds!.length > 0) {
+ const candidateNames: string[] = []
+ roleOptions?.value.forEach((item) => {
+ if (configForm.value?.roleIds!.includes(item.id)) {
+ candidateNames.push(item.name)
+ }
+ })
+ showText = `鎸囧畾瑙掕壊锛�${candidateNames.join(',')}`
+ }
+ }
+ // 鎸囧畾閮ㄩ棬
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER ||
+ configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER ||
+ configForm.value?.candidateStrategy === CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+ ) {
+ if (configForm.value?.deptIds!.length > 0) {
+ const candidateNames: string[] = []
+ deptOptions?.value.forEach((item) => {
+ if (configForm.value?.deptIds!.includes(item.id!)) {
+ candidateNames.push(item.name)
+ }
+ })
+ if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER) {
+ showText = `閮ㄩ棬鎴愬憳锛�${candidateNames.join(',')}`
+ } else if (configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER) {
+ showText = `閮ㄩ棬鐨勮礋璐d汉锛�${candidateNames.join(',')}`
+ } else {
+ showText = `澶氱骇閮ㄩ棬鐨勮礋璐d汉锛�${candidateNames.join(',')}`
+ }
+ }
+ }
+
+ // 鎸囧畾宀椾綅
+ if (configForm.value?.candidateStrategy === CandidateStrategy.POST) {
+ if (configForm.value.postIds!.length > 0) {
+ const candidateNames: string[] = []
+ postOptions?.value.forEach((item) => {
+ if (configForm.value?.postIds!.includes(item.id!)) {
+ candidateNames.push(item.name)
+ }
+ })
+ showText = `鎸囧畾宀椾綅: ${candidateNames.join(',')}`
+ }
+ }
+ // 鎸囧畾鐢ㄦ埛缁�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP) {
+ if (configForm.value?.userGroups!.length > 0) {
+ const candidateNames: string[] = []
+ userGroupOptions?.value.forEach((item) => {
+ if (configForm.value?.userGroups!.includes(item.id)) {
+ candidateNames.push(item.name)
+ }
+ })
+ showText = `鎸囧畾鐢ㄦ埛缁�: ${candidateNames.join(',')}`
+ }
+ }
+
+ // 琛ㄥ崟鍐呯敤鎴峰瓧娈�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) {
+ const formFieldOptions = parseFormCreateFields(unref(formFields))
+ const item = formFieldOptions.find((item) => item.field === configForm.value?.formUser)
+ showText = `琛ㄥ崟鐢ㄦ埛锛�${item?.title}`
+ }
+
+ // 琛ㄥ崟鍐呴儴闂ㄨ礋璐d汉
+ if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER) {
+ showText = `琛ㄥ崟鍐呴儴闂ㄨ礋璐d汉`
+ }
+
+ // 瀹℃壒浜鸿嚜閫�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.APPROVE_USER_SELECT) {
+ showText = `瀹℃壒浜鸿嚜閫塦
+ }
+
+ // 鍙戣捣浜鸿嚜閫�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_SELECT) {
+ showText = `鍙戣捣浜鸿嚜閫塦
+ }
+ // 鍙戣捣浜鸿嚜宸�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) {
+ showText = `鍙戣捣浜鸿嚜宸盽
+ }
+ // 鍙戣捣浜虹殑閮ㄩ棬璐熻矗浜�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER_DEPT_LEADER) {
+ showText = `鍙戣捣浜虹殑閮ㄩ棬璐熻矗浜篳
+ }
+ // 鍙戣捣浜虹殑閮ㄩ棬璐熻矗浜�
+ if (
+ configForm.value?.candidateStrategy === CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+ ) {
+ showText = `鍙戣捣浜鸿繛缁儴闂ㄨ礋璐d汉`
+ }
+ // 娴佺▼琛ㄨ揪寮�
+ if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) {
+ showText = `娴佺▼琛ㄨ揪寮忥細${configForm.value.expression}`
+ }
+ return showText
+ }
+
+ /**
+ * 澶勭悊鍊欓�変汉鍙傛暟鐨勮祴鍊�
+ */
+ const handleCandidateParam = () => {
+ let candidateParam: undefined | string = undefined
+ if (!configForm.value) {
+ return candidateParam
+ }
+ switch (configForm.value.candidateStrategy) {
+ case CandidateStrategy.USER:
+ candidateParam = configForm.value.userIds!.join(',')
+ break
+ case CandidateStrategy.ROLE:
+ candidateParam = configForm.value.roleIds!.join(',')
+ break
+ case CandidateStrategy.POST:
+ candidateParam = configForm.value.postIds!.join(',')
+ break
+ case CandidateStrategy.USER_GROUP:
+ candidateParam = configForm.value.userGroups!.join(',')
+ break
+ case CandidateStrategy.FORM_USER:
+ candidateParam = configForm.value.formUser!
+ break
+ case CandidateStrategy.EXPRESSION:
+ candidateParam = configForm.value.expression!
+ break
+ case CandidateStrategy.DEPT_MEMBER:
+ case CandidateStrategy.DEPT_LEADER:
+ candidateParam = configForm.value.deptIds!.join(',')
+ break
+ // 鍙戣捣浜洪儴闂ㄨ礋璐d汉
+ case CandidateStrategy.START_USER_DEPT_LEADER:
+ case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
+ candidateParam = configForm.value.deptLevel + ''
+ break
+ // 鎸囧畾杩炵画澶氱骇閮ㄩ棬鐨勮礋璐d汉
+ case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+ // 鍊欓�変汉鍙傛暟鏍煎紡: | 鍒嗛殧 銆傚乏杈逛负閮ㄩ棬锛堝涓儴闂ㄧ敤 , 鍒嗛殧锛夈�� 鍙宠竟涓洪儴闂ㄥ眰绾�
+ const deptIds = configForm.value.deptIds!.join(',')
+ candidateParam = deptIds.concat('|' + configForm.value.deptLevel + '')
+ break
+ }
+ // 琛ㄥ崟鍐呴儴闂ㄧ殑璐熻矗浜�
+ case CandidateStrategy.FORM_DEPT_LEADER: {
+ // 鍊欓�変汉鍙傛暟鏍煎紡: | 鍒嗛殧 銆傚乏杈逛负琛ㄥ崟鍐呴儴闂ㄥ瓧娈点�� 鍙宠竟涓洪儴闂ㄥ眰绾�
+ const deptFieldOnForm = configForm.value.formDept!
+ candidateParam = deptFieldOnForm.concat('|' + configForm.value.deptLevel + '')
+ break
+ }
+ default:
+ break
+ }
+ return candidateParam
+ }
+ /**
+ * 瑙f瀽鍊欓�変汉鍙傛暟
+ */
+ const parseCandidateParam = (
+ candidateStrategy: CandidateStrategy,
+ candidateParam: string | undefined
+ ) => {
+ if (!configForm.value || !candidateParam) {
+ return
+ }
+ switch (candidateStrategy) {
+ case CandidateStrategy.USER: {
+ configForm.value.userIds = candidateParam.split(',').map((item) => +item)
+ break
+ }
+ case CandidateStrategy.ROLE:
+ configForm.value.roleIds = candidateParam.split(',').map((item) => +item)
+ break
+ case CandidateStrategy.POST:
+ configForm.value.postIds = candidateParam.split(',').map((item) => +item)
+ break
+ case CandidateStrategy.USER_GROUP:
+ configForm.value.userGroups = candidateParam.split(',').map((item) => +item)
+ break
+ case CandidateStrategy.FORM_USER:
+ configForm.value.formUser = candidateParam
+ break
+ case CandidateStrategy.EXPRESSION:
+ configForm.value.expression = candidateParam
+ break
+ case CandidateStrategy.DEPT_MEMBER:
+ case CandidateStrategy.DEPT_LEADER:
+ configForm.value.deptIds = candidateParam.split(',').map((item) => +item)
+ break
+ // 鍙戣捣浜洪儴闂ㄨ礋璐d汉
+ case CandidateStrategy.START_USER_DEPT_LEADER:
+ case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER:
+ configForm.value.deptLevel = +candidateParam
+ break
+ // 鎸囧畾杩炵画澶氱骇閮ㄩ棬鐨勮礋璐d汉
+ case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: {
+ // 鍊欓�変汉鍙傛暟鏍煎紡: | 鍒嗛殧 銆傚乏杈逛负閮ㄩ棬锛堝涓儴闂ㄧ敤 , 鍒嗛殧锛夈�� 鍙宠竟涓洪儴闂ㄥ眰绾�
+ const paramArray = candidateParam.split('|')
+ configForm.value.deptIds = paramArray[0].split(',').map((item) => +item)
+ configForm.value.deptLevel = +paramArray[1]
+ break
+ }
+ // 琛ㄥ崟鍐呯殑閮ㄩ棬璐熻矗浜�
+ case CandidateStrategy.FORM_DEPT_LEADER: {
+ // 鍊欓�変汉鍙傛暟鏍煎紡: | 鍒嗛殧 銆傚乏杈逛负琛ㄥ崟鍐呯殑閮ㄩ棬瀛楁銆� 鍙宠竟涓洪儴闂ㄥ眰绾�
+ const paramArray = candidateParam.split('|')
+ configForm.value.formDept = paramArray[0]
+ configForm.value.deptLevel = +paramArray[1]
+ break
+ }
+ default:
+ break
+ }
+ }
+ return {
+ configForm,
+ roleOptions,
+ postOptions,
+ userOptions,
+ userGroupOptions,
+ deptTreeOptions,
+ handleCandidateParam,
+ parseCandidateParam,
+ getShowText
+ }
+}
+
+/**
+ * @description 鎶藉眽閰嶇疆
+ */
+export function useDrawer() {
+ // 鎶藉眽閰嶇疆鏄惁鍙
+ const settingVisible = ref(false)
+ // 鍏抽棴閰嶇疆鎶藉眽
+ const closeDrawer = () => {
+ settingVisible.value = false
+ }
+ // 鎵撳紑閰嶇疆鎶藉眽
+ const openDrawer = () => {
+ settingVisible.value = true
+ }
+ return {
+ settingVisible,
+ closeDrawer,
+ openDrawer
+ }
+}
+
+/**
+ * @description 鑺傜偣鍚嶇О閰嶇疆
+ */
+export function useNodeName(nodeType: NodeType) {
+ // 鑺傜偣鍚嶇О
+ const nodeName = ref<string>()
+ // 鑺傜偣鍚嶇О杈撳叆妗�
+ const showInput = ref(false)
+ // 鐐瑰嚮鑺傜偣鍚嶇О缂栬緫鍥炬爣
+ const clickIcon = () => {
+ showInput.value = true
+ }
+ // 鑺傜偣鍚嶇О杈撳叆妗嗗け鍘荤劍鐐�
+ const blurEvent = () => {
+ showInput.value = false
+ nodeName.value = nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string)
+ }
+ return {
+ nodeName,
+ showInput,
+ clickIcon,
+ blurEvent
+ }
+}
+
+export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) {
+ // 鏄剧ず鑺傜偣鍚嶇О杈撳叆妗�
+ const showInput = ref(false)
+ // 鑺傜偣鍚嶇О杈撳叆妗嗗け鍘荤劍鐐�
+ const blurEvent = () => {
+ showInput.value = false
+ node.value.name = node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string)
+ }
+ // 鐐瑰嚮鑺傜偣鏍囬杩涜杈撳叆
+ const clickTitle = () => {
+ showInput.value = true
+ }
+ return {
+ showInput,
+ clickTitle,
+ blurEvent
+ }
+}
+
+/**
+ * @description 鏍规嵁鑺傜偣浠诲姟鐘舵�侊紝鑾峰彇鑺傜偣浠诲姟鐘舵�佹牱寮�
+ */
+export function useTaskStatusClass(taskStatus: TaskStatusEnum | undefined): string {
+ if (!taskStatus) {
+ return ''
+ }
+ if (taskStatus === TaskStatusEnum.APPROVE) {
+ return 'status-pass'
+ }
+ if (taskStatus === TaskStatusEnum.RUNNING) {
+ return 'status-running'
+ }
+ if (taskStatus === TaskStatusEnum.REJECT) {
+ return 'status-reject'
+ }
+ if (taskStatus === TaskStatusEnum.CANCEL) {
+ return 'status-cancel'
+ }
+ return ''
+}
+
+/** 鏉′欢缁勪欢鏂囧瓧灞曠ず */
+export function getConditionShowText(
+ conditionType: ConditionType | undefined,
+ conditionExpression: string | undefined,
+ conditionGroups: ConditionGroup | undefined,
+ fieldOptions: Array<Record<string, any>>
+) {
+ let showText = ''
+ if (conditionType === ConditionType.EXPRESSION) {
+ if (conditionExpression) {
+ showText = `琛ㄨ揪寮忥細${conditionExpression}`
+ }
+ }
+ if (conditionType === ConditionType.RULE) {
+ // 鏉′欢缁勬槸鍚︿负涓庡叧绯�
+ const groupAnd = conditionGroups?.and
+ let warningMessage: undefined | string = undefined
+ const conditionGroup = conditionGroups?.conditions.map((item) => {
+ return (
+ '(' +
+ item.rules
+ .map((rule) => {
+ if (rule.leftSide && rule.rightSide) {
+ return (
+ getFormFieldTitle(fieldOptions, rule.leftSide) +
+ ' ' +
+ getOpName(rule.opCode) +
+ ' ' +
+ rule.rightSide
+ )
+ } else {
+ // 鏈変竴鏉¤鍒欎笉瀹屽杽銆傛彁绀洪敊璇�
+ warningMessage = '璇峰畬鍠勬潯浠惰鍒�'
+ return ''
+ }
+ })
+ .join(item.and ? ' 涓� ' : ' 鎴� ') +
+ ' ) '
+ )
+ })
+ if (warningMessage) {
+ showText = ''
+ } else {
+ showText = conditionGroup!.join(groupAnd ? ' 涓� ' : ' 鎴� ')
+ }
+ }
+ return showText
+}
+
+/** 鑾峰彇琛ㄥ崟瀛楁鍚嶇О*/
+const getFormFieldTitle = (fieldOptions: Array<Record<string, any>>, field: string) => {
+ const item = fieldOptions.find((item) => item.field === field)
+ return item?.title
+}
+
+/** 鑾峰彇鎿嶄綔绗﹀悕绉� */
+const getOpName = (opCode: string): string => {
+ const opName = COMPARISON_OPERATORS.find((item: any) => item.value === opCode)
+ return opName?.label
+}
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue
new file mode 100644
index 0000000..7ec382f
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/ChildProcessNodeConfig.vue
@@ -0,0 +1,610 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="550"
+ :before-close="saveConfig"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <el-tabs type="border-card" v-model="activeTabName">
+ <el-tab-pane label="瀛愭祦绋�" name="child">
+ <div>
+ <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+ <el-form-item label="鏄惁寮傛" prop="async">
+ <el-switch v-model="configForm.async" active-text="寮傛" inactive-text="涓嶅紓姝�" />
+ </el-form-item>
+ <el-form-item label="閫夋嫨瀛愭祦绋�" prop="calledProcessDefinitionKey">
+ <el-select
+ v-model="configForm.calledProcessDefinitionKey"
+ clearable
+ @change="handleCalledElementChange"
+ >
+ <el-option
+ v-for="(item, index) in childProcessOptions"
+ :key="index"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏄惁鑷姩璺宠繃瀛愭祦绋嬪彂璧疯妭鐐�" prop="skipStartUserNode">
+ <el-switch
+ v-model="configForm.skipStartUserNode"
+ active-text="璺宠繃"
+ inactive-text="涓嶈烦杩�"
+ />
+ </el-form-item>
+ <el-form-item label="涓烩啋瀛愬彉閲忎紶閫�" prop="inVariables">
+ <div class="flex pt-2" v-for="(item, index) in configForm.inVariables" :key="index">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`inVariables.${index}.source`"
+ :rules="{
+ required: true,
+ message: '鍙橀噺涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-select class="w-200px!" v-model="item.source">
+ <el-option
+ v-for="(field, fIdx) in formFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`inVariables.${index}.target`"
+ :rules="{
+ required: true,
+ message: '鍙橀噺涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-select class="w-200px!" v-model="item.target">
+ <el-option
+ v-for="(field, fIdx) in childFormFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-1 flex items-center">
+ <Icon
+ icon="ep:delete"
+ :size="18"
+ @click="deleteVariable(index, configForm.inVariables)"
+ />
+ </div>
+ </div>
+ <el-button type="primary" text @click="addVariable(configForm.inVariables)">
+ <Icon icon="ep:plus" class="mr-5px" />娣诲姞涓�琛�
+ </el-button>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.async === false"
+ label="瀛愨啋涓诲彉閲忎紶閫�"
+ prop="outVariables"
+ >
+ <div class="flex pt-2" v-for="(item, index) in configForm.outVariables" :key="index">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`outVariables.${index}.source`"
+ :rules="{
+ required: true,
+ message: '鍙橀噺涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-select class="w-200px!" v-model="item.source">
+ <el-option
+ v-for="(field, fIdx) in childFormFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`outVariables.${index}.target`"
+ :rules="{
+ required: true,
+ message: '鍙橀噺涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-select class="w-200px!" v-model="item.target">
+ <el-option
+ v-for="(field, fIdx) in formFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-1 flex items-center">
+ <Icon
+ icon="ep:delete"
+ :size="18"
+ @click="deleteVariable(index, configForm.outVariables)"
+ />
+ </div>
+ </div>
+ <el-button type="primary" text @click="addVariable(configForm.outVariables)">
+ <Icon icon="ep:plus" class="mr-5px" />娣诲姞涓�琛�
+ </el-button>
+ </el-form-item>
+ <el-form-item label="瀛愭祦绋嬪彂璧蜂汉" prop="startUserType">
+ <el-radio-group v-model="configForm.startUserType">
+ <el-radio
+ v-for="item in CHILD_PROCESS_START_USER_TYPE"
+ :key="item.value"
+ :value="item.value"
+ >
+ {{ item.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.startUserType === ChildProcessStartUserTypeEnum.FROM_FORM"
+ label="褰撳瓙娴佺▼鍙戣捣浜轰负绌烘椂"
+ prop="startUserType"
+ >
+ <el-radio-group v-model="configForm.startUserEmptyType">
+ <el-radio
+ v-for="item in CHILD_PROCESS_START_USER_EMPTY_TYPE"
+ :key="item.value"
+ :value="item.value"
+ >
+ {{ item.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.startUserType === 2"
+ label="鍙戣捣浜鸿〃鍗�"
+ prop="startUserFormField"
+ >
+ <el-select class="w-200px!" v-model="configForm.startUserFormField">
+ <el-option
+ v-for="(field, fIdx) in formFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-divider content-position="left">瓒呮椂璁剧疆</el-divider>
+ <el-form-item label="鍚敤寮�鍏�" prop="timeoutEnable">
+ <el-switch
+ v-model="configForm.timeoutEnable"
+ active-text="寮�鍚�"
+ inactive-text="鍏抽棴"
+ />
+ </el-form-item>
+ <div v-if="configForm.timeoutEnable">
+ <el-form-item prop="timeoutType">
+ <el-radio-group v-model="configForm.timeoutType">
+ <el-radio-button
+ v-for="item in DELAY_TYPE"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="configForm.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION">
+ <el-form-item prop="timeDuration">
+ <el-input-number
+ class="mr-2"
+ :style="{ width: '100px' }"
+ v-model="configForm.timeDuration"
+ :min="1"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }">
+ <el-option
+ v-for="item in TIME_UNIT_TYPES"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-text>鍚庤繘鍏ヤ笅涓�鑺傜偣</el-text>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.timeoutType === DelayTypeEnum.FIXED_DATE_TIME"
+ prop="dateTime"
+ >
+ <el-date-picker
+ class="mr-2"
+ v-model="configForm.dateTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨鏃ユ湡鍜屾椂闂�"
+ value-format="YYYY-MM-DDTHH:mm:ss"
+ />
+ <el-text>鍚庤繘鍏ヤ笅涓�鑺傜偣</el-text>
+ </el-form-item>
+ </div>
+
+ <el-divider content-position="left">澶氬疄渚嬭缃�</el-divider>
+ <el-form-item label="鍚敤寮�鍏�" prop="multiInstanceEnable">
+ <el-switch
+ v-model="configForm.multiInstanceEnable"
+ active-text="寮�鍚�"
+ inactive-text="鍏抽棴"
+ />
+ </el-form-item>
+ <div v-if="configForm.multiInstanceEnable">
+ <el-form-item prop="sequential">
+ <el-switch
+ v-model="configForm.sequential"
+ active-text="涓茶"
+ inactive-text="骞惰"
+ />
+ </el-form-item>
+ <el-form-item prop="approveRatio">
+ <el-text>瀹屾垚姣斾緥(%)</el-text>
+ <el-input-number
+ class="ml-10px"
+ v-model="configForm.approveRatio"
+ :min="10"
+ :max="100"
+ :step="10"
+ />
+ </el-form-item>
+ <el-form-item prop="multiInstanceSourceType">
+ <el-text>澶氬疄渚嬫潵婧�</el-text>
+ <el-select
+ class="ml-10px w-200px!"
+ v-model="configForm.multiInstanceSourceType"
+ @change="handleMultiInstanceSourceTypeChange"
+ >
+ <el-option
+ v-for="item in CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY">
+ <el-input-number v-model="configForm.multiInstanceSource" :min="1" />
+ </el-form-item>
+ <el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM">
+ <el-select class="w-200px!" v-model="configForm.multiInstanceSource">
+ <el-option
+ v-for="(field, fIdx) in digitalFormFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="configForm.multiInstanceSourceType === ChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM">
+ <el-select class="w-200px!" v-model="configForm.multiInstanceSource">
+ <el-option
+ v-for="(field, fIdx) in multiFormFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ </el-form>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import { getModelList } from '@/api/bpm/model'
+import { getForm } from '@/api/bpm/form'
+import {
+ SimpleFlowNode,
+ NodeType,
+ TIME_UNIT_TYPES,
+ TimeUnitType,
+ DelayTypeEnum,
+ DELAY_TYPE,
+ IOParameter,
+ ChildProcessStartUserTypeEnum,
+ CHILD_PROCESS_START_USER_TYPE,
+ ChildProcessStartUserEmptyTypeEnum,
+ CHILD_PROCESS_START_USER_EMPTY_TYPE,
+ CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE,
+ ChildProcessMultiInstanceSourceTypeEnum
+} from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFieldsAndStartUser } from '../node'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { convertTimeUnit } from '../utils'
+defineOptions({
+ name: 'ChildProcessNodeConfig'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 褰撳墠鑺傜偣
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.CHILD_PROCESS_NODE)
+// 婵�娲荤殑 Tab 鏍囩椤�
+const activeTabName = ref('child')
+// 瀛愭祦绋嬭〃鍗曢厤缃�
+const formRef = ref() // 琛ㄥ崟 Ref
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ async: [{ required: true, message: '鏄惁寮傛涓嶈兘涓虹┖', trigger: 'change' }],
+ calledProcessDefinitionKey: [{ required: true, message: '瀛愭祦绋嬩笉鑳戒负绌�', trigger: 'change' }],
+ skipStartUserNode: [
+ { required: true, message: '鏄惁鑷姩璺宠繃瀛愭祦绋嬪彂璧疯妭鐐逛笉鑳戒负绌�', trigger: 'change' }
+ ],
+ startUserType: [{ required: true, message: '瀛愭祦绋嬪彂璧蜂汉涓嶈兘涓虹┖', trigger: 'change' }],
+ startUserEmptyType: [
+ { required: true, message: '褰撳瓙娴佺▼鍙戣捣浜轰负绌烘椂涓嶈兘涓虹┖', trigger: 'change' }
+ ],
+ startUserFormField: [{ required: true, message: '鍙戣捣浜鸿〃鍗曚笉鑳戒负绌�', trigger: 'change' }],
+ timeoutEnable: [{ required: true, message: '瓒呮椂璁剧疆鏄惁寮�鍚笉鑳戒负绌�', trigger: 'change' }],
+ timeoutType: [{ required: true, message: '瓒呮椂璁剧疆鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }],
+ timeDuration: [{ required: true, message: '瓒呮椂璁剧疆鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }],
+ dateTime: [{ required: true, message: '瓒呮椂璁剧疆鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }],
+ multiInstanceEnable: [{ required: true, message: '澶氬疄渚嬭缃笉鑳戒负绌�', trigger: 'change' }]
+})
+type ChildProcessFormType = {
+ async: boolean
+ calledProcessDefinitionKey: string
+ skipStartUserNode: boolean
+ inVariables?: IOParameter[]
+ outVariables?: IOParameter[]
+ startUserType: ChildProcessStartUserTypeEnum
+ startUserEmptyType: ChildProcessStartUserEmptyTypeEnum
+ startUserFormField: string
+ timeoutEnable: boolean
+ timeoutType: DelayTypeEnum
+ timeDuration: number
+ timeUnit: TimeUnitType
+ dateTime: string
+ multiInstanceEnable: boolean
+ sequential: boolean
+ approveRatio: number
+ multiInstanceSourceType: ChildProcessMultiInstanceSourceTypeEnum
+ multiInstanceSource: string
+}
+const configForm = ref<ChildProcessFormType>({
+ async: false,
+ calledProcessDefinitionKey: '',
+ skipStartUserNode: false,
+ inVariables: [],
+ outVariables: [],
+ startUserType: ChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER,
+ startUserEmptyType: ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER,
+ startUserFormField: '',
+ timeoutEnable: false,
+ timeoutType: DelayTypeEnum.FIXED_TIME_DURATION,
+ timeDuration: 1,
+ timeUnit: TimeUnitType.HOUR,
+ dateTime: '',
+ multiInstanceEnable: false,
+ sequential: false,
+ approveRatio: 100,
+ multiInstanceSourceType: ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY,
+ multiInstanceSource: ''
+})
+const childProcessOptions = ref()
+const formFieldOptions = useFormFieldsAndStartUser()
+const digitalFormFieldOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'inputNumber')
+})
+const multiFormFieldOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'select' || item.type === 'checkbox')
+})
+const childFormFieldOptions = ref()
+
+// 淇濆瓨閰嶇疆
+const saveConfig = async () => {
+ activeTabName.value = 'child'
+ if (!formRef) return false
+ const valid = await formRef.value.validate()
+ if (!valid) return false
+ const childInfo = childProcessOptions.value.find(
+ (option: any) => option.key === configForm.value.calledProcessDefinitionKey
+ )
+ currentNode.value.name = nodeName.value!
+ if (currentNode.value.childProcessSetting) {
+ // 1. 鏄惁寮傛
+ currentNode.value.childProcessSetting.async = configForm.value.async
+ // 2. 璋冪敤娴佺▼
+ currentNode.value.childProcessSetting.calledProcessDefinitionKey = childInfo.key
+ currentNode.value.childProcessSetting.calledProcessDefinitionName = childInfo.name
+ // 3. 鏄惁璺宠繃鍙戣捣浜�
+ currentNode.value.childProcessSetting.skipStartUserNode = configForm.value.skipStartUserNode
+ // 4. 涓�->瀛愬彉閲�
+ currentNode.value.childProcessSetting.inVariables = configForm.value.inVariables
+ // 5. 瀛�->涓诲彉閲�
+ currentNode.value.childProcessSetting.outVariables = configForm.value.outVariables
+ // 6. 鍙戣捣浜鸿缃�
+ currentNode.value.childProcessSetting.startUserSetting.type = configForm.value.startUserType
+ currentNode.value.childProcessSetting.startUserSetting.emptyType =
+ configForm.value.startUserEmptyType
+ currentNode.value.childProcessSetting.startUserSetting.formField =
+ configForm.value.startUserFormField
+ // 7. 瓒呮椂璁剧疆
+ currentNode.value.childProcessSetting.timeoutSetting = {
+ enable: configForm.value.timeoutEnable
+ }
+ if (configForm.value.timeoutEnable) {
+ currentNode.value.childProcessSetting.timeoutSetting.type = configForm.value.timeoutType
+ if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
+ currentNode.value.childProcessSetting.timeoutSetting.timeExpression = getIsoTimeDuration()
+ }
+ if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
+ currentNode.value.childProcessSetting.timeoutSetting.timeExpression =
+ configForm.value.dateTime
+ }
+ }
+ // 8. 澶氬疄渚嬭缃�
+ currentNode.value.childProcessSetting.multiInstanceSetting = {
+ enable: configForm.value.multiInstanceEnable
+ }
+ if (configForm.value.multiInstanceEnable) {
+ currentNode.value.childProcessSetting.multiInstanceSetting.sequential =
+ configForm.value.sequential
+ currentNode.value.childProcessSetting.multiInstanceSetting.approveRatio =
+ configForm.value.approveRatio
+ currentNode.value.childProcessSetting.multiInstanceSetting.sourceType =
+ configForm.value.multiInstanceSourceType
+ currentNode.value.childProcessSetting.multiInstanceSetting.source =
+ configForm.value.multiInstanceSource
+ }
+ }
+
+ currentNode.value.showText = `璋冪敤瀛愭祦绋嬶細${childInfo.name}`
+ settingVisible.value = false
+ return true
+}
+// 鏄剧ず瀛愭祦绋嬭妭鐐归厤缃紝 鐢辩埗缁勪欢浼犺繃鏉�
+const showChildProcessNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ if (node.childProcessSetting) {
+ // 1. 鏄惁寮傛
+ configForm.value.async = node.childProcessSetting.async
+ // 2. 璋冪敤娴佺▼
+ configForm.value.calledProcessDefinitionKey =
+ node.childProcessSetting?.calledProcessDefinitionKey
+ // 3. 鏄惁璺宠繃鍙戣捣浜�
+ configForm.value.skipStartUserNode = node.childProcessSetting.skipStartUserNode
+ // 4. 涓�->瀛愬彉閲�
+ configForm.value.inVariables = node.childProcessSetting.inVariables
+ // 5. 瀛�->涓诲彉閲�
+ configForm.value.outVariables = node.childProcessSetting.outVariables
+ // 6. 鍙戣捣浜鸿缃�
+ configForm.value.startUserType = node.childProcessSetting.startUserSetting.type
+ configForm.value.startUserEmptyType = node.childProcessSetting.startUserSetting.emptyType ?? ChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER
+ configForm.value.startUserFormField = node.childProcessSetting.startUserSetting.formField ?? ''
+ // 7. 瓒呮椂璁剧疆
+ configForm.value.timeoutEnable = node.childProcessSetting.timeoutSetting.enable ?? false
+ if (configForm.value.timeoutEnable) {
+ configForm.value.timeoutType =
+ node.childProcessSetting.timeoutSetting.type ?? DelayTypeEnum.FIXED_TIME_DURATION
+ // 鍥哄畾鏃堕暱
+ if (configForm.value.timeoutType === DelayTypeEnum.FIXED_TIME_DURATION) {
+ const strTimeDuration = node.childProcessSetting.timeoutSetting.timeExpression ?? ''
+ let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+ let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+ configForm.value.timeDuration = parseInt(parseTime)
+ configForm.value.timeUnit = convertTimeUnit(parseTimeUnit)
+ }
+ // 鍥哄畾鏃ユ湡鏃堕棿
+ if (configForm.value.timeoutType === DelayTypeEnum.FIXED_DATE_TIME) {
+ configForm.value.dateTime = node.childProcessSetting.timeoutSetting.timeExpression ?? ''
+ }
+ }
+ // 8. 澶氬疄渚嬭缃�
+ configForm.value.multiInstanceEnable =
+ node.childProcessSetting.multiInstanceSetting.enable ?? false
+ if (configForm.value.multiInstanceEnable) {
+ configForm.value.sequential =
+ node.childProcessSetting.multiInstanceSetting.sequential ?? false
+ configForm.value.approveRatio =
+ node.childProcessSetting.multiInstanceSetting.approveRatio ?? 100
+ configForm.value.multiInstanceSourceType =
+ node.childProcessSetting.multiInstanceSetting.sourceType ??
+ ChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY
+ configForm.value.multiInstanceSource =
+ node.childProcessSetting.multiInstanceSetting.source ?? ''
+ }
+ }
+ loadFormInfo()
+}
+
+defineExpose({ openDrawer, showChildProcessNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+
+const addVariable = (arr?: IOParameter[]) => {
+ arr?.push({
+ source: '',
+ target: ''
+ })
+}
+const deleteVariable = (index: number, arr?: IOParameter[]) => {
+ arr?.splice(index, 1)
+}
+const handleCalledElementChange = () => {
+ configForm.value.inVariables = []
+ configForm.value.outVariables = []
+ loadFormInfo()
+}
+const loadFormInfo = async () => {
+ const childInfo = childProcessOptions.value.find(
+ (option) => option.key === configForm.value.calledProcessDefinitionKey
+ )
+ const formInfo = await getForm(childInfo.formId)
+ childFormFieldOptions.value = []
+ if (formInfo.fields) {
+ formInfo.fields.forEach((fieldStr: string) => {
+ parseFormFields(JSON.parse(fieldStr), childFormFieldOptions.value)
+ })
+ }
+}
+const getIsoTimeDuration = () => {
+ let strTimeDuration = 'PT'
+ if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
+ strTimeDuration += configForm.value.timeDuration + 'M'
+ }
+ if (configForm.value.timeUnit === TimeUnitType.HOUR) {
+ strTimeDuration += configForm.value.timeDuration + 'H'
+ }
+ if (configForm.value.timeUnit === TimeUnitType.DAY) {
+ strTimeDuration += configForm.value.timeDuration + 'D'
+ }
+ return strTimeDuration
+}
+const handleMultiInstanceSourceTypeChange = () => {
+ configForm.value.multiInstanceSource = ''
+}
+
+onMounted(async () => {
+ childProcessOptions.value = await getModelList(undefined)
+})
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
new file mode 100644
index 0000000..9020d65
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue
@@ -0,0 +1,222 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="588"
+ :before-close="handleClose"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-name"
+ >{{ currentNode.name }}
+ <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()"
+ /></div>
+
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <div>
+ <div class="mb-3 font-size-16px" v-if="currentNode.conditionSetting?.defaultFlow"
+ >鏈弧瓒冲叾瀹冩潯浠舵椂锛屽皢杩涘叆姝ゅ垎鏀紙璇ュ垎鏀笉鍙紪杈戝拰鍒犻櫎锛�</div
+ >
+ <div v-else>
+ <Condition ref="conditionRef" v-model="condition" />
+ </div>
+ </div>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, ConditionType } from '../consts'
+import { getDefaultConditionNodeName } from '../utils'
+import { useFormFieldsAndStartUser, getConditionShowText } from '../node'
+import Condition from './components/Condition.vue'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({
+ name: 'ConditionNodeConfig'
+})
+const props = defineProps({
+ conditionNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ },
+ nodeIndex: {
+ type: Number,
+ required: true
+ }
+})
+const settingVisible = ref(false)
+const currentNode = ref<SimpleFlowNode>(props.conditionNode)
+const condition = ref<any>({
+ conditionType: ConditionType.RULE, // 璁剧疆榛樿鍊�
+ conditionExpression: '',
+ conditionGroups: {
+ and: true,
+ conditions: [
+ {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ ]
+ }
+ ]
+ }
+})
+const open = () => {
+ // 濡傛灉鏈夊凡瀛樺湪鐨勯厤缃垯浣跨敤锛屽惁鍒欎娇鐢ㄩ粯璁ゅ��
+ if (currentNode.value.conditionSetting) {
+ condition.value = cloneDeep(currentNode.value.conditionSetting)
+ } else {
+ // 閲嶇疆涓洪粯璁ゅ��
+ condition.value = {
+ conditionType: ConditionType.RULE,
+ conditionExpression: '',
+ conditionGroups: {
+ and: true,
+ conditions: [
+ {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ settingVisible.value = true
+}
+
+watch(
+ () => props.conditionNode,
+ (newValue) => {
+ currentNode.value = newValue
+ }
+)
+// 鏄剧ず鍚嶇О杈撳叆妗�
+const showInput = ref(false)
+
+const clickIcon = () => {
+ showInput.value = true
+}
+// 杈撳叆妗嗗け鍘荤劍鐐�
+const blurEvent = () => {
+ showInput.value = false
+ currentNode.value.name =
+ currentNode.value.name ||
+ getDefaultConditionNodeName(props.nodeIndex, currentNode.value?.conditionSetting?.defaultFlow)
+}
+
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+// 鍏抽棴
+const closeDrawer = () => {
+ settingVisible.value = false
+}
+
+const handleClose = async (done: (cancel?: boolean) => void) => {
+ const isSuccess = await saveConfig()
+ if (!isSuccess) {
+ done(true) // 浼犲叆 true 闃绘鍏抽棴
+ } else {
+ done()
+ }
+}
+
+/** 淇濆瓨閰嶇疆 */
+const fieldOptions = useFormFieldsAndStartUser() // 娴佺▼琛ㄥ崟瀛楁鍜屽彂璧蜂汉瀛楁
+const conditionRef = ref()
+const saveConfig = async () => {
+ if (!currentNode.value.conditionSetting?.defaultFlow) {
+ // 鏍¢獙琛ㄥ崟
+ const valid = await conditionRef.value.validate()
+ if (!valid) return false
+ const showText = getConditionShowText(
+ condition.value?.conditionType,
+ condition.value?.conditionExpression,
+ condition.value.conditionGroups,
+ fieldOptions
+ )
+ if (!showText) {
+ return false
+ }
+ currentNode.value.showText = showText
+ // 浣跨敤 cloneDeep 杩涜娣辨嫹璐�
+ currentNode.value.conditionSetting = cloneDeep({
+ ...currentNode.value.conditionSetting,
+ conditionType: condition.value?.conditionType,
+ conditionExpression:
+ condition.value?.conditionType === ConditionType.EXPRESSION
+ ? condition.value?.conditionExpression
+ : undefined,
+ conditionGroups:
+ condition.value?.conditionType === ConditionType.RULE
+ ? condition.value?.conditionGroups
+ : undefined
+ })
+ }
+ settingVisible.value = false
+ return true
+}
+</script>
+
+<style lang="scss" scoped>
+.condition-group-tool {
+ display: flex;
+ justify-content: space-between;
+ width: 500px;
+ margin-bottom: 20px;
+}
+
+.condition-group {
+ position: relative;
+
+ &:hover {
+ border-color: #0089ff;
+
+ .condition-group-delete {
+ opacity: 1;
+ }
+ }
+
+ .condition-group-delete {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ cursor: pointer;
+ opacity: 0;
+ }
+}
+
+::v-deep(.el-card__header) {
+ padding: 8px var(--el-card-padding);
+ border-bottom: 1px solid var(--el-card-border-color);
+ box-sizing: border-box;
+}
+</style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
new file mode 100644
index 0000000..aec32da
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/CopyTaskNodeConfig.vue
@@ -0,0 +1,392 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="550"
+ :before-close="saveConfig"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <el-tabs type="border-card" v-model="activeTabName">
+ <el-tab-pane label="鎶勯�佷汉" name="user">
+ <div>
+ <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+ <el-form-item label="鎶勯�佷汉璁剧疆" prop="candidateStrategy">
+ <el-radio-group
+ v-model="configForm.candidateStrategy"
+ @change="changeCandidateStrategy"
+ >
+ <el-radio
+ v-for="(dict, index) in copyUserStrategies"
+ :key="index"
+ :value="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <el-form-item
+ v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
+ label="鎸囧畾瑙掕壊"
+ prop="roleIds"
+ >
+ <el-select v-model="configForm.roleIds" clearable multiple style="width: 100%">
+ <el-option
+ v-for="item in roleOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="
+ configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+ configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+ "
+ label="鎸囧畾閮ㄩ棬"
+ prop="deptIds"
+ span="24"
+ >
+ <el-tree-select
+ ref="treeRef"
+ v-model="configForm.deptIds"
+ :data="deptTreeOptions"
+ :props="defaultProps"
+ empty-text="鍔犺浇涓紝璇风◢鍚�"
+ multiple
+ node-key="id"
+ style="width: 100%"
+ show-checkbox
+ />
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy == CandidateStrategy.POST"
+ label="鎸囧畾宀椾綅"
+ prop="postIds"
+ span="24"
+ >
+ <el-select v-model="configForm.postIds" clearable multiple style="width: 100%">
+ <el-option
+ v-for="item in postOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy == CandidateStrategy.USER"
+ label="鎸囧畾鐢ㄦ埛"
+ prop="userIds"
+ span="24"
+ >
+ <el-select v-model="configForm.userIds" clearable multiple style="width: 100%">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+ label="鎸囧畾鐢ㄦ埛缁�"
+ prop="userGroups"
+ >
+ <el-select v-model="configForm.userGroups" clearable multiple style="width: 100%">
+ <el-option
+ v-for="item in userGroupOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
+ label="琛ㄥ崟鍐呯敤鎴峰瓧娈�"
+ prop="formUser"
+ >
+ <el-select v-model="configForm.formUser" clearable style="width: 100%">
+ <el-option
+ v-for="(item, idx) in userFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled="!item.required"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+ label="琛ㄥ崟鍐呴儴闂ㄥ瓧娈�"
+ prop="formDept"
+ >
+ <el-select v-model="configForm.formDept" clearable style="width: 100%">
+ <el-option
+ v-for="(item, idx) in deptFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled="!item.required"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ configForm.candidateStrategy ==
+ CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+ "
+ :label="deptLevelLabel!"
+ prop="deptLevel"
+ span="24"
+ >
+ <el-select v-model="configForm.deptLevel" clearable>
+ <el-option
+ v-for="(item, index) in MULTI_LEVEL_DEPT"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+ label="娴佺▼琛ㄨ揪寮�"
+ prop="expression"
+ >
+ <el-input
+ type="textarea"
+ v-model="configForm.expression"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="琛ㄥ崟瀛楁鏉冮檺" name="fields" v-if="formType === 10">
+ <div class="field-setting-pane">
+ <div class="field-setting-desc">瀛楁鏉冮檺</div>
+ <div class="field-permit-title">
+ <div class="setting-title-label first-title"> 瀛楁鍚嶇О </div>
+ <div class="other-titles">
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+ 鍙
+ </span>
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+ 鍙紪杈�
+ </span>
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+ 闅愯棌
+ </span>
+ </div>
+ </div>
+ <div
+ class="field-setting-item"
+ v-for="(item, index) in fieldsPermissionConfig"
+ :key="index"
+ >
+ <div class="field-setting-item-label"> {{ item.title }} </div>
+ <el-radio-group class="field-setting-item-group" v-model="item.permission">
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.READ"
+ size="large"
+ :label="FieldPermissionType.WRITE"
+ ><span></span
+ ></el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.WRITE"
+ size="large"
+ :label="FieldPermissionType.WRITE"
+ disabled
+ ><span></span
+ ></el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.NONE"
+ size="large"
+ :label="FieldPermissionType.NONE"
+ ><span></span
+ ></el-radio>
+ </div>
+ </el-radio-group>
+ </div>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import {
+ SimpleFlowNode,
+ CandidateStrategy,
+ NodeType,
+ CANDIDATE_STRATEGY,
+ FieldPermissionType,
+ MULTI_LEVEL_DEPT
+} from '../consts'
+import {
+ useWatchNode,
+ useDrawer,
+ useNodeName,
+ useFormFieldsPermission,
+ useNodeForm,
+ CopyTaskFormType
+} from '../node'
+import { defaultProps } from '@/utils/tree'
+defineOptions({
+ name: 'CopyTaskNodeConfig'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+const deptLevelLabel = computed(() => {
+ let label = '閮ㄩ棬璐熻矗浜烘潵婧�'
+ if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+ label = label + '(鎸囧畾閮ㄩ棬鍚戜笂)'
+ } else {
+ label = label + '(鍙戣捣浜洪儴闂ㄥ悜涓�)'
+ }
+ return label
+})
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 褰撳墠鑺傜偣
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
+// 婵�娲荤殑 Tab 鏍囩椤�
+const activeTabName = ref('user')
+// 琛ㄥ崟瀛楁鏉冮檺閰嶇疆
+const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
+ useFormFieldsPermission(FieldPermissionType.READ)
+// 琛ㄥ崟鍐呯敤鎴峰瓧娈甸�夐」, 蹇呴』鏄繀濉拰鐢ㄦ埛閫夋嫨鍣�
+const userFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 琛ㄥ崟鍐呴儴闂ㄥ瓧娈甸�夐」, 蹇呴』鏄繀濉拰閮ㄩ棬閫夋嫨鍣�
+const deptFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
+// 鎶勯�佷汉琛ㄥ崟閰嶇疆
+const formRef = ref() // 琛ㄥ崟 Ref
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ candidateStrategy: [{ required: true, message: '鎶勯�佷汉璁剧疆涓嶈兘涓虹┖', trigger: 'change' }],
+ userIds: [{ required: true, message: '鐢ㄦ埛涓嶈兘涓虹┖', trigger: 'change' }],
+ roleIds: [{ required: true, message: '瑙掕壊涓嶈兘涓虹┖', trigger: 'change' }],
+ deptIds: [{ required: true, message: '閮ㄩ棬涓嶈兘涓虹┖', trigger: 'change' }],
+ userGroups: [{ required: true, message: '鐢ㄦ埛缁勪笉鑳戒负绌�', trigger: 'change' }],
+ postIds: [{ required: true, message: '宀椾綅涓嶈兘涓虹┖', trigger: 'change' }],
+ formUser: [{ required: true, message: '琛ㄥ崟鍐呯敤鎴峰瓧娈典笉鑳戒负绌�', trigger: 'change' }],
+ formDept: [{ required: true, message: '琛ㄥ崟鍐呴儴闂ㄥ瓧娈典笉鑳戒负绌�', trigger: 'change' }],
+ expression: [{ required: true, message: '娴佺▼琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }]
+})
+
+const {
+ configForm: tempConfigForm,
+ roleOptions,
+ postOptions,
+ userOptions,
+ userGroupOptions,
+ deptTreeOptions,
+ getShowText,
+ handleCandidateParam,
+ parseCandidateParam
+} = useNodeForm(NodeType.COPY_TASK_NODE)
+const configForm = tempConfigForm as Ref<CopyTaskFormType>
+// 鎶勯�佷汉绛栫暐锛� 鍘绘帀鍙戣捣浜鸿嚜閫� 鍜� 鍙戣捣浜鸿嚜宸�
+const copyUserStrategies = computed(() => {
+ return CANDIDATE_STRATEGY.filter((item) => item.value !== CandidateStrategy.START_USER)
+})
+// 鏀瑰彉鎶勯�佷汉璁剧疆绛栫暐
+const changeCandidateStrategy = () => {
+ configForm.value.userIds = []
+ configForm.value.deptIds = []
+ configForm.value.roleIds = []
+ configForm.value.postIds = []
+ configForm.value.userGroups = []
+ configForm.value.deptLevel = 1
+ configForm.value.formUser = ''
+}
+// 淇濆瓨閰嶇疆
+const saveConfig = async () => {
+ activeTabName.value = 'user'
+ if (!formRef) return false
+ const valid = await formRef.value.validate()
+ if (!valid) return false
+ const showText = getShowText()
+ if (!showText) return false
+ currentNode.value.name = nodeName.value!
+ currentNode.value.candidateParam = handleCandidateParam()
+ currentNode.value.candidateStrategy = configForm.value.candidateStrategy
+ currentNode.value.showText = showText
+ currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+ settingVisible.value = false
+ return true
+}
+// 鏄剧ず鎶勯�佽妭鐐归厤缃紝 鐢辩埗缁勪欢浼犺繃鏉�
+const showCopyTaskNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ // 鎶勯�佷汉璁剧疆
+ configForm.value.candidateStrategy = node.candidateStrategy!
+ parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
+ // 琛ㄥ崟瀛楁鏉冮檺
+ getNodeConfigFormFields(node.fieldsPermission)
+}
+
+/** 鎵归噺鏇存柊鏉冮檺 */
+const updatePermission = (type: string) => {
+ fieldsPermissionConfig.value.forEach((field) => {
+ field.permission =
+ type === 'READ'
+ ? FieldPermissionType.READ
+ : type === 'WRITE'
+ ? FieldPermissionType.WRITE
+ : FieldPermissionType.NONE
+ })
+}
+
+defineExpose({ openDrawer, showCopyTaskNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue
new file mode 100644
index 0000000..741796d
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/DelayTimerNodeConfig.vue
@@ -0,0 +1,190 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="550"
+ :before-close="saveConfig"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <div>
+ <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+ <el-form-item label="寤惰繜鏃堕棿" prop="delayType">
+ <el-radio-group v-model="configForm.delayType">
+ <el-radio-button
+ v-for="item in DELAY_TYPE"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_TIME_DURATION">
+ <el-form-item prop="timeDuration">
+ <el-input-number
+ class="mr-2"
+ :style="{ width: '100px' }"
+ v-model="configForm.timeDuration"
+ :min="1"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-select v-model="configForm.timeUnit" class="mr-2" :style="{ width: '100px' }">
+ <el-option
+ v-for="item in TIME_UNIT_TYPES"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-text>鍚庤繘鍏ヤ笅涓�鑺傜偣</el-text>
+ </el-form-item>
+ <el-form-item v-if="configForm.delayType === DelayTypeEnum.FIXED_DATE_TIME" prop="dateTime">
+ <el-date-picker
+ class="mr-2"
+ v-model="configForm.dateTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨鏃ユ湡鍜屾椂闂�"
+ value-format="YYYY-MM-DDTHH:mm:ss"
+ />
+ <el-text>鍚庤繘鍏ヤ笅涓�鑺傜偣</el-text>
+ </el-form-item>
+ </el-form>
+ </div>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import {
+ SimpleFlowNode,
+ NodeType,
+ TIME_UNIT_TYPES,
+ TimeUnitType,
+ DelayTypeEnum,
+ DELAY_TYPE
+} from '../consts'
+import { useWatchNode, useDrawer, useNodeName } from '../node'
+import { convertTimeUnit } from '../utils'
+defineOptions({
+ name: 'DelayTimerNodeConfig'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 褰撳墠鑺傜偣
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.DELAY_TIMER_NODE)
+// 鎶勯�佷汉琛ㄥ崟閰嶇疆
+const formRef = ref() // 琛ㄥ崟 Ref
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ delayType: [{ required: true, message: '寤惰繜鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }],
+ timeDuration: [{ required: true, message: '寤惰繜鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }],
+ dateTime: [{ required: true, message: '寤惰繜鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }]
+})
+// 閰嶇疆琛ㄥ崟鏁版嵁
+const configForm = ref({
+ delayType: DelayTypeEnum.FIXED_TIME_DURATION,
+ timeDuration: 1,
+ timeUnit: TimeUnitType.HOUR,
+ dateTime: ''
+})
+// 淇濆瓨閰嶇疆
+const saveConfig = async () => {
+ if (!formRef) return false
+ const valid = await formRef.value.validate()
+ if (!valid) return false
+ const showText = getShowText()
+ if (!showText) return false
+ currentNode.value.name = nodeName.value!
+ currentNode.value.showText = showText
+ if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
+ currentNode.value.delaySetting = {
+ delayType: configForm.value.delayType,
+ delayTime: getIsoTimeDuration()
+ }
+ }
+ if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
+ currentNode.value.delaySetting = {
+ delayType: configForm.value.delayType,
+ delayTime: configForm.value.dateTime
+ }
+ }
+ settingVisible.value = false
+ return true
+}
+const getShowText = (): string => {
+ let showText = ''
+ if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
+ showText = `寤惰繜${configForm.value.timeDuration}${TIME_UNIT_TYPES.find((item) => item.value === configForm.value.timeUnit).label}`
+ }
+ if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
+ showText = `寤惰繜鑷�${configForm.value.dateTime.replace('T', ' ')}`
+ }
+ return showText
+}
+const getIsoTimeDuration = () => {
+ let strTimeDuration = 'PT'
+ if (configForm.value.timeUnit === TimeUnitType.MINUTE) {
+ strTimeDuration += configForm.value.timeDuration + 'M'
+ }
+ if (configForm.value.timeUnit === TimeUnitType.HOUR) {
+ strTimeDuration += configForm.value.timeDuration + 'H'
+ }
+ if (configForm.value.timeUnit === TimeUnitType.DAY) {
+ strTimeDuration += configForm.value.timeDuration + 'D'
+ }
+ return strTimeDuration
+}
+// 鏄剧ず寤惰繜鍣ㄨ妭鐐归厤缃紝 鐢辩埗缁勪欢浼犺繃鏉�
+const showDelayTimerNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ if (node.delaySetting) {
+ configForm.value.delayType = node.delaySetting.delayType
+ // 鍥哄畾鏃堕暱
+ if (configForm.value.delayType === DelayTypeEnum.FIXED_TIME_DURATION) {
+ const strTimeDuration = node.delaySetting.delayTime
+ let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+ let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+ configForm.value.timeDuration = parseInt(parseTime)
+ configForm.value.timeUnit = convertTimeUnit(parseTimeUnit)
+ }
+ // 鍥哄畾鏃ユ湡鏃堕棿
+ if (configForm.value.delayType === DelayTypeEnum.FIXED_DATE_TIME) {
+ configForm.value.dateTime = node.delaySetting.delayTime
+ }
+ }
+}
+
+defineExpose({ openDrawer, showDelayTimerNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue
new file mode 100644
index 0000000..4cf6a84
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/RouterNodeConfig.vue
@@ -0,0 +1,201 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="630"
+ :before-close="saveConfig"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <div>
+ <el-form label-position="top">
+ <el-card class="mb-15px" v-for="(item, index) in routerGroups" :key="index">
+ <template #header>
+ <div class="flex flex-items-center">
+ <el-text size="large">璺敱{{ index + 1 }}</el-text>
+ <el-select class="ml-15px" v-model="item.nodeId" style="width: 180px">
+ <el-option
+ v-for="node in nodeOptions"
+ :key="node.value"
+ :label="node.label"
+ :value="node.value"
+ />
+ </el-select>
+ <el-button class="mla" type="danger" link @click="deleteRouterGroup(index)">
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </template>
+ <Condition
+ :ref="($event) => (conditionRef[index] = $event)"
+ v-model="routerGroups[index]"
+ />
+ </el-card>
+ </el-form>
+
+ <el-button class="w-1/1" type="primary" :icon="Plus" @click="addRouterGroup">
+ 鏂板璺敱鍒嗘敮
+ </el-button>
+ </div>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import { Plus } from '@element-plus/icons-vue'
+import { SimpleFlowNode, NodeType, ConditionType, RouterSetting } from '../consts'
+import { useWatchNode, useDrawer, useNodeName } from '../node'
+import Condition from './components/Condition.vue'
+
+defineOptions({
+ name: 'RouterNodeConfig'
+})
+const message = useMessage() // 娑堟伅寮圭獥
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree')
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 褰撳墠鑺傜偣
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.ROUTER_BRANCH_NODE)
+const routerGroups = ref<RouterSetting[]>([])
+const nodeOptions = ref<any>([])
+const conditionRef = ref([])
+
+/** 淇濆瓨閰嶇疆 */
+const saveConfig = async () => {
+ // 鏍¢獙琛ㄥ崟
+ let valid = true
+ for (const item of conditionRef.value) {
+ if (item && !(await item.validate())) {
+ valid = false
+ }
+ }
+ if (!valid) return false
+ const showText = getShowText()
+ if (!showText) return false
+ currentNode.value.name = nodeName.value!
+ currentNode.value.showText = showText
+ currentNode.value.routerGroups = routerGroups.value
+ settingVisible.value = false
+ return true
+}
+// 鏄剧ず璺敱鍒嗘敮鑺傜偣閰嶇疆锛� 鐢辩埗缁勪欢浼犺繃鏉�
+const showRouteNodeConfig = (node: SimpleFlowNode) => {
+ getRouterNode(processNodeTree?.value)
+ routerGroups.value = []
+ nodeName.value = node.name
+ if (node.routerGroups) {
+ routerGroups.value = node.routerGroups
+ }
+}
+
+const getShowText = () => {
+ if (!routerGroups.value || !Array.isArray(routerGroups.value) || routerGroups.value.length <= 0) {
+ message.warning('璇烽厤缃矾鐢憋紒')
+ return ''
+ }
+ for (const route of routerGroups.value) {
+ if (!route.nodeId || !route.conditionType) {
+ message.warning('璇峰畬鍠勮矾鐢遍厤缃」锛�')
+ return ''
+ }
+ if (route.conditionType === ConditionType.EXPRESSION && !route.conditionExpression) {
+ message.warning('璇峰畬鍠勮矾鐢遍厤缃」锛�')
+ return ''
+ }
+ if (route.conditionType === ConditionType.RULE) {
+ for (const condition of route.conditionGroups.conditions) {
+ for (const rule of condition.rules) {
+ if (!rule.leftSide || !rule.rightSide) {
+ message.warning('璇峰畬鍠勮矾鐢遍厤缃」锛�')
+ return ''
+ }
+ }
+ }
+ }
+ }
+ return `${routerGroups.value.length}鏉¤矾鐢卞垎鏀痐
+}
+
+const addRouterGroup = () => {
+ routerGroups.value.push({
+ nodeId: '',
+ conditionType: ConditionType.RULE,
+ conditionExpression: '',
+ conditionGroups: {
+ and: true,
+ conditions: [
+ {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ ]
+ }
+ ]
+ }
+ })
+}
+
+const deleteRouterGroup = (index: number) => {
+ routerGroups.value.splice(index, 1)
+}
+
+// 閫掑綊鑾峰彇鎵�鏈夎妭鐐�
+const getRouterNode = (node) => {
+ // TODO 鏈�濂借繕闇�瑕佹弧瓒充互涓嬭姹�
+ // 骞惰鍒嗘敮銆佸寘瀹瑰垎鏀唴閮ㄨ妭鐐逛笉鑳借烦杞埌澶栭儴鑺傜偣
+ // 鏉′欢鍒嗘敮鑺傜偣鍙互鍚戜笂璺宠浆鍒板閮ㄨ妭鐐�
+ while (true) {
+ if (!node) break
+ if (node.type !== NodeType.ROUTER_BRANCH_NODE && node.type !== NodeType.CONDITION_NODE) {
+ nodeOptions.value.push({
+ label: node.name,
+ value: node.id
+ })
+ }
+ if (!node.childNode || node.type === NodeType.END_EVENT_NODE) {
+ break
+ }
+ if (node.conditionNodes && node.conditionNodes.length) {
+ node.conditionNodes.forEach((item) => {
+ getRouterNode(item)
+ })
+ }
+ node = node.childNode
+ }
+}
+
+defineExpose({ openDrawer, showRouteNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+</script>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
new file mode 100644
index 0000000..9975d9b
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue
@@ -0,0 +1,224 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="550"
+ :before-close="saveConfig"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <el-tabs type="border-card" v-model="activeTabName">
+ <el-tab-pane label="鏉冮檺" name="user">
+ <el-text
+ v-if="
+ (!startUserIds || startUserIds.length === 0) &&
+ (!startDeptIds || startDeptIds.length === 0)
+ "
+ >
+ 鍏ㄩ儴鎴愬憳鍙互鍙戣捣娴佺▼
+ </el-text>
+ <div v-else-if="startUserIds && startUserIds.length > 0">
+ <el-text v-if="startUserIds.length == 1">
+ {{ getUserNicknames(startUserIds) }} 鍙彂璧锋祦绋�
+ </el-text>
+ <el-text v-else>
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="getUserNicknames(startUserIds)"
+ >
+ {{ getUserNicknames(startUserIds.slice(0, 2)) }} 绛�
+ {{ startUserIds.length }} 浜哄彲鍙戣捣娴佺▼
+ </el-tooltip>
+ </el-text>
+ </div>
+ <div v-else-if="startDeptIds && startDeptIds.length > 0">
+ <el-text v-if="startDeptIds.length == 1">
+ {{ getDeptNames(startDeptIds) }} 鍙彂璧锋祦绋�
+ </el-text>
+ <el-text v-else>
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="getDeptNames(startDeptIds)"
+ >
+ {{ getDeptNames(startDeptIds.slice(0, 2)) }} 绛�
+ {{ startDeptIds.length }} 涓儴闂ㄥ彲鍙戣捣娴佺▼
+ </el-tooltip>
+ </el-text>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="琛ㄥ崟瀛楁鏉冮檺" name="fields" v-if="formType === 10">
+ <div class="field-setting-pane">
+ <div class="field-setting-desc">瀛楁鏉冮檺</div>
+ <div class="field-permit-title">
+ <div class="setting-title-label first-title"> 瀛楁鍚嶇О </div>
+ <div class="other-titles">
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+ 鍙
+ </span>
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+ 鍙紪杈�
+ </span>
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+ 闅愯棌
+ </span>
+ </div>
+ </div>
+ <div
+ class="field-setting-item"
+ v-for="(item, index) in fieldsPermissionConfig"
+ :key="index"
+ >
+ <div class="field-setting-item-label"> {{ item.title }} </div>
+ <el-radio-group class="field-setting-item-group" v-model="item.permission">
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.READ"
+ size="large"
+ :label="FieldPermissionType.READ"
+ ><span></span
+ ></el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.WRITE"
+ size="large"
+ :label="FieldPermissionType.WRITE"
+ ><span></span
+ ></el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.NONE"
+ size="large"
+ :label="FieldPermissionType.NONE"
+ ><span></span
+ ></el-radio>
+ </div>
+ </el-radio-group>
+ </div>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
+import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
+defineOptions({
+ name: 'StartUserNodeConfig'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 鍙彂璧锋祦绋嬬殑鐢ㄦ埛缂栧彿
+const startUserIds = inject<Ref<any[]>>('startUserIds')
+// 鍙彂璧锋祦绋嬬殑閮ㄩ棬缂栧彿
+const startDeptIds = inject<Ref<any[]>>('startDeptIds')
+// 鐢ㄦ埛鍒楄〃
+const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
+// 閮ㄩ棬鍒楄〃
+const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList')
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 褰撳墠鑺傜偣
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.COPY_TASK_NODE)
+// 婵�娲荤殑 Tab 鏍囩椤�
+const activeTabName = ref('user')
+// 琛ㄥ崟瀛楁鏉冮檺閰嶇疆
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+ FieldPermissionType.WRITE
+)
+const getUserNicknames = (userIds: number[]): string => {
+ if (!userIds || userIds.length === 0) {
+ return ''
+ }
+ const nicknames: string[] = []
+ userIds.forEach((userId) => {
+ const found = userOptions?.value.find((item) => item.id === userId)
+ if (found && found.nickname) {
+ nicknames.push(found.nickname)
+ }
+ })
+ return nicknames.join(',')
+}
+const getDeptNames = (deptIds: number[]): string => {
+ if (!deptIds || deptIds.length === 0) {
+ return ''
+ }
+ const deptNames: string[] = []
+ deptIds.forEach((deptId) => {
+ const found = deptOptions?.value.find((item) => item.id === deptId)
+ if (found && found.name) {
+ deptNames.push(found.name)
+ }
+ })
+ return deptNames.join(',')
+}
+// 淇濆瓨閰嶇疆
+const saveConfig = async () => {
+ activeTabName.value = 'user'
+ currentNode.value.name = nodeName.value!
+ currentNode.value.showText = '宸茶缃�'
+ // 璁剧疆琛ㄥ崟鏉冮檺
+ currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+ // 璁剧疆鍙戣捣浜虹殑鎸夐挳鏉冮檺
+ currentNode.value.buttonsSetting = START_USER_BUTTON_SETTING
+ settingVisible.value = false
+ return true
+}
+// 鏄剧ず鍙戣捣浜鸿妭鐐归厤缃紝 鐢辩埗缁勪欢浼犺繃鏉�
+const showStartUserNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ // 琛ㄥ崟瀛楁鏉冮檺
+ getNodeConfigFormFields(node.fieldsPermission)
+}
+
+/** 鎵归噺鏇存柊鏉冮檺 */
+const updatePermission = (type: string) => {
+ fieldsPermissionConfig.value.forEach((field) => {
+ field.permission =
+ type === 'READ'
+ ? FieldPermissionType.READ
+ : type === 'WRITE'
+ ? FieldPermissionType.WRITE
+ : FieldPermissionType.NONE
+ })
+}
+defineExpose({ openDrawer, showStartUserNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
new file mode 100644
index 0000000..c4fa63c
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue
@@ -0,0 +1,532 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="630"
+ :before-close="saveConfig"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }} <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <div>
+ <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+ <el-form-item label="瑙﹀彂鍣ㄧ被鍨�" prop="type">
+ <el-select v-model="configForm.type" @change="changeTriggerType">
+ <el-option
+ v-for="(item, index) in TRIGGER_TYPES"
+ :key="index"
+ :value="item.value"
+ :label="item.label"
+ />
+ </el-select>
+ </el-form-item>
+ <!-- HTTP 璇锋眰瑙﹀彂鍣� -->
+ <div
+ v-if="
+ [TriggerTypeEnum.HTTP_REQUEST, TriggerTypeEnum.HTTP_CALLBACK].includes(
+ configForm.type
+ ) && configForm.httpRequestSetting
+ "
+ >
+ <HttpRequestSetting
+ v-model:setting="configForm.httpRequestSetting"
+ :responseEnable="configForm.type === TriggerTypeEnum.HTTP_REQUEST"
+ :formItemPrefix="'httpRequestSetting'"
+ />
+ </div>
+
+ <!-- 琛ㄥ崟鏁版嵁淇敼瑙﹀彂鍣� -->
+ <div v-if="configForm.type === TriggerTypeEnum.FORM_UPDATE">
+ <div v-for="(formSetting, index) in configForm.formSettings" :key="index">
+ <el-card class="w-580px mt-4">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div>淇敼琛ㄥ崟璁剧疆 {{ index + 1 }}</div>
+ <el-button
+ type="primary"
+ plain
+ circle
+ v-if="configForm.formSettings!.length > 1"
+ @click="deleteFormSetting(index)"
+ >
+ <Icon icon="ep:close" />
+ </el-button>
+ </div>
+ </template>
+
+ <!-- 鏉′欢璁剧疆 -->
+ <ConditionDialog
+ :ref="`condition-${index}`"
+ @update-condition="(val) => handleConditionUpdate(index, val)"
+ />
+ <div class="cursor-pointer" v-if="formSetting.conditionType">
+ <el-tag
+ type="success"
+ effect="light"
+ closable
+ @close="deleteFormSettingCondition(formSetting)"
+ @click="openFormSettingCondition(index, formSetting)"
+ >
+ {{ showConditionText(formSetting) }}
+ </el-tag>
+ </div>
+ <el-button
+ v-else
+ type="primary"
+ text
+ @click="addFormSettingCondition(index, formSetting)"
+ >
+ <Icon icon="ep:link" class="mr-5px" />娣诲姞鏉′欢
+ </el-button>
+ <el-divider content-position="left">淇敼琛ㄥ崟瀛楁璁剧疆</el-divider>
+ <!-- 琛ㄥ崟瀛楁淇敼璁剧疆 -->
+ <div
+ class="flex items-center"
+ v-for="key in Object.keys(formSetting.updateFormFields || {})"
+ :key="key"
+ >
+ <div class="mr-2 flex items-center">
+ <el-form-item>
+ <el-select
+ class="w-160px!"
+ :model-value="key"
+ @update:model-value="(newKey) => updateFormFieldKey(formSetting, key, newKey)"
+ placeholder="璇烽�夋嫨琛ㄥ崟瀛楁"
+ :disabled="key !== ''"
+ >
+ <el-option
+ v-for="(field, fIdx) in optionalUpdateFormFields"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ :disabled="field.disabled"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mx-2"><el-form-item>鐨勫�艰缃负</el-form-item></div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`formSettings.${index}.updateFormFields.${key}`"
+ :rules="{
+ required: true,
+ message: '鍊间笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input
+ class="w-160px"
+ v-model="formSetting.updateFormFields![key]"
+ placeholder="璇疯緭鍏�"
+ :disabled="!key"
+ />
+ </el-form-item>
+ </div>
+ <div class="mr-1 pt-1 cursor-pointer">
+ <el-form-item>
+ <Icon
+ icon="ep:delete"
+ :size="18"
+ @click="deleteFormFieldSetting(formSetting, key)"
+ />
+ </el-form-item>
+ </div>
+ </div>
+
+ <!-- 娣诲姞琛ㄥ崟瀛楁鎸夐挳 -->
+ <el-button type="primary" text @click="addFormFieldSetting(formSetting)">
+ <Icon icon="ep:memo" class="mr-5px" />娣诲姞淇敼瀛楁
+ </el-button>
+ </el-card>
+ </div>
+
+ <!-- 娣诲姞鏂扮殑璁剧疆 -->
+ <el-button class="mt-6" type="primary" text @click="addFormSetting">
+ <Icon icon="ep:setting" class="mr-5px" />娣诲姞璁剧疆
+ </el-button>
+ </div>
+
+ <!-- 琛ㄥ崟鏁版嵁鍒犻櫎瑙﹀彂鍣� -->
+ <div v-if="configForm.type === TriggerTypeEnum.FORM_DELETE">
+ <div v-for="(formSetting, index) in configForm.formSettings" :key="index">
+ <el-card class="w-580px mt-4">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div>鍒犻櫎琛ㄥ崟璁剧疆 {{ index + 1 }}</div>
+ <el-button
+ type="primary"
+ plain
+ circle
+ v-if="configForm.formSettings!.length > 1"
+ @click="deleteFormSetting(index)"
+ >
+ <Icon icon="ep:close" />
+ </el-button>
+ </div>
+ </template>
+
+ <!-- 鏉′欢璁剧疆 -->
+ <ConditionDialog
+ :ref="`condition-${index}`"
+ @update-condition="(val) => handleConditionUpdate(index, val)"
+ />
+ <div class="cursor-pointer" v-if="formSetting.conditionType">
+ <el-tag
+ type="warning"
+ effect="light"
+ closable
+ @close="deleteFormSettingCondition(formSetting)"
+ @click="openFormSettingCondition(index, formSetting)"
+ >
+ {{ showConditionText(formSetting) }}
+ </el-tag>
+ </div>
+ <el-button
+ v-else
+ type="primary"
+ text
+ @click="addFormSettingCondition(index, formSetting)"
+ >
+ <Icon icon="ep:link" class="mr-5px" />娣诲姞鏉′欢
+ </el-button>
+
+ <el-divider content-position="left">鍒犻櫎琛ㄥ崟瀛楁璁剧疆</el-divider>
+ <!-- 琛ㄥ崟瀛楁鍒犻櫎璁剧疆 -->
+ <div class="flex flex-wrap gap-2">
+ <el-select
+ v-model="formSetting.deleteFields"
+ multiple
+ placeholder="璇烽�夋嫨瑕佸垹闄ょ殑瀛楁"
+ class="w-full"
+ >
+ <el-option
+ v-for="field in formFields"
+ :key="field.field"
+ :label="field.title"
+ :value="field.field"
+ />
+ </el-select>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 娣诲姞鏂扮殑璁剧疆 -->
+ <el-button class="mt-6" type="primary" text @click="addFormSetting">
+ <Icon icon="ep:setting" class="mr-5px" />娣诲姞璁剧疆
+ </el-button>
+ </div>
+ </el-form>
+ </div>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="cancelConfig">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+<script setup lang="ts">
+import {
+ SimpleFlowNode,
+ NodeType,
+ TriggerSetting,
+ TRIGGER_TYPES,
+ TriggerTypeEnum,
+ FormTriggerSetting,
+ DEFAULT_CONDITION_GROUP_VALUE
+} from '../consts'
+import { useWatchNode, useDrawer, useNodeName, useFormFields, getConditionShowText } from '../node'
+import HttpRequestSetting from './components/HttpRequestSetting.vue'
+import ConditionDialog from './components/ConditionDialog.vue'
+import { cloneDeep } from 'lodash-es'
+const { proxy } = getCurrentInstance() as any
+
+defineOptions({
+ name: 'TriggerNodeConfig'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+const message = useMessage() // 娑堟伅寮圭獥
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 褰撳墠鑺傜偣
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.TRIGGER_NODE)
+// 瑙﹀彂鍣ㄨ〃鍗曢厤缃�
+const formRef = ref() // 琛ㄥ崟 Ref
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ type: [{ required: true, message: '瑙﹀彂鍣ㄧ被鍨嬩笉鑳戒负绌�', trigger: 'change' }],
+ 'httpRequestSetting.url': [{ required: true, message: '璇锋眰鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+// 瑙﹀彂鍣ㄩ厤缃〃鍗曟暟鎹�
+const configForm = ref<TriggerSetting>({
+ type: TriggerTypeEnum.HTTP_REQUEST,
+ httpRequestSetting: {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ },
+ formSettings: [
+ {
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
+ updateFormFields: {},
+ deleteFields: []
+ }
+ ]
+})
+// 娴佺▼琛ㄥ崟瀛楁
+const formFields = useFormFields()
+
+// 鍙�夌殑淇敼鐨勮〃鍗曞瓧娈�
+const optionalUpdateFormFields = computed(() => {
+ return formFields.map((field) => ({
+ title: field.title,
+ field: field.field,
+ disabled: false
+ }))
+})
+
+let originalSetting: TriggerSetting | undefined
+
+/** 瑙﹀彂鍣ㄧ被鍨嬫敼鍙樹簡 */
+const changeTriggerType = () => {
+ if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
+ configForm.value.httpRequestSetting =
+ originalSetting?.type === TriggerTypeEnum.HTTP_REQUEST && originalSetting.httpRequestSetting
+ ? originalSetting.httpRequestSetting
+ : {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ }
+ configForm.value.formSettings = undefined
+ return
+ }
+
+ if (configForm.value.type === TriggerTypeEnum.HTTP_CALLBACK) {
+ configForm.value.httpRequestSetting =
+ originalSetting?.type === TriggerTypeEnum.HTTP_CALLBACK && originalSetting.httpRequestSetting
+ ? originalSetting.httpRequestSetting
+ : {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ }
+ configForm.value.formSettings = undefined
+ return
+ }
+
+ if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
+ configForm.value.formSettings =
+ originalSetting?.type === TriggerTypeEnum.FORM_UPDATE && originalSetting.formSettings
+ ? originalSetting.formSettings
+ : [
+ {
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
+ updateFormFields: {},
+ deleteFields: []
+ }
+ ]
+ configForm.value.httpRequestSetting = undefined
+ return
+ }
+
+ if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
+ configForm.value.formSettings =
+ originalSetting?.type === TriggerTypeEnum.FORM_DELETE && originalSetting.formSettings
+ ? originalSetting.formSettings
+ : [
+ {
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
+ updateFormFields: undefined,
+ deleteFields: []
+ }
+ ]
+ configForm.value.httpRequestSetting = undefined
+ return
+ }
+}
+
+/** 娣诲姞鏂扮殑淇敼琛ㄥ崟璁剧疆 */
+const addFormSetting = () => {
+ configForm.value.formSettings!.push({
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
+ updateFormFields: {},
+ deleteFields: []
+ })
+}
+
+/** 鍒犻櫎淇敼琛ㄥ崟璁剧疆 */
+const deleteFormSetting = (index: number) => {
+ configForm.value.formSettings!.splice(index, 1)
+}
+
+/** 娣诲姞鏉′欢閰嶇疆 */
+const addFormSettingCondition = (index: number, formSetting: FormTriggerSetting) => {
+ const conditionDialog = proxy.$refs[`condition-${index}`][0]
+ conditionDialog.open(formSetting)
+}
+/** 鍒犻櫎鏉′欢閰嶇疆 */
+const deleteFormSettingCondition = (formSetting: FormTriggerSetting) => {
+ formSetting.conditionType = undefined
+}
+/** 鎵撳紑鏉′欢閰嶇疆寮圭獥 */
+const openFormSettingCondition = (index: number, formSetting: FormTriggerSetting) => {
+ const conditionDialog = proxy.$refs[`condition-${index}`][0]
+ conditionDialog.open(formSetting)
+}
+/** 澶勭悊鏉′欢閰嶇疆淇濆瓨 */
+const handleConditionUpdate = (index: number, condition: any) => {
+ configForm.value.formSettings![index].conditionType = condition.conditionType
+ configForm.value.formSettings![index].conditionExpression = condition.conditionExpression
+ configForm.value.formSettings![index].conditionGroups = condition.conditionGroups
+}
+/** 鏉′欢閰嶇疆灞曠ず */
+const showConditionText = (formSetting: FormTriggerSetting) => {
+ return getConditionShowText(
+ formSetting.conditionType,
+ formSetting.conditionExpression,
+ formSetting.conditionGroups,
+ formFields
+ )
+}
+
+/** 娣诲姞淇敼瀛楁璁剧疆椤� */
+const addFormFieldSetting = (formSetting: FormTriggerSetting) => {
+ if (!formSetting) return
+ if (!formSetting.updateFormFields) {
+ formSetting.updateFormFields = {}
+ }
+ formSetting.updateFormFields[''] = undefined
+}
+/** 鏇存柊瀛楁 KEY */
+const updateFormFieldKey = (formSetting: FormTriggerSetting, oldKey: string, newKey: string) => {
+ if (!formSetting?.updateFormFields) return
+ const value = formSetting.updateFormFields[oldKey]
+ delete formSetting.updateFormFields[oldKey]
+ formSetting.updateFormFields[newKey] = value
+}
+
+/** 鍒犻櫎淇敼瀛楁璁剧疆椤� */
+const deleteFormFieldSetting = (formSetting: FormTriggerSetting, key: string) => {
+ if (!formSetting?.updateFormFields) return
+ delete formSetting.updateFormFields[key]
+}
+
+/** 淇濆瓨閰嶇疆 */
+const saveConfig = async () => {
+ if (!formRef) return false
+ const valid = await formRef.value.validate()
+ if (!valid) return false
+ const showText = getShowText()
+ if (!showText) return false
+ currentNode.value.name = nodeName.value!
+ currentNode.value.showText = showText
+ if (configForm.value.type === TriggerTypeEnum.HTTP_REQUEST) {
+ configForm.value.formSettings = undefined
+ } else if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
+ configForm.value.httpRequestSetting = undefined
+ // 娓呯悊鍒犻櫎瀛楁鐩稿叧鐨勬暟鎹�
+ configForm.value.formSettings?.forEach((setting) => {
+ setting.deleteFields = undefined
+ })
+ } else if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
+ configForm.value.httpRequestSetting = undefined
+ // 娓呯悊淇敼瀛楁鐩稿叧鐨勬暟鎹�
+ configForm.value.formSettings?.forEach((setting) => {
+ setting.updateFormFields = undefined
+ })
+ }
+ currentNode.value.triggerSetting = configForm.value
+ settingVisible.value = false
+ return true
+}
+
+/** 鍙栨秷閰嶇疆 */
+const cancelConfig = () => {
+ // 鎭㈠鍘熸潵鐨勯厤缃�
+ currentNode.value.triggerSetting = originalSetting
+ closeDrawer()
+}
+
+/** 鑾峰彇鑺傜偣灞曠ず鍐呭 */
+const getShowText = (): string => {
+ let showText = ''
+ if (
+ configForm.value.type === TriggerTypeEnum.HTTP_REQUEST ||
+ configForm.value.type === TriggerTypeEnum.HTTP_CALLBACK
+ ) {
+ showText = `${configForm.value.httpRequestSetting?.url}`
+ } else if (configForm.value.type === TriggerTypeEnum.FORM_UPDATE) {
+ for (const [index, setting] of configForm.value.formSettings!.entries()) {
+ if (!setting.updateFormFields || Object.keys(setting.updateFormFields).length === 0) {
+ message.warning(`璇锋坊鍔犺〃鍗曡缃�${index + 1}鐨勪慨鏀瑰瓧娈礰)
+ return ''
+ }
+ }
+ showText = '淇敼琛ㄥ崟鏁版嵁'
+ } else if (configForm.value.type === TriggerTypeEnum.FORM_DELETE) {
+ for (const [index, setting] of configForm.value.formSettings!.entries()) {
+ if (!setting.deleteFields || setting.deleteFields.length === 0) {
+ message.warning(`璇烽�夋嫨琛ㄥ崟璁剧疆${index + 1}瑕佸垹闄ょ殑瀛楁`)
+ return ''
+ }
+ }
+ showText = '鍒犻櫎琛ㄥ崟鏁版嵁'
+ }
+ return showText
+}
+
+/** 鏄剧ず瑙﹀彂鍣ㄨ妭鐐归厤缃紝 鐢辩埗缁勪欢浼犺繃鏉� */
+const showTriggerNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ originalSetting = cloneDeep(node.triggerSetting)
+ if (node.triggerSetting) {
+ configForm.value = {
+ type: node.triggerSetting.type,
+ httpRequestSetting: node.triggerSetting.httpRequestSetting || {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ },
+ formSettings: node.triggerSetting.formSettings || [
+ {
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
+ updateFormFields: {},
+ deleteFields: []
+ }
+ ]
+ }
+ }
+}
+
+defineExpose({ openDrawer, showTriggerNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
new file mode 100644
index 0000000..53058c4
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
@@ -0,0 +1,1068 @@
+<template>
+ <el-drawer
+ :append-to-body="true"
+ v-model="settingVisible"
+ :show-close="false"
+ :size="580"
+ :before-close="saveConfig"
+ class="justify-start"
+ >
+ <template #header>
+ <div class="config-header">
+ <input
+ v-if="showInput"
+ type="text"
+ class="config-editable-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="nodeName"
+ :placeholder="nodeName"
+ />
+ <div v-else class="node-name">
+ {{ nodeName }}
+ <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+ </div>
+ <div class="divide-line"></div>
+ </div>
+ </template>
+ <div v-if="currentNode.type === NodeType.USER_TASK_NODE" class="flex flex-items-center mb-3">
+ <span class="font-size-16px mr-3">瀹℃壒绫诲瀷 :</span>
+ <el-radio-group v-model="approveType">
+ <el-radio
+ v-for="(item, index) in APPROVE_TYPE"
+ :key="index"
+ :value="item.value"
+ :label="item.value"
+ >
+ {{ item.label }}
+ </el-radio>
+ </el-radio-group>
+ </div>
+ <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
+ <el-tab-pane :label="`${nodeTypeName}浜篳" name="user">
+ <div>
+ <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+ <el-form-item :label="`${nodeTypeName}浜鸿缃甡" prop="candidateStrategy">
+ <el-radio-group
+ v-model="configForm.candidateStrategy"
+ @change="changeCandidateStrategy"
+ >
+ <el-row>
+ <el-col v-for="(dict, index) in CANDIDATE_STRATEGY" :key="index" :span="8">
+ <el-radio :value="dict.value" :label="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ </el-col>
+ </el-row>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
+ label="鎸囧畾瑙掕壊"
+ prop="roleIds"
+ >
+ <el-select
+ filterable
+ v-model="configForm.roleIds"
+ clearable
+ multiple
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in roleOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="
+ configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+ configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+ "
+ label="鎸囧畾閮ㄩ棬"
+ prop="deptIds"
+ span="24"
+ >
+ <el-tree-select
+ ref="treeRef"
+ v-model="configForm.deptIds"
+ :data="deptTreeOptions"
+ :props="defaultProps"
+ empty-text="鍔犺浇涓紝璇风◢鍚�"
+ multiple
+ node-key="id"
+ :check-strictly="true"
+ style="width: 100%"
+ show-checkbox
+ />
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy == CandidateStrategy.POST"
+ label="鎸囧畾宀椾綅"
+ prop="postIds"
+ span="24"
+ >
+ <el-select
+ filterable
+ v-model="configForm.postIds"
+ clearable
+ multiple
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in postOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy == CandidateStrategy.USER"
+ label="鎸囧畾鐢ㄦ埛"
+ prop="userIds"
+ span="24"
+ >
+ <el-select
+ filterable
+ v-model="configForm.userIds"
+ clearable
+ multiple
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+ label="鎸囧畾鐢ㄦ埛缁�"
+ prop="userGroups"
+ >
+ <el-select
+ filterable
+ v-model="configForm.userGroups"
+ clearable
+ multiple
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userGroupOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
+ label="琛ㄥ崟鍐呯敤鎴峰瓧娈�"
+ prop="formUser"
+ >
+ <el-select filterable v-model="configForm.formUser" clearable style="width: 100%">
+ <el-option
+ v-for="(item, idx) in userFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled="!item.required"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+ label="琛ㄥ崟鍐呴儴闂ㄥ瓧娈�"
+ prop="formDept"
+ >
+ <el-select filterable v-model="configForm.formDept" clearable style="width: 100%">
+ <el-option
+ v-for="(item, idx) in deptFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled="!item.required"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="
+ configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ configForm.candidateStrategy ==
+ CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+ configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+ "
+ :label="deptLevelLabel!"
+ prop="deptLevel"
+ span="24"
+ >
+ <el-select filterable v-model="configForm.deptLevel" clearable>
+ <el-option
+ v-for="(item, index) in MULTI_LEVEL_DEPT"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <!-- TODO @jason锛氬悗缁鏀寔閫夋嫨宸茬粡瀛樺ソ鐨勮〃杈惧紡 -->
+ <el-form-item
+ v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+ label="娴佺▼琛ㄨ揪寮�"
+ prop="expression"
+ >
+ <el-input
+ type="textarea"
+ v-model="configForm.expression"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item :label="`澶氫汉${nodeTypeName}鏂瑰紡`" prop="approveMethod">
+ <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
+ <div class="flex-col">
+ <div
+ v-for="(item, index) in APPROVE_METHODS"
+ :key="index"
+ class="flex items-center"
+ >
+ <el-radio :value="item.value" :label="item.value">
+ {{ item.label }}
+ </el-radio>
+ <el-form-item prop="approveRatio">
+ <el-input-number
+ v-model="configForm.approveRatio"
+ :min="10"
+ :max="100"
+ :step="10"
+ size="small"
+ v-if="
+ item.value === ApproveMethodType.APPROVE_BY_RATIO &&
+ configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
+ "
+ />
+ </el-form-item>
+ </div>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+
+ <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+ <el-divider content-position="left">瀹℃壒浜烘嫆缁濇椂</el-divider>
+ <el-form-item prop="rejectHandlerType">
+ <el-radio-group v-model="configForm.rejectHandlerType">
+ <div class="flex-col">
+ <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </div>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+ label="椹冲洖鑺傜偣"
+ prop="returnNodeId"
+ >
+ <el-select
+ filterable
+ v-model="configForm.returnNodeId"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in returnTaskList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+
+ <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+ <el-divider content-position="left">瀹℃壒浜鸿秴鏃舵湭澶勭悊鏃�</el-divider>
+ <el-form-item label="鍚敤寮�鍏�" prop="timeoutHandlerEnable">
+ <el-switch
+ v-model="configForm.timeoutHandlerEnable"
+ active-text="寮�鍚�"
+ inactive-text="鍏抽棴"
+ @change="timeoutHandlerChange"
+ />
+ </el-form-item>
+ <el-form-item
+ label="鎵ц鍔ㄤ綔"
+ prop="timeoutHandlerType"
+ v-if="configForm.timeoutHandlerEnable"
+ >
+ <el-radio-group
+ v-model="configForm.timeoutHandlerType"
+ @change="timeoutHandlerTypeChanged"
+ >
+ <el-radio-button
+ v-for="item in TIMEOUT_HANDLER_TYPES"
+ :key="item.value"
+ :value="item.value"
+ :label="item.label"
+ />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="瓒呮椂鏃堕棿璁剧疆" v-if="configForm.timeoutHandlerEnable">
+ <span class="mr-2">褰撹秴杩�</span>
+ <el-form-item prop="timeDuration">
+ <el-input-number
+ class="mr-2"
+ :style="{ width: '100px' }"
+ v-model="configForm.timeDuration"
+ :min="1"
+ controls-position="right"
+ />
+ </el-form-item>
+ <el-select
+ filterable
+ v-model="timeUnit"
+ class="mr-2"
+ :style="{ width: '100px' }"
+ @change="timeUnitChange"
+ >
+ <el-option
+ v-for="item in TIME_UNIT_TYPES"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ 鏈鐞�
+ </el-form-item>
+ <el-form-item
+ label="鏈�澶ф彁閱掓鏁�"
+ prop="maxRemindCount"
+ v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
+ >
+ <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
+ </el-form-item>
+ </div>
+
+ <el-divider content-position="left">{{ nodeTypeName }}浜轰负绌烘椂</el-divider>
+ <el-form-item prop="assignEmptyHandlerType">
+ <el-radio-group v-model="configForm.assignEmptyHandlerType">
+ <div class="flex-col">
+ <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </div>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+ label="鎸囧畾鐢ㄦ埛"
+ prop="assignEmptyHandlerUserIds"
+ span="24"
+ >
+ <el-select
+ filterable
+ v-model="configForm.assignEmptyHandlerUserIds"
+ clearable
+ multiple
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+ <el-divider content-position="left">瀹℃壒浜轰笌鎻愪氦浜轰负鍚屼竴浜烘椂</el-divider>
+ <el-form-item prop="assignStartUserHandlerType">
+ <el-radio-group v-model="configForm.assignStartUserHandlerType">
+ <div class="flex-col">
+ <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </div>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+ </div>
+
+ <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+ <el-divider content-position="left">鏄惁闇�瑕佺鍚�</el-divider>
+ <el-form-item prop="signEnable">
+ <el-switch v-model="configForm.signEnable" active-text="鏄�" inactive-text="鍚�" />
+ </el-form-item>
+ </div>
+
+ <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+ <el-divider content-position="left">瀹℃壒鎰忚</el-divider>
+ <el-form-item prop="reasonRequire">
+ <el-switch
+ v-model="configForm.reasonRequire"
+ active-text="蹇呭~"
+ inactive-text="闈炲繀濉�"
+ />
+ </el-form-item>
+ </div>
+ <div>
+ <el-divider content-position="left">璺宠繃琛ㄨ揪寮�</el-divider>
+ <el-form-item prop="skipExpression">
+ <el-input v-model="configForm.skipExpression" type="textarea" />
+ </el-form-item>
+ </div>
+ </el-form>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane
+ label="鎿嶄綔鎸夐挳璁剧疆"
+ v-if="currentNode.type === NodeType.USER_TASK_NODE"
+ name="buttons"
+ >
+ <div class="button-setting-pane">
+ <div class="button-setting-desc">鎿嶄綔鎸夐挳</div>
+ <div class="button-setting-title">
+ <div class="button-title-label">鎿嶄綔鎸夐挳</div>
+ <div class="pl-4 button-title-label">鏄剧ず鍚嶇О</div>
+ <div class="button-title-label">鍚敤</div>
+ </div>
+ <div class="button-setting-item" v-for="(item, index) in buttonsSetting" :key="index">
+ <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
+ <div class="button-setting-item-label">
+ <input
+ type="text"
+ class="editable-title-input"
+ @blur="btnDisplayNameBlurEvent(index)"
+ v-mountedFocus
+ v-model="item.displayName"
+ :placeholder="item.displayName"
+ v-if="btnDisplayNameEdit[index]"
+ />
+ <el-button v-else text @click="changeBtnDisplayName(index)"
+ >{{ item.displayName }} <Icon icon="ep:edit"
+ /></el-button>
+ </div>
+ <div class="button-setting-item-label">
+ <el-switch v-model="item.enable" />
+ </div>
+ </div>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="琛ㄥ崟瀛楁鏉冮檺" name="fields" v-if="formType === 10">
+ <div class="field-setting-pane">
+ <div class="field-setting-desc">瀛楁鏉冮檺</div>
+ <div class="field-permit-title">
+ <div class="setting-title-label first-title"> 瀛楁鍚嶇О </div>
+ <div class="other-titles">
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+ 鍙
+ </span>
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+ 鍙紪杈�
+ </span>
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+ 闅愯棌
+ </span>
+ </div>
+ </div>
+ <div
+ class="field-setting-item"
+ v-for="(item, index) in fieldsPermissionConfig"
+ :key="index"
+ >
+ <div class="field-setting-item-label"> {{ item.title }} </div>
+ <el-radio-group class="field-setting-item-group" v-model="item.permission">
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.READ"
+ size="large"
+ :label="FieldPermissionType.READ"
+ ><span></span
+ ></el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.WRITE"
+ size="large"
+ :label="FieldPermissionType.WRITE"
+ ><span></span
+ ></el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.NONE"
+ size="large"
+ :label="FieldPermissionType.NONE"
+ ><span></span
+ ></el-radio>
+ </div>
+ </el-radio-group>
+ </div>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="鐩戝惉鍣�" name="listener">
+ <UserTaskListener
+ ref="userTaskListenerRef"
+ v-model="configForm"
+ :form-field-options="formFieldOptions"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ <template #footer>
+ <el-divider />
+ <div>
+ <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+ <el-button @click="closeDrawer">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+
+<script setup lang="ts">
+import {
+ SimpleFlowNode,
+ APPROVE_TYPE,
+ ApproveType,
+ APPROVE_METHODS,
+ CandidateStrategy,
+ NodeType,
+ ApproveMethodType,
+ TimeUnitType,
+ RejectHandlerType,
+ TIMEOUT_HANDLER_TYPES,
+ TIME_UNIT_TYPES,
+ REJECT_HANDLER_TYPES,
+ DEFAULT_BUTTON_SETTING,
+ OPERATION_BUTTON_NAME,
+ ButtonSetting,
+ MULTI_LEVEL_DEPT,
+ CANDIDATE_STRATEGY,
+ ASSIGN_START_USER_HANDLER_TYPES,
+ TimeoutHandlerType,
+ ASSIGN_EMPTY_HANDLER_TYPES,
+ AssignEmptyHandlerType,
+ FieldPermissionType,
+ ProcessVariableEnum,
+ TRANSACTOR_DEFAULT_BUTTON_SETTING
+} from '../consts'
+
+import {
+ useWatchNode,
+ useNodeName,
+ useFormFieldsPermission,
+ useNodeForm,
+ UserTaskFormType,
+ useDrawer
+} from '../node'
+import { defaultProps } from '@/utils/tree'
+import { cloneDeep } from 'lodash-es'
+import { convertTimeUnit, getApproveTypeText } from '../utils'
+import UserTaskListener from './components/UserTaskListener.vue'
+defineOptions({
+ name: 'UserTaskNodeConfig'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+const emits = defineEmits<{
+ 'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
+}>()
+const deptLevelLabel = computed(() => {
+ let label = '閮ㄩ棬璐熻矗浜烘潵婧�'
+ if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+ label = label + '(鎸囧畾閮ㄩ棬鍚戜笂)'
+ } else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+ label = label + '(琛ㄥ崟鍐呴儴闂ㄥ悜涓�)'
+ } else {
+ label = label + '(鍙戣捣浜洪儴闂ㄥ悜涓�)'
+ }
+ return label
+})
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 鑺傜偣鍚嶇О閰嶇疆
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE)
+// 婵�娲荤殑 Tab 鏍囩椤�
+const activeTabName = ref('user')
+// 琛ㄥ崟瀛楁鏉冮檺璁剧疆
+const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
+ useFormFieldsPermission(FieldPermissionType.READ)
+// 琛ㄥ崟鍐呯敤鎴峰瓧娈甸�夐」, 蹇呴』鏄繀濉拰鐢ㄦ埛閫夋嫨鍣�
+const userFieldOnFormOptions = computed(() => {
+ // 鍥哄畾娣诲姞鍙戣捣浜� ID 瀛楁
+ formFieldOptions.unshift({
+ field: ProcessVariableEnum.START_USER_ID,
+ title: '鍙戣捣浜�',
+ type: 'UserSelect',
+ required: true
+ })
+ return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 琛ㄥ崟鍐呴儴闂ㄥ瓧娈甸�夐」, 蹇呴』鏄繀濉拰閮ㄩ棬閫夋嫨鍣�
+const deptFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
+// 鎿嶄綔鎸夐挳璁剧疆
+const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
+ useButtonsSetting()
+const approveType = ref(ApproveType.USER)
+// 瀹℃壒浜鸿〃鍗曡缃�
+const formRef = ref() // 琛ㄥ崟 Ref
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ candidateStrategy: [{ required: true, message: '瀹℃壒浜鸿缃笉鑳戒负绌�', trigger: 'change' }],
+ userIds: [{ required: true, message: '鐢ㄦ埛涓嶈兘涓虹┖', trigger: 'change' }],
+ roleIds: [{ required: true, message: '瑙掕壊涓嶈兘涓虹┖', trigger: 'change' }],
+ deptIds: [{ required: true, message: '閮ㄩ棬涓嶈兘涓虹┖', trigger: 'change' }],
+ userGroups: [{ required: true, message: '鐢ㄦ埛缁勪笉鑳戒负绌�', trigger: 'change' }],
+ formUser: [{ required: true, message: '琛ㄥ崟鍐呯敤鎴峰瓧娈典笉鑳戒负绌�', trigger: 'change' }],
+ formDept: [{ required: true, message: '琛ㄥ崟鍐呴儴闂ㄥ瓧娈典笉鑳戒负绌�', trigger: 'change' }],
+ postIds: [{ required: true, message: '宀椾綅涓嶈兘涓虹┖', trigger: 'change' }],
+ expression: [{ required: true, message: '娴佺▼琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }],
+ approveMethod: [{ required: true, message: '澶氫汉瀹℃壒鏂瑰紡涓嶈兘涓虹┖', trigger: 'change' }],
+ approveRatio: [{ required: true, message: '閫氳繃姣斾緥涓嶈兘涓虹┖', trigger: 'blur' }],
+ returnNodeId: [{ required: true, message: '椹冲洖鑺傜偣涓嶈兘涓虹┖', trigger: 'change' }],
+ timeoutHandlerEnable: [{ required: true }],
+ timeoutHandlerType: [{ required: true }],
+ timeDuration: [{ required: true, message: '瓒呮椂鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }],
+ maxRemindCount: [{ required: true, message: '鎻愰啋娆℃暟涓嶈兘涓虹┖', trigger: 'blur' }],
+ assignEmptyHandlerType: [{ required: true }],
+ assignEmptyHandlerUserIds: [{ required: true, message: '鐢ㄦ埛涓嶈兘涓虹┖', trigger: 'change' }],
+ assignStartUserHandlerType: [{ required: true }]
+})
+
+const {
+ configForm: tempConfigForm,
+ roleOptions,
+ postOptions,
+ userOptions,
+ userGroupOptions,
+ deptTreeOptions,
+ handleCandidateParam,
+ parseCandidateParam,
+ getShowText
+} = useNodeForm(currentNode.value.type)
+const configForm = tempConfigForm as Ref<UserTaskFormType>
+
+// 鏀瑰彉瀹℃壒浜鸿缃瓥鐣�
+const changeCandidateStrategy = () => {
+ configForm.value.userIds = []
+ configForm.value.deptIds = []
+ configForm.value.roleIds = []
+ configForm.value.postIds = []
+ configForm.value.userGroups = []
+ configForm.value.deptLevel = 1
+ configForm.value.formUser = ''
+ configForm.value.formDept = ''
+ configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
+}
+
+// 瀹℃壒鏂瑰紡鏀瑰彉
+const approveMethodChanged = () => {
+ configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
+ if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+ configForm.value.approveRatio = 100
+ }
+ formRef.value.clearValidate('approveRatio')
+}
+// 瀹℃壒鎷掔粷 鍙��鍥炵殑鑺傜偣
+const returnTaskList = ref<SimpleFlowNode[]>([])
+// 瀹℃壒浜鸿秴鏃舵湭澶勭悊璁剧疆
+const {
+ timeoutHandlerChange,
+ cTimeoutType,
+ timeoutHandlerTypeChanged,
+ timeUnit,
+ timeUnitChange,
+ isoTimeDuration,
+ cTimeoutMaxRemindCount
+} = useTimeoutHandler()
+
+const userTaskListenerRef = ref()
+
+/** 鑺傜偣绫诲瀷鍚嶇О */
+const nodeTypeName = computed(() => {
+ return currentNode.value.type === NodeType.TRANSACTOR_NODE ? '鍔炵悊' : '瀹℃壒'
+})
+
+/** 淇濆瓨閰嶇疆 */
+const saveConfig = async () => {
+ // activeTabName.value = 'user'
+ // 璁剧疆瀹℃壒鑺傜偣鍚嶇О
+ currentNode.value.name = nodeName.value!
+ // 璁剧疆瀹℃壒绫诲瀷
+ currentNode.value.approveType = approveType.value
+ // 濡傛灉涓嶆槸浜哄伐瀹℃壒銆傝繑鍥�
+ if (approveType.value !== ApproveType.USER) {
+ currentNode.value.showText = getApproveTypeText(approveType.value)
+ settingVisible.value = false
+ return true
+ }
+
+ if (!formRef) return false
+ if (!userTaskListenerRef) return false
+ const valid = (await formRef.value.validate()) && (await userTaskListenerRef.value.validate())
+ if (!valid) return false
+ const showText = getShowText()
+ if (!showText) return false
+
+ currentNode.value.candidateStrategy = configForm.value.candidateStrategy
+ // 澶勭悊 candidateParam 鍙傛暟
+ currentNode.value.candidateParam = handleCandidateParam()
+ // 璁剧疆瀹℃壒鏂瑰紡
+ currentNode.value.approveMethod = configForm.value.approveMethod
+ if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+ currentNode.value.approveRatio = configForm.value.approveRatio
+ }
+ // 璁剧疆鎷掔粷澶勭悊
+ currentNode.value.rejectHandler = {
+ type: configForm.value.rejectHandlerType!,
+ returnNodeId: configForm.value.returnNodeId
+ }
+ // 璁剧疆瓒呮椂澶勭悊
+ currentNode.value.timeoutHandler = {
+ enable: configForm.value.timeoutHandlerEnable!,
+ type: cTimeoutType.value,
+ timeDuration: isoTimeDuration.value,
+ maxRemindCount: cTimeoutMaxRemindCount.value
+ }
+ // 璁剧疆瀹℃壒浜轰负绌烘椂
+ currentNode.value.assignEmptyHandler = {
+ type: configForm.value.assignEmptyHandlerType!,
+ userIds:
+ configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER
+ ? configForm.value.assignEmptyHandlerUserIds
+ : undefined
+ }
+ // 璁剧疆瀹℃壒浜轰笌鍙戣捣浜虹浉鍚屾椂
+ currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType
+ // 璁剧疆琛ㄥ崟鏉冮檺
+ currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+ // 璁剧疆鎸夐挳鏉冮檺
+ currentNode.value.buttonsSetting = buttonsSetting.value
+ // 鍒涘缓浠诲姟鐩戝惉鍣�
+ currentNode.value.taskCreateListener = {
+ enable: configForm.value.taskCreateListenerEnable ?? false,
+ path: configForm.value.taskCreateListenerPath,
+ header: configForm.value.taskCreateListener?.header,
+ body: configForm.value.taskCreateListener?.body
+ }
+ // 鎸囨淳浠诲姟鐩戝惉鍣�
+ currentNode.value.taskAssignListener = {
+ enable: configForm.value.taskAssignListenerEnable ?? false,
+ path: configForm.value.taskAssignListenerPath,
+ header: configForm.value.taskAssignListener?.header,
+ body: configForm.value.taskAssignListener?.body
+ }
+ // 瀹屾垚浠诲姟鐩戝惉鍣�
+ currentNode.value.taskCompleteListener = {
+ enable: configForm.value.taskCompleteListenerEnable ?? false,
+ path: configForm.value.taskCompleteListenerPath,
+ header: configForm.value.taskCompleteListener?.header,
+ body: configForm.value.taskCompleteListener?.body
+ }
+ // 绛惧悕
+ currentNode.value.signEnable = configForm.value.signEnable
+ // 瀹℃壒鎰忚
+ currentNode.value.reasonRequire = configForm.value.reasonRequire
+ // 璺宠繃琛ㄨ揪寮�
+ currentNode.value.skipExpression = configForm.value.skipExpression
+
+ currentNode.value.showText = showText
+ settingVisible.value = false
+ return true
+}
+
+/** 鏄剧ず瀹℃壒鑺傜偣閰嶇疆锛� 鐢辩埗缁勪欢浼犺繃鏉� */
+const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
+ nodeName.value = node.name
+ // 1 瀹℃壒绫诲瀷
+ approveType.value = node.approveType ? node.approveType : ApproveType.USER
+ // 濡傛灉瀹℃壒绫诲瀷涓嶆槸浜哄伐瀹℃壒杩斿洖
+ if (approveType.value !== ApproveType.USER) {
+ return
+ }
+
+ //2.1 瀹℃壒浜鸿缃�
+ configForm.value.candidateStrategy = node.candidateStrategy!
+ // 瑙f瀽鍊欓�変汉鍙傛暟
+ parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
+ // 2.2 璁剧疆瀹℃壒鏂瑰紡
+ configForm.value.approveMethod = node.approveMethod!
+ if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
+ configForm.value.approveRatio = node.approveRatio!
+ }
+ // 2.3 璁剧疆瀹℃壒鎷掔粷澶勭悊
+ configForm.value.rejectHandlerType = node.rejectHandler?.type
+ configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
+ const matchNodeList = []
+ emits('find:returnTaskNodes', matchNodeList)
+ returnTaskList.value = matchNodeList
+ // 2.4 璁剧疆瀹℃壒瓒呮椂澶勭悊
+ configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable
+ if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
+ const strTimeDuration = node.timeoutHandler.timeDuration
+ let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+ let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+ configForm.value.timeDuration = parseInt(parseTime)
+ timeUnit.value = convertTimeUnit(parseTimeUnit)
+ }
+ configForm.value.timeoutHandlerType = node.timeoutHandler?.type
+ configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount
+ // 2.5 璁剧疆瀹℃壒浜轰负绌烘椂
+ configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type
+ configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds
+ // 2.6 璁剧疆鐢ㄦ埛浠诲姟鐨勫鎵逛汉涓庡彂璧蜂汉鐩稿悓鏃�
+ configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
+ // 3. 鎿嶄綔鎸夐挳璁剧疆
+ buttonsSetting.value =
+ cloneDeep(node.buttonsSetting) ||
+ (node.type === NodeType.TRANSACTOR_NODE
+ ? TRANSACTOR_DEFAULT_BUTTON_SETTING
+ : DEFAULT_BUTTON_SETTING)
+ // 4. 琛ㄥ崟瀛楁鏉冮檺閰嶇疆
+ getNodeConfigFormFields(node.fieldsPermission)
+ // 5. 鐩戝惉鍣�
+ // 5.1 鍒涘缓浠诲姟
+ configForm.value.taskCreateListenerEnable = node.taskCreateListener?.enable
+ configForm.value.taskCreateListenerPath = node.taskCreateListener?.path
+ configForm.value.taskCreateListener = {
+ header: node.taskCreateListener?.header ?? [],
+ body: node.taskCreateListener?.body ?? []
+ }
+ // 5.2 鎸囨淳浠诲姟
+ configForm.value.taskAssignListenerEnable = node.taskAssignListener?.enable
+ configForm.value.taskAssignListenerPath = node.taskAssignListener?.path
+ configForm.value.taskAssignListener = {
+ header: node.taskAssignListener?.header ?? [],
+ body: node.taskAssignListener?.body ?? []
+ }
+ // 5.3 瀹屾垚浠诲姟
+ configForm.value.taskCompleteListenerEnable = node.taskCompleteListener?.enable
+ configForm.value.taskCompleteListenerPath = node.taskCompleteListener?.path
+ configForm.value.taskCompleteListener = {
+ header: node.taskCompleteListener?.header ?? [],
+ body: node.taskCompleteListener?.body ?? []
+ }
+ // 6. 绛惧悕
+ configForm.value.signEnable = node?.signEnable ?? false
+ // 7. 瀹℃壒鎰忚
+ configForm.value.reasonRequire = node?.reasonRequire ?? false
+ // 8. 璺宠繃琛ㄨ揪寮�
+ configForm.value.skipExpression = node?.skipExpression ?? ''
+}
+
+defineExpose({ openDrawer, showUserTaskNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+
+/** 鎿嶄綔鎸夐挳璁剧疆 */
+function useButtonsSetting() {
+ const buttonsSetting = ref<ButtonSetting[]>()
+ // 鎿嶄綔鎸夐挳鏄剧ず鍚嶇О鍙紪杈�
+ const btnDisplayNameEdit = ref<boolean[]>([])
+ const changeBtnDisplayName = (index: number) => {
+ btnDisplayNameEdit.value[index] = true
+ }
+ const btnDisplayNameBlurEvent = (index: number) => {
+ btnDisplayNameEdit.value[index] = false
+ const buttonItem = buttonsSetting.value![index]
+ buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
+ }
+ return {
+ buttonsSetting,
+ btnDisplayNameEdit,
+ changeBtnDisplayName,
+ btnDisplayNameBlurEvent
+ }
+}
+
+/** 瀹℃壒浜鸿秴鏃舵湭澶勭悊閰嶇疆 */
+function useTimeoutHandler() {
+ // 鏃堕棿鍗曚綅
+ const timeUnit = ref(TimeUnitType.HOUR)
+
+ // 瓒呮椂寮�鍏虫敼鍙�
+ const timeoutHandlerChange = () => {
+ if (configForm.value.timeoutHandlerEnable) {
+ timeUnit.value = 2
+ configForm.value.timeDuration = 6
+ configForm.value.timeoutHandlerType = 1
+ configForm.value.maxRemindCount = 1
+ }
+ }
+ // 瓒呮椂鎵ц鐨勫姩浣�
+ const cTimeoutType = computed(() => {
+ if (!configForm.value.timeoutHandlerEnable) {
+ return undefined
+ }
+ return configForm.value.timeoutHandlerType
+ })
+
+ // 瓒呮椂澶勭悊鍔ㄤ綔鏀瑰彉
+ const timeoutHandlerTypeChanged = () => {
+ if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) {
+ configForm.value.maxRemindCount = 1 // 瓒呮椂鎻愰啋娆℃暟锛岄粯璁や负1
+ }
+ }
+
+ // 鏃堕棿鍗曚綅鏀瑰彉
+ const timeUnitChange = () => {
+ // 鍒嗛挓锛岄粯璁ゆ槸 60 鍒嗛挓
+ if (timeUnit.value === TimeUnitType.MINUTE) {
+ configForm.value.timeDuration = 60
+ }
+ // 灏忔椂锛岄粯璁ゆ槸 6 涓皬鏃�
+ if (timeUnit.value === TimeUnitType.HOUR) {
+ configForm.value.timeDuration = 6
+ }
+ // 澶╋紝 榛樿 1澶�
+ if (timeUnit.value === TimeUnitType.DAY) {
+ configForm.value.timeDuration = 1
+ }
+ }
+ // 瓒呮椂鏃堕棿鐨� ISO 琛ㄧず
+ const isoTimeDuration = computed(() => {
+ if (!configForm.value.timeoutHandlerEnable) {
+ return undefined
+ }
+ let strTimeDuration = 'PT'
+ if (timeUnit.value === TimeUnitType.MINUTE) {
+ strTimeDuration += configForm.value.timeDuration + 'M'
+ }
+ if (timeUnit.value === TimeUnitType.HOUR) {
+ strTimeDuration += configForm.value.timeDuration + 'H'
+ }
+ if (timeUnit.value === TimeUnitType.DAY) {
+ strTimeDuration += configForm.value.timeDuration + 'D'
+ }
+ return strTimeDuration
+ })
+
+ // 瓒呮椂鏈�澶ф彁閱掓鏁�
+ const cTimeoutMaxRemindCount = computed(() => {
+ if (!configForm.value.timeoutHandlerEnable) {
+ return undefined
+ }
+ if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) {
+ return undefined
+ }
+ return configForm.value.maxRemindCount
+ })
+
+ return {
+ timeoutHandlerChange,
+ cTimeoutType,
+ timeoutHandlerTypeChanged,
+ timeUnit,
+ timeUnitChange,
+ isoTimeDuration,
+ cTimeoutMaxRemindCount
+ }
+}
+
+/** 鎵归噺鏇存柊鏉冮檺 */
+const updatePermission = (type: string) => {
+ fieldsPermissionConfig.value.forEach((field) => {
+ field.permission =
+ type === 'READ'
+ ? FieldPermissionType.READ
+ : type === 'WRITE'
+ ? FieldPermissionType.WRITE
+ : FieldPermissionType.NONE
+ })
+}
+</script>
+
+<style lang="scss" scoped>
+.button-setting-pane {
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+
+ .button-setting-desc {
+ padding-right: 8px;
+ margin-bottom: 16px;
+ font-size: 16px;
+ font-weight: 700;
+ }
+
+ .button-setting-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 45px;
+ padding-left: 12px;
+ background-color: #f8fafc0a;
+ border: 1px solid #1f38581a;
+
+ & > :first-child {
+ width: 100px !important;
+ text-align: left !important;
+ }
+
+ & > :last-child {
+ text-align: center !important;
+ }
+
+ .button-title-label {
+ width: 150px;
+ font-size: 13px;
+ font-weight: 700;
+ color: #000;
+ text-align: left;
+ }
+ }
+
+ .button-setting-item {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ height: 38px;
+ padding-left: 12px;
+ border: 1px solid #1f38581a;
+ border-top: 0;
+
+ & > :first-child {
+ width: 100px !important;
+ }
+
+ & > :last-child {
+ text-align: center !important;
+ }
+
+ .button-setting-item-label {
+ width: 150px;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .editable-title-input {
+ height: 24px;
+ max-width: 130px;
+ margin-left: 4px;
+ line-height: 24px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ &:focus {
+ border-color: #40a9ff;
+ outline: 0;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
new file mode 100644
index 0000000..7ef092d
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue
@@ -0,0 +1,277 @@
+<template>
+ <el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
+ <el-form-item label="閰嶇疆鏂瑰紡" prop="conditionType">
+ <el-radio-group v-model="condition.conditionType" @change="changeConditionType">
+ <el-radio
+ v-for="(dict, indexConditionType) in conditionConfigTypes"
+ :key="indexConditionType"
+ :value="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups"
+ label="鏉′欢瑙勫垯"
+ >
+ <div class="condition-group-tool">
+ <div class="flex items-center">
+ <div class="mr-4">鏉′欢缁勫叧绯�</div>
+ <el-switch
+ v-model="condition.conditionGroups.and"
+ inline-prompt
+ active-text="涓�"
+ inactive-text="鎴�"
+ />
+ </div>
+ </div>
+ <el-space direction="vertical" :spacer="condition.conditionGroups.and ? '涓�' : '鎴�'">
+ <el-card
+ class="condition-group"
+ style="width: 530px"
+ v-for="(equation, cIdx) in condition.conditionGroups.conditions"
+ :key="cIdx"
+ >
+ <div
+ class="condition-group-delete"
+ v-if="condition.conditionGroups.conditions.length > 1"
+ >
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
+ />
+ </div>
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div>鏉′欢缁�</div>
+ <div class="flex">
+ <div class="mr-4">瑙勫垯鍏崇郴</div>
+ <el-switch
+ v-model="equation.and"
+ inline-prompt
+ active-text="涓�"
+ inactive-text="鎴�"
+ />
+ </div>
+ </div>
+ </template>
+
+ <div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
+ :rules="{
+ required: true,
+ message: '宸﹀�间笉鑳戒负绌�',
+ trigger: 'change'
+ }"
+ >
+ <el-select style="width: 160px" v-model="rule.leftSide" clearable>
+ <el-option
+ v-for="(field, fIdx) in fieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ :disabled="!field.required"
+ >
+ <el-tooltip
+ content="琛ㄥ崟瀛楁闈炲繀濉椂涓嶈兘浣滀负娴佺▼鍒嗘敮鏉′欢"
+ effect="dark"
+ placement="right-start"
+ v-if="!field.required"
+ >
+ <span>{{ field.title }}</span>
+ </el-tooltip>
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-select v-model="rule.opCode" style="width: 100px">
+ <el-option
+ v-for="operator in COMPARISON_OPERATORS"
+ :key="operator.value"
+ :label="operator.label"
+ :value="operator.value"
+ />
+ </el-select>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
+ :rules="{
+ required: true,
+ message: '鍙冲�间笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input v-model="rule.rightSide" style="width: 160px" />
+ </el-form-item>
+ </div>
+ <div class="mr-1 flex items-center" v-if="equation.rules.length > 1">
+ <Icon icon="ep:delete" :size="18" @click="deleteConditionRule(equation, rIdx)" />
+ </div>
+ <div class="flex items-center">
+ <Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
+ </div>
+ </div>
+ </el-card>
+ </el-space>
+ <div title="娣诲姞鏉′欢缁�" class="mt-4 cursor-pointer">
+ <Icon
+ color="#0089ff"
+ icon="ep:plus"
+ :size="24"
+ @click="addConditionGroup(condition.conditionGroups?.conditions)"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item
+ v-if="condition.conditionType === ConditionType.EXPRESSION"
+ label="鏉′欢琛ㄨ揪寮�"
+ prop="conditionExpression"
+ >
+ <el-input
+ type="textarea"
+ v-model="condition.conditionExpression"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import {
+ COMPARISON_OPERATORS,
+ CONDITION_CONFIG_TYPES,
+ ConditionType,
+ DEFAULT_CONDITION_GROUP_VALUE
+} from '../../consts'
+import { BpmModelFormType } from '@/utils/constants'
+import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ required: true
+ }
+})
+const emit = defineEmits(['update:modelValue'])
+const condition = computed({
+ get() {
+ return props.modelValue
+ },
+ set(newValue) {
+ emit('update:modelValue', newValue)
+ }
+})
+const formType = inject<Ref<number>>('formType') // 琛ㄥ崟绫诲瀷
+const conditionConfigTypes = computed(() => {
+ return CONDITION_CONFIG_TYPES.filter((item) => {
+ // 涓氬姟琛ㄥ崟鏆傛椂鍘绘帀鏉′欢瑙勫垯閫夐」
+ if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
+ return false
+ } else {
+ return true
+ }
+ })
+})
+
+/** 鏉′欢瑙勫垯鍙�夋嫨鐨勮〃鍗曞瓧娈� */
+const fieldOptions = useFormFieldsAndStartUser()
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ conditionType: [{ required: true, message: '閰嶇疆鏂瑰紡涓嶈兘涓虹┖', trigger: 'blur' }],
+ conditionExpression: [{ required: true, message: '鏉′欢琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鍒囨崲鏉′欢閰嶇疆鏂瑰紡 */
+const changeConditionType = () => {
+ if (condition.value.conditionType === ConditionType.RULE) {
+ if (!condition.value.conditionGroups) {
+ condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+ }
+ }
+}
+const deleteConditionGroup = (conditions, index) => {
+ conditions.splice(index, 1)
+}
+
+const deleteConditionRule = (condition, index) => {
+ condition.rules.splice(index, 1)
+}
+
+const addConditionRule = (condition, index) => {
+ const rule = {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ condition.rules.splice(index + 1, 0, rule)
+}
+
+const addConditionGroup = (conditions) => {
+ const condition = {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ ]
+ }
+ conditions.push(condition)
+}
+
+const validate = async () => {
+ if (!formRef) return false
+ return await formRef.value.validate()
+}
+
+defineExpose({ validate })
+</script>
+
+<style lang="scss" scoped>
+.condition-group-tool {
+ display: flex;
+ justify-content: space-between;
+ width: 500px;
+ margin-bottom: 20px;
+}
+
+.condition-group {
+ position: relative;
+
+ &:hover {
+ border-color: #0089ff;
+
+ .condition-group-delete {
+ opacity: 1;
+ }
+ }
+
+ .condition-group-delete {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ cursor: pointer;
+ opacity: 0;
+ }
+}
+
+::v-deep(.el-card__header) {
+ padding: 8px var(--el-card-padding);
+ border-bottom: 1px solid var(--el-card-border-color);
+ box-sizing: border-box;
+}
+</style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue
new file mode 100644
index 0000000..79816c6
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue
@@ -0,0 +1,309 @@
+<!-- TODO @jason锛氭湁鍙兘锛屽畠閲岄潰濂� Condition 涔堬紵 -->
+<!-- TODO 鎬曞奖鍝嶅叾瀹冭妭鐐瑰姛鑳斤紝鍚庨潰鐪嬬湅濡備綍濡備綍澶嶇敤 Condtion -->
+<template>
+ <Dialog v-model="dialogVisible" title="鏉′欢閰嶇疆" width="600px" :fullscreen="false">
+ <div class="h-410px">
+ <el-scrollbar wrap-class="h-full">
+ <el-form ref="formRef" :model="condition" :rules="formRules" label-position="top">
+ <el-form-item label="閰嶇疆鏂瑰紡" prop="conditionType">
+ <el-radio-group v-model="condition.conditionType" @change="changeConditionType">
+ <el-radio
+ v-for="(dict, indexConditionType) in conditionConfigTypes"
+ :key="indexConditionType"
+ :value="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="condition.conditionType === ConditionType.RULE && condition.conditionGroups"
+ label="鏉′欢瑙勫垯"
+ >
+ <div class="condition-group-tool">
+ <div class="flex items-center">
+ <div class="mr-4">鏉′欢缁勫叧绯�</div>
+ <el-switch
+ v-model="condition.conditionGroups.and"
+ inline-prompt
+ active-text="涓�"
+ inactive-text="鎴�"
+ />
+ </div>
+ </div>
+ <el-space direction="vertical" :spacer="condition.conditionGroups.and ? '涓�' : '鎴�'">
+ <el-card
+ class="condition-group"
+ style="width: 530px"
+ v-for="(equation, cIdx) in condition.conditionGroups.conditions"
+ :key="cIdx"
+ >
+ <div
+ class="condition-group-delete"
+ v-if="condition.conditionGroups.conditions.length > 1"
+ >
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteConditionGroup(condition.conditionGroups.conditions, cIdx)"
+ />
+ </div>
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div>鏉′欢缁�</div>
+ <div class="flex">
+ <div class="mr-4">瑙勫垯鍏崇郴</div>
+ <el-switch
+ v-model="equation.and"
+ inline-prompt
+ active-text="涓�"
+ inactive-text="鎴�"
+ />
+ </div>
+ </div>
+ </template>
+
+ <div class="flex pt-2" v-for="(rule, rIdx) in equation.rules" :key="rIdx">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.leftSide`"
+ :rules="{
+ required: true,
+ message: '宸﹀�间笉鑳戒负绌�',
+ trigger: 'change'
+ }"
+ >
+ <el-select style="width: 160px" v-model="rule.leftSide">
+ <el-option
+ v-for="(field, fIdx) in fieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ :disabled="!field.required"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-select v-model="rule.opCode" style="width: 100px">
+ <el-option
+ v-for="operator in COMPARISON_OPERATORS"
+ :key="operator.value"
+ :label="operator.label"
+ :value="operator.value"
+ />
+ </el-select>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`conditionGroups.conditions.${cIdx}.rules.${rIdx}.rightSide`"
+ :rules="{
+ required: true,
+ message: '鍙冲�间笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input v-model="rule.rightSide" style="width: 160px" />
+ </el-form-item>
+ </div>
+ <div
+ class="cursor-pointer mr-1 flex items-center"
+ v-if="equation.rules.length > 1"
+ >
+ <Icon
+ icon="ep:delete"
+ :size="18"
+ @click="deleteConditionRule(equation, rIdx)"
+ />
+ </div>
+ <div class="cursor-pointer flex items-center">
+ <Icon icon="ep:plus" :size="18" @click="addConditionRule(equation, rIdx)" />
+ </div>
+ </div>
+ </el-card>
+ </el-space>
+ <div title="娣诲姞鏉′欢缁�" class="mt-4 cursor-pointer">
+ <Icon
+ color="#0089ff"
+ icon="ep:plus"
+ :size="24"
+ @click="addConditionGroup(condition.conditionGroups?.conditions)"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item
+ v-if="condition.conditionType === ConditionType.EXPRESSION"
+ label="鏉′欢琛ㄨ揪寮�"
+ prop="conditionExpression"
+ >
+ <el-input
+ type="textarea"
+ v-model="condition.conditionExpression"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-form>
+ </el-scrollbar>
+ </div>
+ <template #footer>
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import {
+ COMPARISON_OPERATORS,
+ CONDITION_CONFIG_TYPES,
+ ConditionType,
+ ConditionGroup,
+ DEFAULT_CONDITION_GROUP_VALUE
+} from '../../consts'
+import { BpmModelFormType } from '@/utils/constants'
+import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
+defineOptions({
+ name: 'ConditionDialog'
+})
+
+const condition = ref<{
+ conditionType: ConditionType
+ conditionExpression?: string
+ conditionGroups?: ConditionGroup
+}>({
+ conditionType: ConditionType.RULE,
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+})
+
+const emit = defineEmits<{
+ updateCondition: [condition: object]
+}>()
+const message = useMessage() // 娑堟伅寮圭獥
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+
+const formType = inject<Ref<number>>('formType') // 琛ㄥ崟绫诲瀷
+const conditionConfigTypes = computed(() => {
+ return CONDITION_CONFIG_TYPES.filter((item) => {
+ // 涓氬姟琛ㄥ崟鏆傛椂鍘绘帀鏉′欢瑙勫垯閫夐」
+ if (formType?.value === BpmModelFormType.CUSTOM && item.value === ConditionType.RULE) {
+ return false
+ } else {
+ return true
+ }
+ })
+})
+
+/** 鏉′欢瑙勫垯鍙�夋嫨鐨勮〃鍗曞瓧娈� */
+const fieldOptions = useFormFieldsAndStartUser()
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+ conditionType: [{ required: true, message: '閰嶇疆鏂瑰紡涓嶈兘涓虹┖', trigger: 'blur' }],
+ conditionExpression: [{ required: true, message: '鏉′欢琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鍒囨崲鏉′欢閰嶇疆鏂瑰紡 */
+const changeConditionType = () => {
+ if (condition.value.conditionType === ConditionType.RULE) {
+ if (!condition.value.conditionGroups) {
+ condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+ }
+ }
+}
+const deleteConditionGroup = (conditions, index) => {
+ conditions.splice(index, 1)
+}
+
+const deleteConditionRule = (condition, index) => {
+ condition.rules.splice(index, 1)
+}
+
+const addConditionRule = (condition, index) => {
+ const rule = {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ condition.rules.splice(index + 1, 0, rule)
+}
+
+const addConditionGroup = (conditions) => {
+ const condition = {
+ and: true,
+ rules: [
+ {
+ opCode: '==',
+ leftSide: '',
+ rightSide: ''
+ }
+ ]
+ }
+ conditions.push(condition)
+}
+
+/** 淇濆瓨鏉′欢璁剧疆 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) {
+ message.warning('璇峰畬鍠勬潯浠惰鍒�')
+ return
+ }
+ dialogVisible.value = false
+ // 璁剧疆瀹岀殑鏉′欢浼犻�掔粰鐖剁粍浠�
+ emit('updateCondition', condition.value)
+}
+
+const open = (conditionObj: any | undefined) => {
+ if (conditionObj) {
+ condition.value.conditionType = conditionObj.conditionType
+ condition.value.conditionExpression = conditionObj.conditionExpression
+ condition.value.conditionGroups = conditionObj.conditionGroups
+ }
+ dialogVisible.value = true
+}
+
+defineExpose({ open })
+</script>
+
+<style lang="scss" scoped>
+.condition-group-tool {
+ display: flex;
+ justify-content: space-between;
+ width: 500px;
+ margin-bottom: 20px;
+}
+
+.condition-group {
+ position: relative;
+
+ &:hover {
+ border-color: #0089ff;
+
+ .condition-group-delete {
+ opacity: 1;
+ }
+ }
+
+ .condition-group-delete {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ cursor: pointer;
+ opacity: 0;
+ }
+}
+
+::v-deep(.el-card__header) {
+ padding: 8px var(--el-card-padding);
+ border-bottom: 1px solid var(--el-card-border-color);
+ box-sizing: border-box;
+}
+</style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue
new file mode 100644
index 0000000..7d617ba
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestParamSetting.vue
@@ -0,0 +1,199 @@
+<template>
+ <el-form-item label-position="top" label="璇锋眰澶�">
+ <div class="flex pb-4" v-for="(item, index) in props.header" :key="index">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`${bind}.header.${index}.key`"
+ :rules="{
+ required: true,
+ message: '鍙傛暟鍚嶄笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input v-model="item.key" style="width: 160px" />
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item>
+ <el-select v-model="item.type" style="width: 160px" @change="handleTypeChange(item)">
+ <el-option
+ v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
+ :key="types.value"
+ :label="types.label"
+ :value="types.value"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`${bind}.header.${index}.value`"
+ :rules="{
+ required: true,
+ message: '鍙傛暟鍊间笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input
+ v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
+ v-model="item.value"
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item
+ :prop="`${bind}.header.${index}.value`"
+ :rules="{
+ required: true,
+ message: '鍙傛暟鍊间笉鑳戒负绌�',
+ trigger: 'change'
+ }"
+ >
+ <el-select
+ v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
+ v-model="item.value"
+ style="width: 200px"
+ >
+ <el-option
+ v-for="(field, fIdx) in formFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ :disabled="!field.required"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-1 flex items-center">
+ <Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.header, index)" />
+ </div>
+ </div>
+ <el-button type="primary" text @click="addHttpRequestParam(props.header)">
+ <Icon icon="ep:plus" class="mr-5px" />娣诲姞涓�琛�
+ </el-button>
+ </el-form-item>
+ <el-form-item label-position="top" label="璇锋眰浣�">
+ <div class="flex pb-4" v-for="(item, index) in props.body" :key="index">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`${bind}.body.${index}.key`"
+ :rules="{
+ required: true,
+ message: '鍙傛暟鍚嶄笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input v-model="item.key" style="width: 160px" />
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item>
+ <el-select v-model="item.type" style="width: 160px" @change="handleTypeChange(item)">
+ <el-option
+ v-for="types in BPM_HTTP_REQUEST_PARAM_TYPES"
+ :key="types.value"
+ :label="types.label"
+ :value="types.value"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`${bind}.body.${index}.value`"
+ :rules="{
+ required: true,
+ message: '鍙傛暟鍊间笉鑳戒负绌�',
+ trigger: 'blur'
+ }"
+ >
+ <el-input
+ v-if="item.type === BpmHttpRequestParamTypeEnum.FIXED_VALUE"
+ v-model="item.value"
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item
+ :prop="`${bind}.body.${index}.value`"
+ :rules="{
+ required: true,
+ message: '鍙傛暟鍊间笉鑳戒负绌�',
+ trigger: 'change'
+ }"
+ >
+ <el-select
+ v-if="item.type === BpmHttpRequestParamTypeEnum.FROM_FORM"
+ v-model="item.value"
+ style="width: 200px"
+ >
+ <el-option
+ v-for="(field, fIdx) in formFieldOptions"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ :disabled="!field.required"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-1 flex items-center">
+ <Icon icon="ep:delete" :size="18" @click="deleteHttpRequestParam(props.body, index)" />
+ </div>
+ </div>
+ <el-button type="primary" text @click="addHttpRequestParam(props.body)">
+ <Icon icon="ep:plus" class="mr-5px" />娣诲姞涓�琛�
+ </el-button>
+ </el-form-item>
+</template>
+<script setup lang="ts">
+import {
+ HttpRequestParam,
+ BPM_HTTP_REQUEST_PARAM_TYPES,
+ BpmHttpRequestParamTypeEnum
+} from '../../consts'
+import { useFormFieldsAndStartUser } from '../../node'
+defineOptions({
+ name: 'HttpRequestParamSetting'
+})
+
+const props = defineProps({
+ header: {
+ type: Array as () => HttpRequestParam[],
+ required: false,
+ default: () => []
+ },
+ body: {
+ type: Array as () => HttpRequestParam[],
+ required: false,
+ default: () => []
+ },
+ bind: {
+ type: String,
+ required: true
+ }
+})
+
+// 娴佺▼琛ㄥ崟瀛楁锛屽彂璧蜂汉瀛楁
+const formFieldOptions = useFormFieldsAndStartUser()
+
+/** 鐩戝惉绫诲瀷鍙樺寲锛屾竻绌哄�� */
+const handleTypeChange = (item: HttpRequestParam) => {
+ // 褰撶被鍨嬫敼鍙樻椂锛屾竻绌哄��
+ item.value = ''
+}
+
+/** 娣诲姞璇锋眰閰嶇疆椤� */
+const addHttpRequestParam = (arr: HttpRequestParam[]) => {
+ arr.push({
+ key: '',
+ type: BpmHttpRequestParamTypeEnum.FIXED_VALUE,
+ value: ''
+ })
+}
+
+/** 鍒犻櫎璇锋眰閰嶇疆椤� */
+const deleteHttpRequestParam = (arr: HttpRequestParam[], index: number) => {
+ arr.splice(index, 1)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue
new file mode 100644
index 0000000..b522275
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue
@@ -0,0 +1,129 @@
+<template>
+ <el-form-item>
+ <el-alert
+ title="浠呮敮鎸� POST 璇锋眰锛屼互璇锋眰浣撴柟寮忔帴鏀跺弬鏁�"
+ type="warning"
+ show-icon
+ :closable="false"
+ />
+ </el-form-item>
+ <!-- 璇锋眰鍦板潃-->
+ <el-form-item
+ label-position="top"
+ label="璇锋眰鍦板潃"
+ :prop="`${formItemPrefix}.url`"
+ :rules="{
+ required: true,
+ message: '璇锋眰鍦板潃涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-input v-model="setting.url" />
+ </el-form-item>
+ <!-- 璇锋眰澶达紝璇锋眰浣撹缃�-->
+ <HttpRequestParamSetting :header="setting.header" :body="setting.body" :bind="formItemPrefix" />
+ <!-- 杩斿洖鍊艰缃�-->
+ <div v-if="responseEnable">
+ <el-form-item label="杩斿洖鍊�" label-position="top">
+ <el-alert
+ title="閫氳繃璇锋眰杩斿洖鍊�, 鍙互淇敼娴佺▼琛ㄥ崟鐨勫��"
+ type="warning"
+ show-icon
+ :closable="false"
+ />
+ </el-form-item>
+ <el-form-item>
+ <div class="flex pt-4" v-for="(item, index) in setting.response" :key="index">
+ <div class="mr-2">
+ <el-form-item
+ :prop="`${formItemPrefix}.response.${index}.key`"
+ :rules="{
+ required: true,
+ message: '琛ㄥ崟瀛楁涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-select class="w-160px!" v-model="item.key" placeholder="璇烽�夋嫨琛ㄥ崟瀛楁">
+ <el-option
+ v-for="(field, fIdx) in formFields"
+ :key="fIdx"
+ :label="field.title"
+ :value="field.field"
+ :disabled="!field.required"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ <div class="mr-2">
+ <el-form-item
+ :prop="`${formItemPrefix}.response.${index}.value`"
+ :rules="{
+ required: true,
+ message: '璇锋眰杩斿洖瀛楁涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-input class="w-160px" v-model="item.value" placeholder="璇锋眰杩斿洖瀛楁" />
+ </el-form-item>
+ </div>
+ <div class="mr-1 pt-1 cursor-pointer">
+ <Icon
+ icon="ep:delete"
+ :size="18"
+ @click="deleteHttpResponseSetting(setting.response!, index)"
+ />
+ </div>
+ </div>
+ </el-form-item>
+ <div class="pt-1">
+ <el-button type="primary" text @click="addHttpResponseSetting(setting.response!)">
+ <Icon icon="ep:plus" class="mr-5px" />娣诲姞涓�琛�
+ </el-button>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
+import { useFormFields } from '../../node'
+
+const props = defineProps({
+ setting: {
+ type: Object,
+ required: true
+ },
+ responseEnable: {
+ type: Boolean,
+ required: true
+ },
+ formItemPrefix: {
+ type: String,
+ required: true
+ }
+})
+const { setting } = toRefs(props)
+const emits = defineEmits(['update:setting'])
+watch(
+ () => setting,
+ (val) => {
+ emits('update:setting', val)
+ }
+)
+
+/** 娴佺▼琛ㄥ崟瀛楁 */
+const formFields = useFormFields()
+
+/** 娣诲姞 HTTP 璇锋眰杩斿洖鍊艰缃」 */
+const addHttpResponseSetting = (responseSetting: Record<string, string>[]) => {
+ responseSetting.push({
+ key: '',
+ value: ''
+ })
+}
+
+/** 鍒犻櫎 HTTP 璇锋眰杩斿洖鍊艰缃」 */
+const deleteHttpResponseSetting = (responseSetting: Record<string, string>[], index: number) => {
+ responseSetting.splice(index, 1)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue
new file mode 100644
index 0000000..728f568
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/components/UserTaskListener.vue
@@ -0,0 +1,88 @@
+<template>
+ <el-form ref="listenerFormRef" :model="configForm" label-position="top">
+ <div v-for="(listener, listenerIdx) in taskListener" :key="listenerIdx">
+ <el-divider content-position="left">
+ <el-text tag="b" size="large">{{ listener.name }}</el-text>
+ </el-divider>
+ <el-form-item>
+ <el-switch
+ v-model="configForm[`task${listener.type}ListenerEnable`]"
+ active-text="寮�鍚�"
+ inactive-text="鍏抽棴"
+ />
+ </el-form-item>
+ <div v-if="configForm[`task${listener.type}ListenerEnable`]">
+ <el-form-item>
+ <el-alert
+ title="浠呮敮鎸� POST 璇锋眰锛屼互璇锋眰浣撴柟寮忔帴鏀跺弬鏁�"
+ type="warning"
+ show-icon
+ :closable="false"
+ />
+ </el-form-item>
+ <el-form-item
+ label="璇锋眰鍦板潃"
+ :prop="`task${listener.type}ListenerPath`"
+ :rules="{
+ required: true,
+ message: '璇锋眰鍦板潃涓嶈兘涓虹┖',
+ trigger: 'blur'
+ }"
+ >
+ <el-input v-model="configForm[`task${listener.type}ListenerPath`]" />
+ </el-form-item>
+ <HttpRequestParamSetting
+ :header="configForm[`task${listener.type}Listener`].header"
+ :body="configForm[`task${listener.type}Listener`].body"
+ :bind="`task${listener.type}Listener`"
+ />
+ </div>
+ </div>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import HttpRequestParamSetting from './HttpRequestParamSetting.vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ required: true
+ },
+ formFieldOptions: {
+ type: Object,
+ required: true
+ }
+})
+const emit = defineEmits(['update:modelValue'])
+const listenerFormRef = ref()
+const configForm = computed({
+ get() {
+ return props.modelValue
+ },
+ set(newValue) {
+ emit('update:modelValue', newValue)
+ }
+})
+const taskListener = ref([
+ {
+ name: '鍒涘缓浠诲姟',
+ type: 'Create'
+ },
+ {
+ name: '鎸囨淳浠诲姟鎵ц浜哄憳',
+ type: 'Assign'
+ },
+ {
+ name: '瀹屾垚浠诲姟',
+ type: 'Complete'
+ }
+])
+
+const validate = async () => {
+ if (!listenerFormRef) return false
+ return await listenerFormRef.value.validate()
+}
+
+defineExpose({ validate })
+</script>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue
new file mode 100644
index 0000000..0b36244
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/ChildProcessNode.vue
@@ -0,0 +1,106 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div
+ :class="`node-title-icon ${currentNode.childProcessSetting?.async === true ? 'async-child-process' : 'child-process'}`"
+ >
+ <span
+ :class="`iconfont ${currentNode.childProcessSetting?.async === true ? 'icon-async-child-process' : 'icon-child-process'}`"
+ >
+ </span>
+ </div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="openNodeConfig">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.CHILD_PROCESS_NODE) }}
+ </div>
+ <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ /></div>
+ </div>
+ </div>
+
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ <ChildProcessNodeConfig
+ v-if="!readonly && currentNode"
+ ref="nodeSetting"
+ :flow-node="currentNode"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
+import ChildProcessNodeConfig from '../nodes-config/ChildProcessNodeConfig.vue'
+
+defineOptions({
+ name: 'ChildProcessNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢銆�
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.CHILD_PROCESS_NODE)
+const nodeSetting = ref()
+
+// 鎵撳紑鑺傜偣閰嶇疆
+const openNodeConfig = () => {
+ if (readonly) {
+ return
+ }
+ nodeSetting.value.showChildProcessNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+}
+
+// 鍒犻櫎鑺傜偣銆傛洿鏂板綋鍓嶈妭鐐逛负瀛╁瓙鑺傜偣
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue
new file mode 100644
index 0000000..8b97ee5
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/CopyTaskNode.vue
@@ -0,0 +1,97 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div class="node-title-icon copy-task"><span class="iconfont icon-copy"></span></div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="openNodeConfig">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.COPY_TASK_NODE) }}
+ </div>
+ <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ /></div>
+ </div>
+ </div>
+
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ <CopyTaskNodeConfig
+ v-if="!readonly && currentNode"
+ ref="nodeSetting"
+ :flow-node="currentNode"
+ />
+ </div>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
+import CopyTaskNodeConfig from '../nodes-config/CopyTaskNodeConfig.vue'
+defineOptions({
+ name: 'CopyTaskNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢銆�
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.COPY_TASK_NODE)
+
+const nodeSetting = ref()
+// 鎵撳紑鑺傜偣閰嶇疆
+const openNodeConfig = () => {
+ if (readonly) {
+ return
+ }
+ nodeSetting.value.showCopyTaskNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+}
+
+// 鍒犻櫎鑺傜偣銆傛洿鏂板綋鍓嶈妭鐐逛负瀛╁瓙鑺傜偣
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue
new file mode 100644
index 0000000..ad6795a
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/DelayTimerNode.vue
@@ -0,0 +1,97 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div class="node-title-icon delay-node"><span class="iconfont icon-delay"></span></div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="openNodeConfig">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.DELAY_TIMER_NODE) }}
+ </div>
+ <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ /></div>
+ </div>
+ </div>
+
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ <DelayTimerNodeConfig
+ v-if="!readonly && currentNode"
+ ref="nodeSetting"
+ :flow-node="currentNode"
+ />
+ </div>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
+import DelayTimerNodeConfig from '../nodes-config/DelayTimerNodeConfig.vue'
+defineOptions({
+ name: 'DelayTimerNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢銆�
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.DELAY_TIMER_NODE)
+
+const nodeSetting = ref()
+// 鎵撳紑鑺傜偣閰嶇疆
+const openNodeConfig = () => {
+ if (readonly) {
+ return
+ }
+ nodeSetting.value.showDelayTimerNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+}
+
+// 鍒犻櫎鑺傜偣銆傛洿鏂板綋鍓嶈妭鐐逛负瀛╁瓙鑺傜偣
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
new file mode 100644
index 0000000..0af0310
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/EndEventNode.vue
@@ -0,0 +1,102 @@
+<template>
+ <div class="end-node-wrapper">
+ <div class="end-node-box cursor-pointer" :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" @click="nodeClick">
+ <span class="node-fixed-name" title="缁撴潫">缁撴潫</span>
+ </div>
+ </div>
+ <el-dialog title="瀹℃壒淇℃伅" v-model="dialogVisible" width="1000px" append-to-body>
+ <el-row>
+ <el-table
+ :data="processInstanceInfos"
+ size="small"
+ border
+ header-cell-class-name="table-header-gray"
+ >
+ <el-table-column
+ label="搴忓彿"
+ header-align="center"
+ align="center"
+ type="index"
+ width="50"
+ />
+ <el-table-column
+ label="鍙戣捣浜�"
+ prop="assigneeUser.nickname"
+ min-width="100"
+ align="center"
+ />
+ <el-table-column label="閮ㄩ棬" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�濮嬫椂闂�"
+ prop="createTime"
+ min-width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ min-width="140"
+ />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" min-width="90">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="100">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-row>
+ </el-dialog>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode } from '../consts'
+import { useWatchNode, useTaskStatusClass } from '../node'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+defineOptions({
+ name: 'EndEventNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ default: () => null
+ }
+})
+// 鐩戞帶鑺傜偣鍙樺寲
+const currentNode = useWatchNode(props)
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+const processInstance = inject<Ref<any>>('processInstance', ref({}))
+// 瀹℃壒淇℃伅鐨勫脊绐楁樉绀猴紝鐢ㄤ簬鍙妯″紡
+const dialogVisible = ref(false) // 寮圭獥鍙鎬�
+const processInstanceInfos = ref<any[]>([]) // 娴佺▼鐨勫鎵逛俊鎭�
+
+const nodeClick = () => {
+ if (readonly) {
+ if(processInstance && processInstance.value){
+ processInstanceInfos.value = [
+ {
+ assigneeUser: processInstance.value.startUser,
+ createTime: processInstance.value.startTime,
+ endTime: processInstance.value.endTime,
+ status: processInstance.value.status,
+ durationInMillis: processInstance.value.durationInMillis
+ }
+ ]
+ dialogVisible.value = true
+ }
+ }
+}
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
new file mode 100644
index 0000000..09b32ed
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue
@@ -0,0 +1,240 @@
+<template>
+ <div class="branch-node-wrapper">
+ <div class="branch-node-container">
+ <div
+ v-if="readonly"
+ class="branch-node-readonly"
+ :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+ >
+ <span class="iconfont icon-exclusive icon-size condition"></span>
+ </div>
+ <el-button v-else class="branch-node-add" color="#67c23a" @click="addCondition" plain
+ >娣诲姞鏉′欢</el-button
+ >
+
+ <div
+ class="branch-node-item"
+ v-for="(item, index) in currentNode.conditionNodes"
+ :key="index"
+ >
+ <template v-if="index == 0">
+ <div class="branch-line-first-top"> </div>
+ <div class="branch-line-first-bottom"></div>
+ </template>
+ <template v-if="index + 1 == currentNode.conditionNodes?.length">
+ <div class="branch-line-last-top"></div>
+ <div class="branch-line-last-bottom"></div>
+ </template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !item.showText },
+ `${useTaskStatusClass(item.activityStatus)}`
+ ]"
+ >
+ <div class="branch-node-title-container">
+ <div v-if="!readonly && showInputs[index]">
+ <input
+ type="text"
+ class="input-max-width editable-title-input"
+ @blur="blurEvent(index)"
+ v-mountedFocus
+ v-model="item.name"
+ />
+ </div>
+ <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+ <div class="branch-priority"> 浼樺厛绾{ index + 1 }} </div>
+ </div>
+ <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+ <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+ {{ item.showText }}
+ </div>
+ <div class="branch-node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+ </div>
+ </div>
+ <div
+ class="node-toolbar"
+ v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
+ >
+ <div class="toolbar-icon">
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteCondition(index)"
+ />
+ </div>
+ </div>
+ <div
+ class="branch-node-move move-node-left"
+ v-if="index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+ @click="moveNode(index, -1)"
+ >
+ <Icon icon="ep:arrow-left" />
+ </div>
+
+ <div
+ class="branch-node-move move-node-right"
+ v-if="currentNode.conditionNodes && index < currentNode.conditionNodes.length - 2"
+ @click="moveNode(index, 1)"
+ >
+ <Icon icon="ep:arrow-right" />
+ </div>
+ </div>
+ <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
+ </div>
+ </div>
+ <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
+ <!-- 閫掑綊鏄剧ず瀛愯妭鐐� -->
+ <ProcessNodeTree
+ v-if="item && item.childNode"
+ :parent-node="item"
+ v-model:flow-node="item.childNode"
+ @find:recursive-find-parent-node="recursiveFindParentNode"
+ />
+ </div>
+ </div>
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import {
+ SimpleFlowNode,
+ NodeType,
+ ConditionType,
+ DEFAULT_CONDITION_GROUP_VALUE,
+ NODE_DEFAULT_TEXT
+} from '../consts'
+import { getDefaultConditionNodeName } from '../utils'
+import { useTaskStatusClass } from '../node'
+import { generateUUID } from '@/utils'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+ name: 'ExclusiveNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢
+const emits = defineEmits<{
+ 'update:modelValue': [node: SimpleFlowNode | undefined]
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+ 'find:recursiveFindParentNode': [
+ nodeList: SimpleFlowNode[],
+ curentNode: SimpleFlowNode,
+ nodeType: number
+ ]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+watch(
+ () => props.flowNode,
+ (newValue) => {
+ currentNode.value = newValue
+ }
+)
+
+const showInputs = ref<boolean[]>([])
+// 澶卞幓鐒︾偣
+const blurEvent = (index: number) => {
+ showInputs.value[index] = false
+ const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+ conditionNode.name =
+ conditionNode.name ||
+ getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+}
+
+// 鐐瑰嚮鏉′欢鍚嶇О
+const clickEvent = (index: number) => {
+ showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId: string) => {
+ if (readonly) {
+ return
+ }
+ const conditionNode = proxy.$refs[nodeId][0]
+ conditionNode.open()
+}
+
+// 鏂板鏉′欢
+const addCondition = () => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ const len = conditionNodes.length
+ let lastIndex = len - 1
+ const conditionData: SimpleFlowNode = {
+ id: 'Flow_' + generateUUID(),
+ name: '鏉′欢' + len,
+ showText: '',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionNodes: [],
+ conditionSetting: {
+ defaultFlow: false,
+ conditionType: ConditionType.RULE,
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+ }
+ }
+ conditionNodes.splice(lastIndex, 0, conditionData)
+ }
+}
+
+// 鍒犻櫎鏉′欢
+const deleteCondition = (index: number) => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ conditionNodes.splice(index, 1)
+ if (conditionNodes.length == 1) {
+ const childNode = currentNode.value.childNode
+ // 鏇存柊姝よ妭鐐逛负鍚庣画瀛╁瓙鑺傜偣
+ emits('update:modelValue', childNode)
+ }
+ }
+}
+
+// 绉诲姩鑺傜偣
+const moveNode = (index: number, to: number) => {
+ // -1 锛氬悜宸� 1锛� 鍚戝彸
+ if (currentNode.value.conditionNodes) {
+ currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
+ index + to,
+ 1,
+ currentNode.value.conditionNodes[index]
+ )[0]
+ }
+}
+// 閫掑綊浠庣埗鑺傜偣涓煡璇㈠尮閰嶇殑鑺傜偣
+const recursiveFindParentNode = (
+ nodeList: SimpleFlowNode[],
+ node: SimpleFlowNode,
+ nodeType: number
+) => {
+ if (!node || node.type === NodeType.START_USER_NODE) {
+ return
+ }
+ if (node.type === nodeType) {
+ nodeList.push(node)
+ }
+ // 鏉′欢鑺傜偣 (NodeType.CONDITION_NODE) 姣旇緝鐗规畩銆傞渶瑕佽皟鐢ㄥ叾鐖惰妭鐐规潯浠跺垎鏀妭鐐癸紙NodeType.EXCLUSIVE_NODE) 缁х画鏌ユ壘
+ emits('find:parentNode', nodeList, nodeType)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
new file mode 100644
index 0000000..51c44d4
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue
@@ -0,0 +1,244 @@
+<template>
+ <div class="branch-node-wrapper">
+ <div class="branch-node-container">
+ <div
+ v-if="readonly"
+ class="branch-node-readonly"
+ :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+ >
+ <span class="iconfont icon-inclusive icon-size inclusive"></span>
+ </div>
+ <el-button v-else class="branch-node-add" color="#345da2" @click="addCondition" plain
+ >娣诲姞鏉′欢</el-button
+ >
+ <div
+ class="branch-node-item"
+ v-for="(item, index) in currentNode.conditionNodes"
+ :key="index"
+ >
+ <template v-if="index == 0">
+ <div class="branch-line-first-top"> </div>
+ <div class="branch-line-first-bottom"></div>
+ </template>
+ <template v-if="index + 1 == currentNode.conditionNodes?.length">
+ <div class="branch-line-last-top"></div>
+ <div class="branch-line-last-bottom"></div>
+ </template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !item.showText },
+ `${useTaskStatusClass(item.activityStatus)}`
+ ]"
+ >
+ <div class="branch-node-title-container">
+ <div v-if="!readonly && showInputs[index]">
+ <input
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent(index)"
+ v-mountedFocus
+ v-model="item.name"
+ />
+ </div>
+ <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+ </div>
+ <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+ <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+ {{ item.showText }}
+ </div>
+ <div class="branch-node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+ </div>
+ </div>
+ <div
+ class="node-toolbar"
+ v-if="!readonly && index + 1 !== currentNode.conditionNodes?.length"
+ >
+ <div class="toolbar-icon">
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteCondition(index)"
+ />
+ </div>
+ </div>
+ <div
+ class="branch-node-move move-node-left"
+ v-if="!readonly && index != 0 && index + 1 !== currentNode.conditionNodes?.length"
+ @click="moveNode(index, -1)"
+ >
+ <Icon icon="ep:arrow-left" />
+ </div>
+
+ <div
+ class="branch-node-move move-node-right"
+ v-if="
+ !readonly &&
+ currentNode.conditionNodes &&
+ index < currentNode.conditionNodes.length - 2
+ "
+ @click="moveNode(index, 1)"
+ >
+ <Icon icon="ep:arrow-right" />
+ </div>
+ </div>
+ <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
+ </div>
+ </div>
+ <ConditionNodeConfig :node-index="index" :condition-node="item" :ref="item.id" />
+ <!-- 閫掑綊鏄剧ず瀛愯妭鐐� -->
+ <ProcessNodeTree
+ v-if="item && item.childNode"
+ :parent-node="item"
+ v-model:flow-node="item.childNode"
+ @find:recursive-find-parent-node="recursiveFindParentNode"
+ />
+ </div>
+ </div>
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import {
+ SimpleFlowNode,
+ NodeType,
+ ConditionType,
+ DEFAULT_CONDITION_GROUP_VALUE,
+ NODE_DEFAULT_TEXT
+} from '../consts'
+import { useTaskStatusClass } from '../node'
+import { getDefaultInclusiveConditionNodeName } from '../utils'
+import { generateUUID } from '@/utils'
+import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+ name: 'InclusiveNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢
+const emits = defineEmits<{
+ 'update:modelValue': [node: SimpleFlowNode | undefined]
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+ 'find:recursiveFindParentNode': [
+ nodeList: SimpleFlowNode[],
+ curentNode: SimpleFlowNode,
+ nodeType: number
+ ]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+
+watch(
+ () => props.flowNode,
+ (newValue) => {
+ currentNode.value = newValue
+ }
+)
+
+const showInputs = ref<boolean[]>([])
+// 澶卞幓鐒︾偣
+const blurEvent = (index: number) => {
+ showInputs.value[index] = false
+ const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+ conditionNode.name =
+ conditionNode.name ||
+ getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+}
+
+// 鐐瑰嚮鏉′欢鍚嶇О
+const clickEvent = (index: number) => {
+ showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId: string) => {
+ if (readonly) {
+ return
+ }
+ const conditionNode = proxy.$refs[nodeId][0]
+ conditionNode.open()
+}
+
+// 鏂板鏉′欢
+const addCondition = () => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ const len = conditionNodes.length
+ let lastIndex = len - 1
+ const conditionData: SimpleFlowNode = {
+ id: 'Flow_' + generateUUID(),
+ name: '鍖呭鏉′欢' + len,
+ showText: '',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionNodes: [],
+ conditionSetting: {
+ defaultFlow: false,
+ conditionType: ConditionType.RULE,
+ conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
+ }
+ }
+ conditionNodes.splice(lastIndex, 0, conditionData)
+ }
+}
+
+// 鍒犻櫎鏉′欢
+const deleteCondition = (index: number) => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ conditionNodes.splice(index, 1)
+ if (conditionNodes.length == 1) {
+ const childNode = currentNode.value.childNode
+ // 鏇存柊姝よ妭鐐逛负鍚庣画瀛╁瓙鑺傜偣
+ emits('update:modelValue', childNode)
+ }
+ }
+}
+
+// 绉诲姩鑺傜偣
+const moveNode = (index: number, to: number) => {
+ // -1 锛氬悜宸� 1锛� 鍚戝彸
+ if (currentNode.value.conditionNodes) {
+ currentNode.value.conditionNodes[index] = currentNode.value.conditionNodes.splice(
+ index + to,
+ 1,
+ currentNode.value.conditionNodes[index]
+ )[0]
+ }
+}
+// 閫掑綊浠庣埗鑺傜偣涓煡璇㈠尮閰嶇殑鑺傜偣
+const recursiveFindParentNode = (
+ nodeList: SimpleFlowNode[],
+ node: SimpleFlowNode,
+ nodeType: number
+) => {
+ if (!node || node.type === NodeType.START_USER_NODE) {
+ return
+ }
+ if (node.type === nodeType) {
+ nodeList.push(node)
+ }
+ // 鏉′欢鑺傜偣 (NodeType.CONDITION_NODE) 姣旇緝鐗规畩銆傞渶瑕佽皟鐢ㄥ叾鐖惰妭鐐规潯浠跺垎鏀妭鐐癸紙NodeType.INCLUSIVE_BRANCH_NODE) 缁х画鏌ユ壘
+ emits('find:parentNode', nodeList, nodeType)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue
new file mode 100644
index 0000000..7aa6793
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/ParallelNode.vue
@@ -0,0 +1,184 @@
+<template>
+ <div class="branch-node-wrapper">
+ <div class="branch-node-container">
+ <div
+ v-if="readonly"
+ class="branch-node-readonly"
+ :class="`${useTaskStatusClass(currentNode?.activityStatus)}`"
+ >
+ <span class="iconfont icon-parallel icon-size parallel"></span>
+ </div>
+ <el-button v-else class="branch-node-add" color="#626aef" @click="addCondition" plain
+ >娣诲姞鍒嗘敮</el-button
+ >
+ <div
+ class="branch-node-item"
+ v-for="(item, index) in currentNode.conditionNodes"
+ :key="index"
+ >
+ <template v-if="index == 0">
+ <div class="branch-line-first-top"></div>
+ <div class="branch-line-first-bottom"></div>
+ </template>
+ <template v-if="index + 1 == currentNode.conditionNodes?.length">
+ <div class="branch-line-last-top"></div>
+ <div class="branch-line-last-bottom"></div>
+ </template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div class="node-box" :class="`${useTaskStatusClass(item.activityStatus)}`">
+ <div class="branch-node-title-container">
+ <div v-if="showInputs[index]">
+ <input
+ type="text"
+ class="input-max-width editable-title-input"
+ @blur="blurEvent(index)"
+ v-mountedFocus
+ v-model="item.name"
+ />
+ </div>
+ <div v-else class="branch-title" @click="clickEvent(index)"> {{ item.name }} </div>
+ <div class="branch-priority">鏃犱紭鍏堢骇</div>
+ </div>
+ <div class="branch-node-content" @click="conditionNodeConfig(item.id)">
+ <div class="branch-node-text" :title="item.showText" v-if="item.showText">
+ {{ item.showText }}
+ </div>
+ <div class="branch-node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.CONDITION_NODE) }}
+ </div>
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon">
+ <Icon
+ color="#0089ff"
+ icon="ep:circle-close-filled"
+ :size="18"
+ @click="deleteCondition(index)"
+ />
+ </div>
+ </div>
+ </div>
+ <NodeHandler v-model:child-node="item.childNode" :current-node="item" />
+ </div>
+ </div>
+ <!-- 閫掑綊鏄剧ず瀛愯妭鐐� -->
+ <ProcessNodeTree
+ v-if="item && item.childNode"
+ :parent-node="item"
+ v-model:flow-node="item.childNode"
+ @find:recursive-find-parent-node="recursiveFindParentNode"
+ />
+ </div>
+ </div>
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import ProcessNodeTree from '../ProcessNodeTree.vue'
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { useTaskStatusClass } from '../node'
+import { generateUUID } from '@/utils'
+const { proxy } = getCurrentInstance() as any
+defineOptions({
+ name: 'ParallelNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢
+const emits = defineEmits<{
+ 'update:modelValue': [node: SimpleFlowNode | undefined]
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: number]
+ 'find:recursiveFindParentNode': [
+ nodeList: SimpleFlowNode[],
+ curentNode: SimpleFlowNode,
+ nodeType: number
+ ]
+}>()
+
+const currentNode = ref<SimpleFlowNode>(props.flowNode)
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+
+watch(
+ () => props.flowNode,
+ (newValue) => {
+ currentNode.value = newValue
+ }
+)
+
+const showInputs = ref<boolean[]>([])
+// 澶卞幓鐒︾偣
+const blurEvent = (index: number) => {
+ showInputs.value[index] = false
+ const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
+ conditionNode.name = conditionNode.name || `骞惰${index + 1}`
+}
+
+// 鐐瑰嚮鏉′欢鍚嶇О
+const clickEvent = (index: number) => {
+ showInputs.value[index] = true
+}
+
+const conditionNodeConfig = (nodeId: string) => {
+ const conditionNode = proxy.$refs[nodeId][0]
+ conditionNode.open()
+}
+
+// 鏂板鏉′欢
+const addCondition = () => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ const len = conditionNodes.length
+ let lastIndex = len - 1
+ const conditionData: SimpleFlowNode = {
+ id: 'Flow_' + generateUUID(),
+ name: '骞惰' + len,
+ showText: '鏃犻渶閰嶇疆鏉′欢鍚屾椂鎵ц',
+ type: NodeType.CONDITION_NODE,
+ childNode: undefined,
+ conditionNodes: []
+ }
+ conditionNodes.splice(lastIndex, 0, conditionData)
+ }
+}
+
+// 鍒犻櫎鏉′欢
+const deleteCondition = (index: number) => {
+ const conditionNodes = currentNode.value.conditionNodes
+ if (conditionNodes) {
+ conditionNodes.splice(index, 1)
+ if (conditionNodes.length == 1) {
+ const childNode = currentNode.value.childNode
+ // 鏇存柊姝よ妭鐐逛负鍚庣画瀛╁瓙鑺傜偣
+ emits('update:modelValue', childNode)
+ }
+ }
+}
+
+// 閫掑綊浠庣埗鑺傜偣涓煡璇㈠尮閰嶇殑鑺傜偣
+const recursiveFindParentNode = (
+ nodeList: SimpleFlowNode[],
+ node: SimpleFlowNode,
+ nodeType: number
+) => {
+ if (!node || node.type === NodeType.START_USER_NODE) {
+ return
+ }
+ if (node.type === nodeType) {
+ nodeList.push(node)
+ }
+ // 鏉′欢鑺傜偣 (NodeType.CONDITION_NODE) 姣旇緝鐗规畩銆傞渶瑕佽皟鐢ㄥ叾鐖惰妭鐐瑰苟琛岃妭鐐癸紙NodeType.PARALLEL_NODE) 缁х画鏌ユ壘
+ emits('find:parentNode', nodeList, nodeType)
+}
+</script>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue
new file mode 100644
index 0000000..3997c09
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/RouterNode.vue
@@ -0,0 +1,97 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div class="node-title-icon router-node">
+ <span class="iconfont icon-router"></span>
+ </div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="openNodeConfig">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.ROUTER_BRANCH_NODE) }}
+ </div>
+ <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ /></div>
+ </div>
+ </div>
+
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ <RouterNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
+ </div>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
+import RouterNodeConfig from '../nodes-config/RouterNodeConfig.vue'
+
+defineOptions({
+ name: 'RouterNode'
+})
+
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.ROUTER_BRANCH_NODE)
+
+const nodeSetting = ref()
+// 鎵撳紑鑺傜偣閰嶇疆
+const openNodeConfig = () => {
+ if (readonly) {
+ return
+ }
+ nodeSetting.value.showRouteNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+}
+
+// 鍒犻櫎鑺傜偣銆傛洿鏂板綋鍓嶈妭鐐逛负瀛╁瓙鑺傜偣
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
new file mode 100644
index 0000000..4abe38f
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/StartUserNode.vue
@@ -0,0 +1,154 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div class="node-title-icon start-user"
+ ><span class="iconfont icon-start-user"></span
+ ></div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="nodeClick">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }}
+ </div>
+ <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
+ </div>
+ </div>
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ </div>
+ <StartUserNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
+ <!-- 瀹℃壒璁板綍 -->
+ <el-dialog
+ :title="dialogTitle || '瀹℃壒璁板綍'"
+ v-model="dialogVisible"
+ width="1000px"
+ append-to-body
+ >
+ <el-row>
+ <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
+ <el-table-column
+ label="搴忓彿"
+ header-align="center"
+ align="center"
+ type="index"
+ width="50"
+ />
+ <el-table-column label="瀹℃壒浜�" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+ </template>
+ </el-table-column>
+
+ <el-table-column label="閮ㄩ棬" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�濮嬫椂闂�"
+ prop="createTime"
+ min-width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ min-width="140"
+ />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" min-width="90">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹℃壒寤鸿" prop="reason" min-width="120" />
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="100">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-row>
+ </el-dialog>
+</template>
+<script setup lang="ts">
+import NodeHandler from '../NodeHandler.vue'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
+import { SimpleFlowNode, NODE_DEFAULT_TEXT, NodeType } from '../consts'
+import StartUserNodeConfig from '../nodes-config/StartUserNodeConfig.vue'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+defineOptions({
+ name: 'StartEventNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ default: () => null
+ }
+})
+const readonly = inject<Boolean>('readonly') // 鏄惁鍙
+const tasks = inject<Ref<any[]>>('tasks', ref([]))
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢銆�
+const emits = defineEmits<{
+ 'update:modelValue': [node: SimpleFlowNode | undefined]
+}>()
+// 鐩戞帶鑺傜偣鍙樺寲
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
+
+const nodeSetting = ref()
+//
+const nodeClick = () => {
+ if (readonly) {
+ // 鍙妯″紡锛屽脊绐楁樉绀轰换鍔′俊鎭�
+ if (tasks && tasks.value) {
+ dialogTitle.value = currentNode.value.name
+ selectTasks.value = tasks.value.filter(
+ (item: any) => item?.taskDefinitionKey === currentNode.value.id
+ )
+ dialogVisible.value = true
+ }
+ } else {
+ // 缂栬緫妯″紡锛屾墦寮�鑺傜偣閰嶇疆銆佹妸褰撳墠鑺傜偣浼犻�掔粰閰嶇疆缁勪欢
+ nodeSetting.value.showStartUserNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+ }
+}
+
+// 浠诲姟鐨勫脊绐楁樉绀猴紝鐢ㄤ簬鍙妯″紡
+const dialogVisible = ref(false) // 寮圭獥鍙鎬�
+const dialogTitle = ref<string | undefined>(undefined) // 寮圭獥鏍囬
+const selectTasks = ref<any[] | undefined>([]) // 閫変腑鐨勪换鍔℃暟缁�
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue
new file mode 100644
index 0000000..00f1c82
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/TriggerNode.vue
@@ -0,0 +1,97 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div class="node-title-icon trigger-node">
+ <span class="iconfont icon-trigger"></span>
+ </div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="openNodeConfig">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(NodeType.TRIGGER_NODE) }}
+ </div>
+ <Icon v-if="!readonly" icon="ep:arrow-right-bold" />
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ /></div>
+ </div>
+ </div>
+
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ <TriggerNodeConfig v-if="!readonly && currentNode" ref="nodeSetting" :flow-node="currentNode" />
+ </div>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import NodeHandler from '../NodeHandler.vue'
+import { useNodeName2, useWatchNode, useTaskStatusClass } from '../node'
+import TriggerNodeConfig from '../nodes-config/TriggerNodeConfig.vue'
+
+defineOptions({
+ name: 'TriggerNode'
+})
+
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+// 瀹氫箟浜嬩欢锛屾洿鏂扮埗缁勪欢
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+}>()
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.TRIGGER_NODE)
+
+const nodeSetting = ref()
+// 鎵撳紑鑺傜偣閰嶇疆
+const openNodeConfig = () => {
+ if (readonly) {
+ return
+ }
+ nodeSetting.value.showTriggerNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+}
+
+// 鍒犻櫎鑺傜偣銆傛洿鏂板綋鍓嶈妭鐐逛负瀛╁瓙鑺傜偣
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue b/src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
new file mode 100644
index 0000000..ae1af6c
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes/UserTaskNode.vue
@@ -0,0 +1,181 @@
+<template>
+ <div class="node-wrapper">
+ <div class="node-container">
+ <div
+ class="node-box"
+ :class="[
+ { 'node-config-error': !currentNode.showText },
+ `${useTaskStatusClass(currentNode?.activityStatus)}`
+ ]"
+ >
+ <div class="node-title-container">
+ <div
+ :class="`node-title-icon ${currentNode.type === NodeType.TRANSACTOR_NODE ? 'transactor-task' : 'user-task'}`"
+ >
+ <span
+ :class="`iconfont ${currentNode.type === NodeType.TRANSACTOR_NODE ? 'icon-transactor' : 'icon-approve'}`"
+ >
+ </span>
+ </div>
+ <input
+ v-if="!readonly && showInput"
+ type="text"
+ class="editable-title-input"
+ @blur="blurEvent()"
+ v-mountedFocus
+ v-model="currentNode.name"
+ :placeholder="currentNode.name"
+ />
+ <div v-else class="node-title" @click="clickTitle">
+ {{ currentNode.name }}
+ </div>
+ </div>
+ <div class="node-content" @click="nodeClick">
+ <div class="node-text" :title="currentNode.showText" v-if="currentNode.showText">
+ {{ currentNode.showText }}
+ </div>
+ <div class="node-text" v-else>
+ {{ NODE_DEFAULT_TEXT.get(currentNode.type) }}
+ </div>
+ <Icon icon="ep:arrow-right-bold" v-if="!readonly" />
+ </div>
+ <div v-if="!readonly" class="node-toolbar">
+ <div class="toolbar-icon"
+ ><Icon color="#0089ff" icon="ep:circle-close-filled" :size="18" @click="deleteNode"
+ /></div>
+ </div>
+ </div>
+ <!-- 浼犻�掑瓙鑺傜偣缁欐坊鍔犺妭鐐圭粍浠躲�備細鍦ㄥ瓙鑺傜偣鍓嶉潰娣诲姞鑺傜偣 -->
+ <NodeHandler
+ v-if="currentNode"
+ v-model:child-node="currentNode.childNode"
+ :current-node="currentNode"
+ />
+ </div>
+ </div>
+ <UserTaskNodeConfig
+ v-if="currentNode"
+ ref="nodeSetting"
+ :flow-node="currentNode"
+ @find:return-task-nodes="findReturnTaskNodes"
+ />
+ <!-- 瀹℃壒璁板綍 -->
+ <el-dialog
+ :title="dialogTitle || '瀹℃壒璁板綍'"
+ v-model="dialogVisible"
+ width="1000px"
+ append-to-body
+ >
+ <el-row>
+ <el-table :data="selectTasks" size="small" border header-cell-class-name="table-header-gray">
+ <el-table-column
+ label="搴忓彿"
+ header-align="center"
+ align="center"
+ type="index"
+ width="50"
+ />
+ <el-table-column label="瀹℃壒浜�" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+ </template>
+ </el-table-column>
+
+ <el-table-column label="閮ㄩ棬" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�濮嬫椂闂�"
+ prop="createTime"
+ min-width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ min-width="140"
+ />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" min-width="90">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹℃壒寤鸿" prop="reason" min-width="120" />
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="100">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-row>
+ </el-dialog>
+</template>
+<script setup lang="ts">
+import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from '../consts'
+import { useWatchNode, useNodeName2, useTaskStatusClass } from '../node'
+import NodeHandler from '../NodeHandler.vue'
+import UserTaskNodeConfig from '../nodes-config/UserTaskNodeConfig.vue'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+defineOptions({
+ name: 'UserTaskNode'
+})
+const props = defineProps({
+ flowNode: {
+ type: Object as () => SimpleFlowNode,
+ required: true
+ }
+})
+const emits = defineEmits<{
+ 'update:flowNode': [node: SimpleFlowNode | undefined]
+ 'find:parentNode': [nodeList: SimpleFlowNode[], nodeType: NodeType]
+}>()
+
+// 鏄惁鍙
+const readonly = inject<Boolean>('readonly')
+const tasks = inject<Ref<any[]>>('tasks', ref([]))
+// 鐩戞帶鑺傜偣鍙樺寲
+const currentNode = useWatchNode(props)
+// 鑺傜偣鍚嶇О缂栬緫
+const { showInput, blurEvent, clickTitle } = useNodeName2(currentNode, NodeType.START_USER_NODE)
+const nodeSetting = ref()
+
+const nodeClick = () => {
+ if (readonly) {
+ if (tasks && tasks.value) {
+ dialogTitle.value = currentNode.value.name
+ // 鍙妯″紡锛屽脊绐楁樉绀轰换鍔′俊鎭�
+ selectTasks.value = tasks.value.filter(
+ (item: any) => item?.taskDefinitionKey === currentNode.value.id
+ )
+ dialogVisible.value = true
+ }
+ } else {
+ // 缂栬緫妯″紡锛屾墦寮�鑺傜偣閰嶇疆銆佹妸褰撳墠鑺傜偣浼犻�掔粰閰嶇疆缁勪欢
+ nodeSetting.value.showUserTaskNodeConfig(currentNode.value)
+ nodeSetting.value.openDrawer()
+ }
+}
+
+const deleteNode = () => {
+ emits('update:flowNode', currentNode.value.childNode)
+}
+// 鏌ユ壘鍙互椹冲洖鐢ㄦ埛鑺傜偣
+const findReturnTaskNodes = (
+ matchNodeList: SimpleFlowNode[] // 鍖归厤鐨勮妭鐐�
+) => {
+ // 浠庣埗鑺傜偣鏌ユ壘
+ emits('find:parentNode', matchNodeList, NodeType.USER_TASK_NODE)
+}
+
+// 浠诲姟鐨勫脊绐楁樉绀猴紝鐢ㄤ簬鍙妯″紡
+const dialogVisible = ref(false) // 寮圭獥鍙鎬�
+const dialogTitle = ref<string | undefined>(undefined) // 寮圭獥鏍囬
+const selectTasks = ref<any[] | undefined>([]) // 閫変腑鐨勪换鍔℃暟缁�
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/components/SimpleProcessDesignerV2/src/utils.ts b/src/components/SimpleProcessDesignerV2/src/utils.ts
new file mode 100644
index 0000000..8e715b4
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/utils.ts
@@ -0,0 +1,41 @@
+import { TimeUnitType, ApproveType, APPROVE_TYPE } from './consts'
+
+// 鑾峰彇鏉′欢鑺傜偣榛樿鐨勫悕绉�
+export const getDefaultConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
+ if (defaultFlow) {
+ return '鍏跺畠鎯呭喌'
+ }
+ return '鏉′欢' + (index + 1)
+}
+
+// 鑾峰彇鍖呭鍒嗘敮鏉′欢鑺傜偣榛樿鐨勫悕绉�
+export const getDefaultInclusiveConditionNodeName = (index: number, defaultFlow: boolean | undefined): string => {
+ if (defaultFlow) {
+ return '鍏跺畠鎯呭喌'
+ }
+ return '鍖呭鏉′欢' + (index + 1)
+}
+
+export const convertTimeUnit = (strTimeUnit: string) => {
+ if (strTimeUnit === 'M') {
+ return TimeUnitType.MINUTE
+ }
+ if (strTimeUnit === 'H') {
+ return TimeUnitType.HOUR
+ }
+ if (strTimeUnit === 'D') {
+ return TimeUnitType.DAY
+ }
+ return TimeUnitType.HOUR
+}
+
+export const getApproveTypeText = (approveType: ApproveType): string => {
+ let approveTypeText = ''
+ APPROVE_TYPE.forEach((item) => {
+ if (item.value === approveType) {
+ approveTypeText = item.label
+ return
+ }
+ })
+ return approveTypeText
+}
diff --git a/src/components/SimpleProcessDesignerV2/theme/iconfont.ttf b/src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
new file mode 100644
index 0000000..06f4e31
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/theme/iconfont.ttf
Binary files differ
diff --git a/src/components/SimpleProcessDesignerV2/theme/iconfont.woff b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff
new file mode 100644
index 0000000..0724e75
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff
Binary files differ
diff --git a/src/components/SimpleProcessDesignerV2/theme/iconfont.woff2 b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
new file mode 100644
index 0000000..c904bb6
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/theme/iconfont.woff2
Binary files differ
diff --git a/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss b/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
new file mode 100644
index 0000000..3379563
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss
@@ -0,0 +1,825 @@
+// 閰嶇疆鑺傜偣澶撮儴
+.config-header {
+ display: flex;
+ flex-direction: column;
+
+ .node-name {
+ display: flex;
+ height: 24px;
+ line-height: 24px;
+ font-size: 16px;
+ cursor: pointer;
+ align-items: center;
+ }
+
+ .divide-line {
+ width: 100%;
+ height: 1px;
+ margin-top: 16px;
+ background: #eee;
+ }
+
+ .config-editable-input {
+ height: 24px;
+ max-width: 510px;
+ font-size: 16px;
+ line-height: 24px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ &:focus {
+ border-color: #40a9ff;
+ outline: 0;
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+ }
+ }
+}
+
+// 琛ㄥ崟瀛楁鏉冮檺
+.field-setting-pane {
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+
+ .field-setting-desc {
+ padding-right: 8px;
+ margin-bottom: 16px;
+ font-size: 16px;
+ font-weight: 700;
+ }
+
+ .field-permit-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 45px;
+ padding-left: 12px;
+ line-height: 45px;
+ background-color: #f8fafc0a;
+ border: 1px solid #1f38581a;
+
+ .first-title {
+ text-align: left !important;
+ }
+
+ .other-titles {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .setting-title-label {
+ display: inline-block;
+ width: 110px;
+ padding: 5px 0;
+ font-size: 13px;
+ font-weight: 700;
+ color: #000;
+ text-align: center;
+ }
+ }
+
+ .field-setting-item {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ height: 38px;
+ padding-left: 12px;
+ border: 1px solid #1f38581a;
+ border-top: 0;
+
+ .field-setting-item-label {
+ display: inline-block;
+ width: 110px;
+ min-height: 16px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ cursor: text;
+ }
+
+ .field-setting-item-group {
+ display: flex;
+ justify-content: space-between;
+
+ .item-radio-wrap {
+ display: inline-block;
+ width: 110px;
+ text-align: center;
+ }
+ }
+ }
+}
+
+// 鑺傜偣杩炵嚎姘旀场鍗$墖鏍峰紡
+.handler-item-wrapper {
+ width: 320px;
+ display: flex;
+ flex-wrap: wrap;
+ cursor: pointer;
+
+ .handler-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 12px;
+ }
+
+ .handler-item-icon {
+ width: 50px;
+ height: 50px;
+ background: #fff;
+ border: 1px solid #e2e2e2;
+ border-radius: 50%;
+ user-select: none;
+ text-align: center;
+
+ &:hover {
+ background: #e2e2e2;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
+ }
+
+ .icon-size {
+ font-size: 25px;
+ line-height: 50px;
+ }
+ }
+
+ .approve {
+ color: #ff943e;
+ }
+
+ .copy {
+ color: #3296fa;
+ }
+
+ .condition {
+ color: #67c23a;
+ }
+
+ .parallel {
+ color: #626aef;
+ }
+
+ .inclusive {
+ color: #345da2;
+ }
+
+ .delay {
+ color: #e47470;
+ }
+
+ .trigger {
+ color: #3373d2;
+ }
+
+ .router {
+ color: #ca3a31
+ }
+
+ .transactor {
+ color: #330099;
+ }
+
+ .child-process {
+ color: #996633;
+ }
+
+ .async-child-process {
+ color: #006666;
+ }
+
+ .handler-item-text {
+ margin-top: 4px;
+ width: 80px;
+ text-align: center;
+ font-size: 13px;
+ }
+}
+// Simple 娴佺▼妯″瀷鏍峰紡
+.simple-process-model-container {
+ height: 100%;
+ padding-top: 32px;
+ background-color: #fafafa;
+ overflow-x: auto;
+ width: 100%;
+
+ .simple-process-model {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ transform-origin: 50% 0 0;
+ min-width: fit-content;
+ transform: scale(1);
+ background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
+ // 鑺傜偣瀹瑰櫒 瀹氫箟鑺傜偣瀹藉害
+ .node-container {
+ width: 200px;
+ }
+ // 鑺傜偣
+ .node-box {
+ position: relative;
+ display: flex;
+ min-height: 70px;
+ padding: 5px 10px 8px;
+ cursor: pointer;
+ background-color: #fff;
+ flex-direction: column;
+ border: 2px solid transparent;
+ border-radius: 8px;
+ box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%);
+ transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+ &.status-pass {
+ background-color: #a9da90;
+ border-color: #67c23a;
+ }
+
+ &.status-pass:hover {
+ border-color: #67c23a;
+ }
+
+ &.status-running {
+ background-color: #e7f0fe;
+ border-color: #5a9cf8;
+ }
+
+ &.status-running:hover {
+ border-color: #5a9cf8;
+ }
+
+ &.status-reject {
+ background-color: #f6e5e5;
+ border-color: #e47470;
+ }
+
+ &.status-reject:hover {
+ border-color: #e47470;
+ }
+
+ &:hover {
+ border-color: #0089ff;
+
+ .node-toolbar {
+ opacity: 1;
+ }
+
+ .branch-node-move {
+ display: flex;
+ }
+ }
+
+ // 鏅�氳妭鐐规爣棰�
+ .node-title-container {
+ display: flex;
+ padding: 4px;
+ cursor: pointer;
+ border-radius: 4px 4px 0 0;
+ align-items: center;
+
+ .node-title-icon {
+ display: flex;
+ align-items: center;
+
+ &.user-task {
+ color: #ff943e;
+ }
+
+ &.copy-task {
+ color: #3296fa;
+ }
+
+ &.start-user {
+ color: #676565;
+ }
+
+ &.delay-node {
+ color: #e47470;
+ }
+
+ &.trigger-node {
+ color: #3373d2;
+ }
+
+ &.router-node {
+ color: #ca3a31
+ }
+
+ &.transactor-task {
+ color: #330099;
+ }
+
+ &.child-process {
+ color: #996633;
+ }
+
+ &.async-child-process {
+ color: #006666;
+ }
+ }
+
+ .node-title {
+ margin-left: 4px;
+ overflow: hidden;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 18px;
+ color: #1f1f1f;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &:hover {
+ border-bottom: 1px dashed #f60;
+ }
+ }
+ }
+
+ // 鏉′欢鑺傜偣鏍囬
+ .branch-node-title-container {
+ display: flex;
+ padding: 4px 0;
+ cursor: pointer;
+ border-radius: 4px 4px 0 0;
+ align-items: center;
+ justify-content: space-between;
+
+ .input-max-width {
+ max-width: 115px !important;
+ }
+
+ .branch-title {
+ overflow: hidden;
+ font-size: 13px;
+ font-weight: 600;
+ color: #f60;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &:hover {
+ border-bottom: 1px dashed #000;
+ }
+ }
+
+ .branch-priority {
+ min-width: 50px;
+ font-size: 12px;
+ }
+ }
+
+ .node-content {
+ display: flex;
+ min-height: 32px;
+ padding: 4px 8px;
+ margin-top: 4px;
+ line-height: 32px;
+ justify-content: space-between;
+ align-items: center;
+ color: #111f2c;
+ background: rgb(0 0 0 / 3%);
+ border-radius: 4px;
+
+ .node-text {
+ display: -webkit-box;
+ overflow: hidden;
+ font-size: 14px;
+ line-height: 24px;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ -webkit-line-clamp: 2; /* 杩欏皢闄愬埗鏂囨湰鏄剧ず涓轰袱琛� */
+ -webkit-box-orient: vertical;
+ }
+ }
+
+ //鏉′欢鑺傜偣鍐呭
+ .branch-node-content {
+ display: flex;
+ min-height: 32px;
+ padding: 4px 0;
+ margin-top: 4px;
+ line-height: 32px;
+ align-items: center;
+ color: #111f2c;
+ border-radius: 4px;
+
+ .branch-node-text {
+ overflow: hidden;
+ font-size: 12px;
+ line-height: 24px;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ -webkit-line-clamp: 2; /* 杩欏皢闄愬埗鏂囨湰鏄剧ず涓轰袱琛� */
+ -webkit-box-orient: vertical;
+ }
+ }
+
+ // 鑺傜偣鎿嶄綔 锛氬垹闄�
+ .node-toolbar {
+ position: absolute;
+ top: -20px;
+ right: 0;
+ display: flex;
+ opacity: 0;
+
+ .toolbar-icon {
+ text-align: center;
+ vertical-align: middle;
+ }
+ }
+
+ // 鏉′欢鑺傜偣宸﹀彸绉诲姩
+ .branch-node-move {
+ position: absolute;
+ display: none;
+ width: 10px;
+ height: 100%;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .move-node-left {
+ top: 0;
+ left: -2px;
+ background: rgb(126 134 142 / 8%);
+ border-bottom-left-radius: 8px;
+ border-top-left-radius: 8px;
+ }
+
+ .move-node-right {
+ top: 0;
+ right: -2px;
+ background: rgb(126 134 142 / 8%);
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ }
+ }
+
+ .node-config-error {
+ border-color: #ff5219 !important;
+ }
+ // 鏅�氳妭鐐瑰寘瑁�
+ .node-wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+ // 鑺傜偣杩炵嚎澶勭悊
+ .node-handler-wrapper {
+ position: relative;
+ display: flex;
+ height: 70px;
+ align-items: center;
+ user-select: none;
+ justify-content: center;
+ flex-direction: column;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ z-index: 0;
+ width: 2px;
+ height: 100%;
+ margin: auto;
+ background-color: #dedede;
+ content: '';
+ }
+
+ .node-handler {
+ .add-icon {
+ position: relative;
+ top: -5px;
+ display: flex;
+ width: 25px;
+ height: 25px;
+ color: #fff;
+ cursor: pointer;
+ background-color: #0089ff;
+ border-radius: 50%;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ transform: scale(1.1);
+ }
+ }
+ }
+
+ .node-handler-arrow {
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ display: flex;
+ transform: translateX(-50%);
+ }
+ }
+
+ // 鏉′欢鑺傜偣鍖呰
+ .branch-node-wrapper {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ margin-top: 16px;
+
+ .branch-node-container {
+ position: relative;
+ display: flex;
+ min-width: fit-content;
+
+ &::before {
+ position: absolute;
+ left: 50%;
+ width: 4px;
+ height: 100%;
+ background-color: #fafafa;
+ content: '';
+ transform: translate(-50%);
+ }
+
+ .branch-node-add {
+ position: absolute;
+ top: -18px;
+ left: 50%;
+ z-index: 1;
+ height: 36px;
+ padding: 0 10px;
+ font-size: 12px;
+ line-height: 36px;
+ border: 2px solid #dedede;
+ border-radius: 18px;
+ transform: translateX(-50%);
+ transform-origin: center center;
+ }
+
+ .branch-node-readonly {
+ position: absolute;
+ top: -18px;
+ left: 50%;
+ z-index: 1;
+ display: flex;
+ width: 36px;
+ height: 36px;
+ background-color: #fff;
+ border: 2px solid #dedede;
+ border-radius: 50%;
+ transform: translateX(-50%);
+ align-items: center;
+ justify-content: center;
+ transform-origin: center center;
+
+ &.status-pass {
+ background-color: #e9f4e2;
+ border-color: #6bb63c;
+ }
+
+ &.status-pass:hover {
+ border-color: #6bb63c;
+ }
+
+ .icon-size {
+ font-size: 22px;
+ &.condition {
+ color: #67c23a;
+ }
+ &.parallel {
+ color: #626aef;
+ }
+ &.inclusive {
+ color: #345da2;
+ }
+ }
+ }
+
+ .branch-node-item {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 280px;
+ padding: 40px 40px 0;
+ background: transparent;
+ border-top: 2px solid #dedede;
+ border-bottom: 2px solid #dedede;
+ flex-shrink: 0;
+
+ &::before {
+ position: absolute;
+ width: 2px;
+ height: 100%;
+ margin: auto;
+ inset: 0;
+ background-color: #dedede;
+ content: '';
+ }
+ }
+ // 瑕嗙洊鏉′欢鑺傜偣绗竴涓妭鐐瑰乏涓婅鐨勭嚎
+ .branch-line-first-top {
+ position: absolute;
+ top: -5px;
+ left: -1px;
+ width: 50%;
+ height: 7px;
+ background-color: #fafafa;
+ content: '';
+ }
+ // 瑕嗙洊鏉′欢鑺傜偣绗竴涓妭鐐瑰乏涓嬭鐨勭嚎
+ .branch-line-first-bottom {
+ position: absolute;
+ bottom: -5px;
+ left: -1px;
+ width: 50%;
+ height: 7px;
+ background-color: #fafafa;
+ content: '';
+ }
+ // 瑕嗙洊鏉′欢鑺傜偣鏈�鍚庝竴涓妭鐐瑰彸涓婅鐨勭嚎
+ .branch-line-last-top {
+ position: absolute;
+ top: -5px;
+ right: -1px;
+ width: 50%;
+ height: 7px;
+ background-color: #fafafa;
+ content: '';
+ }
+ // 瑕嗙洊鏉′欢鑺傜偣鏈�鍚庝竴涓妭鐐瑰彸涓嬭鐨勭嚎
+ .branch-line-last-bottom {
+ position: absolute;
+ right: -1px;
+ bottom: -5px;
+ width: 50%;
+ height: 7px;
+ background-color: #fafafa;
+ content: '';
+ }
+ }
+ }
+
+ .node-fixed-name {
+ display: inline-block;
+ width: auto;
+ padding: 0 4px;
+ overflow: hidden;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ // 寮�濮嬭妭鐐瑰寘瑁�
+ .start-node-wrapper {
+ position: relative;
+ margin-top: 16px;
+
+ .start-node-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ .start-node-box {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 90px;
+ height: 36px;
+ padding: 3px 4px;
+ color: #212121;
+ cursor: pointer;
+ background: #fafafa;
+ border-radius: 30px;
+ box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+ box-sizing: border-box;
+ }
+ }
+ }
+
+ // 缁撴潫鑺傜偣鍖呰
+ .end-node-wrapper {
+ margin-bottom: 16px;
+
+ .end-node-box {
+ display: flex;
+ width: 80px;
+ height: 36px;
+ color: #212121;
+ border: 2px solid #fafafa;
+ border-radius: 30px;
+ box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%);
+ box-sizing: border-box;
+ justify-content: center;
+ align-items: center;
+
+ &.status-pass {
+ background-color: #a9da90;
+ border-color: #6bb63c;
+ }
+
+ &.status-pass:hover {
+ border-color: #6bb63c;
+ }
+
+ &.status-reject {
+ background-color: #f6e5e5;
+ border-color: #e47470;
+ }
+
+ &.status-reject:hover {
+ border-color: #e47470;
+ }
+
+ &.status-cancel {
+ background-color: #eaeaeb;
+ border-color: #919398;
+ }
+
+ &.status-cancel:hover {
+ border-color: #919398;
+ }
+ }
+ }
+
+ // 鍙紪杈戠殑 title 杈撳叆妗�
+ .editable-title-input {
+ height: 20px;
+ max-width: 145px;
+ margin-left: 4px;
+ font-size: 12px;
+ line-height: 20px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ &:focus {
+ border-color: #40a9ff;
+ outline: 0;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ }
+}
+
+// iconfont 鏍峰紡
+@font-face {
+ font-family: "iconfont"; /* Project id 4495938 */
+ src: url('iconfont.woff2?t=1737639517142') format('woff2'),
+ url('iconfont.woff?t=1737639517142') format('woff'),
+ url('iconfont.ttf?t=1737639517142') format('truetype');
+}
+
+.iconfont {
+ font-family: "iconfont" !important;
+ font-size: 16px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-trigger:before {
+ content: "\e6d3";
+}
+
+.icon-router:before {
+ content: "\e6b2";
+}
+
+.icon-delay:before {
+ content: "\e600";
+}
+
+.icon-start-user:before {
+ content: "\e679";
+}
+
+.icon-inclusive:before {
+ content: "\e602";
+}
+
+.icon-copy:before {
+ content: "\e7eb";
+}
+
+.icon-transactor:before {
+ content: "\e61c";
+}
+
+.icon-exclusive:before {
+ content: "\e717";
+}
+
+.icon-approve:before {
+ content: "\e715";
+}
+
+.icon-parallel:before {
+ content: "\e688";
+}
+
+.icon-async-child-process:before {
+ content: "\e6f2";
+}
+
+.icon-child-process:before {
+ content: "\e6c1";
+}
diff --git a/src/components/Sticky/index.ts b/src/components/Sticky/index.ts
new file mode 100644
index 0000000..5e1de45
--- /dev/null
+++ b/src/components/Sticky/index.ts
@@ -0,0 +1,3 @@
+import Sticky from './src/Sticky.vue'
+
+export { Sticky }
diff --git a/src/components/Sticky/src/Sticky.vue b/src/components/Sticky/src/Sticky.vue
new file mode 100644
index 0000000..28ecbcb
--- /dev/null
+++ b/src/components/Sticky/src/Sticky.vue
@@ -0,0 +1,143 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { isClient, useEventListener, useWindowSize } from '@vueuse/core'
+import type { CSSProperties } from 'vue'
+
+defineOptions({ name: 'Sticky' })
+
+const props = defineProps({
+ // 璺濈椤堕儴鎴栬�呭簳閮ㄧ殑璺濈(鍗曚綅px)
+ offset: propTypes.number.def(0),
+ // 璁剧疆鍏冪礌鐨勫爢鍙犻『搴�
+ zIndex: propTypes.number.def(999),
+ // 璁剧疆鎸囧畾鐨刢lass
+ className: propTypes.string.def(''),
+ // 瀹氫綅鏂瑰紡锛岄粯璁や负(top)锛岃〃绀鸿窛绂婚《閮ㄤ綅缃紝鍙互璁剧疆涓簍op鎴栬�卋ottom
+ position: {
+ type: String,
+ validator: function (value: string) {
+ return ['top', 'bottom'].indexOf(value) !== -1
+ },
+ default: 'top'
+ }
+})
+const width = ref('auto' as string)
+const height = ref('auto' as string)
+const isSticky = ref(false)
+const refSticky = shallowRef<HTMLElement>()
+const scrollContainer = shallowRef<HTMLElement | Window>()
+const { height: windowHeight } = useWindowSize()
+onMounted(() => {
+ height.value = refSticky.value?.getBoundingClientRect().height + 'px'
+
+ scrollContainer.value = getScrollContainer(refSticky.value!, true)
+ useEventListener(scrollContainer, 'scroll', handleScroll)
+ useEventListener('resize', handleResize)
+ handleScroll()
+})
+onActivated(() => {
+ handleScroll()
+})
+
+const camelize = (str: string): string => {
+ return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
+}
+
+const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
+ if (!isClient || !element || !styleName) return ''
+
+ let key = camelize(styleName)
+ if (key === 'float') key = 'cssFloat'
+ try {
+ const style = element.style[styleName]
+ if (style) return style
+ const computed = document.defaultView?.getComputedStyle(element, '')
+ return computed ? computed[styleName] : ''
+ } catch {
+ return element.style[styleName]
+ }
+}
+const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
+ if (!isClient) return false
+ const key = (
+ {
+ undefined: 'overflow',
+ true: 'overflow-y',
+ false: 'overflow-x'
+ } as const
+ )[String(isVertical)]!
+ const overflow = getStyle(el, key)
+ return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
+}
+
+const getScrollContainer = (
+ el: HTMLElement,
+ isVertical: boolean
+): Window | HTMLElement | undefined => {
+ if (!isClient) return
+ let parent = el
+ while (parent) {
+ if ([window, document, document.documentElement].includes(parent)) return window
+ if (isScroll(parent, isVertical)) return parent
+ parent = parent.parentNode as HTMLElement
+ }
+ return parent
+}
+
+const handleScroll = () => {
+ width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
+ if (props.position === 'top') {
+ const offsetTop = refSticky.value?.getBoundingClientRect().top
+ if (offsetTop !== undefined && offsetTop < props.offset) {
+ sticky()
+ return
+ }
+ reset()
+ } else {
+ const offsetBottom = refSticky.value?.getBoundingClientRect().bottom
+
+ if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
+ sticky()
+ return
+ }
+ reset()
+ }
+}
+const handleResize = () => {
+ if (isSticky.value && refSticky.value) {
+ width.value = refSticky.value.getBoundingClientRect().width + 'px'
+ }
+}
+const sticky = () => {
+ if (isSticky.value) {
+ return
+ }
+ isSticky.value = true
+}
+const reset = () => {
+ if (!isSticky.value) {
+ return
+ }
+ width.value = 'auto'
+ isSticky.value = false
+}
+</script>
+<template>
+ <div ref="refSticky" :style="{ height: height, zIndex: zIndex }">
+ <div
+ :class="className"
+ :style="{
+ top: position === 'top' ? offset + 'px' : '',
+ bottom: position !== 'top' ? offset + 'px' : '',
+ zIndex: zIndex,
+ position: isSticky ? 'fixed' : 'static',
+ width: width,
+ height: height
+ }"
+ >
+ <slot>
+ <div>sticky</div>
+ </slot>
+ </div>
+ </div>
+</template>
diff --git a/src/components/SummaryCard/index.vue b/src/components/SummaryCard/index.vue
new file mode 100644
index 0000000..52da6da
--- /dev/null
+++ b/src/components/SummaryCard/index.vue
@@ -0,0 +1,52 @@
+<template>
+ <div class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4">
+ <div
+ class="h-12 w-12 flex flex-shrink-0 items-center justify-center rounded-1"
+ :class="`${iconColor} ${iconBgColor}`"
+ >
+ <Icon :icon="icon" class="!text-6" />
+ </div>
+ <div class="flex flex-col gap-1">
+ <div class="flex items-center gap-1 text-gray-500">
+ <span class="text-3.5">{{ title }}</span>
+ <el-tooltip :content="tooltip" placement="top-start" v-if="tooltip">
+ <Icon icon="ep:warning" class="item-center flex !text-3" />
+ </el-tooltip>
+ </div>
+ <div class="flex flex-row items-baseline gap-2">
+ <div class="text-7">
+ <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" />
+ </div>
+ <span
+ v-if="percent != undefined"
+ :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'"
+ >
+ <span class="text-sm">{{ Math.abs(toNumber(percent)) }}%</span>
+ <Icon
+ :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'"
+ class="ml-0.5 !text-3"
+ />
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+
+/** 缁熻鍗$墖 */
+defineOptions({ name: 'SummaryCard' })
+
+defineProps({
+ title: propTypes.string.def(''),
+ tooltip: propTypes.string.def(''),
+ icon: propTypes.string.def(''),
+ iconColor: propTypes.string.def(''),
+ iconBgColor: propTypes.string.def(''),
+ prefix: propTypes.string.def(''),
+ value: propTypes.number.def(0),
+ decimals: propTypes.number.def(0),
+ percent: propTypes.oneOfType([Number, String]).def(undefined)
+})
+</script>
diff --git a/src/components/Table/index.ts b/src/components/Table/index.ts
new file mode 100644
index 0000000..9f89317
--- /dev/null
+++ b/src/components/Table/index.ts
@@ -0,0 +1,13 @@
+import Table from './src/Table.vue'
+import { ElTable } from 'element-plus'
+import { TableSetPropsType } from '@/types/table'
+import TableSelectForm from './src/TableSelectForm.vue'
+
+export interface TableExpose {
+ setProps: (props: Recordable) => void
+ setColumn: (columnProps: TableSetPropsType[]) => void
+ selections: Recordable[]
+ elTableRef: ComponentRef<typeof ElTable>
+}
+
+export { Table, TableSelectForm }
diff --git a/src/components/Table/src/Table.vue b/src/components/Table/src/Table.vue
new file mode 100644
index 0000000..e9e50af
--- /dev/null
+++ b/src/components/Table/src/Table.vue
@@ -0,0 +1,311 @@
+<script lang="tsx">
+import { ElTable, ElTableColumn, ElPagination } from 'element-plus'
+import { defineComponent, PropType, ref, computed, unref, watch, onMounted } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { setIndex } from './helper'
+import { getSlot } from '@/utils/tsxHelper'
+import type { TableProps } from './types'
+import { set } from 'lodash-es'
+import { Pagination, TableColumn, TableSetPropsType, TableSlotDefault } from '@/types/table'
+
+export default defineComponent({
+ // eslint-disable-next-line vue/no-reserved-component-names
+ name: 'Table',
+ props: {
+ pageSize: propTypes.number.def(10),
+ currentPage: propTypes.number.def(1),
+ // 鏄惁澶氶��
+ selection: propTypes.bool.def(false),
+ // 鏄惁鎵�鏈夌殑瓒呭嚭闅愯棌锛屼紭鍏堢骇浣庝簬schema涓殑showOverflowTooltip,
+ showOverflowTooltip: propTypes.bool.def(true),
+ // 琛ㄥご
+ columns: {
+ type: Array as PropType<TableColumn[]>,
+ default: () => []
+ },
+ // 灞曞紑琛�
+ expand: propTypes.bool.def(false),
+ // 鏄惁灞曠ず鍒嗛〉
+ pagination: {
+ type: Object as PropType<Pagination>,
+ default: (): Pagination | undefined => undefined
+ },
+ // 浠呭 type=selection 鐨勫垪鏈夋晥锛岀被鍨嬩负 Boolean锛屼负 true 鍒欎細鍦ㄦ暟鎹洿鏂颁箣鍚庝繚鐣欎箣鍓嶉�変腑鐨勬暟鎹紙闇�鎸囧畾 row-key锛�
+ reserveSelection: propTypes.bool.def(false),
+ // 鍔犺浇鐘舵��
+ loading: propTypes.bool.def(false),
+ // 鏄惁鍙犲姞绱㈠紩
+ reserveIndex: propTypes.bool.def(false),
+ // 瀵归綈鏂瑰紡
+ align: propTypes.string
+ .validate((v: string) => ['left', 'center', 'right'].includes(v))
+ .def('center'),
+ // 琛ㄥご瀵归綈鏂瑰紡
+ headerAlign: propTypes.string
+ .validate((v: string) => ['left', 'center', 'right'].includes(v))
+ .def('center'),
+ data: {
+ type: Array as PropType<Recordable[]>,
+ default: () => []
+ }
+ },
+ emits: ['update:pageSize', 'update:currentPage', 'register'],
+ setup(props, { attrs, slots, emit, expose }) {
+ const elTableRef = ref<ComponentRef<typeof ElTable>>()
+
+ // 娉ㄥ唽
+ onMounted(() => {
+ const tableRef = unref(elTableRef)
+ emit('register', tableRef?.$parent, elTableRef.value)
+ })
+
+ const pageSizeRef = ref(props.pageSize)
+
+ const currentPageRef = ref(props.currentPage)
+
+ // useTable浼犲叆鐨刾rops
+ const outsideProps = ref<TableProps>({})
+
+ const mergeProps = ref<TableProps>({})
+
+ const getProps = computed(() => {
+ const propsObj = { ...props }
+ Object.assign(propsObj, unref(mergeProps))
+ return propsObj
+ })
+
+ const setProps = (props: TableProps = {}) => {
+ mergeProps.value = Object.assign(unref(mergeProps), props)
+ outsideProps.value = props
+ }
+
+ const setColumn = (columnProps: TableSetPropsType[], columnsChildren?: TableColumn[]) => {
+ const { columns } = unref(getProps)
+ for (const v of columnsChildren || columns) {
+ for (const item of columnProps) {
+ if (v.field === item.field) {
+ set(v, item.path, item.value)
+ } else if (v.children?.length) {
+ setColumn(columnProps, v.children)
+ }
+ }
+ }
+ }
+
+ const selections = ref<Recordable[]>([])
+
+ const selectionChange = (selection: Recordable[]) => {
+ selections.value = selection
+ }
+
+ expose({
+ setProps,
+ setColumn,
+ selections
+ })
+
+ const pagination = computed(() => {
+ // update by 鑺嬭壙锛氫繚鎸佸拰 Pagination 缁勪欢鐨勯�昏緫涓�鑷�
+ return Object.assign(
+ {
+ small: false,
+ background: true,
+ pagerCount: document.body.clientWidth < 992 ? 5 : 7,
+ layout: 'total, sizes, prev, pager, next, jumper',
+ pageSizes: [10, 20, 30, 50, 100],
+ disabled: false,
+ hideOnSinglePage: false,
+ total: 10
+ },
+ unref(getProps).pagination
+ )
+ })
+
+ watch(
+ () => unref(getProps).pageSize,
+ (val: number) => {
+ pageSizeRef.value = val
+ }
+ )
+
+ watch(
+ () => unref(getProps).currentPage,
+ (val: number) => {
+ currentPageRef.value = val
+ }
+ )
+
+ watch(
+ () => pageSizeRef.value,
+ (val: number) => {
+ emit('update:pageSize', val)
+ }
+ )
+
+ watch(
+ () => currentPageRef.value,
+ (val: number) => {
+ emit('update:currentPage', val)
+ }
+ )
+
+ const getBindValue = computed(() => {
+ const bindValue: Recordable = { ...attrs, ...props }
+ delete bindValue.columns
+ delete bindValue.data
+ return bindValue
+ })
+
+ const renderTableSelection = () => {
+ const { selection, reserveSelection, align, headerAlign } = unref(getProps)
+ // 娓叉煋澶氶��
+ return selection ? (
+ <ElTableColumn
+ type="selection"
+ reserveSelection={reserveSelection}
+ align={align}
+ headerAlign={headerAlign}
+ width="50"
+ ></ElTableColumn>
+ ) : undefined
+ }
+
+ const renderTableExpand = () => {
+ const { align, headerAlign, expand } = unref(getProps)
+ // 娓叉煋灞曞紑琛�
+ return expand ? (
+ <ElTableColumn type="expand" align={align} headerAlign={headerAlign}>
+ {{
+ // @ts-ignore
+ default: (data: TableSlotDefault) => getSlot(slots, 'expand', data)
+ }}
+ </ElTableColumn>
+ ) : undefined
+ }
+
+ const rnderTreeTableColumn = (columnsChildren: TableColumn[]) => {
+ const { align, headerAlign, showOverflowTooltip } = unref(getProps)
+ return columnsChildren.map((v) => {
+ const props = { ...v }
+ if (props.children) delete props.children
+ return (
+ <ElTableColumn
+ showOverflowTooltip={showOverflowTooltip}
+ align={align}
+ headerAlign={headerAlign}
+ {...props}
+ prop={v.field}
+ >
+ {{
+ default: (data: TableSlotDefault) =>
+ v.children && v.children.length
+ ? rnderTableColumn(v.children)
+ : // @ts-ignore
+ getSlot(slots, v.field, data) ||
+ v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
+ data.row[v.field],
+ // @ts-ignore
+ header: getSlot(slots, `${v.field}-header`)
+ }}
+ </ElTableColumn>
+ )
+ })
+ }
+
+ const rnderTableColumn = (columnsChildren?: TableColumn[]) => {
+ const {
+ columns,
+ reserveIndex,
+ pageSize,
+ currentPage,
+ align,
+ headerAlign,
+ showOverflowTooltip
+ } = unref(getProps)
+ return [...[renderTableExpand()], ...[renderTableSelection()]].concat(
+ (columnsChildren || columns).map((v) => {
+ // 鑷畾鐢熸垚搴忓彿
+ if (v.type === 'index') {
+ return (
+ <ElTableColumn
+ type="index"
+ index={
+ v.index
+ ? v.index
+ : (index) => setIndex(reserveIndex, index, pageSize, currentPage)
+ }
+ align={v.align || align}
+ headerAlign={v.headerAlign || headerAlign}
+ label={v.label}
+ width="65px"
+ ></ElTableColumn>
+ )
+ } else {
+ const props = { ...v }
+ if (props.children) delete props.children
+ return (
+ <ElTableColumn
+ showOverflowTooltip={showOverflowTooltip}
+ align={align}
+ headerAlign={headerAlign}
+ {...props}
+ prop={v.field}
+ >
+ {{
+ default: (data: TableSlotDefault) =>
+ v.children && v.children.length
+ ? rnderTreeTableColumn(v.children)
+ : // @ts-ignore
+ getSlot(slots, v.field, data) ||
+ v?.formatter?.(data.row, data.column, data.row[v.field], data.$index) ||
+ data.row[v.field],
+ // @ts-ignore
+ header: () => getSlot(slots, `${v.field}-header`) || v.label
+ }}
+ </ElTableColumn>
+ )
+ }
+ })
+ )
+ }
+
+ return () => (
+ <div v-loading={unref(getProps).loading}>
+ <ElTable
+ // @ts-ignore
+ ref={elTableRef}
+ data={unref(getProps).data}
+ onSelection-change={selectionChange}
+ {...unref(getBindValue)}
+ >
+ {{
+ default: () => rnderTableColumn(),
+ // @ts-ignore
+ append: () => getSlot(slots, 'append')
+ }}
+ </ElTable>
+ {unref(getProps).pagination ? (
+ // update by 鑺嬭壙锛氫繚鎸佸拰 Pagination 缁勪欢涓�鑷�
+ <ElPagination
+ v-model:pageSize={pageSizeRef.value}
+ v-model:currentPage={currentPageRef.value}
+ class="float-right mb-15px mt-15px"
+ {...unref(pagination)}
+ ></ElPagination>
+ ) : undefined}
+ </div>
+ )
+ }
+})
+</script>
+<style lang="scss" scoped>
+:deep(.el-button.is-text) {
+ padding: 8px 4px;
+ margin-left: 0;
+}
+
+:deep(.el-button.is-link) {
+ padding: 8px 4px;
+ margin-left: 0;
+}
+</style>
diff --git a/src/components/Table/src/TableSelectForm.vue b/src/components/Table/src/TableSelectForm.vue
new file mode 100644
index 0000000..5378a1b
--- /dev/null
+++ b/src/components/Table/src/TableSelectForm.vue
@@ -0,0 +1,92 @@
+<!-- 鍒楄〃閫夋嫨閫氱敤缁勪欢锛屽弬鑰� ProductList 缁勪欢浣跨敤 -->
+<!-- TODO 鑺嬭壙锛氬彲鑳戒細绉婚櫎 -->
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" :scroll="true" :title="title" width="60%">
+ <el-table
+ ref="multipleTableRef"
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <slot></slot>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+
+defineOptions({ name: 'TableSelectForm' })
+withDefaults(
+ defineProps<{
+ modelValue: any[]
+ title: string
+ }>(),
+ { modelValue: () => [], title: '閫夋嫨' }
+)
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false)
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+// 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢
+const emits = defineEmits<{
+ (e: 'update:modelValue', v: number[]): void
+}>()
+const multipleTableRef = ref<InstanceType<typeof ElTable>>()
+const multipleSelection = ref<any[]>([])
+const handleSelectionChange = (val: any[]) => {
+ multipleSelection.value = val
+}
+/** 瑙﹀彂 */
+const submitForm = () => {
+ formLoading.value = true
+ try {
+ emits('update:modelValue', multipleSelection.value) // 杩斿洖閫夋嫨鐨勫師濮嬫暟鎹敱浣跨敤鏂瑰鐞�
+ } finally {
+ formLoading.value = false
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+const getList = async (getListFunc: Function) => {
+ loading.value = true
+ try {
+ const data = await getListFunc(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (getListFunc: Function) => {
+ dialogVisible.value = true
+ await nextTick()
+ if (multipleSelection.value.length > 0) {
+ multipleTableRef.value!.clearSelection()
+ }
+ await getList(getListFunc)
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/components/Table/src/helper.ts b/src/components/Table/src/helper.ts
new file mode 100644
index 0000000..d8b34a8
--- /dev/null
+++ b/src/components/Table/src/helper.ts
@@ -0,0 +1,8 @@
+export const setIndex = (reserveIndex: boolean, index: number, size: number, current: number) => {
+ const newIndex = index + 1
+ if (reserveIndex) {
+ return size * (current - 1) + newIndex
+ } else {
+ return newIndex
+ }
+}
diff --git a/src/components/Table/src/types.ts b/src/components/Table/src/types.ts
new file mode 100644
index 0000000..1c7ff76
--- /dev/null
+++ b/src/components/Table/src/types.ts
@@ -0,0 +1,26 @@
+import { Pagination, TableColumn } from '@/types/table'
+
+export type TableProps = {
+ pageSize?: number
+ currentPage?: number
+ // 鏄惁澶氶��
+ selection?: boolean
+ // 鏄惁鎵�鏈夌殑瓒呭嚭闅愯棌锛屼紭鍏堢骇浣庝簬schema涓殑showOverflowTooltip,
+ showOverflowTooltip?: boolean
+ // 琛ㄥご
+ columns?: TableColumn[]
+ // 鏄惁灞曠ず鍒嗛〉
+ pagination?: Pagination | undefined
+ // 浠呭 type=selection 鐨勫垪鏈夋晥锛岀被鍨嬩负 Boolean锛屼负 true 鍒欎細鍦ㄦ暟鎹洿鏂颁箣鍚庝繚鐣欎箣鍓嶉�変腑鐨勬暟鎹紙闇�鎸囧畾 row-key锛�
+ reserveSelection?: boolean
+ // 鍔犺浇鐘舵��
+ loading?: boolean
+ // 鏄惁鍙犲姞绱㈠紩
+ reserveIndex?: boolean
+ // 瀵归綈鏂瑰紡
+ align?: 'left' | 'center' | 'right'
+ // 琛ㄥご瀵归綈鏂瑰紡
+ headerAlign?: 'left' | 'center' | 'right'
+ data?: Recordable
+ expand?: boolean
+} & Recordable
diff --git a/src/components/Tinyflow/Tinyflow.vue b/src/components/Tinyflow/Tinyflow.vue
new file mode 100644
index 0000000..6c4f21c
--- /dev/null
+++ b/src/components/Tinyflow/Tinyflow.vue
@@ -0,0 +1,63 @@
+<template>
+ <div ref="divRef" :class="['tinyflow', className]" :style="style" style="height: 100%"> </div>
+</template>
+
+<script setup lang="ts">
+import { Item, Tinyflow as TinyflowNative } from './ui'
+import './ui/index.css'
+import { onMounted, onUnmounted, ref } from 'vue'
+
+const props = defineProps<{
+ className?: string
+ style?: Record<string, string>
+ data?: Record<string, any>
+ provider?: {
+ llm?: () => Item[] | Promise<Item[]>
+ knowledge?: () => Item[] | Promise<Item[]>
+ internal?: () => Item[] | Promise<Item[]>
+ }
+}>()
+
+const divRef = ref<HTMLDivElement | null>(null)
+let tinyflow: TinyflowNative | null = null
+// 瀹氫箟榛樿鐨� provider 鏂规硶
+const defaultProvider = {
+ llm: () => [] as Item[],
+ knowledge: () => [] as Item[],
+ internal: () => [] as Item[]
+}
+
+onMounted(() => {
+ if (divRef.value) {
+ // 鍚堝苟榛樿 provider 鍜屼紶鍏ョ殑 props.provider
+ const mergedProvider = {
+ ...defaultProvider,
+ ...props.provider
+ }
+ tinyflow = new TinyflowNative({
+ element: divRef.value as Element,
+ data: props.data || {},
+ provider: mergedProvider
+ })
+ }
+})
+
+onUnmounted(() => {
+ if (tinyflow) {
+ tinyflow.destroy()
+ tinyflow = null
+ }
+})
+
+const getData = () => {
+ if (tinyflow) {
+ return tinyflow.getData()
+ }
+ console.warn('Tinyflow instance is not initialized')
+ return null
+}
+
+defineExpose({
+ getData
+})
+</script>
diff --git a/src/components/Tinyflow/ui/index.css b/src/components/Tinyflow/ui/index.css
new file mode 100644
index 0000000..8fa10c2
--- /dev/null
+++ b/src/components/Tinyflow/ui/index.css
@@ -0,0 +1 @@
+.svelte-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgb(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.svelte-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgb(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #777;--xy-background-pattern-lines-color-default: #777;--xy-background-pattern-cross-color-default: #777;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.svelte-flow__background{background-color:var(--xy-background-color, var(--xy-background-color-props, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.svelte-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.svelte-flow__pane{z-index:1}.svelte-flow__pane.draggable{cursor:grab}.svelte-flow__pane.dragging{cursor:grabbing}.svelte-flow__pane.selection{cursor:pointer}.svelte-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.svelte-flow__renderer{z-index:4}.svelte-flow__selection{z-index:6}.svelte-flow__nodesselection-rect:focus,.svelte-flow__nodesselection-rect:focus-visible{outline:none}.svelte-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.svelte-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.svelte-flow .svelte-flow__edges{position:absolute}.svelte-flow .svelte-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.svelte-flow__edge{pointer-events:visibleStroke}.svelte-flow__edge.selectable{cursor:pointer}.svelte-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.svelte-flow__edge.animated path.svelte-flow__edge-interaction{stroke-dasharray:none;animation:none}.svelte-flow__edge.inactive{pointer-events:none}.svelte-flow__edge.selected,.svelte-flow__edge:focus,.svelte-flow__edge:focus-visible{outline:none}.svelte-flow__edge.selected .svelte-flow__edge-path,.svelte-flow__edge.selectable:focus .svelte-flow__edge-path,.svelte-flow__edge.selectable:focus-visible .svelte-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.svelte-flow__edge-textwrapper{pointer-events:all}.svelte-flow__edge .svelte-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.svelte-flow__connection{pointer-events:none}.svelte-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.svelte-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.svelte-flow__nodes{pointer-events:none;transform-origin:0 0}.svelte-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.svelte-flow__node.selectable{cursor:pointer}.svelte-flow__node.draggable{cursor:grab;pointer-events:all}.svelte-flow__node.draggable.dragging{cursor:grabbing}.svelte-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.svelte-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.svelte-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.svelte-flow__handle.connectingfrom{pointer-events:all}.svelte-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.svelte-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.svelte-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.svelte-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.svelte-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.svelte-flow__edgeupdater{cursor:move;pointer-events:all}.svelte-flow__panel{position:absolute;z-index:5;margin:15px}.svelte-flow__panel.top{top:0}.svelte-flow__panel.bottom{bottom:0}.svelte-flow__panel.left{left:0}.svelte-flow__panel.right{right:0}.svelte-flow__panel.center{left:50%;transform:translate(-50%)}.svelte-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.svelte-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.svelte-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.svelte-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.svelte-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.svelte-flow__minimap-svg{display:block}.svelte-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.svelte-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.svelte-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.svelte-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.svelte-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.svelte-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.svelte-flow__controls.horizontal{flex-direction:row}.svelte-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.svelte-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.svelte-flow__edge.updating .svelte-flow__edge-path{stroke:#777}.svelte-flow__edge-text{font-size:10px}.svelte-flow__node.selectable:focus,.svelte-flow__node.selectable:focus-visible{outline:none}.svelte-flow__node-input,.svelte-flow__node-default,.svelte-flow__node-output,.svelte-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.svelte-flow__node-input.selectable:hover,.svelte-flow__node-default.selectable:hover,.svelte-flow__node-output.selectable:hover,.svelte-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.svelte-flow__node-input.selectable.selected,.svelte-flow__node-input.selectable:focus,.svelte-flow__node-input.selectable:focus-visible,.svelte-flow__node-default.selectable.selected,.svelte-flow__node-default.selectable:focus,.svelte-flow__node-default.selectable:focus-visible,.svelte-flow__node-output.selectable.selected,.svelte-flow__node-output.selectable:focus,.svelte-flow__node-output.selectable:focus-visible,.svelte-flow__node-group.selectable.selected,.svelte-flow__node-group.selectable:focus,.svelte-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.svelte-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.svelte-flow__nodesselection-rect,.svelte-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.svelte-flow__nodesselection-rect:focus,.svelte-flow__nodesselection-rect:focus-visible,.svelte-flow__selection:focus,.svelte-flow__selection:focus-visible{outline:none}.svelte-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.svelte-flow__controls-button:disabled{pointer-events:none}.svelte-flow__controls-button:disabled svg{fill-opacity:.4}.svelte-flow__controls-button:last-child{border-bottom:none}.svelte-flow__resize-control{position:absolute}.svelte-flow__resize-control.left,.svelte-flow__resize-control.right{cursor:ew-resize}.svelte-flow__resize-control.top,.svelte-flow__resize-control.bottom{cursor:ns-resize}.svelte-flow__resize-control.top.left,.svelte-flow__resize-control.bottom.right{cursor:nwse-resize}.svelte-flow__resize-control.bottom.left,.svelte-flow__resize-control.top.right{cursor:nesw-resize}.svelte-flow__resize-control.handle{width:4px;height:4px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));transform:translate(-50%,-50%)}.svelte-flow__resize-control.handle.left{left:0;top:50%}.svelte-flow__resize-control.handle.right{left:100%;top:50%}.svelte-flow__resize-control.handle.top{left:50%;top:0}.svelte-flow__resize-control.handle.bottom{left:50%;top:100%}.svelte-flow__resize-control.handle.top.left,.svelte-flow__resize-control.handle.bottom.left{left:0}.svelte-flow__resize-control.handle.top.right,.svelte-flow__resize-control.handle.bottom.right{left:100%}.svelte-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.svelte-flow__resize-control.line.left,.svelte-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.svelte-flow__resize-control.line.left{left:0;border-left-width:1px}.svelte-flow__resize-control.line.right{left:100%;border-right-width:1px}.svelte-flow__resize-control.line.top,.svelte-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.svelte-flow__resize-control.line.top{top:0;border-top-width:1px}.svelte-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.svelte-flow__edge-label{text-align:center;position:absolute;padding:2px;font-size:10px;cursor:pointer;color:var(--xy-edge-label-color, var(--xy-edge-label-color-default));background:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.svelte-flow__nodes,.svelte-flow__edgelabel-renderer{z-index:0}:root,:root .tf-theme-light{--tf-primary-color: #2563EB;--xy-node-boxshadow-selected: 0 0 0 1px var(--tf-primary-color);--xy-handle-background-color: var(--tf-primary-color)}.tf-btn{display:flex;align-items:center;justify-content:center;gap:2px;background:#fff;border:1px solid #ccc;cursor:pointer;border-radius:5px;padding:5px;margin:0;height:fit-content;width:fit-content}.tf-btn svg{fill:currentColor;width:16px;height:16px}.tf-btn:hover{border:1px solid var(--tf-primary-color)}.tf-input,.tf-textarea{display:flex;border-radius:5px;border:1px solid #ccc;padding:5px 10px;box-sizing:border-box;resize:vertical;outline:none}.tf-input::placeholder,.tf-textarea::placeholder{color:#ccc}.tf-input:focus,.tf-textarea:focus{border-color:var(--tf-primary-color);box-shadow:0 0 5px #51cbee33}.tf-input[disabled],.tf-textarea[disabled]{background-color:#f0f0f0;cursor:not-allowed;color:#aaa}.tf-select-input{display:flex;border:1px solid #ccc;padding:3px 10px;border-radius:5px;font-size:14px;justify-content:space-between;align-items:center;cursor:pointer;background:#fff;height:27px}.tf-select-input:focus{border-color:var(--tf-primary-color);box-shadow:0 0 5px #51cbee33}.tf-select-input-value{height:21px;min-width:10px;font-size:12px;display:flex;align-items:center}.tf-select-input-arrow{display:block;width:16px;height:16px;color:#666}.tf-select-input-placeholder{color:#ccc}.tf-select-content{display:flex;flex-direction:column;background:#fff;margin-top:5px;border:1px solid #ccc;border-radius:5px;padding:5px;width:max-content;min-width:100%;z-index:9999;box-sizing:border-box}.tf-select-content-item{display:flex;align-items:center;padding:5px 10px;border:none;background:#fff;border-radius:5px;cursor:pointer;line-height:100%;gap:2px}.tf-select-content-item span{width:16px;display:flex}.tf-select-content-item svg{width:16px;height:16px;margin:auto}.tf-select-content-item:hover{background:#f0f0f0}.tf-select-content-children{padding-left:14px}.tf-checkbox{width:14px;height:14px}.tf-tabs{display:flex;align-items:center;justify-content:center;gap:5px;padding:5px;border-radius:5px;border:none;background:#f4f4f5}.tf-tabs .tf-tabs-item{flex-grow:1;padding:5px 10px;cursor:pointer;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:14px;color:#808088}.tf-tabs .tf-tabs-item.active{background:#fff;color:#333;font-weight:500;box-shadow:0 0 5px #00000026}h3.tf-heading{font-weight:700;font-size:14px;margin-top:2px;margin-bottom:3px;color:#333}.tf-collapse{border:none;border-radius:5px}.tf-collapse-item-title{display:flex;align-items:center;cursor:pointer;font-size:14px}.tf-collapse-item-title-icon{display:flex;width:26px;height:26px;color:#2563eb;background:#cedafb;border-radius:5px;padding:3px;justify-content:center;align-items:center;margin-right:10px}.tf-collapse-item-title-icon svg{width:22px;height:22px;color:#3474ff}.tf-collapse-item-title-arrow{display:block;width:16px;height:16px;margin-left:auto}.tf-collapse-item-description{font-size:12px;margin:10px 0;color:#999}.svelte-flow__nodes .svelte-flow__node{border:3px solid transparent;border-radius:5px;box-sizing:border-box}.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle{width:16px;height:16px;background:transparent;display:flex;justify-content:center;align-items:center;border:none}.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle:after{content:" ";background:#2563eb;width:8px;height:8px;border-radius:100%;transition:width .1s,height .1s}.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle:hover:after{width:16px;height:16px}.svelte-flow__nodes .svelte-flow__node div.loop_handle_wrapper:after{content:"寰幆浣�";background:#2563eb;width:100px;height:20px;border-radius:0;display:flex;color:#fff;justify-content:center;align-items:center}.svelte-flow__nodes .svelte-flow__node div.loop_handle_wrapper:hover:after{width:100px;height:20px}.svelte-flow__nodes .svelte-flow__node:after{content:" ";position:absolute;border-radius:5px;top:-2px;left:-2px;border:1px solid #ccc;height:calc(100% + 2px);width:calc(100% + 2px)}.svelte-flow__nodes .svelte-flow__node:hover{border:3px solid #bacaef7d}.svelte-flow__nodes .svelte-flow__node.selectable.selected{border:3px solid #bacaef7d;box-shadow:var(--xy-node-boxshadow-selected)}.svelte-flow__nodes .svelte-flow__node:hover:after{display:none}.svelte-flow__nodes .svelte-flow__node.selectable.selected:after{display:none}.tf-node-wrapper{border-radius:5px;min-width:300px;background:#fff}.tf-node-wrapper-title{height:30px;background:#eff1f5;color:#bcbcbc;font-size:12px;display:flex;align-items:center;padding-left:5px;border-bottom:1px solid #ccc;font-weight:300;letter-spacing:1px}.tf-node-wrapper-body{padding:10px}.svelte-flow__attribution a{display:none}.tf-toolbar{position:absolute;top:10px;left:10px;z-index:9999;display:flex;gap:5px;transition:transform .5s ease,opacity .5s ease;transform:translate(-220px)}.tf-toolbar.show{transform:translate(0)}.tf-toolbar-container{background:#fff;border:1px solid #eee;border-radius:5px;box-shadow:0 0 5px #0000001a;padding:10px;width:180px}.tf-toolbar-container-header{display:flex}.tf-toolbar-container-body{display:flex;margin-top:20px}.tf-toolbar-container-body .tf-toolbar-container-base,.tf-toolbar-container-body .tf-toolbar-container-tools{display:flex;flex-direction:column;gap:4px;flex-grow:1}.tf-toolbar-container-body .tf-toolbar-container-base .tf-btn,.tf-toolbar-container-body .tf-toolbar-container-tools .tf-btn{border:none;width:100%;justify-content:flex-start;height:40px;gap:10px;cursor:grabbing;border-radius:5px}.tf-toolbar-container-body .tf-toolbar-container-base .tf-btn svg,.tf-toolbar-container-body .tf-toolbar-container-tools .tf-btn svg{width:20px;height:20px;fill:#2563eb}.tf-toolbar-container-body .tf-toolbar-container-base .tf-btn:hover,.tf-toolbar-container-body .tf-toolbar-container-tools .tf-btn:hover{background:#f1f1f1}.tinyflow-logo:after{content:"Tinyflow.ai";font-size:145px;display:flex;align-items:center;justify-content:center;width:100%;height:100%;font-weight:800;color:#03153b54;text-shadow:1px 3px 6px #cedafb,0 0 0 #000,1px 3px 6px #fff;opacity:.1}
diff --git a/src/components/Tinyflow/ui/index.d.ts b/src/components/Tinyflow/ui/index.d.ts
new file mode 100644
index 0000000..38a3132
--- /dev/null
+++ b/src/components/Tinyflow/ui/index.d.ts
@@ -0,0 +1,41 @@
+import { Edge } from '@xyflow/svelte';
+import { Node as Node_2 } from '@xyflow/svelte';
+import { useSvelteFlow } from '@xyflow/svelte';
+import { Viewport } from '@xyflow/svelte';
+
+export declare type Item = {
+ value: number | string;
+ label: string;
+ children?: Item[];
+};
+
+export declare class Tinyflow {
+ private options;
+ private rootEl;
+ private svelteFlowInstance;
+ constructor(options: TinyflowOptions);
+ private _init;
+ private _setOptions;
+ getOptions(): TinyflowOptions;
+ getData(): {
+ nodes: Node_2[];
+ edges: Edge[];
+ viewport: Viewport;
+ };
+ setData(data: TinyflowData): void;
+ destroy(): void;
+}
+
+export declare type TinyflowData = Partial<ReturnType<ReturnType<typeof useSvelteFlow>['toObject']>>;
+
+export declare type TinyflowOptions = {
+ element: string | Element;
+ data?: TinyflowData;
+ provider?: {
+ llm?: () => Item[] | Promise<Item[]>;
+ knowledge?: () => Item[] | Promise<Item[]>;
+ internal?: () => Item[] | Promise<Item[]>;
+ };
+};
+
+export { }
diff --git a/src/components/Tinyflow/ui/index.js b/src/components/Tinyflow/ui/index.js
new file mode 100644
index 0000000..80e77b5
--- /dev/null
+++ b/src/components/Tinyflow/ui/index.js
@@ -0,0 +1,16984 @@
+var tf = Object.defineProperty;
+var Pa = (e) => {
+ throw TypeError(e);
+};
+var nf = (e, t, n) => t in e ? tf(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
+var wt = (e, t, n) => nf(e, typeof t != "symbol" ? t + "" : t, n), Ji = (e, t, n) => t.has(e) || Pa("Cannot " + n);
+var it = (e, t, n) => (Ji(e, t, "read from private field"), n ? n.call(e) : t.get(e)), rr = (e, t, n) => t.has(e) ? Pa("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, n), Gr = (e, t, n, r) => (Ji(e, t, "write to private field"), r ? r.call(e, n) : t.set(e, n), n), Na = (e, t, n) => (Ji(e, t, "access private method"), n);
+const rf = "5";
+var Ll;
+typeof window < "u" && ((Ll = window.__svelte ?? (window.__svelte = {})).v ?? (Ll.v = /* @__PURE__ */ new Set())).add(rf);
+let Br = !1, of = !1;
+function sf() {
+ Br = !0;
+}
+sf();
+const Os = 1, Is = 2, Ol = 4, af = 8, lf = 16, uf = 1, cf = 2, Il = 4, df = 8, ff = 16, zl = 1, gf = 2, zs = "[", Rs = "[!", Bs = "]", _r = {}, Pt = Symbol(), Rl = "http://www.w3.org/2000/svg", Ma = !1, nn = 2, Bl = 4, Si = 8, Ys = 16, On = 32, Yr = 64, ti = 128, qt = 256, ni = 512, mt = 1024, In = 2048, gr = 4096, Mn = 8192, Pi = 16384, hf = 32768, Zr = 65536, vf = 1 << 17, pf = 1 << 19, Yl = 1 << 20, Wn = Symbol("$state"), Zs = Symbol("legacy props"), mf = Symbol("");
+var Co = Array.isArray, yf = Array.prototype.indexOf, Xs = Array.from, ri = Object.keys, so = Object.defineProperty, Tn = Object.getOwnPropertyDescriptor, Zl = Object.getOwnPropertyDescriptors, wf = Object.prototype, _f = Array.prototype, Fs = Object.getPrototypeOf;
+function Ur(e) {
+ return typeof e == "function";
+}
+const dt = () => {
+};
+function xf(e) {
+ return e();
+}
+function ao(e) {
+ for (var t = 0; t < e.length; t++)
+ e[t]();
+}
+const bf = typeof requestIdleCallback > "u" ? (e) => setTimeout(e, 1) : requestIdleCallback;
+let lo = [], uo = [];
+function Xl() {
+ var e = lo;
+ lo = [], ao(e);
+}
+function Fl() {
+ var e = uo;
+ uo = [], ao(e);
+}
+function ko(e) {
+ lo.length === 0 && queueMicrotask(Xl), lo.push(e);
+}
+function Cf(e) {
+ uo.length === 0 && bf(Fl), uo.push(e);
+}
+function Ta() {
+ lo.length > 0 && Xl(), uo.length > 0 && Fl();
+}
+function Wl(e) {
+ return e === this.v;
+}
+function Ws(e, t) {
+ return e != e ? t == t : e !== t || e !== null && typeof e == "object" || typeof e == "function";
+}
+function Ks(e) {
+ return !Ws(e, this.v);
+}
+function kf(e) {
+ throw new Error("https://svelte.dev/e/effect_in_teardown");
+}
+function $f() {
+ throw new Error("https://svelte.dev/e/effect_in_unowned_derived");
+}
+function Ef(e) {
+ throw new Error("https://svelte.dev/e/effect_orphan");
+}
+function Sf() {
+ throw new Error("https://svelte.dev/e/effect_update_depth_exceeded");
+}
+function Pf() {
+ throw new Error("https://svelte.dev/e/hydration_failed");
+}
+function Nf(e) {
+ throw new Error("https://svelte.dev/e/props_invalid_value");
+}
+function Mf() {
+ throw new Error("https://svelte.dev/e/state_descriptors_fixed");
+}
+function Tf() {
+ throw new Error("https://svelte.dev/e/state_prototype_fixed");
+}
+function Hf() {
+ throw new Error("https://svelte.dev/e/state_unsafe_local_read");
+}
+function Vf() {
+ throw new Error("https://svelte.dev/e/state_unsafe_mutation");
+}
+function Mt(e, t) {
+ var n = {
+ f: 0,
+ // TODO ideally we could skip this altogether, but it causes type errors
+ v: e,
+ reactions: null,
+ equals: Wl,
+ rv: 0,
+ wv: 0
+ };
+ return n;
+}
+function Un(e) {
+ return /* @__PURE__ */ Kl(Mt(e));
+}
+// @__NO_SIDE_EFFECTS__
+function $o(e, t = !1) {
+ var r;
+ const n = Mt(e);
+ return t || (n.equals = Ks), Br && Ze !== null && Ze.l !== null && ((r = Ze.l).s ?? (r.s = [])).push(n), n;
+}
+function re(e, t = !1) {
+ return /* @__PURE__ */ Kl(/* @__PURE__ */ $o(e, t));
+}
+// @__NO_SIDE_EFFECTS__
+function Kl(e) {
+ return je !== null && !en && je.f & nn && (vn === null ? Lf([e]) : vn.push(e)), e;
+}
+function U(e, t) {
+ return je !== null && !en && Di() && je.f & (nn | Ys) && // If the source was created locally within the current derived, then
+ // we allow the mutation.
+ (vn === null || !vn.includes(e)) && Vf(), gs(e, t);
+}
+function gs(e, t) {
+ return e.equals(t) || (e.v, e.v = t, e.wv = tu(), ql(e, In), Di() && qe !== null && qe.f & mt && !(qe.f & (On | Yr)) && (En === null ? Of([e]) : En.push(e))), t;
+}
+function Ha(e, t = 1) {
+ var n = h(e), r = t === 1 ? n++ : n--;
+ return U(e, n), r;
+}
+function ql(e, t) {
+ var n = e.reactions;
+ if (n !== null)
+ for (var r = Di(), o = n.length, i = 0; i < o; i++) {
+ var s = n[i], a = s.f;
+ a & In || !r && s === qe || (rn(s, t), a & (mt | qt) && (a & nn ? ql(
+ /** @type {Derived} */
+ s,
+ gr
+ ) : Hi(
+ /** @type {Effect} */
+ s
+ )));
+ }
+}
+// @__NO_SIDE_EFFECTS__
+function Me(e) {
+ var t = nn | In, n = je !== null && je.f & nn ? (
+ /** @type {Derived} */
+ je
+ ) : null;
+ return qe === null || n !== null && n.f & qt ? t |= qt : qe.f |= Yl, {
+ ctx: Ze,
+ deps: null,
+ effects: null,
+ equals: Wl,
+ f: t,
+ fn: e,
+ reactions: null,
+ rv: 0,
+ v: (
+ /** @type {V} */
+ null
+ ),
+ wv: 0,
+ parent: n ?? qe
+ };
+}
+// @__NO_SIDE_EFFECTS__
+function pe(e) {
+ const t = /* @__PURE__ */ Me(e);
+ return t.equals = Ks, t;
+}
+function Gl(e) {
+ var t = e.effects;
+ if (t !== null) {
+ e.effects = null;
+ for (var n = 0; n < t.length; n += 1)
+ Gt(
+ /** @type {Effect} */
+ t[n]
+ );
+ }
+}
+function Df(e) {
+ for (var t = e.parent; t !== null; ) {
+ if (!(t.f & nn))
+ return (
+ /** @type {Effect} */
+ t
+ );
+ t = t.parent;
+ }
+ return null;
+}
+function Af(e) {
+ var t, n = qe;
+ Jn(Df(e));
+ try {
+ Gl(e), t = ru(e);
+ } finally {
+ Jn(n);
+ }
+ return t;
+}
+function Ul(e) {
+ var t = Af(e), n = (Xn || e.f & qt) && e.deps !== null ? gr : mt;
+ rn(e, n), e.equals(t) || (e.v = t, e.wv = tu());
+}
+function Ni(e) {
+ console.warn("https://svelte.dev/e/hydration_mismatch");
+}
+let Pe = !1;
+function It(e) {
+ Pe = e;
+}
+let De;
+function Ct(e) {
+ if (e === null)
+ throw Ni(), _r;
+ return De = e;
+}
+function yn() {
+ return Ct(
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(De)
+ );
+}
+function Z(e) {
+ if (Pe) {
+ if (/* @__PURE__ */ xn(De) !== null)
+ throw Ni(), _r;
+ De = e;
+ }
+}
+function Se(e = 1) {
+ if (Pe) {
+ for (var t = e, n = De; t--; )
+ n = /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(n);
+ De = n;
+ }
+}
+function hs() {
+ for (var e = 0, t = De; ; ) {
+ if (t.nodeType === 8) {
+ var n = (
+ /** @type {Comment} */
+ t.data
+ );
+ if (n === Bs) {
+ if (e === 0) return t;
+ e -= 1;
+ } else (n === zs || n === Rs) && (e += 1);
+ }
+ var r = (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(t)
+ );
+ t.remove(), t = r;
+ }
+}
+function Tt(e, t = null, n) {
+ if (typeof e != "object" || e === null || Wn in e)
+ return e;
+ const r = Fs(e);
+ if (r !== wf && r !== _f)
+ return e;
+ var o = /* @__PURE__ */ new Map(), i = Co(e), s = Mt(0);
+ i && o.set("length", Mt(
+ /** @type {any[]} */
+ e.length
+ ));
+ var a;
+ return new Proxy(
+ /** @type {any} */
+ e,
+ {
+ defineProperty(l, u, c) {
+ (!("value" in c) || c.configurable === !1 || c.enumerable === !1 || c.writable === !1) && Mf();
+ var f = o.get(u);
+ return f === void 0 ? (f = Mt(c.value), o.set(u, f)) : U(f, Tt(c.value, a)), !0;
+ },
+ deleteProperty(l, u) {
+ var c = o.get(u);
+ if (c === void 0)
+ u in l && o.set(u, Mt(Pt));
+ else {
+ if (i && typeof u == "string") {
+ var f = (
+ /** @type {Source<number>} */
+ o.get("length")
+ ), d = Number(u);
+ Number.isInteger(d) && d < f.v && U(f, d);
+ }
+ U(c, Pt), Va(s);
+ }
+ return !0;
+ },
+ get(l, u, c) {
+ var p;
+ if (u === Wn)
+ return e;
+ var f = o.get(u), d = u in l;
+ if (f === void 0 && (!d || (p = Tn(l, u)) != null && p.writable) && (f = Mt(Tt(d ? l[u] : Pt, a)), o.set(u, f)), f !== void 0) {
+ var g = h(f);
+ return g === Pt ? void 0 : g;
+ }
+ return Reflect.get(l, u, c);
+ },
+ getOwnPropertyDescriptor(l, u) {
+ var c = Reflect.getOwnPropertyDescriptor(l, u);
+ if (c && "value" in c) {
+ var f = o.get(u);
+ f && (c.value = h(f));
+ } else if (c === void 0) {
+ var d = o.get(u), g = d == null ? void 0 : d.v;
+ if (d !== void 0 && g !== Pt)
+ return {
+ enumerable: !0,
+ configurable: !0,
+ value: g,
+ writable: !0
+ };
+ }
+ return c;
+ },
+ has(l, u) {
+ var g;
+ if (u === Wn)
+ return !0;
+ var c = o.get(u), f = c !== void 0 && c.v !== Pt || Reflect.has(l, u);
+ if (c !== void 0 || qe !== null && (!f || (g = Tn(l, u)) != null && g.writable)) {
+ c === void 0 && (c = Mt(f ? Tt(l[u], a) : Pt), o.set(u, c));
+ var d = h(c);
+ if (d === Pt)
+ return !1;
+ }
+ return f;
+ },
+ set(l, u, c, f) {
+ var _;
+ var d = o.get(u), g = u in l;
+ if (i && u === "length")
+ for (var p = c; p < /** @type {Source<number>} */
+ d.v; p += 1) {
+ var x = o.get(p + "");
+ x !== void 0 ? U(x, Pt) : p in l && (x = Mt(Pt), o.set(p + "", x));
+ }
+ d === void 0 ? (!g || (_ = Tn(l, u)) != null && _.writable) && (d = Mt(void 0), U(d, Tt(c, a)), o.set(u, d)) : (g = d.v !== Pt, U(d, Tt(c, a)));
+ var C = Reflect.getOwnPropertyDescriptor(l, u);
+ if (C != null && C.set && C.set.call(f, c), !g) {
+ if (i && typeof u == "string") {
+ var $ = (
+ /** @type {Source<number>} */
+ o.get("length")
+ ), m = Number(u);
+ Number.isInteger(m) && m >= $.v && U($, m + 1);
+ }
+ Va(s);
+ }
+ return !0;
+ },
+ ownKeys(l) {
+ h(s);
+ var u = Reflect.ownKeys(l).filter((d) => {
+ var g = o.get(d);
+ return g === void 0 || g.v !== Pt;
+ });
+ for (var [c, f] of o)
+ f.v !== Pt && !(c in l) && u.push(c);
+ return u;
+ },
+ setPrototypeOf() {
+ Tf();
+ }
+ }
+ );
+}
+function Va(e, t = 1) {
+ U(e, e.v + t);
+}
+var Nt, jl, Jl, Ql;
+function vs() {
+ if (Nt === void 0) {
+ Nt = window, jl = /Firefox/.test(navigator.userAgent);
+ var e = Element.prototype, t = Node.prototype;
+ Jl = Tn(t, "firstChild").get, Ql = Tn(t, "nextSibling").get, e.__click = void 0, e.__className = void 0, e.__attributes = null, e.__styles = null, e.__e = void 0, Text.prototype.__t = void 0;
+ }
+}
+function Vn(e = "") {
+ return document.createTextNode(e);
+}
+// @__NO_SIDE_EFFECTS__
+function bt(e) {
+ return Jl.call(e);
+}
+// @__NO_SIDE_EFFECTS__
+function xn(e) {
+ return Ql.call(e);
+}
+function X(e, t) {
+ if (!Pe)
+ return /* @__PURE__ */ bt(e);
+ var n = (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ bt(De)
+ );
+ if (n === null)
+ n = De.appendChild(Vn());
+ else if (t && n.nodeType !== 3) {
+ var r = Vn();
+ return n == null || n.before(r), Ct(r), r;
+ }
+ return Ct(n), n;
+}
+function be(e, t) {
+ if (!Pe) {
+ var n = (
+ /** @type {DocumentFragment} */
+ /* @__PURE__ */ bt(
+ /** @type {Node} */
+ e
+ )
+ );
+ return n instanceof Comment && n.data === "" ? /* @__PURE__ */ xn(n) : n;
+ }
+ return De;
+}
+function z(e, t = 1, n = !1) {
+ let r = Pe ? De : e;
+ for (var o; t--; )
+ o = r, r = /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(r);
+ if (!Pe)
+ return r;
+ var i = r == null ? void 0 : r.nodeType;
+ if (n && i !== 3) {
+ var s = Vn();
+ return r === null ? o == null || o.after(s) : r.before(s), Ct(s), s;
+ }
+ return Ct(r), /** @type {TemplateNode} */
+ r;
+}
+function qs(e) {
+ e.textContent = "";
+}
+let qo = !1, oi = !1, ii = null, ir = !1, Gs = !1;
+function Da(e) {
+ Gs = e;
+}
+let oo = [];
+let je = null, en = !1;
+function jn(e) {
+ je = e;
+}
+let qe = null;
+function Jn(e) {
+ qe = e;
+}
+let vn = null;
+function Lf(e) {
+ vn = e;
+}
+let _t = null, Lt = 0, En = null;
+function Of(e) {
+ En = e;
+}
+let eu = 1, si = 0, Xn = !1;
+function tu() {
+ return ++eu;
+}
+function Xr(e) {
+ var f;
+ var t = e.f;
+ if (t & In)
+ return !0;
+ if (t & gr) {
+ var n = e.deps, r = (t & qt) !== 0;
+ if (n !== null) {
+ var o, i, s = (t & ni) !== 0, a = r && qe !== null && !Xn, l = n.length;
+ if (s || a) {
+ var u = (
+ /** @type {Derived} */
+ e
+ ), c = u.parent;
+ for (o = 0; o < l; o++)
+ i = n[o], (s || !((f = i == null ? void 0 : i.reactions) != null && f.includes(u))) && (i.reactions ?? (i.reactions = [])).push(u);
+ s && (u.f ^= ni), a && c !== null && !(c.f & qt) && (u.f ^= qt);
+ }
+ for (o = 0; o < l; o++)
+ if (i = n[o], Xr(
+ /** @type {Derived} */
+ i
+ ) && Ul(
+ /** @type {Derived} */
+ i
+ ), i.wv > e.wv)
+ return !0;
+ }
+ (!r || qe !== null && !Xn) && rn(e, mt);
+ }
+ return !1;
+}
+function If(e, t) {
+ for (var n = t; n !== null; ) {
+ if (n.f & ti)
+ try {
+ n.fn(e);
+ return;
+ } catch {
+ n.f ^= ti;
+ }
+ n = n.parent;
+ }
+ throw qo = !1, e;
+}
+function zf(e) {
+ return (e.f & Pi) === 0 && (e.parent === null || (e.parent.f & ti) === 0);
+}
+function Mi(e, t, n, r) {
+ if (qo) {
+ if (n === null && (qo = !1), zf(t))
+ throw e;
+ return;
+ }
+ n !== null && (qo = !0);
+ {
+ If(e, t);
+ return;
+ }
+}
+function nu(e, t, n = !0) {
+ var r = e.reactions;
+ if (r !== null)
+ for (var o = 0; o < r.length; o++) {
+ var i = r[o];
+ i.f & nn ? nu(
+ /** @type {Derived} */
+ i,
+ t,
+ !1
+ ) : t === i && (n ? rn(i, In) : i.f & mt && rn(i, gr), Hi(
+ /** @type {Effect} */
+ i
+ ));
+ }
+}
+function ru(e) {
+ var g;
+ var t = _t, n = Lt, r = En, o = je, i = Xn, s = vn, a = Ze, l = en, u = e.f;
+ _t = /** @type {null | Value[]} */
+ null, Lt = 0, En = null, Xn = (u & qt) !== 0 && (en || !ir || je === null), je = u & (On | Yr) ? null : e, vn = null, Aa(e.ctx), en = !1, si++;
+ try {
+ var c = (
+ /** @type {Function} */
+ (0, e.fn)()
+ ), f = e.deps;
+ if (_t !== null) {
+ var d;
+ if (ai(e, Lt), f !== null && Lt > 0)
+ for (f.length = Lt + _t.length, d = 0; d < _t.length; d++)
+ f[Lt + d] = _t[d];
+ else
+ e.deps = f = _t;
+ if (!Xn)
+ for (d = Lt; d < f.length; d++)
+ ((g = f[d]).reactions ?? (g.reactions = [])).push(e);
+ } else f !== null && Lt < f.length && (ai(e, Lt), f.length = Lt);
+ if (Di() && En !== null && !en && f !== null && !(e.f & (nn | gr | In)))
+ for (d = 0; d < /** @type {Source[]} */
+ En.length; d++)
+ nu(
+ En[d],
+ /** @type {Effect} */
+ e
+ );
+ return o !== null && si++, c;
+ } finally {
+ _t = t, Lt = n, En = r, je = o, Xn = i, vn = s, Aa(a), en = l;
+ }
+}
+function Rf(e, t) {
+ let n = t.reactions;
+ if (n !== null) {
+ var r = yf.call(n, e);
+ if (r !== -1) {
+ var o = n.length - 1;
+ o === 0 ? n = t.reactions = null : (n[r] = n[o], n.pop());
+ }
+ }
+ n === null && t.f & nn && // Destroying a child effect while updating a parent effect can cause a dependency to appear
+ // to be unused, when in fact it is used by the currently-updating parent. Checking `new_deps`
+ // allows us to skip the expensive work of disconnecting and immediately reconnecting it
+ (_t === null || !_t.includes(t)) && (rn(t, gr), t.f & (qt | ni) || (t.f ^= ni), Gl(
+ /** @type {Derived} **/
+ t
+ ), ai(
+ /** @type {Derived} **/
+ t,
+ 0
+ ));
+}
+function ai(e, t) {
+ var n = e.deps;
+ if (n !== null)
+ for (var r = t; r < n.length; r++)
+ Rf(e, n[r]);
+}
+function Ti(e) {
+ var t = e.f;
+ if (!(t & Pi)) {
+ rn(e, mt);
+ var n = qe, r = Ze, o = ir;
+ qe = e, ir = !0;
+ try {
+ t & Ys ? Gf(e) : lu(e), au(e);
+ var i = ru(e);
+ e.teardown = typeof i == "function" ? i : null, e.wv = eu;
+ var s = e.deps, a;
+ Ma && of && e.f & In;
+ } catch (l) {
+ Mi(l, e, n, r || e.ctx);
+ } finally {
+ ir = o, qe = n;
+ }
+ }
+}
+function Bf() {
+ try {
+ Sf();
+ } catch (e) {
+ if (ii !== null)
+ Mi(e, ii, null);
+ else
+ throw e;
+ }
+}
+function ou() {
+ var e = ir;
+ try {
+ var t = 0;
+ for (ir = !0; oo.length > 0; ) {
+ t++ > 1e3 && Bf();
+ var n = oo, r = n.length;
+ oo = [];
+ for (var o = 0; o < r; o++) {
+ var i = n[o];
+ i.f & mt || (i.f ^= mt);
+ var s = Zf(i);
+ Yf(s);
+ }
+ }
+ } finally {
+ oi = !1, ir = e, ii = null;
+ }
+}
+function Yf(e) {
+ var t = e.length;
+ if (t !== 0)
+ for (var n = 0; n < t; n++) {
+ var r = e[n];
+ if (!(r.f & (Pi | Mn)))
+ try {
+ Xr(r) && (Ti(r), r.deps === null && r.first === null && r.nodes_start === null && (r.teardown === null ? uu(r) : r.fn = null));
+ } catch (o) {
+ Mi(o, r, null, r.ctx);
+ }
+ }
+}
+function Hi(e) {
+ oi || (oi = !0, queueMicrotask(ou));
+ for (var t = ii = e; t.parent !== null; ) {
+ t = t.parent;
+ var n = t.f;
+ if (n & (Yr | On)) {
+ if (!(n & mt)) return;
+ t.f ^= mt;
+ }
+ }
+ oo.push(t);
+}
+function Zf(e) {
+ for (var t = [], n = e.first; n !== null; ) {
+ var r = n.f, o = (r & On) !== 0, i = o && (r & mt) !== 0;
+ if (!i && !(r & Mn)) {
+ if (r & Bl)
+ t.push(n);
+ else if (o)
+ n.f ^= mt;
+ else {
+ var s = je;
+ try {
+ je = n, Xr(n) && Ti(n);
+ } catch (u) {
+ Mi(u, n, null, n.ctx);
+ } finally {
+ je = s;
+ }
+ }
+ var a = n.first;
+ if (a !== null) {
+ n = a;
+ continue;
+ }
+ }
+ var l = n.parent;
+ for (n = n.next; n === null && l !== null; )
+ n = l.next, l = l.parent;
+ }
+ return t;
+}
+function y(e) {
+ var t;
+ for (Ta(); oo.length > 0; )
+ oi = !0, ou(), Ta();
+ return (
+ /** @type {T} */
+ t
+ );
+}
+function h(e) {
+ var t = e.f, n = (t & nn) !== 0;
+ if (je !== null && !en) {
+ vn !== null && vn.includes(e) && Hf();
+ var r = je.deps;
+ e.rv < si && (e.rv = si, _t === null && r !== null && r[Lt] === e ? Lt++ : _t === null ? _t = [e] : (!Xn || !_t.includes(e)) && _t.push(e));
+ } else if (n && /** @type {Derived} */
+ e.deps === null && /** @type {Derived} */
+ e.effects === null) {
+ var o = (
+ /** @type {Derived} */
+ e
+ ), i = o.parent;
+ i !== null && !(i.f & qt) && (o.f ^= qt);
+ }
+ return n && (o = /** @type {Derived} */
+ e, Xr(o) && Ul(o)), e.v;
+}
+function wn(e) {
+ var t = en;
+ try {
+ return en = !0, e();
+ } finally {
+ en = t;
+ }
+}
+const Xf = -7169;
+function rn(e, t) {
+ e.f = e.f & Xf | t;
+}
+function j(e) {
+ if (!(typeof e != "object" || !e || e instanceof EventTarget)) {
+ if (Wn in e)
+ ps(e);
+ else if (!Array.isArray(e))
+ for (let t in e) {
+ const n = e[t];
+ typeof n == "object" && n && Wn in n && ps(n);
+ }
+ }
+}
+function ps(e, t = /* @__PURE__ */ new Set()) {
+ if (typeof e == "object" && e !== null && // We don't want to traverse DOM elements
+ !(e instanceof EventTarget) && !t.has(e)) {
+ t.add(e), e instanceof Date && e.getTime();
+ for (let r in e)
+ try {
+ ps(e[r], t);
+ } catch {
+ }
+ const n = Fs(e);
+ if (n !== Object.prototype && n !== Array.prototype && n !== Map.prototype && n !== Set.prototype && n !== Date.prototype) {
+ const r = Zl(n);
+ for (let o in r) {
+ const i = r[o].get;
+ if (i)
+ try {
+ i.call(e);
+ } catch {
+ }
+ }
+ }
+ }
+}
+function iu(e) {
+ qe === null && je === null && Ef(), je !== null && je.f & qt && qe === null && $f(), Gs && kf();
+}
+function Ff(e, t) {
+ var n = t.last;
+ n === null ? t.last = t.first = e : (n.next = e, e.prev = n, t.last = e);
+}
+function hr(e, t, n, r = !0) {
+ var o = (e & Yr) !== 0, i = qe, s = {
+ ctx: Ze,
+ deps: null,
+ nodes_start: null,
+ nodes_end: null,
+ f: e | In,
+ first: null,
+ fn: t,
+ last: null,
+ next: null,
+ parent: o ? null : i,
+ prev: null,
+ teardown: null,
+ transitions: null,
+ wv: 0
+ };
+ if (n)
+ try {
+ Ti(s), s.f |= hf;
+ } catch (u) {
+ throw Gt(s), u;
+ }
+ else t !== null && Hi(s);
+ var a = n && s.deps === null && s.first === null && s.nodes_start === null && s.teardown === null && (s.f & (Yl | ti)) === 0;
+ if (!a && !o && r && (i !== null && Ff(s, i), je !== null && je.f & nn)) {
+ var l = (
+ /** @type {Derived} */
+ je
+ );
+ (l.effects ?? (l.effects = [])).push(s);
+ }
+ return s;
+}
+function su(e) {
+ const t = hr(Si, null, !1);
+ return rn(t, mt), t.teardown = e, t;
+}
+function Nr(e) {
+ iu();
+ var t = qe !== null && (qe.f & On) !== 0 && Ze !== null && !Ze.m;
+ if (t) {
+ var n = (
+ /** @type {ComponentContext} */
+ Ze
+ );
+ (n.e ?? (n.e = [])).push({
+ fn: e,
+ effect: qe,
+ reaction: je
+ });
+ } else {
+ var r = Ot(e);
+ return r;
+ }
+}
+function Wf(e) {
+ return iu(), Fr(e);
+}
+function Kf(e) {
+ const t = hr(Yr, e, !0);
+ return () => {
+ Gt(t);
+ };
+}
+function qf(e) {
+ const t = hr(Yr, e, !0);
+ return (n = {}) => new Promise((r) => {
+ n.outro ? Mr(t, () => {
+ Gt(t), r(void 0);
+ }) : (Gt(t), r(void 0));
+ });
+}
+function Ot(e) {
+ return hr(Bl, e, !1);
+}
+function he(e, t) {
+ var n = (
+ /** @type {ComponentContextLegacy} */
+ Ze
+ ), r = { effect: null, ran: !1 };
+ n.l.r1.push(r), r.effect = Fr(() => {
+ e(), !r.ran && (r.ran = !0, U(n.l.r2, !0), wn(t));
+ });
+}
+function gt() {
+ var e = (
+ /** @type {ComponentContextLegacy} */
+ Ze
+ );
+ Fr(() => {
+ if (h(e.l.r2)) {
+ for (var t of e.l.r1) {
+ var n = t.effect;
+ n.f & mt && rn(n, gr), Xr(n) && Ti(n), t.ran = !1;
+ }
+ e.l.r2.v = !1;
+ }
+ });
+}
+function Fr(e) {
+ return hr(Si, e, !0);
+}
+function Ee(e, t = [], n = Me) {
+ const r = t.map(n);
+ return vr(() => e(...r.map(h)));
+}
+function vr(e, t = 0) {
+ return hr(Si | Ys | t, e, !0);
+}
+function Dn(e, t = !0) {
+ return hr(Si | On, e, !0, t);
+}
+function au(e) {
+ var t = e.teardown;
+ if (t !== null) {
+ const n = Gs, r = je;
+ Da(!0), jn(null);
+ try {
+ t.call(null);
+ } finally {
+ Da(n), jn(r);
+ }
+ }
+}
+function lu(e, t = !1) {
+ var n = e.first;
+ for (e.first = e.last = null; n !== null; ) {
+ var r = n.next;
+ Gt(n, t), n = r;
+ }
+}
+function Gf(e) {
+ for (var t = e.first; t !== null; ) {
+ var n = t.next;
+ t.f & On || Gt(t), t = n;
+ }
+}
+function Gt(e, t = !0) {
+ var n = !1;
+ if ((t || e.f & pf) && e.nodes_start !== null) {
+ for (var r = e.nodes_start, o = e.nodes_end; r !== null; ) {
+ var i = r === o ? null : (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(r)
+ );
+ r.remove(), r = i;
+ }
+ n = !0;
+ }
+ lu(e, t && !n), ai(e, 0), rn(e, Pi);
+ var s = e.transitions;
+ if (s !== null)
+ for (const l of s)
+ l.stop();
+ au(e);
+ var a = e.parent;
+ a !== null && a.first !== null && uu(e), e.next = e.prev = e.teardown = e.ctx = e.deps = e.fn = e.nodes_start = e.nodes_end = null;
+}
+function uu(e) {
+ var t = e.parent, n = e.prev, r = e.next;
+ n !== null && (n.next = r), r !== null && (r.prev = n), t !== null && (t.first === e && (t.first = r), t.last === e && (t.last = n));
+}
+function Mr(e, t) {
+ var n = [];
+ Us(e, n, !0), cu(n, () => {
+ Gt(e), t && t();
+ });
+}
+function cu(e, t) {
+ var n = e.length;
+ if (n > 0) {
+ var r = () => --n || t();
+ for (var o of e)
+ o.out(r);
+ } else
+ t();
+}
+function Us(e, t, n) {
+ if (!(e.f & Mn)) {
+ if (e.f ^= Mn, e.transitions !== null)
+ for (const s of e.transitions)
+ (s.is_global || n) && t.push(s);
+ for (var r = e.first; r !== null; ) {
+ var o = r.next, i = (r.f & Zr) !== 0 || (r.f & On) !== 0;
+ Us(r, t, i ? n : !1), r = o;
+ }
+ }
+}
+function co(e) {
+ du(e, !0);
+}
+function du(e, t) {
+ if (e.f & Mn) {
+ e.f ^= Mn, e.f & mt || (e.f ^= mt), Xr(e) && (rn(e, In), Hi(e));
+ for (var n = e.first; n !== null; ) {
+ var r = n.next, o = (n.f & Zr) !== 0 || (n.f & On) !== 0;
+ du(n, o ? t : !1), n = r;
+ }
+ if (e.transitions !== null)
+ for (const i of e.transitions)
+ (i.is_global || t) && i.in();
+ }
+}
+function Vi(e) {
+ throw new Error("https://svelte.dev/e/lifecycle_outside_component");
+}
+let Ze = null;
+function Aa(e) {
+ Ze = e;
+}
+function ar(e) {
+ return (
+ /** @type {T} */
+ js().get(e)
+ );
+}
+function Tr(e, t) {
+ return js().set(e, t), t;
+}
+function Uf(e) {
+ return js().has(e);
+}
+function de(e, t = !1, n) {
+ Ze = {
+ p: Ze,
+ c: null,
+ e: null,
+ m: !1,
+ s: e,
+ x: null,
+ l: null
+ }, Br && !t && (Ze.l = {
+ s: null,
+ u: null,
+ r1: [],
+ r2: Mt(!1)
+ });
+}
+function fe(e) {
+ const t = Ze;
+ if (t !== null) {
+ e !== void 0 && (t.x = e);
+ const s = t.e;
+ if (s !== null) {
+ var n = qe, r = je;
+ t.e = null;
+ try {
+ for (var o = 0; o < s.length; o++) {
+ var i = s[o];
+ Jn(i.effect), jn(i.reaction), Ot(i.fn);
+ }
+ } finally {
+ Jn(n), jn(r);
+ }
+ }
+ Ze = t.p, t.m = !0;
+ }
+ return e || /** @type {T} */
+ {};
+}
+function Di() {
+ return !Br || Ze !== null && Ze.l === null;
+}
+function js(e) {
+ return Ze === null && Vi(), Ze.c ?? (Ze.c = new Map(jf(Ze) || void 0));
+}
+function jf(e) {
+ let t = e.p;
+ for (; t !== null; ) {
+ const n = t.c;
+ if (n !== null)
+ return n;
+ t = t.p;
+ }
+ return null;
+}
+function Jf(e) {
+ return e.endsWith("capture") && e !== "gotpointercapture" && e !== "lostpointercapture";
+}
+const Qf = [
+ "beforeinput",
+ "click",
+ "change",
+ "dblclick",
+ "contextmenu",
+ "focusin",
+ "focusout",
+ "input",
+ "keydown",
+ "keyup",
+ "mousedown",
+ "mousemove",
+ "mouseout",
+ "mouseover",
+ "mouseup",
+ "pointerdown",
+ "pointermove",
+ "pointerout",
+ "pointerover",
+ "pointerup",
+ "touchend",
+ "touchmove",
+ "touchstart"
+];
+function e1(e) {
+ return Qf.includes(e);
+}
+const t1 = {
+ // no `class: 'className'` because we handle that separately
+ formnovalidate: "formNoValidate",
+ ismap: "isMap",
+ nomodule: "noModule",
+ playsinline: "playsInline",
+ readonly: "readOnly",
+ defaultvalue: "defaultValue",
+ defaultchecked: "defaultChecked",
+ srcobject: "srcObject",
+ novalidate: "noValidate",
+ allowfullscreen: "allowFullscreen",
+ disablepictureinpicture: "disablePictureInPicture",
+ disableremoteplayback: "disableRemotePlayback"
+};
+function n1(e) {
+ return e = e.toLowerCase(), t1[e] ?? e;
+}
+const r1 = ["touchstart", "touchmove"];
+function o1(e) {
+ return r1.includes(e);
+}
+const i1 = (
+ /** @type {const} */
+ ["textarea", "script", "style", "title"]
+);
+function s1(e) {
+ return i1.includes(
+ /** @type {RAW_TEXT_ELEMENTS[number]} */
+ e
+ );
+}
+function a1(e, t) {
+ if (t) {
+ const n = document.body;
+ e.autofocus = !0, ko(() => {
+ document.activeElement === n && e.focus();
+ });
+ }
+}
+function l1(e) {
+ Pe && /* @__PURE__ */ bt(e) !== null && qs(e);
+}
+let La = !1;
+function u1() {
+ La || (La = !0, document.addEventListener(
+ "reset",
+ (e) => {
+ Promise.resolve().then(() => {
+ var t;
+ if (!e.defaultPrevented)
+ for (
+ const n of
+ /**@type {HTMLFormElement} */
+ e.target.elements
+ )
+ (t = n.__on_r) == null || t.call(n);
+ });
+ },
+ // In the capture phase to guarantee we get noticed of it (no possiblity of stopPropagation)
+ { capture: !0 }
+ ));
+}
+function c1(e) {
+ var t = je, n = qe;
+ jn(null), Jn(null);
+ try {
+ return e();
+ } finally {
+ jn(t), Jn(n);
+ }
+}
+const fu = /* @__PURE__ */ new Set(), ms = /* @__PURE__ */ new Set();
+function gu(e, t, n, r = {}) {
+ function o(i) {
+ if (r.capture || eo.call(t, i), !i.cancelBubble)
+ return c1(() => n == null ? void 0 : n.call(this, i));
+ }
+ return e.startsWith("pointer") || e.startsWith("touch") || e === "wheel" ? ko(() => {
+ t.addEventListener(e, o, r);
+ }) : t.addEventListener(e, o, r), o;
+}
+function Ye(e, t, n, r, o) {
+ var i = { capture: r, passive: o }, s = gu(e, t, n, i);
+ (t === document.body || t === window || t === document) && su(() => {
+ t.removeEventListener(e, s, i);
+ });
+}
+function Ai(e) {
+ for (var t = 0; t < e.length; t++)
+ fu.add(e[t]);
+ for (var n of ms)
+ n(e);
+}
+function eo(e) {
+ var m;
+ var t = this, n = (
+ /** @type {Node} */
+ t.ownerDocument
+ ), r = e.type, o = ((m = e.composedPath) == null ? void 0 : m.call(e)) || [], i = (
+ /** @type {null | Element} */
+ o[0] || e.target
+ ), s = 0, a = e.__root;
+ if (a) {
+ var l = o.indexOf(a);
+ if (l !== -1 && (t === document || t === /** @type {any} */
+ window)) {
+ e.__root = t;
+ return;
+ }
+ var u = o.indexOf(t);
+ if (u === -1)
+ return;
+ l <= u && (s = l);
+ }
+ if (i = /** @type {Element} */
+ o[s] || e.target, i !== t) {
+ so(e, "currentTarget", {
+ configurable: !0,
+ get() {
+ return i || n;
+ }
+ });
+ var c = je, f = qe;
+ jn(null), Jn(null);
+ try {
+ for (var d, g = []; i !== null; ) {
+ var p = i.assignedSlot || i.parentNode || /** @type {any} */
+ i.host || null;
+ try {
+ var x = i["__" + r];
+ if (x !== void 0 && (!/** @type {any} */
+ i.disabled || // DOM could've been updated already by the time this is reached, so we check this as well
+ // -> the target could not have been disabled because it emits the event in the first place
+ e.target === i))
+ if (Co(x)) {
+ var [C, ...$] = x;
+ C.apply(i, [e, ...$]);
+ } else
+ x.call(i, e);
+ } catch (_) {
+ d ? g.push(_) : d = _;
+ }
+ if (e.cancelBubble || p === t || p === null)
+ break;
+ i = p;
+ }
+ if (d) {
+ for (let _ of g)
+ queueMicrotask(() => {
+ throw _;
+ });
+ throw d;
+ }
+ } finally {
+ e.__root = t, delete e.currentTarget, jn(c), Jn(f);
+ }
+ }
+}
+function Js(e) {
+ var t = document.createElement("template");
+ return t.innerHTML = e, t.content;
+}
+function Vt(e, t) {
+ var n = (
+ /** @type {Effect} */
+ qe
+ );
+ n.nodes_start === null && (n.nodes_start = e, n.nodes_end = t);
+}
+// @__NO_SIDE_EFFECTS__
+function ne(e, t) {
+ var n = (t & zl) !== 0, r = (t & gf) !== 0, o, i = !e.startsWith("<!>");
+ return () => {
+ if (Pe)
+ return Vt(De, null), De;
+ o === void 0 && (o = Js(i ? e : "<!>" + e), n || (o = /** @type {Node} */
+ /* @__PURE__ */ bt(o)));
+ var s = (
+ /** @type {TemplateNode} */
+ r || jl ? document.importNode(o, !0) : o.cloneNode(!0)
+ );
+ if (n) {
+ var a = (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ bt(s)
+ ), l = (
+ /** @type {TemplateNode} */
+ s.lastChild
+ );
+ Vt(a, l);
+ } else
+ Vt(s, s);
+ return s;
+ };
+}
+// @__NO_SIDE_EFFECTS__
+function _e(e, t, n = "svg") {
+ var r = !e.startsWith("<!>"), o = (t & zl) !== 0, i = `<${n}>${r ? e : "<!>" + e}</${n}>`, s;
+ return () => {
+ if (Pe)
+ return Vt(De, null), De;
+ if (!s) {
+ var a = (
+ /** @type {DocumentFragment} */
+ Js(i)
+ ), l = (
+ /** @type {Element} */
+ /* @__PURE__ */ bt(a)
+ );
+ if (o)
+ for (s = document.createDocumentFragment(); /* @__PURE__ */ bt(l); )
+ s.appendChild(
+ /** @type {Node} */
+ /* @__PURE__ */ bt(l)
+ );
+ else
+ s = /** @type {Element} */
+ /* @__PURE__ */ bt(l);
+ }
+ var u = (
+ /** @type {TemplateNode} */
+ s.cloneNode(!0)
+ );
+ if (o) {
+ var c = (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ bt(u)
+ ), f = (
+ /** @type {TemplateNode} */
+ u.lastChild
+ );
+ Vt(c, f);
+ } else
+ Vt(u, u);
+ return u;
+ };
+}
+function Ie(e = "") {
+ if (!Pe) {
+ var t = Vn(e + "");
+ return Vt(t, t), t;
+ }
+ var n = De;
+ return n.nodeType !== 3 && (n.before(n = Vn()), Ct(n)), Vt(n, n), n;
+}
+function et() {
+ if (Pe)
+ return Vt(De, null), De;
+ var e = document.createDocumentFragment(), t = document.createComment(""), n = Vn();
+ return e.append(t, n), Vt(t, n), e;
+}
+function L(e, t) {
+ if (Pe) {
+ qe.nodes_end = De, yn();
+ return;
+ }
+ e !== null && e.before(
+ /** @type {Node} */
+ t
+ );
+}
+function Rt(e, t) {
+ var n = t == null ? "" : typeof t == "object" ? t + "" : t;
+ n !== (e.__t ?? (e.__t = e.nodeValue)) && (e.__t = n, e.nodeValue = n + "");
+}
+function hu(e, t) {
+ return vu(e, t);
+}
+function d1(e, t) {
+ vs(), t.intro = t.intro ?? !1;
+ const n = t.target, r = Pe, o = De;
+ try {
+ for (var i = (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ bt(n)
+ ); i && (i.nodeType !== 8 || /** @type {Comment} */
+ i.data !== zs); )
+ i = /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(i);
+ if (!i)
+ throw _r;
+ It(!0), Ct(
+ /** @type {Comment} */
+ i
+ ), yn();
+ const s = vu(e, { ...t, anchor: i });
+ if (De === null || De.nodeType !== 8 || /** @type {Comment} */
+ De.data !== Bs)
+ throw Ni(), _r;
+ return It(!1), /** @type {Exports} */
+ s;
+ } catch (s) {
+ if (s === _r)
+ return t.recover === !1 && Pf(), vs(), qs(n), It(!1), hu(e, t);
+ throw s;
+ } finally {
+ It(r), Ct(o);
+ }
+}
+const mr = /* @__PURE__ */ new Map();
+function vu(e, { target: t, anchor: n, props: r = {}, events: o, context: i, intro: s = !0 }) {
+ vs();
+ var a = /* @__PURE__ */ new Set(), l = (f) => {
+ for (var d = 0; d < f.length; d++) {
+ var g = f[d];
+ if (!a.has(g)) {
+ a.add(g);
+ var p = o1(g);
+ t.addEventListener(g, eo, { passive: p });
+ var x = mr.get(g);
+ x === void 0 ? (document.addEventListener(g, eo, { passive: p }), mr.set(g, 1)) : mr.set(g, x + 1);
+ }
+ }
+ };
+ l(Xs(fu)), ms.add(l);
+ var u = void 0, c = qf(() => {
+ var f = n ?? t.appendChild(Vn());
+ return Dn(() => {
+ if (i) {
+ de({});
+ var d = (
+ /** @type {ComponentContext} */
+ Ze
+ );
+ d.c = i;
+ }
+ o && (r.$$events = o), Pe && Vt(
+ /** @type {TemplateNode} */
+ f,
+ null
+ ), u = e(f, r) || {}, Pe && (qe.nodes_end = De), i && fe();
+ }), () => {
+ var p;
+ for (var d of a) {
+ t.removeEventListener(d, eo);
+ var g = (
+ /** @type {number} */
+ mr.get(d)
+ );
+ --g === 0 ? (document.removeEventListener(d, eo), mr.delete(d)) : mr.set(d, g);
+ }
+ ms.delete(l), f !== n && ((p = f.parentNode) == null || p.removeChild(f));
+ };
+ });
+ return ys.set(u, c), u;
+}
+let ys = /* @__PURE__ */ new WeakMap();
+function f1(e, t) {
+ const n = ys.get(e);
+ return n ? (ys.delete(e), n(t)) : Promise.resolve();
+}
+function ke(e, t, [n, r] = [0, 0]) {
+ Pe && n === 0 && yn();
+ var o = e, i = null, s = null, a = Pt, l = n > 0 ? Zr : 0, u = !1;
+ const c = (d, g = !0) => {
+ u = !0, f(g, d);
+ }, f = (d, g) => {
+ if (a === (a = d)) return;
+ let p = !1;
+ if (Pe && r !== -1) {
+ if (n === 0) {
+ const C = (
+ /** @type {Comment} */
+ o.data
+ );
+ C === zs ? r = 0 : C === Rs ? r = 1 / 0 : (r = parseInt(C.substring(1)), r !== r && (r = a ? 1 / 0 : -1));
+ }
+ const x = r > n;
+ !!a === x && (o = hs(), Ct(o), It(!1), p = !0, r = -1);
+ }
+ a ? (i ? co(i) : g && (i = Dn(() => g(o))), s && Mr(s, () => {
+ s = null;
+ })) : (s ? co(s) : g && (s = Dn(() => g(o, [n + 1, r]))), i && Mr(i, () => {
+ i = null;
+ })), p && It(!0);
+ };
+ vr(() => {
+ u = !1, t(c), u || f(null, null);
+ }, l), Pe && (o = De);
+}
+function Li(e, t) {
+ return t;
+}
+function g1(e, t, n, r) {
+ for (var o = [], i = t.length, s = 0; s < i; s++)
+ Us(t[s].e, o, !0);
+ var a = i > 0 && o.length === 0 && n !== null;
+ if (a) {
+ var l = (
+ /** @type {Element} */
+ /** @type {Element} */
+ n.parentNode
+ );
+ qs(l), l.append(
+ /** @type {Element} */
+ n
+ ), r.clear(), Bn(e, t[0].prev, t[i - 1].next);
+ }
+ cu(o, () => {
+ for (var u = 0; u < i; u++) {
+ var c = t[u];
+ a || (r.delete(c.k), Bn(e, c.prev, c.next)), Gt(c.e, !a);
+ }
+ });
+}
+function Yt(e, t, n, r, o, i = null) {
+ var s = e, a = { flags: t, items: /* @__PURE__ */ new Map(), first: null }, l = (t & Ol) !== 0;
+ if (l) {
+ var u = (
+ /** @type {Element} */
+ e
+ );
+ s = Pe ? Ct(
+ /** @type {Comment | Text} */
+ /* @__PURE__ */ bt(u)
+ ) : u.appendChild(Vn());
+ }
+ Pe && yn();
+ var c = null, f = !1, d = /* @__PURE__ */ pe(() => {
+ var g = n();
+ return Co(g) ? g : g == null ? [] : Xs(g);
+ });
+ vr(() => {
+ var g = h(d), p = g.length;
+ if (f && p === 0)
+ return;
+ f = p === 0;
+ let x = !1;
+ if (Pe) {
+ var C = (
+ /** @type {Comment} */
+ s.data === Rs
+ );
+ C !== (p === 0) && (s = hs(), Ct(s), It(!1), x = !0);
+ }
+ if (Pe) {
+ for (var $ = null, m, _ = 0; _ < p; _++) {
+ if (De.nodeType === 8 && /** @type {Comment} */
+ De.data === Bs) {
+ s = /** @type {Comment} */
+ De, x = !0, It(!1);
+ break;
+ }
+ var v = g[_], b = r(v, _);
+ m = pu(
+ De,
+ a,
+ $,
+ null,
+ v,
+ b,
+ _,
+ o,
+ t,
+ n
+ ), a.items.set(b, m), $ = m;
+ }
+ p > 0 && Ct(hs());
+ }
+ Pe || h1(g, a, s, o, t, r, n), i !== null && (p === 0 ? c ? co(c) : c = Dn(() => i(s)) : c !== null && Mr(c, () => {
+ c = null;
+ })), x && It(!0), h(d);
+ }), Pe && (s = De);
+}
+function h1(e, t, n, r, o, i, s) {
+ var S, T, k, P;
+ var a = (o & af) !== 0, l = (o & (Os | Is)) !== 0, u = e.length, c = t.items, f = t.first, d = f, g, p = null, x, C = [], $ = [], m, _, v, b;
+ if (a)
+ for (b = 0; b < u; b += 1)
+ m = e[b], _ = i(m, b), v = c.get(_), v !== void 0 && ((S = v.a) == null || S.measure(), (x ?? (x = /* @__PURE__ */ new Set())).add(v));
+ for (b = 0; b < u; b += 1) {
+ if (m = e[b], _ = i(m, b), v = c.get(_), v === void 0) {
+ var N = d ? (
+ /** @type {TemplateNode} */
+ d.e.nodes_start
+ ) : n;
+ p = pu(
+ N,
+ t,
+ p,
+ p === null ? t.first : p.next,
+ m,
+ _,
+ b,
+ r,
+ o,
+ s
+ ), c.set(_, p), C = [], $ = [], d = p.next;
+ continue;
+ }
+ if (l && v1(v, m, b, o), v.e.f & Mn && (co(v.e), a && ((T = v.a) == null || T.unfix(), (x ?? (x = /* @__PURE__ */ new Set())).delete(v))), v !== d) {
+ if (g !== void 0 && g.has(v)) {
+ if (C.length < $.length) {
+ var E = $[0], M;
+ p = E.prev;
+ var D = C[0], V = C[C.length - 1];
+ for (M = 0; M < C.length; M += 1)
+ Oa(C[M], E, n);
+ for (M = 0; M < $.length; M += 1)
+ g.delete($[M]);
+ Bn(t, D.prev, V.next), Bn(t, p, D), Bn(t, V, E), d = E, p = V, b -= 1, C = [], $ = [];
+ } else
+ g.delete(v), Oa(v, d, n), Bn(t, v.prev, v.next), Bn(t, v, p === null ? t.first : p.next), Bn(t, p, v), p = v;
+ continue;
+ }
+ for (C = [], $ = []; d !== null && d.k !== _; )
+ d.e.f & Mn || (g ?? (g = /* @__PURE__ */ new Set())).add(d), $.push(d), d = d.next;
+ if (d === null)
+ continue;
+ v = d;
+ }
+ C.push(v), p = v, d = v.next;
+ }
+ if (d !== null || g !== void 0) {
+ for (var A = g === void 0 ? [] : Xs(g); d !== null; )
+ d.e.f & Mn || A.push(d), d = d.next;
+ var O = A.length;
+ if (O > 0) {
+ var R = o & Ol && u === 0 ? n : null;
+ if (a) {
+ for (b = 0; b < O; b += 1)
+ (k = A[b].a) == null || k.measure();
+ for (b = 0; b < O; b += 1)
+ (P = A[b].a) == null || P.fix();
+ }
+ g1(t, A, R, c);
+ }
+ }
+ a && ko(() => {
+ var H;
+ if (x !== void 0)
+ for (v of x)
+ (H = v.a) == null || H.apply();
+ }), qe.first = t.first && t.first.e, qe.last = p && p.e;
+}
+function v1(e, t, n, r) {
+ r & Os && gs(e.v, t), r & Is ? gs(
+ /** @type {Value<number>} */
+ e.i,
+ n
+ ) : e.i = n;
+}
+function pu(e, t, n, r, o, i, s, a, l, u) {
+ var c = (l & Os) !== 0, f = (l & lf) === 0, d = c ? f ? /* @__PURE__ */ $o(o) : Mt(o) : o, g = l & Is ? Mt(s) : s, p = {
+ i: g,
+ v: d,
+ k: i,
+ a: null,
+ // @ts-expect-error
+ e: null,
+ prev: n,
+ next: r
+ };
+ try {
+ return p.e = Dn(() => a(e, d, g, u), Pe), p.e.prev = n && n.e, p.e.next = r && r.e, n === null ? t.first = p : (n.next = p, n.e.next = p.e), r !== null && (r.prev = p, r.e.prev = p.e), p;
+ } finally {
+ }
+}
+function Oa(e, t, n) {
+ for (var r = e.next ? (
+ /** @type {TemplateNode} */
+ e.next.e.nodes_start
+ ) : n, o = t ? (
+ /** @type {TemplateNode} */
+ t.e.nodes_start
+ ) : n, i = (
+ /** @type {TemplateNode} */
+ e.e.nodes_start
+ ); i !== r; ) {
+ var s = (
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(i)
+ );
+ o.before(i), i = s;
+ }
+}
+function Bn(e, t, n) {
+ t === null ? e.first = n : (t.next = n, t.e.next = n && n.e), n !== null && (n.prev = t, n.e.prev = t && t.e);
+}
+function mu(e, t, n, r, o) {
+ var i = e, s = "", a;
+ vr(() => {
+ if (s === (s = t() ?? "")) {
+ Pe && yn();
+ return;
+ }
+ a !== void 0 && (Gt(a), a = void 0), s !== "" && (a = Dn(() => {
+ if (Pe) {
+ De.data;
+ for (var l = yn(), u = l; l !== null && (l.nodeType !== 8 || /** @type {Comment} */
+ l.data !== ""); )
+ u = l, l = /** @type {TemplateNode} */
+ /* @__PURE__ */ xn(l);
+ if (l === null)
+ throw Ni(), _r;
+ Vt(De, u), i = Ct(l);
+ return;
+ }
+ var c = s + "", f = Js(c);
+ Vt(
+ /** @type {TemplateNode} */
+ /* @__PURE__ */ bt(f),
+ /** @type {TemplateNode} */
+ f.lastChild
+ ), i.before(f);
+ }));
+ });
+}
+function pt(e, t, n, r, o) {
+ var a;
+ Pe && yn();
+ var i = (a = t.$$slots) == null ? void 0 : a[n], s = !1;
+ i === !0 && (i = t[n === "default" ? "children" : n], s = !0), i === void 0 || i(e, s ? () => r : r);
+}
+function p1(e) {
+ const t = {};
+ e.children && (t.default = !0);
+ for (const n in e.$$slots)
+ t[n] = !0;
+ return t;
+}
+function lr(e, t, ...n) {
+ var r = e, o = dt, i;
+ vr(() => {
+ o !== (o = t()) && (i && (Gt(i), i = null), i = Dn(() => (
+ /** @type {SnippetFn} */
+ o(r, ...n)
+ )));
+ }, Zr), Pe && (r = De);
+}
+function yu(e, t, n) {
+ Pe && yn();
+ var r = e, o, i;
+ vr(() => {
+ o !== (o = t()) && (i && (Mr(i), i = null), o && (i = Dn(() => n(r, o))));
+ }, Zr), Pe && (r = De);
+}
+function m1(e, t, n, r, o, i) {
+ let s = Pe;
+ Pe && yn();
+ var a, l, u = null;
+ Pe && De.nodeType === 1 && (u = /** @type {Element} */
+ De, yn());
+ var c = (
+ /** @type {TemplateNode} */
+ Pe ? De : e
+ ), f;
+ vr(() => {
+ const d = t() || null;
+ var g = d === "svg" ? Rl : null;
+ d !== a && (f && (d === null ? Mr(f, () => {
+ f = null, l = null;
+ }) : d === l ? co(f) : Gt(f)), d && d !== l && (f = Dn(() => {
+ if (u = Pe ? (
+ /** @type {Element} */
+ u
+ ) : g ? document.createElementNS(g, d) : document.createElement(d), Vt(u, u), r) {
+ Pe && s1(d) && u.append(document.createComment(""));
+ var p = (
+ /** @type {TemplateNode} */
+ Pe ? /* @__PURE__ */ bt(u) : u.appendChild(Vn())
+ );
+ Pe && (p === null ? It(!1) : Ct(p)), r(u, p);
+ }
+ qe.nodes_end = u, c.before(u);
+ })), a = d, a && (l = a));
+ }, Zr), s && (It(!0), Ct(c));
+}
+function Je(e, t) {
+ ko(() => {
+ var n = e.getRootNode(), r = (
+ /** @type {ShadowRoot} */
+ n.host ? (
+ /** @type {ShadowRoot} */
+ n
+ ) : (
+ /** @type {Document} */
+ n.head ?? /** @type {Document} */
+ n.ownerDocument.head
+ )
+ );
+ if (!r.querySelector("#" + t.hash)) {
+ const o = document.createElement("style");
+ o.id = t.hash, o.textContent = t.code, r.appendChild(o);
+ }
+ });
+}
+function vt(e, t, n) {
+ Ot(() => {
+ var r = wn(() => t(e, n == null ? void 0 : n()) || {});
+ if (n && (r != null && r.update)) {
+ var o = !1, i = (
+ /** @type {any} */
+ {}
+ );
+ Fr(() => {
+ var s = n();
+ j(s), o && Ws(i, s) && (i = s, r.update(s));
+ }), o = !0;
+ }
+ if (r != null && r.destroy)
+ return () => (
+ /** @type {Function} */
+ r.destroy()
+ );
+ });
+}
+function wu(e) {
+ var t, n, r = "";
+ if (typeof e == "string" || typeof e == "number") r += e;
+ else if (typeof e == "object") if (Array.isArray(e)) {
+ var o = e.length;
+ for (t = 0; t < o; t++) e[t] && (n = wu(e[t])) && (r && (r += " "), r += n);
+ } else for (n in e) e[n] && (r && (r += " "), r += n);
+ return r;
+}
+function y1() {
+ for (var e, t, n = 0, r = "", o = arguments.length; n < o; n++) (e = arguments[n]) && (t = wu(e)) && (r && (r += " "), r += t);
+ return r;
+}
+function bn(e) {
+ return typeof e == "object" ? y1(e) : e ?? "";
+}
+const Ia = [...`
+\r\f聽\v\uFEFF`];
+function w1(e, t, n) {
+ var r = e == null ? "" : "" + e;
+ if (t && (r = r ? r + " " + t : t), n) {
+ for (var o in n)
+ if (n[o])
+ r = r ? r + " " + o : o;
+ else if (r.length)
+ for (var i = o.length, s = 0; (s = r.indexOf(o, s)) >= 0; ) {
+ var a = s + i;
+ (s === 0 || Ia.includes(r[s - 1])) && (a === r.length || Ia.includes(r[a])) ? r = (s === 0 ? "" : r.substring(0, s)) + r.substring(a + 1) : s = a;
+ }
+ }
+ return r === "" ? null : r;
+}
+function kt(e, t, n, r, o, i) {
+ var s = e.__className;
+ if (Pe || s !== n) {
+ var a = w1(n, r, i);
+ (!Pe || a !== e.getAttribute("class")) && (a == null ? e.removeAttribute("class") : t ? e.className = a : e.setAttribute("class", a)), e.__className = n;
+ } else if (i)
+ for (var l in i) {
+ var u = !!i[l];
+ (o == null || u !== !!o[l]) && e.classList.toggle(l, u);
+ }
+ return i;
+}
+const jr = Symbol("class");
+function io(e) {
+ if (Pe) {
+ var t = !1, n = () => {
+ if (!t) {
+ if (t = !0, e.hasAttribute("value")) {
+ var r = e.value;
+ ce(e, "value", null), e.value = r;
+ }
+ if (e.hasAttribute("checked")) {
+ var o = e.checked;
+ ce(e, "checked", null), e.checked = o;
+ }
+ }
+ };
+ e.__on_r = n, Cf(n), u1();
+ }
+}
+function Qi(e, t) {
+ var n = e.__attributes ?? (e.__attributes = {});
+ n.value === (n.value = // treat null and undefined the same for the initial value
+ t ?? void 0) || // @ts-expect-error
+ // `progress` elements always need their value set when it's `0`
+ e.value === t && (t !== 0 || e.nodeName !== "PROGRESS") || (e.value = t ?? "");
+}
+function _1(e, t) {
+ t ? e.hasAttribute("selected") || e.setAttribute("selected", "") : e.removeAttribute("selected");
+}
+function ce(e, t, n, r) {
+ var o = e.__attributes ?? (e.__attributes = {});
+ Pe && (o[t] = e.getAttribute(t), t === "src" || t === "srcset" || t === "href" && e.nodeName === "LINK") || o[t] !== (o[t] = n) && (t === "style" && "__styles" in e && (e.__styles = {}), t === "loading" && (e[mf] = n), n == null ? e.removeAttribute(t) : typeof n != "string" && _u(e).includes(t) ? e[t] = n : e.setAttribute(t, n));
+}
+function on(e, t, n, r, o = !1, i = !1, s = !1) {
+ let a = Pe && i;
+ a && It(!1);
+ var l = t || {}, u = e.tagName === "OPTION";
+ for (var c in t)
+ c in n || (n[c] = null);
+ n.class ? n.class = bn(n.class) : (r || n[jr]) && (n.class = null);
+ var f = _u(e), d = (
+ /** @type {Record<string, unknown>} **/
+ e.__attributes ?? (e.__attributes = {})
+ );
+ for (const _ in n) {
+ let v = n[_];
+ if (u && _ === "value" && v == null) {
+ e.value = e.__value = "", l[_] = v;
+ continue;
+ }
+ if (_ === "class") {
+ var g = e.namespaceURI === "http://www.w3.org/1999/xhtml";
+ kt(e, g, v, r, t == null ? void 0 : t[jr], n[jr]), l[_] = v, l[jr] = n[jr];
+ continue;
+ }
+ var p = l[_];
+ if (v !== p) {
+ l[_] = v;
+ var x = _[0] + _[1];
+ if (x !== "$$") {
+ if (x === "on") {
+ const b = {}, N = "$$" + _;
+ let E = _.slice(2);
+ var C = e1(E);
+ if (Jf(E) && (E = E.slice(0, -7), b.capture = !0), !C && p) {
+ if (v != null) continue;
+ e.removeEventListener(E, l[N], b), l[N] = null;
+ }
+ if (v != null)
+ if (C)
+ e[`__${E}`] = v, Ai([E]);
+ else {
+ let M = function(D) {
+ l[_].call(this, D);
+ };
+ l[N] = gu(E, e, M, b);
+ }
+ else C && (e[`__${E}`] = void 0);
+ } else if (_ === "style" && v != null)
+ e.style.cssText = v + "";
+ else if (_ === "autofocus")
+ a1(
+ /** @type {HTMLElement} */
+ e,
+ !!v
+ );
+ else if (!i && (_ === "__value" || _ === "value" && v != null))
+ e.value = e.__value = v;
+ else if (_ === "selected" && u)
+ _1(
+ /** @type {HTMLOptionElement} */
+ e,
+ v
+ );
+ else {
+ var $ = _;
+ o || ($ = n1($));
+ var m = $ === "defaultValue" || $ === "defaultChecked";
+ if (v == null && !i && !m)
+ if (d[_] = null, $ === "value" || $ === "checked") {
+ let b = (
+ /** @type {HTMLInputElement} */
+ e
+ );
+ const N = t === void 0;
+ if ($ === "value") {
+ let E = b.defaultValue;
+ b.removeAttribute($), b.defaultValue = E, b.value = b.__value = N ? E : null;
+ } else {
+ let E = b.defaultChecked;
+ b.removeAttribute($), b.defaultChecked = E, b.checked = N ? E : !1;
+ }
+ } else
+ e.removeAttribute(_);
+ else m || f.includes($) && (i || typeof v != "string") ? e[$] = v : typeof v != "function" && ce(e, $, v);
+ }
+ _ === "style" && "__styles" in e && (e.__styles = {});
+ }
+ }
+ }
+ return a && It(!0), l;
+}
+var za = /* @__PURE__ */ new Map();
+function _u(e) {
+ var t = za.get(e.nodeName);
+ if (t) return t;
+ za.set(e.nodeName, t = []);
+ for (var n, r = e, o = Element.prototype; o !== r; ) {
+ n = Zl(r);
+ for (var i in n)
+ n[i].set && t.push(i);
+ r = Fs(r);
+ }
+ return t;
+}
+function st(e, t, n, r) {
+ var o = e.__styles ?? (e.__styles = {});
+ o[t] !== n && (o[t] = n, n == null ? e.style.removeProperty(t) : e.style.setProperty(t, n, ""));
+}
+var Zn, Pr, bo, $i, xu;
+const Ei = class Ei {
+ /** @param {ResizeObserverOptions} options */
+ constructor(t) {
+ rr(this, $i);
+ /** */
+ rr(this, Zn, /* @__PURE__ */ new WeakMap());
+ /** @type {ResizeObserver | undefined} */
+ rr(this, Pr);
+ /** @type {ResizeObserverOptions} */
+ rr(this, bo);
+ Gr(this, bo, t);
+ }
+ /**
+ * @param {Element} element
+ * @param {(entry: ResizeObserverEntry) => any} listener
+ */
+ observe(t, n) {
+ var r = it(this, Zn).get(t) || /* @__PURE__ */ new Set();
+ return r.add(n), it(this, Zn).set(t, r), Na(this, $i, xu).call(this).observe(t, it(this, bo)), () => {
+ var o = it(this, Zn).get(t);
+ o.delete(n), o.size === 0 && (it(this, Zn).delete(t), it(this, Pr).unobserve(t));
+ };
+ }
+};
+Zn = new WeakMap(), Pr = new WeakMap(), bo = new WeakMap(), $i = new WeakSet(), xu = function() {
+ return it(this, Pr) ?? Gr(this, Pr, new ResizeObserver(
+ /** @param {any} entries */
+ (t) => {
+ for (var n of t) {
+ Ei.entries.set(n.target, n);
+ for (var r of it(this, Zn).get(n.target) || [])
+ r(n);
+ }
+ }
+ ));
+}, /** @static */
+wt(Ei, "entries", /* @__PURE__ */ new WeakMap());
+let ws = Ei;
+var x1 = /* @__PURE__ */ new ws({
+ box: "border-box"
+});
+function Ra(e, t, n) {
+ var r = x1.observe(e, () => n(e[t]));
+ Ot(() => (wn(() => n(e[t])), r));
+}
+function Ba(e, t) {
+ return e === t || (e == null ? void 0 : e[Wn]) === t;
+}
+function An(e = {}, t, n, r) {
+ return Ot(() => {
+ var o, i;
+ return Fr(() => {
+ o = i, i = [], wn(() => {
+ e !== n(...i) && (t(e, ...i), o && Ba(n(...o), e) && t(null, ...o));
+ });
+ }), () => {
+ ko(() => {
+ i && Ba(n(...i), e) && t(null, ...i);
+ });
+ };
+ }), e;
+}
+function es(e) {
+ return function(...t) {
+ var n = (
+ /** @type {Event} */
+ t[0]
+ );
+ return n.stopPropagation(), e == null ? void 0 : e.apply(this, t);
+ };
+}
+function He(e = !1) {
+ const t = (
+ /** @type {ComponentContextLegacy} */
+ Ze
+ ), n = t.l.u;
+ if (!n) return;
+ let r = () => j(t.s);
+ if (e) {
+ let o = 0, i = (
+ /** @type {Record<string, any>} */
+ {}
+ );
+ const s = /* @__PURE__ */ Me(() => {
+ let a = !1;
+ const l = t.s;
+ for (const u in l)
+ l[u] !== i[u] && (i[u] = l[u], a = !0);
+ return a && o++, o;
+ });
+ r = () => h(s);
+ }
+ n.b.length && Wf(() => {
+ Ya(t, r), ao(n.b);
+ }), Nr(() => {
+ const o = wn(() => n.m.map(xf));
+ return () => {
+ for (const i of o)
+ typeof i == "function" && i();
+ };
+ }), n.a.length && Nr(() => {
+ Ya(t, r), ao(n.a);
+ });
+}
+function Ya(e, t) {
+ if (e.l.s)
+ for (const n of e.l.s) h(n);
+ t();
+}
+function Ve(e, t) {
+ var i;
+ var n = (
+ /** @type {Record<string, Function[] | Function>} */
+ (i = e.$$events) == null ? void 0 : i[t.type]
+ ), r = Co(n) ? n.slice() : n == null ? [] : [n];
+ for (var o of r)
+ o.call(this, t);
+}
+function un(e) {
+ Ze === null && Vi(), Br && Ze.l !== null ? C1(Ze).m.push(e) : Nr(() => {
+ const t = wn(e);
+ if (typeof t == "function") return (
+ /** @type {() => void} */
+ t
+ );
+ });
+}
+function Qs(e) {
+ Ze === null && Vi(), un(() => () => wn(e));
+}
+function b1(e, t, { bubbles: n = !1, cancelable: r = !1 } = {}) {
+ return new CustomEvent(e, { detail: t, bubbles: n, cancelable: r });
+}
+function Oi() {
+ const e = Ze;
+ return e === null && Vi(), (t, n, r) => {
+ var i;
+ const o = (
+ /** @type {Record<string, Function | Function[]>} */
+ (i = e.s.$$events) == null ? void 0 : i[
+ /** @type {any} */
+ t
+ ]
+ );
+ if (o) {
+ const s = Co(o) ? o.slice() : [o], a = b1(
+ /** @type {string} */
+ t,
+ n,
+ r
+ );
+ for (const l of s)
+ l.call(e.x, a);
+ return !a.defaultPrevented;
+ }
+ return !0;
+ };
+}
+function C1(e) {
+ var t = (
+ /** @type {ComponentContextLegacy} */
+ e.l
+ );
+ return t.u ?? (t.u = { a: [], b: [], m: [] });
+}
+function ea(e, t, n) {
+ if (e == null)
+ return t(void 0), n && n(void 0), dt;
+ const r = wn(
+ () => e.subscribe(
+ t,
+ // @ts-expect-error
+ n
+ )
+ );
+ return r.unsubscribe ? () => r.unsubscribe() : r;
+}
+const yr = [];
+function Ft(e, t) {
+ return {
+ subscribe: we(e, t).subscribe
+ };
+}
+function we(e, t = dt) {
+ let n = null;
+ const r = /* @__PURE__ */ new Set();
+ function o(a) {
+ if (Ws(e, a) && (e = a, n)) {
+ const l = !yr.length;
+ for (const u of r)
+ u[1](), yr.push(u, e);
+ if (l) {
+ for (let u = 0; u < yr.length; u += 2)
+ yr[u][0](yr[u + 1]);
+ yr.length = 0;
+ }
+ }
+ }
+ function i(a) {
+ o(a(
+ /** @type {T} */
+ e
+ ));
+ }
+ function s(a, l = dt) {
+ const u = [a, l];
+ return r.add(u), r.size === 1 && (n = t(o, i) || dt), a(
+ /** @type {T} */
+ e
+ ), () => {
+ r.delete(u), r.size === 0 && n && (n(), n = null);
+ };
+ }
+ return { set: o, update: i, subscribe: s };
+}
+function Kn(e, t, n) {
+ const r = !Array.isArray(e), o = r ? [e] : e;
+ if (!o.every(Boolean))
+ throw new Error("derived() expects stores as input, got a falsy value");
+ const i = t.length < 2;
+ return Ft(n, (s, a) => {
+ let l = !1;
+ const u = [];
+ let c = 0, f = dt;
+ const d = () => {
+ if (c)
+ return;
+ f();
+ const p = t(r ? u[0] : u, s, a);
+ i ? s(p) : f = typeof p == "function" ? p : dt;
+ }, g = o.map(
+ (p, x) => ea(
+ p,
+ (C) => {
+ u[x] = C, c &= ~(1 << x), l && d();
+ },
+ () => {
+ c |= 1 << x;
+ }
+ )
+ );
+ return l = !0, d(), function() {
+ ao(g), f(), l = !1;
+ };
+ });
+}
+function q(e) {
+ let t;
+ return ea(e, (n) => t = n)(), t;
+}
+let Bo = !1, _s = Symbol();
+function Q(e, t, n) {
+ const r = n[t] ?? (n[t] = {
+ store: null,
+ source: /* @__PURE__ */ $o(void 0),
+ unsubscribe: dt
+ });
+ if (r.store !== e && !(_s in n))
+ if (r.unsubscribe(), r.store = e ?? null, e == null)
+ r.source.v = void 0, r.unsubscribe = dt;
+ else {
+ var o = !0;
+ r.unsubscribe = ea(e, (i) => {
+ o ? r.source.v = i : U(r.source, i);
+ }), o = !1;
+ }
+ return e && _s in n ? q(e) : h(r.source);
+}
+function k1(e, t, n) {
+ let r = n[t];
+ return r && r.store !== e && (r.unsubscribe(), r.unsubscribe = dt), e;
+}
+function li(e, t) {
+ return e.set(t), t;
+}
+function tt() {
+ const e = {};
+ function t() {
+ su(() => {
+ for (var n in e)
+ e[n].unsubscribe();
+ so(e, _s, {
+ enumerable: !1,
+ value: !0
+ });
+ });
+ }
+ return [e, t];
+}
+function $1(e) {
+ var t = Bo;
+ try {
+ return Bo = !1, [e(), Bo];
+ } finally {
+ Bo = t;
+ }
+}
+const E1 = {
+ get(e, t) {
+ if (!e.exclude.includes(t))
+ return e.props[t];
+ },
+ set(e, t) {
+ return !1;
+ },
+ getOwnPropertyDescriptor(e, t) {
+ if (!e.exclude.includes(t) && t in e.props)
+ return {
+ enumerable: !0,
+ configurable: !0,
+ value: e.props[t]
+ };
+ },
+ has(e, t) {
+ return e.exclude.includes(t) ? !1 : t in e.props;
+ },
+ ownKeys(e) {
+ return Reflect.ownKeys(e.props).filter((t) => !e.exclude.includes(t));
+ }
+};
+// @__NO_SIDE_EFFECTS__
+function yt(e, t, n) {
+ return new Proxy(
+ { props: e, exclude: t },
+ E1
+ );
+}
+const S1 = {
+ get(e, t) {
+ if (!e.exclude.includes(t))
+ return h(e.version), t in e.special ? e.special[t]() : e.props[t];
+ },
+ set(e, t, n) {
+ return t in e.special || (e.special[t] = w(
+ {
+ get [t]() {
+ return e.props[t];
+ }
+ },
+ /** @type {string} */
+ t,
+ Il
+ )), e.special[t](n), Ha(e.version), !0;
+ },
+ getOwnPropertyDescriptor(e, t) {
+ if (!e.exclude.includes(t) && t in e.props)
+ return {
+ enumerable: !0,
+ configurable: !0,
+ value: e.props[t]
+ };
+ },
+ deleteProperty(e, t) {
+ return e.exclude.includes(t) || (e.exclude.push(t), Ha(e.version)), !0;
+ },
+ has(e, t) {
+ return e.exclude.includes(t) ? !1 : t in e.props;
+ },
+ ownKeys(e) {
+ return Reflect.ownKeys(e.props).filter((t) => !e.exclude.includes(t));
+ }
+};
+function nt(e, t) {
+ return new Proxy({ props: e, exclude: t, special: {}, version: Mt(0) }, S1);
+}
+const P1 = {
+ get(e, t) {
+ let n = e.props.length;
+ for (; n--; ) {
+ let r = e.props[n];
+ if (Ur(r) && (r = r()), typeof r == "object" && r !== null && t in r) return r[t];
+ }
+ },
+ set(e, t, n) {
+ let r = e.props.length;
+ for (; r--; ) {
+ let o = e.props[r];
+ Ur(o) && (o = o());
+ const i = Tn(o, t);
+ if (i && i.set)
+ return i.set(n), !0;
+ }
+ return !1;
+ },
+ getOwnPropertyDescriptor(e, t) {
+ let n = e.props.length;
+ for (; n--; ) {
+ let r = e.props[n];
+ if (Ur(r) && (r = r()), typeof r == "object" && r !== null && t in r) {
+ const o = Tn(r, t);
+ return o && !o.configurable && (o.configurable = !0), o;
+ }
+ }
+ },
+ has(e, t) {
+ if (t === Wn || t === Zs) return !1;
+ for (let n of e.props)
+ if (Ur(n) && (n = n()), n != null && t in n) return !0;
+ return !1;
+ },
+ ownKeys(e) {
+ const t = [];
+ for (let n of e.props) {
+ Ur(n) && (n = n());
+ for (const r in n)
+ t.includes(r) || t.push(r);
+ }
+ return t;
+ }
+};
+function ut(...e) {
+ return new Proxy({ props: e }, P1);
+}
+function w(e, t, n, r) {
+ var N;
+ var o = (n & uf) !== 0, i = !Br || (n & cf) !== 0, s = (n & df) !== 0, a = (n & ff) !== 0, l = !1, u;
+ s ? [u, l] = $1(() => (
+ /** @type {V} */
+ e[t]
+ )) : u = /** @type {V} */
+ e[t];
+ var c = Wn in e || Zs in e, f = s && (((N = Tn(e, t)) == null ? void 0 : N.set) ?? (c && t in e && ((E) => e[t] = E))) || void 0, d = (
+ /** @type {V} */
+ r
+ ), g = !0, p = !1, x = () => (p = !0, g && (g = !1, a ? d = wn(
+ /** @type {() => V} */
+ r
+ ) : d = /** @type {V} */
+ r), d);
+ u === void 0 && r !== void 0 && (f && i && Nf(), u = x(), f && f(u));
+ var C;
+ if (i)
+ C = () => {
+ var E = (
+ /** @type {V} */
+ e[t]
+ );
+ return E === void 0 ? x() : (g = !0, p = !1, E);
+ };
+ else {
+ var $ = (o ? Me : pe)(
+ () => (
+ /** @type {V} */
+ e[t]
+ )
+ );
+ $.f |= vf, C = () => {
+ var E = h($);
+ return E !== void 0 && (d = /** @type {V} */
+ void 0), E === void 0 ? d : E;
+ };
+ }
+ if (!(n & Il))
+ return C;
+ if (f) {
+ var m = e.$$legacy;
+ return function(E, M) {
+ return arguments.length > 0 ? ((!i || !M || m || l) && f(M ? C() : E), E) : C();
+ };
+ }
+ var _ = !1, v = /* @__PURE__ */ $o(u), b = /* @__PURE__ */ Me(() => {
+ var E = C(), M = h(v);
+ return _ ? (_ = !1, M) : v.v = E;
+ });
+ return o || (b.equals = Ks), function(E, M) {
+ if (arguments.length > 0) {
+ const D = M ? h(b) : i && s ? Tt(E) : E;
+ return b.equals(D) || (_ = !0, U(v, D), p && d !== void 0 && (d = D), wn(() => h(b))), E;
+ }
+ return h(b);
+ };
+}
+function N1(e) {
+ return new M1(e);
+}
+var Sn, Wt;
+class M1 {
+ /**
+ * @param {ComponentConstructorOptions & {
+ * component: any;
+ * }} options
+ */
+ constructor(t) {
+ /** @type {any} */
+ rr(this, Sn);
+ /** @type {Record<string, any>} */
+ rr(this, Wt);
+ var i;
+ var n = /* @__PURE__ */ new Map(), r = (s, a) => {
+ var l = /* @__PURE__ */ $o(a);
+ return n.set(s, l), l;
+ };
+ const o = new Proxy(
+ { ...t.props || {}, $$events: {} },
+ {
+ get(s, a) {
+ return h(n.get(a) ?? r(a, Reflect.get(s, a)));
+ },
+ has(s, a) {
+ return a === Zs ? !0 : (h(n.get(a) ?? r(a, Reflect.get(s, a))), Reflect.has(s, a));
+ },
+ set(s, a, l) {
+ return U(n.get(a) ?? r(a, l), l), Reflect.set(s, a, l);
+ }
+ }
+ );
+ Gr(this, Wt, (t.hydrate ? d1 : hu)(t.component, {
+ target: t.target,
+ anchor: t.anchor,
+ props: o,
+ context: t.context,
+ intro: t.intro ?? !1,
+ recover: t.recover
+ })), (!((i = t == null ? void 0 : t.props) != null && i.$$host) || t.sync === !1) && y(), Gr(this, Sn, o.$$events);
+ for (const s of Object.keys(it(this, Wt)))
+ s === "$set" || s === "$destroy" || s === "$on" || so(this, s, {
+ get() {
+ return it(this, Wt)[s];
+ },
+ /** @param {any} value */
+ set(a) {
+ it(this, Wt)[s] = a;
+ },
+ enumerable: !0
+ });
+ it(this, Wt).$set = /** @param {Record<string, any>} next */
+ (s) => {
+ Object.assign(o, s);
+ }, it(this, Wt).$destroy = () => {
+ f1(it(this, Wt));
+ };
+ }
+ /** @param {Record<string, any>} props */
+ $set(t) {
+ it(this, Wt).$set(t);
+ }
+ /**
+ * @param {string} event
+ * @param {(...args: any[]) => any} callback
+ * @returns {any}
+ */
+ $on(t, n) {
+ it(this, Sn)[t] = it(this, Sn)[t] || [];
+ const r = (...o) => n.call(this, ...o);
+ return it(this, Sn)[t].push(r), () => {
+ it(this, Sn)[t] = it(this, Sn)[t].filter(
+ /** @param {any} fn */
+ (o) => o !== r
+ );
+ };
+ }
+ $destroy() {
+ it(this, Wt).$destroy();
+ }
+}
+Sn = new WeakMap(), Wt = new WeakMap();
+let bu;
+typeof HTMLElement == "function" && (bu = class extends HTMLElement {
+ /**
+ * @param {*} $$componentCtor
+ * @param {*} $$slots
+ * @param {*} use_shadow_dom
+ */
+ constructor(t, n, r) {
+ super();
+ /** The Svelte component constructor */
+ wt(this, "$$ctor");
+ /** Slots */
+ wt(this, "$$s");
+ /** @type {any} The Svelte component instance */
+ wt(this, "$$c");
+ /** Whether or not the custom element is connected */
+ wt(this, "$$cn", !1);
+ /** @type {Record<string, any>} Component props data */
+ wt(this, "$$d", {});
+ /** `true` if currently in the process of reflecting component props back to attributes */
+ wt(this, "$$r", !1);
+ /** @type {Record<string, CustomElementPropDefinition>} Props definition (name, reflected, type etc) */
+ wt(this, "$$p_d", {});
+ /** @type {Record<string, EventListenerOrEventListenerObject[]>} Event listeners */
+ wt(this, "$$l", {});
+ /** @type {Map<EventListenerOrEventListenerObject, Function>} Event listener unsubscribe functions */
+ wt(this, "$$l_u", /* @__PURE__ */ new Map());
+ /** @type {any} The managed render effect for reflecting attributes */
+ wt(this, "$$me");
+ this.$$ctor = t, this.$$s = n, r && this.attachShadow({ mode: "open" });
+ }
+ /**
+ * @param {string} type
+ * @param {EventListenerOrEventListenerObject} listener
+ * @param {boolean | AddEventListenerOptions} [options]
+ */
+ addEventListener(t, n, r) {
+ if (this.$$l[t] = this.$$l[t] || [], this.$$l[t].push(n), this.$$c) {
+ const o = this.$$c.$on(t, n);
+ this.$$l_u.set(n, o);
+ }
+ super.addEventListener(t, n, r);
+ }
+ /**
+ * @param {string} type
+ * @param {EventListenerOrEventListenerObject} listener
+ * @param {boolean | AddEventListenerOptions} [options]
+ */
+ removeEventListener(t, n, r) {
+ if (super.removeEventListener(t, n, r), this.$$c) {
+ const o = this.$$l_u.get(n);
+ o && (o(), this.$$l_u.delete(n));
+ }
+ }
+ async connectedCallback() {
+ if (this.$$cn = !0, !this.$$c) {
+ let t = function(o) {
+ return (i) => {
+ const s = document.createElement("slot");
+ o !== "default" && (s.name = o), L(i, s);
+ };
+ };
+ if (await Promise.resolve(), !this.$$cn || this.$$c)
+ return;
+ const n = {}, r = T1(this);
+ for (const o of this.$$s)
+ o in r && (o === "default" && !this.$$d.children ? (this.$$d.children = t(o), n.default = !0) : n[o] = t(o));
+ for (const o of this.attributes) {
+ const i = this.$$g_p(o.name);
+ i in this.$$d || (this.$$d[i] = Go(i, o.value, this.$$p_d, "toProp"));
+ }
+ for (const o in this.$$p_d)
+ !(o in this.$$d) && this[o] !== void 0 && (this.$$d[o] = this[o], delete this[o]);
+ this.$$c = N1({
+ component: this.$$ctor,
+ target: this.shadowRoot || this,
+ props: {
+ ...this.$$d,
+ $$slots: n,
+ $$host: this
+ }
+ }), this.$$me = Kf(() => {
+ Fr(() => {
+ var o;
+ this.$$r = !0;
+ for (const i of ri(this.$$c)) {
+ if (!((o = this.$$p_d[i]) != null && o.reflect)) continue;
+ this.$$d[i] = this.$$c[i];
+ const s = Go(
+ i,
+ this.$$d[i],
+ this.$$p_d,
+ "toAttribute"
+ );
+ s == null ? this.removeAttribute(this.$$p_d[i].attribute || i) : this.setAttribute(this.$$p_d[i].attribute || i, s);
+ }
+ this.$$r = !1;
+ });
+ });
+ for (const o in this.$$l)
+ for (const i of this.$$l[o]) {
+ const s = this.$$c.$on(o, i);
+ this.$$l_u.set(i, s);
+ }
+ this.$$l = {};
+ }
+ }
+ // We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
+ // and setting attributes through setAttribute etc, this is helpful
+ /**
+ * @param {string} attr
+ * @param {string} _oldValue
+ * @param {string} newValue
+ */
+ attributeChangedCallback(t, n, r) {
+ var o;
+ this.$$r || (t = this.$$g_p(t), this.$$d[t] = Go(t, r, this.$$p_d, "toProp"), (o = this.$$c) == null || o.$set({ [t]: this.$$d[t] }));
+ }
+ disconnectedCallback() {
+ this.$$cn = !1, Promise.resolve().then(() => {
+ !this.$$cn && this.$$c && (this.$$c.$destroy(), this.$$me(), this.$$c = void 0);
+ });
+ }
+ /**
+ * @param {string} attribute_name
+ */
+ $$g_p(t) {
+ return ri(this.$$p_d).find(
+ (n) => this.$$p_d[n].attribute === t || !this.$$p_d[n].attribute && n.toLowerCase() === t
+ ) || t;
+ }
+});
+function Go(e, t, n, r) {
+ var i;
+ const o = (i = n[e]) == null ? void 0 : i.type;
+ if (t = o === "Boolean" && typeof t != "boolean" ? t != null : t, !r || !n[e])
+ return t;
+ if (r === "toAttribute")
+ switch (o) {
+ case "Object":
+ case "Array":
+ return t == null ? null : JSON.stringify(t);
+ case "Boolean":
+ return t ? "" : null;
+ case "Number":
+ return t ?? null;
+ default:
+ return t;
+ }
+ else
+ switch (o) {
+ case "Object":
+ case "Array":
+ return t && JSON.parse(t);
+ case "Boolean":
+ return t;
+ // conversion already handled above
+ case "Number":
+ return t != null ? +t : t;
+ default:
+ return t;
+ }
+}
+function T1(e) {
+ const t = {};
+ return e.childNodes.forEach((n) => {
+ t[
+ /** @type {Element} node */
+ n.slot || "default"
+ ] = !0;
+ }), t;
+}
+function ae(e, t, n, r, o, i) {
+ let s = class extends bu {
+ constructor() {
+ super(e, n, o), this.$$p_d = t;
+ }
+ static get observedAttributes() {
+ return ri(t).map(
+ (a) => (t[a].attribute || a).toLowerCase()
+ );
+ }
+ };
+ return ri(t).forEach((a) => {
+ so(s.prototype, a, {
+ get() {
+ return this.$$c && a in this.$$c ? this.$$c[a] : this.$$d[a];
+ },
+ set(l) {
+ var f;
+ l = Go(a, l, t), this.$$d[a] = l;
+ var u = this.$$c;
+ if (u) {
+ var c = (f = Tn(u, a)) == null ? void 0 : f.get;
+ c ? u[a] = l : u.$set({ [a]: l });
+ }
+ }
+ });
+ }), r.forEach((a) => {
+ so(s.prototype, a, {
+ get() {
+ var l;
+ return (l = this.$$c) == null ? void 0 : l[a];
+ }
+ });
+ }), e.element = /** @type {any} */
+ s, s;
+}
+function Et(e) {
+ if (typeof e == "string" || typeof e == "number") return "" + e;
+ let t = "";
+ if (Array.isArray(e))
+ for (let n = 0, r; n < e.length; n++)
+ (r = Et(e[n])) !== "" && (t += (t && " ") + r);
+ else
+ for (let n in e)
+ e[n] && (t += (t && " ") + n);
+ return t;
+}
+var H1 = { value: () => {
+} };
+function Ii() {
+ for (var e = 0, t = arguments.length, n = {}, r; e < t; ++e) {
+ if (!(r = arguments[e] + "") || r in n || /[\s.]/.test(r)) throw new Error("illegal type: " + r);
+ n[r] = [];
+ }
+ return new Uo(n);
+}
+function Uo(e) {
+ this._ = e;
+}
+function V1(e, t) {
+ return e.trim().split(/^|\s+/).map(function(n) {
+ var r = "", o = n.indexOf(".");
+ if (o >= 0 && (r = n.slice(o + 1), n = n.slice(0, o)), n && !t.hasOwnProperty(n)) throw new Error("unknown type: " + n);
+ return { type: n, name: r };
+ });
+}
+Uo.prototype = Ii.prototype = {
+ constructor: Uo,
+ on: function(e, t) {
+ var n = this._, r = V1(e + "", n), o, i = -1, s = r.length;
+ if (arguments.length < 2) {
+ for (; ++i < s; ) if ((o = (e = r[i]).type) && (o = D1(n[o], e.name))) return o;
+ return;
+ }
+ if (t != null && typeof t != "function") throw new Error("invalid callback: " + t);
+ for (; ++i < s; )
+ if (o = (e = r[i]).type) n[o] = Za(n[o], e.name, t);
+ else if (t == null) for (o in n) n[o] = Za(n[o], e.name, null);
+ return this;
+ },
+ copy: function() {
+ var e = {}, t = this._;
+ for (var n in t) e[n] = t[n].slice();
+ return new Uo(e);
+ },
+ call: function(e, t) {
+ if ((o = arguments.length - 2) > 0) for (var n = new Array(o), r = 0, o, i; r < o; ++r) n[r] = arguments[r + 2];
+ if (!this._.hasOwnProperty(e)) throw new Error("unknown type: " + e);
+ for (i = this._[e], r = 0, o = i.length; r < o; ++r) i[r].value.apply(t, n);
+ },
+ apply: function(e, t, n) {
+ if (!this._.hasOwnProperty(e)) throw new Error("unknown type: " + e);
+ for (var r = this._[e], o = 0, i = r.length; o < i; ++o) r[o].value.apply(t, n);
+ }
+};
+function D1(e, t) {
+ for (var n = 0, r = e.length, o; n < r; ++n)
+ if ((o = e[n]).name === t)
+ return o.value;
+}
+function Za(e, t, n) {
+ for (var r = 0, o = e.length; r < o; ++r)
+ if (e[r].name === t) {
+ e[r] = H1, e = e.slice(0, r).concat(e.slice(r + 1));
+ break;
+ }
+ return n != null && e.push({ name: t, value: n }), e;
+}
+var xs = "http://www.w3.org/1999/xhtml";
+const Xa = {
+ svg: "http://www.w3.org/2000/svg",
+ xhtml: xs,
+ xlink: "http://www.w3.org/1999/xlink",
+ xml: "http://www.w3.org/XML/1998/namespace",
+ xmlns: "http://www.w3.org/2000/xmlns/"
+};
+function zi(e) {
+ var t = e += "", n = t.indexOf(":");
+ return n >= 0 && (t = e.slice(0, n)) !== "xmlns" && (e = e.slice(n + 1)), Xa.hasOwnProperty(t) ? { space: Xa[t], local: e } : e;
+}
+function A1(e) {
+ return function() {
+ var t = this.ownerDocument, n = this.namespaceURI;
+ return n === xs && t.documentElement.namespaceURI === xs ? t.createElement(e) : t.createElementNS(n, e);
+ };
+}
+function L1(e) {
+ return function() {
+ return this.ownerDocument.createElementNS(e.space, e.local);
+ };
+}
+function Cu(e) {
+ var t = zi(e);
+ return (t.local ? L1 : A1)(t);
+}
+function O1() {
+}
+function ta(e) {
+ return e == null ? O1 : function() {
+ return this.querySelector(e);
+ };
+}
+function I1(e) {
+ typeof e != "function" && (e = ta(e));
+ for (var t = this._groups, n = t.length, r = new Array(n), o = 0; o < n; ++o)
+ for (var i = t[o], s = i.length, a = r[o] = new Array(s), l, u, c = 0; c < s; ++c)
+ (l = i[c]) && (u = e.call(l, l.__data__, c, i)) && ("__data__" in l && (u.__data__ = l.__data__), a[c] = u);
+ return new Zt(r, this._parents);
+}
+function z1(e) {
+ return e == null ? [] : Array.isArray(e) ? e : Array.from(e);
+}
+function R1() {
+ return [];
+}
+function ku(e) {
+ return e == null ? R1 : function() {
+ return this.querySelectorAll(e);
+ };
+}
+function B1(e) {
+ return function() {
+ return z1(e.apply(this, arguments));
+ };
+}
+function Y1(e) {
+ typeof e == "function" ? e = B1(e) : e = ku(e);
+ for (var t = this._groups, n = t.length, r = [], o = [], i = 0; i < n; ++i)
+ for (var s = t[i], a = s.length, l, u = 0; u < a; ++u)
+ (l = s[u]) && (r.push(e.call(l, l.__data__, u, s)), o.push(l));
+ return new Zt(r, o);
+}
+function $u(e) {
+ return function() {
+ return this.matches(e);
+ };
+}
+function Eu(e) {
+ return function(t) {
+ return t.matches(e);
+ };
+}
+var Z1 = Array.prototype.find;
+function X1(e) {
+ return function() {
+ return Z1.call(this.children, e);
+ };
+}
+function F1() {
+ return this.firstElementChild;
+}
+function W1(e) {
+ return this.select(e == null ? F1 : X1(typeof e == "function" ? e : Eu(e)));
+}
+var K1 = Array.prototype.filter;
+function q1() {
+ return Array.from(this.children);
+}
+function G1(e) {
+ return function() {
+ return K1.call(this.children, e);
+ };
+}
+function U1(e) {
+ return this.selectAll(e == null ? q1 : G1(typeof e == "function" ? e : Eu(e)));
+}
+function j1(e) {
+ typeof e != "function" && (e = $u(e));
+ for (var t = this._groups, n = t.length, r = new Array(n), o = 0; o < n; ++o)
+ for (var i = t[o], s = i.length, a = r[o] = [], l, u = 0; u < s; ++u)
+ (l = i[u]) && e.call(l, l.__data__, u, i) && a.push(l);
+ return new Zt(r, this._parents);
+}
+function Su(e) {
+ return new Array(e.length);
+}
+function J1() {
+ return new Zt(this._enter || this._groups.map(Su), this._parents);
+}
+function ui(e, t) {
+ this.ownerDocument = e.ownerDocument, this.namespaceURI = e.namespaceURI, this._next = null, this._parent = e, this.__data__ = t;
+}
+ui.prototype = {
+ constructor: ui,
+ appendChild: function(e) {
+ return this._parent.insertBefore(e, this._next);
+ },
+ insertBefore: function(e, t) {
+ return this._parent.insertBefore(e, t);
+ },
+ querySelector: function(e) {
+ return this._parent.querySelector(e);
+ },
+ querySelectorAll: function(e) {
+ return this._parent.querySelectorAll(e);
+ }
+};
+function Q1(e) {
+ return function() {
+ return e;
+ };
+}
+function eg(e, t, n, r, o, i) {
+ for (var s = 0, a, l = t.length, u = i.length; s < u; ++s)
+ (a = t[s]) ? (a.__data__ = i[s], r[s] = a) : n[s] = new ui(e, i[s]);
+ for (; s < l; ++s)
+ (a = t[s]) && (o[s] = a);
+}
+function tg(e, t, n, r, o, i, s) {
+ var a, l, u = /* @__PURE__ */ new Map(), c = t.length, f = i.length, d = new Array(c), g;
+ for (a = 0; a < c; ++a)
+ (l = t[a]) && (d[a] = g = s.call(l, l.__data__, a, t) + "", u.has(g) ? o[a] = l : u.set(g, l));
+ for (a = 0; a < f; ++a)
+ g = s.call(e, i[a], a, i) + "", (l = u.get(g)) ? (r[a] = l, l.__data__ = i[a], u.delete(g)) : n[a] = new ui(e, i[a]);
+ for (a = 0; a < c; ++a)
+ (l = t[a]) && u.get(d[a]) === l && (o[a] = l);
+}
+function ng(e) {
+ return e.__data__;
+}
+function rg(e, t) {
+ if (!arguments.length) return Array.from(this, ng);
+ var n = t ? tg : eg, r = this._parents, o = this._groups;
+ typeof e != "function" && (e = Q1(e));
+ for (var i = o.length, s = new Array(i), a = new Array(i), l = new Array(i), u = 0; u < i; ++u) {
+ var c = r[u], f = o[u], d = f.length, g = og(e.call(c, c && c.__data__, u, r)), p = g.length, x = a[u] = new Array(p), C = s[u] = new Array(p), $ = l[u] = new Array(d);
+ n(c, f, x, C, $, g, t);
+ for (var m = 0, _ = 0, v, b; m < p; ++m)
+ if (v = x[m]) {
+ for (m >= _ && (_ = m + 1); !(b = C[_]) && ++_ < p; ) ;
+ v._next = b || null;
+ }
+ }
+ return s = new Zt(s, r), s._enter = a, s._exit = l, s;
+}
+function og(e) {
+ return typeof e == "object" && "length" in e ? e : Array.from(e);
+}
+function ig() {
+ return new Zt(this._exit || this._groups.map(Su), this._parents);
+}
+function sg(e, t, n) {
+ var r = this.enter(), o = this, i = this.exit();
+ return typeof e == "function" ? (r = e(r), r && (r = r.selection())) : r = r.append(e + ""), t != null && (o = t(o), o && (o = o.selection())), n == null ? i.remove() : n(i), r && o ? r.merge(o).order() : o;
+}
+function ag(e) {
+ for (var t = e.selection ? e.selection() : e, n = this._groups, r = t._groups, o = n.length, i = r.length, s = Math.min(o, i), a = new Array(o), l = 0; l < s; ++l)
+ for (var u = n[l], c = r[l], f = u.length, d = a[l] = new Array(f), g, p = 0; p < f; ++p)
+ (g = u[p] || c[p]) && (d[p] = g);
+ for (; l < o; ++l)
+ a[l] = n[l];
+ return new Zt(a, this._parents);
+}
+function lg() {
+ for (var e = this._groups, t = -1, n = e.length; ++t < n; )
+ for (var r = e[t], o = r.length - 1, i = r[o], s; --o >= 0; )
+ (s = r[o]) && (i && s.compareDocumentPosition(i) ^ 4 && i.parentNode.insertBefore(s, i), i = s);
+ return this;
+}
+function ug(e) {
+ e || (e = cg);
+ function t(f, d) {
+ return f && d ? e(f.__data__, d.__data__) : !f - !d;
+ }
+ for (var n = this._groups, r = n.length, o = new Array(r), i = 0; i < r; ++i) {
+ for (var s = n[i], a = s.length, l = o[i] = new Array(a), u, c = 0; c < a; ++c)
+ (u = s[c]) && (l[c] = u);
+ l.sort(t);
+ }
+ return new Zt(o, this._parents).order();
+}
+function cg(e, t) {
+ return e < t ? -1 : e > t ? 1 : e >= t ? 0 : NaN;
+}
+function dg() {
+ var e = arguments[0];
+ return arguments[0] = this, e.apply(null, arguments), this;
+}
+function fg() {
+ return Array.from(this);
+}
+function gg() {
+ for (var e = this._groups, t = 0, n = e.length; t < n; ++t)
+ for (var r = e[t], o = 0, i = r.length; o < i; ++o) {
+ var s = r[o];
+ if (s) return s;
+ }
+ return null;
+}
+function hg() {
+ let e = 0;
+ for (const t of this) ++e;
+ return e;
+}
+function vg() {
+ return !this.node();
+}
+function pg(e) {
+ for (var t = this._groups, n = 0, r = t.length; n < r; ++n)
+ for (var o = t[n], i = 0, s = o.length, a; i < s; ++i)
+ (a = o[i]) && e.call(a, a.__data__, i, o);
+ return this;
+}
+function mg(e) {
+ return function() {
+ this.removeAttribute(e);
+ };
+}
+function yg(e) {
+ return function() {
+ this.removeAttributeNS(e.space, e.local);
+ };
+}
+function wg(e, t) {
+ return function() {
+ this.setAttribute(e, t);
+ };
+}
+function _g(e, t) {
+ return function() {
+ this.setAttributeNS(e.space, e.local, t);
+ };
+}
+function xg(e, t) {
+ return function() {
+ var n = t.apply(this, arguments);
+ n == null ? this.removeAttribute(e) : this.setAttribute(e, n);
+ };
+}
+function bg(e, t) {
+ return function() {
+ var n = t.apply(this, arguments);
+ n == null ? this.removeAttributeNS(e.space, e.local) : this.setAttributeNS(e.space, e.local, n);
+ };
+}
+function Cg(e, t) {
+ var n = zi(e);
+ if (arguments.length < 2) {
+ var r = this.node();
+ return n.local ? r.getAttributeNS(n.space, n.local) : r.getAttribute(n);
+ }
+ return this.each((t == null ? n.local ? yg : mg : typeof t == "function" ? n.local ? bg : xg : n.local ? _g : wg)(n, t));
+}
+function Pu(e) {
+ return e.ownerDocument && e.ownerDocument.defaultView || e.document && e || e.defaultView;
+}
+function kg(e) {
+ return function() {
+ this.style.removeProperty(e);
+ };
+}
+function $g(e, t, n) {
+ return function() {
+ this.style.setProperty(e, t, n);
+ };
+}
+function Eg(e, t, n) {
+ return function() {
+ var r = t.apply(this, arguments);
+ r == null ? this.style.removeProperty(e) : this.style.setProperty(e, r, n);
+ };
+}
+function Sg(e, t, n) {
+ return arguments.length > 1 ? this.each((t == null ? kg : typeof t == "function" ? Eg : $g)(e, t, n ?? "")) : Hr(this.node(), e);
+}
+function Hr(e, t) {
+ return e.style.getPropertyValue(t) || Pu(e).getComputedStyle(e, null).getPropertyValue(t);
+}
+function Pg(e) {
+ return function() {
+ delete this[e];
+ };
+}
+function Ng(e, t) {
+ return function() {
+ this[e] = t;
+ };
+}
+function Mg(e, t) {
+ return function() {
+ var n = t.apply(this, arguments);
+ n == null ? delete this[e] : this[e] = n;
+ };
+}
+function Tg(e, t) {
+ return arguments.length > 1 ? this.each((t == null ? Pg : typeof t == "function" ? Mg : Ng)(e, t)) : this.node()[e];
+}
+function Nu(e) {
+ return e.trim().split(/^|\s+/);
+}
+function na(e) {
+ return e.classList || new Mu(e);
+}
+function Mu(e) {
+ this._node = e, this._names = Nu(e.getAttribute("class") || "");
+}
+Mu.prototype = {
+ add: function(e) {
+ var t = this._names.indexOf(e);
+ t < 0 && (this._names.push(e), this._node.setAttribute("class", this._names.join(" ")));
+ },
+ remove: function(e) {
+ var t = this._names.indexOf(e);
+ t >= 0 && (this._names.splice(t, 1), this._node.setAttribute("class", this._names.join(" ")));
+ },
+ contains: function(e) {
+ return this._names.indexOf(e) >= 0;
+ }
+};
+function Tu(e, t) {
+ for (var n = na(e), r = -1, o = t.length; ++r < o; ) n.add(t[r]);
+}
+function Hu(e, t) {
+ for (var n = na(e), r = -1, o = t.length; ++r < o; ) n.remove(t[r]);
+}
+function Hg(e) {
+ return function() {
+ Tu(this, e);
+ };
+}
+function Vg(e) {
+ return function() {
+ Hu(this, e);
+ };
+}
+function Dg(e, t) {
+ return function() {
+ (t.apply(this, arguments) ? Tu : Hu)(this, e);
+ };
+}
+function Ag(e, t) {
+ var n = Nu(e + "");
+ if (arguments.length < 2) {
+ for (var r = na(this.node()), o = -1, i = n.length; ++o < i; ) if (!r.contains(n[o])) return !1;
+ return !0;
+ }
+ return this.each((typeof t == "function" ? Dg : t ? Hg : Vg)(n, t));
+}
+function Lg() {
+ this.textContent = "";
+}
+function Og(e) {
+ return function() {
+ this.textContent = e;
+ };
+}
+function Ig(e) {
+ return function() {
+ var t = e.apply(this, arguments);
+ this.textContent = t ?? "";
+ };
+}
+function zg(e) {
+ return arguments.length ? this.each(e == null ? Lg : (typeof e == "function" ? Ig : Og)(e)) : this.node().textContent;
+}
+function Rg() {
+ this.innerHTML = "";
+}
+function Bg(e) {
+ return function() {
+ this.innerHTML = e;
+ };
+}
+function Yg(e) {
+ return function() {
+ var t = e.apply(this, arguments);
+ this.innerHTML = t ?? "";
+ };
+}
+function Zg(e) {
+ return arguments.length ? this.each(e == null ? Rg : (typeof e == "function" ? Yg : Bg)(e)) : this.node().innerHTML;
+}
+function Xg() {
+ this.nextSibling && this.parentNode.appendChild(this);
+}
+function Fg() {
+ return this.each(Xg);
+}
+function Wg() {
+ this.previousSibling && this.parentNode.insertBefore(this, this.parentNode.firstChild);
+}
+function Kg() {
+ return this.each(Wg);
+}
+function qg(e) {
+ var t = typeof e == "function" ? e : Cu(e);
+ return this.select(function() {
+ return this.appendChild(t.apply(this, arguments));
+ });
+}
+function Gg() {
+ return null;
+}
+function Ug(e, t) {
+ var n = typeof e == "function" ? e : Cu(e), r = t == null ? Gg : typeof t == "function" ? t : ta(t);
+ return this.select(function() {
+ return this.insertBefore(n.apply(this, arguments), r.apply(this, arguments) || null);
+ });
+}
+function jg() {
+ var e = this.parentNode;
+ e && e.removeChild(this);
+}
+function Jg() {
+ return this.each(jg);
+}
+function Qg() {
+ var e = this.cloneNode(!1), t = this.parentNode;
+ return t ? t.insertBefore(e, this.nextSibling) : e;
+}
+function eh() {
+ var e = this.cloneNode(!0), t = this.parentNode;
+ return t ? t.insertBefore(e, this.nextSibling) : e;
+}
+function th(e) {
+ return this.select(e ? eh : Qg);
+}
+function nh(e) {
+ return arguments.length ? this.property("__data__", e) : this.node().__data__;
+}
+function rh(e) {
+ return function(t) {
+ e.call(this, t, this.__data__);
+ };
+}
+function oh(e) {
+ return e.trim().split(/^|\s+/).map(function(t) {
+ var n = "", r = t.indexOf(".");
+ return r >= 0 && (n = t.slice(r + 1), t = t.slice(0, r)), { type: t, name: n };
+ });
+}
+function ih(e) {
+ return function() {
+ var t = this.__on;
+ if (t) {
+ for (var n = 0, r = -1, o = t.length, i; n < o; ++n)
+ i = t[n], (!e.type || i.type === e.type) && i.name === e.name ? this.removeEventListener(i.type, i.listener, i.options) : t[++r] = i;
+ ++r ? t.length = r : delete this.__on;
+ }
+ };
+}
+function sh(e, t, n) {
+ return function() {
+ var r = this.__on, o, i = rh(t);
+ if (r) {
+ for (var s = 0, a = r.length; s < a; ++s)
+ if ((o = r[s]).type === e.type && o.name === e.name) {
+ this.removeEventListener(o.type, o.listener, o.options), this.addEventListener(o.type, o.listener = i, o.options = n), o.value = t;
+ return;
+ }
+ }
+ this.addEventListener(e.type, i, n), o = { type: e.type, name: e.name, value: t, listener: i, options: n }, r ? r.push(o) : this.__on = [o];
+ };
+}
+function ah(e, t, n) {
+ var r = oh(e + ""), o, i = r.length, s;
+ if (arguments.length < 2) {
+ var a = this.node().__on;
+ if (a) {
+ for (var l = 0, u = a.length, c; l < u; ++l)
+ for (o = 0, c = a[l]; o < i; ++o)
+ if ((s = r[o]).type === c.type && s.name === c.name)
+ return c.value;
+ }
+ return;
+ }
+ for (a = t ? sh : ih, o = 0; o < i; ++o) this.each(a(r[o], t, n));
+ return this;
+}
+function Vu(e, t, n) {
+ var r = Pu(e), o = r.CustomEvent;
+ typeof o == "function" ? o = new o(t, n) : (o = r.document.createEvent("Event"), n ? (o.initEvent(t, n.bubbles, n.cancelable), o.detail = n.detail) : o.initEvent(t, !1, !1)), e.dispatchEvent(o);
+}
+function lh(e, t) {
+ return function() {
+ return Vu(this, e, t);
+ };
+}
+function uh(e, t) {
+ return function() {
+ return Vu(this, e, t.apply(this, arguments));
+ };
+}
+function ch(e, t) {
+ return this.each((typeof t == "function" ? uh : lh)(e, t));
+}
+function* dh() {
+ for (var e = this._groups, t = 0, n = e.length; t < n; ++t)
+ for (var r = e[t], o = 0, i = r.length, s; o < i; ++o)
+ (s = r[o]) && (yield s);
+}
+var Du = [null];
+function Zt(e, t) {
+ this._groups = e, this._parents = t;
+}
+function Eo() {
+ return new Zt([[document.documentElement]], Du);
+}
+function fh() {
+ return this;
+}
+Zt.prototype = Eo.prototype = {
+ constructor: Zt,
+ select: I1,
+ selectAll: Y1,
+ selectChild: W1,
+ selectChildren: U1,
+ filter: j1,
+ data: rg,
+ enter: J1,
+ exit: ig,
+ join: sg,
+ merge: ag,
+ selection: fh,
+ order: lg,
+ sort: ug,
+ call: dg,
+ nodes: fg,
+ node: gg,
+ size: hg,
+ empty: vg,
+ each: pg,
+ attr: Cg,
+ style: Sg,
+ property: Tg,
+ classed: Ag,
+ text: zg,
+ html: Zg,
+ raise: Fg,
+ lower: Kg,
+ append: qg,
+ insert: Ug,
+ remove: Jg,
+ clone: th,
+ datum: nh,
+ on: ah,
+ dispatch: ch,
+ [Symbol.iterator]: dh
+};
+function Kt(e) {
+ return typeof e == "string" ? new Zt([[document.querySelector(e)]], [document.documentElement]) : new Zt([[e]], Du);
+}
+function gh(e) {
+ let t;
+ for (; t = e.sourceEvent; ) e = t;
+ return e;
+}
+function Qt(e, t) {
+ if (e = gh(e), t === void 0 && (t = e.currentTarget), t) {
+ var n = t.ownerSVGElement || t;
+ if (n.createSVGPoint) {
+ var r = n.createSVGPoint();
+ return r.x = e.clientX, r.y = e.clientY, r = r.matrixTransform(t.getScreenCTM().inverse()), [r.x, r.y];
+ }
+ if (t.getBoundingClientRect) {
+ var o = t.getBoundingClientRect();
+ return [e.clientX - o.left - t.clientLeft, e.clientY - o.top - t.clientTop];
+ }
+ }
+ return [e.pageX, e.pageY];
+}
+const hh = { passive: !1 }, fo = { capture: !0, passive: !1 };
+function ts(e) {
+ e.stopImmediatePropagation();
+}
+function xr(e) {
+ e.preventDefault(), e.stopImmediatePropagation();
+}
+function Au(e) {
+ var t = e.document.documentElement, n = Kt(e).on("dragstart.drag", xr, fo);
+ "onselectstart" in t ? n.on("selectstart.drag", xr, fo) : (t.__noselect = t.style.MozUserSelect, t.style.MozUserSelect = "none");
+}
+function Lu(e, t) {
+ var n = e.document.documentElement, r = Kt(e).on("dragstart.drag", null);
+ t && (r.on("click.drag", xr, fo), setTimeout(function() {
+ r.on("click.drag", null);
+ }, 0)), "onselectstart" in n ? r.on("selectstart.drag", null) : (n.style.MozUserSelect = n.__noselect, delete n.__noselect);
+}
+const Yo = (e) => () => e;
+function bs(e, {
+ sourceEvent: t,
+ subject: n,
+ target: r,
+ identifier: o,
+ active: i,
+ x: s,
+ y: a,
+ dx: l,
+ dy: u,
+ dispatch: c
+}) {
+ Object.defineProperties(this, {
+ type: { value: e, enumerable: !0, configurable: !0 },
+ sourceEvent: { value: t, enumerable: !0, configurable: !0 },
+ subject: { value: n, enumerable: !0, configurable: !0 },
+ target: { value: r, enumerable: !0, configurable: !0 },
+ identifier: { value: o, enumerable: !0, configurable: !0 },
+ active: { value: i, enumerable: !0, configurable: !0 },
+ x: { value: s, enumerable: !0, configurable: !0 },
+ y: { value: a, enumerable: !0, configurable: !0 },
+ dx: { value: l, enumerable: !0, configurable: !0 },
+ dy: { value: u, enumerable: !0, configurable: !0 },
+ _: { value: c }
+ });
+}
+bs.prototype.on = function() {
+ var e = this._.on.apply(this._, arguments);
+ return e === this._ ? this : e;
+};
+function vh(e) {
+ return !e.ctrlKey && !e.button;
+}
+function ph() {
+ return this.parentNode;
+}
+function mh(e, t) {
+ return t ?? { x: e.x, y: e.y };
+}
+function yh() {
+ return navigator.maxTouchPoints || "ontouchstart" in this;
+}
+function wh() {
+ var e = vh, t = ph, n = mh, r = yh, o = {}, i = Ii("start", "drag", "end"), s = 0, a, l, u, c, f = 0;
+ function d(v) {
+ v.on("mousedown.drag", g).filter(r).on("touchstart.drag", C).on("touchmove.drag", $, hh).on("touchend.drag touchcancel.drag", m).style("touch-action", "none").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+ }
+ function g(v, b) {
+ if (!(c || !e.call(this, v, b))) {
+ var N = _(this, t.call(this, v, b), v, b, "mouse");
+ N && (Kt(v.view).on("mousemove.drag", p, fo).on("mouseup.drag", x, fo), Au(v.view), ts(v), u = !1, a = v.clientX, l = v.clientY, N("start", v));
+ }
+ }
+ function p(v) {
+ if (xr(v), !u) {
+ var b = v.clientX - a, N = v.clientY - l;
+ u = b * b + N * N > f;
+ }
+ o.mouse("drag", v);
+ }
+ function x(v) {
+ Kt(v.view).on("mousemove.drag mouseup.drag", null), Lu(v.view, u), xr(v), o.mouse("end", v);
+ }
+ function C(v, b) {
+ if (e.call(this, v, b)) {
+ var N = v.changedTouches, E = t.call(this, v, b), M = N.length, D, V;
+ for (D = 0; D < M; ++D)
+ (V = _(this, E, v, b, N[D].identifier, N[D])) && (ts(v), V("start", v, N[D]));
+ }
+ }
+ function $(v) {
+ var b = v.changedTouches, N = b.length, E, M;
+ for (E = 0; E < N; ++E)
+ (M = o[b[E].identifier]) && (xr(v), M("drag", v, b[E]));
+ }
+ function m(v) {
+ var b = v.changedTouches, N = b.length, E, M;
+ for (c && clearTimeout(c), c = setTimeout(function() {
+ c = null;
+ }, 500), E = 0; E < N; ++E)
+ (M = o[b[E].identifier]) && (ts(v), M("end", v, b[E]));
+ }
+ function _(v, b, N, E, M, D) {
+ var V = i.copy(), A = Qt(D || N, b), O, R, S;
+ if ((S = n.call(v, new bs("beforestart", {
+ sourceEvent: N,
+ target: d,
+ identifier: M,
+ active: s,
+ x: A[0],
+ y: A[1],
+ dx: 0,
+ dy: 0,
+ dispatch: V
+ }), E)) != null)
+ return O = S.x - A[0] || 0, R = S.y - A[1] || 0, function T(k, P, H) {
+ var I = A, B;
+ switch (k) {
+ case "start":
+ o[M] = T, B = s++;
+ break;
+ case "end":
+ delete o[M], --s;
+ // falls through
+ case "drag":
+ A = Qt(H || P, b), B = s;
+ break;
+ }
+ V.call(
+ k,
+ v,
+ new bs(k, {
+ sourceEvent: P,
+ subject: S,
+ target: d,
+ identifier: M,
+ active: B,
+ x: A[0] + O,
+ y: A[1] + R,
+ dx: A[0] - I[0],
+ dy: A[1] - I[1],
+ dispatch: V
+ }),
+ E
+ );
+ };
+ }
+ return d.filter = function(v) {
+ return arguments.length ? (e = typeof v == "function" ? v : Yo(!!v), d) : e;
+ }, d.container = function(v) {
+ return arguments.length ? (t = typeof v == "function" ? v : Yo(v), d) : t;
+ }, d.subject = function(v) {
+ return arguments.length ? (n = typeof v == "function" ? v : Yo(v), d) : n;
+ }, d.touchable = function(v) {
+ return arguments.length ? (r = typeof v == "function" ? v : Yo(!!v), d) : r;
+ }, d.on = function() {
+ var v = i.on.apply(i, arguments);
+ return v === i ? d : v;
+ }, d.clickDistance = function(v) {
+ return arguments.length ? (f = (v = +v) * v, d) : Math.sqrt(f);
+ }, d;
+}
+function ra(e, t, n) {
+ e.prototype = t.prototype = n, n.constructor = e;
+}
+function Ou(e, t) {
+ var n = Object.create(e.prototype);
+ for (var r in t) n[r] = t[r];
+ return n;
+}
+function So() {
+}
+var go = 0.7, ci = 1 / go, br = "\\s*([+-]?\\d+)\\s*", ho = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*", pn = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*", _h = /^#([0-9a-f]{3,8})$/, xh = new RegExp(`^rgb\\(${br},${br},${br}\\)$`), bh = new RegExp(`^rgb\\(${pn},${pn},${pn}\\)$`), Ch = new RegExp(`^rgba\\(${br},${br},${br},${ho}\\)$`), kh = new RegExp(`^rgba\\(${pn},${pn},${pn},${ho}\\)$`), $h = new RegExp(`^hsl\\(${ho},${pn},${pn}\\)$`), Eh = new RegExp(`^hsla\\(${ho},${pn},${pn},${ho}\\)$`), Fa = {
+ aliceblue: 15792383,
+ antiquewhite: 16444375,
+ aqua: 65535,
+ aquamarine: 8388564,
+ azure: 15794175,
+ beige: 16119260,
+ bisque: 16770244,
+ black: 0,
+ blanchedalmond: 16772045,
+ blue: 255,
+ blueviolet: 9055202,
+ brown: 10824234,
+ burlywood: 14596231,
+ cadetblue: 6266528,
+ chartreuse: 8388352,
+ chocolate: 13789470,
+ coral: 16744272,
+ cornflowerblue: 6591981,
+ cornsilk: 16775388,
+ crimson: 14423100,
+ cyan: 65535,
+ darkblue: 139,
+ darkcyan: 35723,
+ darkgoldenrod: 12092939,
+ darkgray: 11119017,
+ darkgreen: 25600,
+ darkgrey: 11119017,
+ darkkhaki: 12433259,
+ darkmagenta: 9109643,
+ darkolivegreen: 5597999,
+ darkorange: 16747520,
+ darkorchid: 10040012,
+ darkred: 9109504,
+ darksalmon: 15308410,
+ darkseagreen: 9419919,
+ darkslateblue: 4734347,
+ darkslategray: 3100495,
+ darkslategrey: 3100495,
+ darkturquoise: 52945,
+ darkviolet: 9699539,
+ deeppink: 16716947,
+ deepskyblue: 49151,
+ dimgray: 6908265,
+ dimgrey: 6908265,
+ dodgerblue: 2003199,
+ firebrick: 11674146,
+ floralwhite: 16775920,
+ forestgreen: 2263842,
+ fuchsia: 16711935,
+ gainsboro: 14474460,
+ ghostwhite: 16316671,
+ gold: 16766720,
+ goldenrod: 14329120,
+ gray: 8421504,
+ green: 32768,
+ greenyellow: 11403055,
+ grey: 8421504,
+ honeydew: 15794160,
+ hotpink: 16738740,
+ indianred: 13458524,
+ indigo: 4915330,
+ ivory: 16777200,
+ khaki: 15787660,
+ lavender: 15132410,
+ lavenderblush: 16773365,
+ lawngreen: 8190976,
+ lemonchiffon: 16775885,
+ lightblue: 11393254,
+ lightcoral: 15761536,
+ lightcyan: 14745599,
+ lightgoldenrodyellow: 16448210,
+ lightgray: 13882323,
+ lightgreen: 9498256,
+ lightgrey: 13882323,
+ lightpink: 16758465,
+ lightsalmon: 16752762,
+ lightseagreen: 2142890,
+ lightskyblue: 8900346,
+ lightslategray: 7833753,
+ lightslategrey: 7833753,
+ lightsteelblue: 11584734,
+ lightyellow: 16777184,
+ lime: 65280,
+ limegreen: 3329330,
+ linen: 16445670,
+ magenta: 16711935,
+ maroon: 8388608,
+ mediumaquamarine: 6737322,
+ mediumblue: 205,
+ mediumorchid: 12211667,
+ mediumpurple: 9662683,
+ mediumseagreen: 3978097,
+ mediumslateblue: 8087790,
+ mediumspringgreen: 64154,
+ mediumturquoise: 4772300,
+ mediumvioletred: 13047173,
+ midnightblue: 1644912,
+ mintcream: 16121850,
+ mistyrose: 16770273,
+ moccasin: 16770229,
+ navajowhite: 16768685,
+ navy: 128,
+ oldlace: 16643558,
+ olive: 8421376,
+ olivedrab: 7048739,
+ orange: 16753920,
+ orangered: 16729344,
+ orchid: 14315734,
+ palegoldenrod: 15657130,
+ palegreen: 10025880,
+ paleturquoise: 11529966,
+ palevioletred: 14381203,
+ papayawhip: 16773077,
+ peachpuff: 16767673,
+ peru: 13468991,
+ pink: 16761035,
+ plum: 14524637,
+ powderblue: 11591910,
+ purple: 8388736,
+ rebeccapurple: 6697881,
+ red: 16711680,
+ rosybrown: 12357519,
+ royalblue: 4286945,
+ saddlebrown: 9127187,
+ salmon: 16416882,
+ sandybrown: 16032864,
+ seagreen: 3050327,
+ seashell: 16774638,
+ sienna: 10506797,
+ silver: 12632256,
+ skyblue: 8900331,
+ slateblue: 6970061,
+ slategray: 7372944,
+ slategrey: 7372944,
+ snow: 16775930,
+ springgreen: 65407,
+ steelblue: 4620980,
+ tan: 13808780,
+ teal: 32896,
+ thistle: 14204888,
+ tomato: 16737095,
+ turquoise: 4251856,
+ violet: 15631086,
+ wheat: 16113331,
+ white: 16777215,
+ whitesmoke: 16119285,
+ yellow: 16776960,
+ yellowgreen: 10145074
+};
+ra(So, vo, {
+ copy(e) {
+ return Object.assign(new this.constructor(), this, e);
+ },
+ displayable() {
+ return this.rgb().displayable();
+ },
+ hex: Wa,
+ // Deprecated! Use color.formatHex.
+ formatHex: Wa,
+ formatHex8: Sh,
+ formatHsl: Ph,
+ formatRgb: Ka,
+ toString: Ka
+});
+function Wa() {
+ return this.rgb().formatHex();
+}
+function Sh() {
+ return this.rgb().formatHex8();
+}
+function Ph() {
+ return Iu(this).formatHsl();
+}
+function Ka() {
+ return this.rgb().formatRgb();
+}
+function vo(e) {
+ var t, n;
+ return e = (e + "").trim().toLowerCase(), (t = _h.exec(e)) ? (n = t[1].length, t = parseInt(t[1], 16), n === 6 ? qa(t) : n === 3 ? new Ht(t >> 8 & 15 | t >> 4 & 240, t >> 4 & 15 | t & 240, (t & 15) << 4 | t & 15, 1) : n === 8 ? Zo(t >> 24 & 255, t >> 16 & 255, t >> 8 & 255, (t & 255) / 255) : n === 4 ? Zo(t >> 12 & 15 | t >> 8 & 240, t >> 8 & 15 | t >> 4 & 240, t >> 4 & 15 | t & 240, ((t & 15) << 4 | t & 15) / 255) : null) : (t = xh.exec(e)) ? new Ht(t[1], t[2], t[3], 1) : (t = bh.exec(e)) ? new Ht(t[1] * 255 / 100, t[2] * 255 / 100, t[3] * 255 / 100, 1) : (t = Ch.exec(e)) ? Zo(t[1], t[2], t[3], t[4]) : (t = kh.exec(e)) ? Zo(t[1] * 255 / 100, t[2] * 255 / 100, t[3] * 255 / 100, t[4]) : (t = $h.exec(e)) ? ja(t[1], t[2] / 100, t[3] / 100, 1) : (t = Eh.exec(e)) ? ja(t[1], t[2] / 100, t[3] / 100, t[4]) : Fa.hasOwnProperty(e) ? qa(Fa[e]) : e === "transparent" ? new Ht(NaN, NaN, NaN, 0) : null;
+}
+function qa(e) {
+ return new Ht(e >> 16 & 255, e >> 8 & 255, e & 255, 1);
+}
+function Zo(e, t, n, r) {
+ return r <= 0 && (e = t = n = NaN), new Ht(e, t, n, r);
+}
+function Nh(e) {
+ return e instanceof So || (e = vo(e)), e ? (e = e.rgb(), new Ht(e.r, e.g, e.b, e.opacity)) : new Ht();
+}
+function Cs(e, t, n, r) {
+ return arguments.length === 1 ? Nh(e) : new Ht(e, t, n, r ?? 1);
+}
+function Ht(e, t, n, r) {
+ this.r = +e, this.g = +t, this.b = +n, this.opacity = +r;
+}
+ra(Ht, Cs, Ou(So, {
+ brighter(e) {
+ return e = e == null ? ci : Math.pow(ci, e), new Ht(this.r * e, this.g * e, this.b * e, this.opacity);
+ },
+ darker(e) {
+ return e = e == null ? go : Math.pow(go, e), new Ht(this.r * e, this.g * e, this.b * e, this.opacity);
+ },
+ rgb() {
+ return this;
+ },
+ clamp() {
+ return new Ht(sr(this.r), sr(this.g), sr(this.b), di(this.opacity));
+ },
+ displayable() {
+ return -0.5 <= this.r && this.r < 255.5 && -0.5 <= this.g && this.g < 255.5 && -0.5 <= this.b && this.b < 255.5 && 0 <= this.opacity && this.opacity <= 1;
+ },
+ hex: Ga,
+ // Deprecated! Use color.formatHex.
+ formatHex: Ga,
+ formatHex8: Mh,
+ formatRgb: Ua,
+ toString: Ua
+}));
+function Ga() {
+ return `#${or(this.r)}${or(this.g)}${or(this.b)}`;
+}
+function Mh() {
+ return `#${or(this.r)}${or(this.g)}${or(this.b)}${or((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`;
+}
+function Ua() {
+ const e = di(this.opacity);
+ return `${e === 1 ? "rgb(" : "rgba("}${sr(this.r)}, ${sr(this.g)}, ${sr(this.b)}${e === 1 ? ")" : `, ${e})`}`;
+}
+function di(e) {
+ return isNaN(e) ? 1 : Math.max(0, Math.min(1, e));
+}
+function sr(e) {
+ return Math.max(0, Math.min(255, Math.round(e) || 0));
+}
+function or(e) {
+ return e = sr(e), (e < 16 ? "0" : "") + e.toString(16);
+}
+function ja(e, t, n, r) {
+ return r <= 0 ? e = t = n = NaN : n <= 0 || n >= 1 ? e = t = NaN : t <= 0 && (e = NaN), new tn(e, t, n, r);
+}
+function Iu(e) {
+ if (e instanceof tn) return new tn(e.h, e.s, e.l, e.opacity);
+ if (e instanceof So || (e = vo(e)), !e) return new tn();
+ if (e instanceof tn) return e;
+ e = e.rgb();
+ var t = e.r / 255, n = e.g / 255, r = e.b / 255, o = Math.min(t, n, r), i = Math.max(t, n, r), s = NaN, a = i - o, l = (i + o) / 2;
+ return a ? (t === i ? s = (n - r) / a + (n < r) * 6 : n === i ? s = (r - t) / a + 2 : s = (t - n) / a + 4, a /= l < 0.5 ? i + o : 2 - i - o, s *= 60) : a = l > 0 && l < 1 ? 0 : s, new tn(s, a, l, e.opacity);
+}
+function Th(e, t, n, r) {
+ return arguments.length === 1 ? Iu(e) : new tn(e, t, n, r ?? 1);
+}
+function tn(e, t, n, r) {
+ this.h = +e, this.s = +t, this.l = +n, this.opacity = +r;
+}
+ra(tn, Th, Ou(So, {
+ brighter(e) {
+ return e = e == null ? ci : Math.pow(ci, e), new tn(this.h, this.s, this.l * e, this.opacity);
+ },
+ darker(e) {
+ return e = e == null ? go : Math.pow(go, e), new tn(this.h, this.s, this.l * e, this.opacity);
+ },
+ rgb() {
+ var e = this.h % 360 + (this.h < 0) * 360, t = isNaN(e) || isNaN(this.s) ? 0 : this.s, n = this.l, r = n + (n < 0.5 ? n : 1 - n) * t, o = 2 * n - r;
+ return new Ht(
+ ns(e >= 240 ? e - 240 : e + 120, o, r),
+ ns(e, o, r),
+ ns(e < 120 ? e + 240 : e - 120, o, r),
+ this.opacity
+ );
+ },
+ clamp() {
+ return new tn(Ja(this.h), Xo(this.s), Xo(this.l), di(this.opacity));
+ },
+ displayable() {
+ return (0 <= this.s && this.s <= 1 || isNaN(this.s)) && 0 <= this.l && this.l <= 1 && 0 <= this.opacity && this.opacity <= 1;
+ },
+ formatHsl() {
+ const e = di(this.opacity);
+ return `${e === 1 ? "hsl(" : "hsla("}${Ja(this.h)}, ${Xo(this.s) * 100}%, ${Xo(this.l) * 100}%${e === 1 ? ")" : `, ${e})`}`;
+ }
+}));
+function Ja(e) {
+ return e = (e || 0) % 360, e < 0 ? e + 360 : e;
+}
+function Xo(e) {
+ return Math.max(0, Math.min(1, e || 0));
+}
+function ns(e, t, n) {
+ return (e < 60 ? t + (n - t) * e / 60 : e < 180 ? n : e < 240 ? t + (n - t) * (240 - e) / 60 : t) * 255;
+}
+const zu = (e) => () => e;
+function Hh(e, t) {
+ return function(n) {
+ return e + n * t;
+ };
+}
+function Vh(e, t, n) {
+ return e = Math.pow(e, n), t = Math.pow(t, n) - e, n = 1 / n, function(r) {
+ return Math.pow(e + r * t, n);
+ };
+}
+function Dh(e) {
+ return (e = +e) == 1 ? Ru : function(t, n) {
+ return n - t ? Vh(t, n, e) : zu(isNaN(t) ? n : t);
+ };
+}
+function Ru(e, t) {
+ var n = t - e;
+ return n ? Hh(e, n) : zu(isNaN(e) ? t : e);
+}
+const Qa = function e(t) {
+ var n = Dh(t);
+ function r(o, i) {
+ var s = n((o = Cs(o)).r, (i = Cs(i)).r), a = n(o.g, i.g), l = n(o.b, i.b), u = Ru(o.opacity, i.opacity);
+ return function(c) {
+ return o.r = s(c), o.g = a(c), o.b = l(c), o.opacity = u(c), o + "";
+ };
+ }
+ return r.gamma = e, r;
+}(1);
+function Yn(e, t) {
+ return e = +e, t = +t, function(n) {
+ return e * (1 - n) + t * n;
+ };
+}
+var ks = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, rs = new RegExp(ks.source, "g");
+function Ah(e) {
+ return function() {
+ return e;
+ };
+}
+function Lh(e) {
+ return function(t) {
+ return e(t) + "";
+ };
+}
+function Oh(e, t) {
+ var n = ks.lastIndex = rs.lastIndex = 0, r, o, i, s = -1, a = [], l = [];
+ for (e = e + "", t = t + ""; (r = ks.exec(e)) && (o = rs.exec(t)); )
+ (i = o.index) > n && (i = t.slice(n, i), a[s] ? a[s] += i : a[++s] = i), (r = r[0]) === (o = o[0]) ? a[s] ? a[s] += o : a[++s] = o : (a[++s] = null, l.push({ i: s, x: Yn(r, o) })), n = rs.lastIndex;
+ return n < t.length && (i = t.slice(n), a[s] ? a[s] += i : a[++s] = i), a.length < 2 ? l[0] ? Lh(l[0].x) : Ah(t) : (t = l.length, function(u) {
+ for (var c = 0, f; c < t; ++c) a[(f = l[c]).i] = f.x(u);
+ return a.join("");
+ });
+}
+var el = 180 / Math.PI, $s = {
+ translateX: 0,
+ translateY: 0,
+ rotate: 0,
+ skewX: 0,
+ scaleX: 1,
+ scaleY: 1
+};
+function Bu(e, t, n, r, o, i) {
+ var s, a, l;
+ return (s = Math.sqrt(e * e + t * t)) && (e /= s, t /= s), (l = e * n + t * r) && (n -= e * l, r -= t * l), (a = Math.sqrt(n * n + r * r)) && (n /= a, r /= a, l /= a), e * r < t * n && (e = -e, t = -t, l = -l, s = -s), {
+ translateX: o,
+ translateY: i,
+ rotate: Math.atan2(t, e) * el,
+ skewX: Math.atan(l) * el,
+ scaleX: s,
+ scaleY: a
+ };
+}
+var Fo;
+function Ih(e) {
+ const t = new (typeof DOMMatrix == "function" ? DOMMatrix : WebKitCSSMatrix)(e + "");
+ return t.isIdentity ? $s : Bu(t.a, t.b, t.c, t.d, t.e, t.f);
+}
+function zh(e) {
+ return e == null || (Fo || (Fo = document.createElementNS("http://www.w3.org/2000/svg", "g")), Fo.setAttribute("transform", e), !(e = Fo.transform.baseVal.consolidate())) ? $s : (e = e.matrix, Bu(e.a, e.b, e.c, e.d, e.e, e.f));
+}
+function Yu(e, t, n, r) {
+ function o(u) {
+ return u.length ? u.pop() + " " : "";
+ }
+ function i(u, c, f, d, g, p) {
+ if (u !== f || c !== d) {
+ var x = g.push("translate(", null, t, null, n);
+ p.push({ i: x - 4, x: Yn(u, f) }, { i: x - 2, x: Yn(c, d) });
+ } else (f || d) && g.push("translate(" + f + t + d + n);
+ }
+ function s(u, c, f, d) {
+ u !== c ? (u - c > 180 ? c += 360 : c - u > 180 && (u += 360), d.push({ i: f.push(o(f) + "rotate(", null, r) - 2, x: Yn(u, c) })) : c && f.push(o(f) + "rotate(" + c + r);
+ }
+ function a(u, c, f, d) {
+ u !== c ? d.push({ i: f.push(o(f) + "skewX(", null, r) - 2, x: Yn(u, c) }) : c && f.push(o(f) + "skewX(" + c + r);
+ }
+ function l(u, c, f, d, g, p) {
+ if (u !== f || c !== d) {
+ var x = g.push(o(g) + "scale(", null, ",", null, ")");
+ p.push({ i: x - 4, x: Yn(u, f) }, { i: x - 2, x: Yn(c, d) });
+ } else (f !== 1 || d !== 1) && g.push(o(g) + "scale(" + f + "," + d + ")");
+ }
+ return function(u, c) {
+ var f = [], d = [];
+ return u = e(u), c = e(c), i(u.translateX, u.translateY, c.translateX, c.translateY, f, d), s(u.rotate, c.rotate, f, d), a(u.skewX, c.skewX, f, d), l(u.scaleX, u.scaleY, c.scaleX, c.scaleY, f, d), u = c = null, function(g) {
+ for (var p = -1, x = d.length, C; ++p < x; ) f[(C = d[p]).i] = C.x(g);
+ return f.join("");
+ };
+ };
+}
+var Rh = Yu(Ih, "px, ", "px)", "deg)"), Bh = Yu(zh, ", ", ")", ")"), Yh = 1e-12;
+function tl(e) {
+ return ((e = Math.exp(e)) + 1 / e) / 2;
+}
+function Zh(e) {
+ return ((e = Math.exp(e)) - 1 / e) / 2;
+}
+function Xh(e) {
+ return ((e = Math.exp(2 * e)) - 1) / (e + 1);
+}
+const Fh = function e(t, n, r) {
+ function o(i, s) {
+ var a = i[0], l = i[1], u = i[2], c = s[0], f = s[1], d = s[2], g = c - a, p = f - l, x = g * g + p * p, C, $;
+ if (x < Yh)
+ $ = Math.log(d / u) / t, C = function(E) {
+ return [
+ a + E * g,
+ l + E * p,
+ u * Math.exp(t * E * $)
+ ];
+ };
+ else {
+ var m = Math.sqrt(x), _ = (d * d - u * u + r * x) / (2 * u * n * m), v = (d * d - u * u - r * x) / (2 * d * n * m), b = Math.log(Math.sqrt(_ * _ + 1) - _), N = Math.log(Math.sqrt(v * v + 1) - v);
+ $ = (N - b) / t, C = function(E) {
+ var M = E * $, D = tl(b), V = u / (n * m) * (D * Xh(t * M + b) - Zh(b));
+ return [
+ a + V * g,
+ l + V * p,
+ u * D / tl(t * M + b)
+ ];
+ };
+ }
+ return C.duration = $ * 1e3 * t / Math.SQRT2, C;
+ }
+ return o.rho = function(i) {
+ var s = Math.max(1e-3, +i), a = s * s, l = a * a;
+ return e(s, a, l);
+ }, o;
+}(Math.SQRT2, 2, 4);
+var Vr = 0, to = 0, Jr = 0, Zu = 1e3, fi, no, gi = 0, ur = 0, Ri = 0, po = typeof performance == "object" && performance.now ? performance : Date, Xu = typeof window == "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(e) {
+ setTimeout(e, 17);
+};
+function oa() {
+ return ur || (Xu(Wh), ur = po.now() + Ri);
+}
+function Wh() {
+ ur = 0;
+}
+function hi() {
+ this._call = this._time = this._next = null;
+}
+hi.prototype = Fu.prototype = {
+ constructor: hi,
+ restart: function(e, t, n) {
+ if (typeof e != "function") throw new TypeError("callback is not a function");
+ n = (n == null ? oa() : +n) + (t == null ? 0 : +t), !this._next && no !== this && (no ? no._next = this : fi = this, no = this), this._call = e, this._time = n, Es();
+ },
+ stop: function() {
+ this._call && (this._call = null, this._time = 1 / 0, Es());
+ }
+};
+function Fu(e, t, n) {
+ var r = new hi();
+ return r.restart(e, t, n), r;
+}
+function Kh() {
+ oa(), ++Vr;
+ for (var e = fi, t; e; )
+ (t = ur - e._time) >= 0 && e._call.call(void 0, t), e = e._next;
+ --Vr;
+}
+function nl() {
+ ur = (gi = po.now()) + Ri, Vr = to = 0;
+ try {
+ Kh();
+ } finally {
+ Vr = 0, Gh(), ur = 0;
+ }
+}
+function qh() {
+ var e = po.now(), t = e - gi;
+ t > Zu && (Ri -= t, gi = e);
+}
+function Gh() {
+ for (var e, t = fi, n, r = 1 / 0; t; )
+ t._call ? (r > t._time && (r = t._time), e = t, t = t._next) : (n = t._next, t._next = null, t = e ? e._next = n : fi = n);
+ no = e, Es(r);
+}
+function Es(e) {
+ if (!Vr) {
+ to && (to = clearTimeout(to));
+ var t = e - ur;
+ t > 24 ? (e < 1 / 0 && (to = setTimeout(nl, e - po.now() - Ri)), Jr && (Jr = clearInterval(Jr))) : (Jr || (gi = po.now(), Jr = setInterval(qh, Zu)), Vr = 1, Xu(nl));
+ }
+}
+function rl(e, t, n) {
+ var r = new hi();
+ return t = t == null ? 0 : +t, r.restart((o) => {
+ r.stop(), e(o + t);
+ }, t, n), r;
+}
+var Uh = Ii("start", "end", "cancel", "interrupt"), jh = [], Wu = 0, ol = 1, Ss = 2, jo = 3, il = 4, Ps = 5, Jo = 6;
+function Bi(e, t, n, r, o, i) {
+ var s = e.__transition;
+ if (!s) e.__transition = {};
+ else if (n in s) return;
+ Jh(e, n, {
+ name: t,
+ index: r,
+ // For context during callback.
+ group: o,
+ // For context during callback.
+ on: Uh,
+ tween: jh,
+ time: i.time,
+ delay: i.delay,
+ duration: i.duration,
+ ease: i.ease,
+ timer: null,
+ state: Wu
+ });
+}
+function ia(e, t) {
+ var n = cn(e, t);
+ if (n.state > Wu) throw new Error("too late; already scheduled");
+ return n;
+}
+function Cn(e, t) {
+ var n = cn(e, t);
+ if (n.state > jo) throw new Error("too late; already running");
+ return n;
+}
+function cn(e, t) {
+ var n = e.__transition;
+ if (!n || !(n = n[t])) throw new Error("transition not found");
+ return n;
+}
+function Jh(e, t, n) {
+ var r = e.__transition, o;
+ r[t] = n, n.timer = Fu(i, 0, n.time);
+ function i(u) {
+ n.state = ol, n.timer.restart(s, n.delay, n.time), n.delay <= u && s(u - n.delay);
+ }
+ function s(u) {
+ var c, f, d, g;
+ if (n.state !== ol) return l();
+ for (c in r)
+ if (g = r[c], g.name === n.name) {
+ if (g.state === jo) return rl(s);
+ g.state === il ? (g.state = Jo, g.timer.stop(), g.on.call("interrupt", e, e.__data__, g.index, g.group), delete r[c]) : +c < t && (g.state = Jo, g.timer.stop(), g.on.call("cancel", e, e.__data__, g.index, g.group), delete r[c]);
+ }
+ if (rl(function() {
+ n.state === jo && (n.state = il, n.timer.restart(a, n.delay, n.time), a(u));
+ }), n.state = Ss, n.on.call("start", e, e.__data__, n.index, n.group), n.state === Ss) {
+ for (n.state = jo, o = new Array(d = n.tween.length), c = 0, f = -1; c < d; ++c)
+ (g = n.tween[c].value.call(e, e.__data__, n.index, n.group)) && (o[++f] = g);
+ o.length = f + 1;
+ }
+ }
+ function a(u) {
+ for (var c = u < n.duration ? n.ease.call(null, u / n.duration) : (n.timer.restart(l), n.state = Ps, 1), f = -1, d = o.length; ++f < d; )
+ o[f].call(e, c);
+ n.state === Ps && (n.on.call("end", e, e.__data__, n.index, n.group), l());
+ }
+ function l() {
+ n.state = Jo, n.timer.stop(), delete r[t];
+ for (var u in r) return;
+ delete e.__transition;
+ }
+}
+function Qo(e, t) {
+ var n = e.__transition, r, o, i = !0, s;
+ if (n) {
+ t = t == null ? null : t + "";
+ for (s in n) {
+ if ((r = n[s]).name !== t) {
+ i = !1;
+ continue;
+ }
+ o = r.state > Ss && r.state < Ps, r.state = Jo, r.timer.stop(), r.on.call(o ? "interrupt" : "cancel", e, e.__data__, r.index, r.group), delete n[s];
+ }
+ i && delete e.__transition;
+ }
+}
+function Qh(e) {
+ return this.each(function() {
+ Qo(this, e);
+ });
+}
+function ev(e, t) {
+ var n, r;
+ return function() {
+ var o = Cn(this, e), i = o.tween;
+ if (i !== n) {
+ r = n = i;
+ for (var s = 0, a = r.length; s < a; ++s)
+ if (r[s].name === t) {
+ r = r.slice(), r.splice(s, 1);
+ break;
+ }
+ }
+ o.tween = r;
+ };
+}
+function tv(e, t, n) {
+ var r, o;
+ if (typeof n != "function") throw new Error();
+ return function() {
+ var i = Cn(this, e), s = i.tween;
+ if (s !== r) {
+ o = (r = s).slice();
+ for (var a = { name: t, value: n }, l = 0, u = o.length; l < u; ++l)
+ if (o[l].name === t) {
+ o[l] = a;
+ break;
+ }
+ l === u && o.push(a);
+ }
+ i.tween = o;
+ };
+}
+function nv(e, t) {
+ var n = this._id;
+ if (e += "", arguments.length < 2) {
+ for (var r = cn(this.node(), n).tween, o = 0, i = r.length, s; o < i; ++o)
+ if ((s = r[o]).name === e)
+ return s.value;
+ return null;
+ }
+ return this.each((t == null ? ev : tv)(n, e, t));
+}
+function sa(e, t, n) {
+ var r = e._id;
+ return e.each(function() {
+ var o = Cn(this, r);
+ (o.value || (o.value = {}))[t] = n.apply(this, arguments);
+ }), function(o) {
+ return cn(o, r).value[t];
+ };
+}
+function Ku(e, t) {
+ var n;
+ return (typeof t == "number" ? Yn : t instanceof vo ? Qa : (n = vo(t)) ? (t = n, Qa) : Oh)(e, t);
+}
+function rv(e) {
+ return function() {
+ this.removeAttribute(e);
+ };
+}
+function ov(e) {
+ return function() {
+ this.removeAttributeNS(e.space, e.local);
+ };
+}
+function iv(e, t, n) {
+ var r, o = n + "", i;
+ return function() {
+ var s = this.getAttribute(e);
+ return s === o ? null : s === r ? i : i = t(r = s, n);
+ };
+}
+function sv(e, t, n) {
+ var r, o = n + "", i;
+ return function() {
+ var s = this.getAttributeNS(e.space, e.local);
+ return s === o ? null : s === r ? i : i = t(r = s, n);
+ };
+}
+function av(e, t, n) {
+ var r, o, i;
+ return function() {
+ var s, a = n(this), l;
+ return a == null ? void this.removeAttribute(e) : (s = this.getAttribute(e), l = a + "", s === l ? null : s === r && l === o ? i : (o = l, i = t(r = s, a)));
+ };
+}
+function lv(e, t, n) {
+ var r, o, i;
+ return function() {
+ var s, a = n(this), l;
+ return a == null ? void this.removeAttributeNS(e.space, e.local) : (s = this.getAttributeNS(e.space, e.local), l = a + "", s === l ? null : s === r && l === o ? i : (o = l, i = t(r = s, a)));
+ };
+}
+function uv(e, t) {
+ var n = zi(e), r = n === "transform" ? Bh : Ku;
+ return this.attrTween(e, typeof t == "function" ? (n.local ? lv : av)(n, r, sa(this, "attr." + e, t)) : t == null ? (n.local ? ov : rv)(n) : (n.local ? sv : iv)(n, r, t));
+}
+function cv(e, t) {
+ return function(n) {
+ this.setAttribute(e, t.call(this, n));
+ };
+}
+function dv(e, t) {
+ return function(n) {
+ this.setAttributeNS(e.space, e.local, t.call(this, n));
+ };
+}
+function fv(e, t) {
+ var n, r;
+ function o() {
+ var i = t.apply(this, arguments);
+ return i !== r && (n = (r = i) && dv(e, i)), n;
+ }
+ return o._value = t, o;
+}
+function gv(e, t) {
+ var n, r;
+ function o() {
+ var i = t.apply(this, arguments);
+ return i !== r && (n = (r = i) && cv(e, i)), n;
+ }
+ return o._value = t, o;
+}
+function hv(e, t) {
+ var n = "attr." + e;
+ if (arguments.length < 2) return (n = this.tween(n)) && n._value;
+ if (t == null) return this.tween(n, null);
+ if (typeof t != "function") throw new Error();
+ var r = zi(e);
+ return this.tween(n, (r.local ? fv : gv)(r, t));
+}
+function vv(e, t) {
+ return function() {
+ ia(this, e).delay = +t.apply(this, arguments);
+ };
+}
+function pv(e, t) {
+ return t = +t, function() {
+ ia(this, e).delay = t;
+ };
+}
+function mv(e) {
+ var t = this._id;
+ return arguments.length ? this.each((typeof e == "function" ? vv : pv)(t, e)) : cn(this.node(), t).delay;
+}
+function yv(e, t) {
+ return function() {
+ Cn(this, e).duration = +t.apply(this, arguments);
+ };
+}
+function wv(e, t) {
+ return t = +t, function() {
+ Cn(this, e).duration = t;
+ };
+}
+function _v(e) {
+ var t = this._id;
+ return arguments.length ? this.each((typeof e == "function" ? yv : wv)(t, e)) : cn(this.node(), t).duration;
+}
+function xv(e, t) {
+ if (typeof t != "function") throw new Error();
+ return function() {
+ Cn(this, e).ease = t;
+ };
+}
+function bv(e) {
+ var t = this._id;
+ return arguments.length ? this.each(xv(t, e)) : cn(this.node(), t).ease;
+}
+function Cv(e, t) {
+ return function() {
+ var n = t.apply(this, arguments);
+ if (typeof n != "function") throw new Error();
+ Cn(this, e).ease = n;
+ };
+}
+function kv(e) {
+ if (typeof e != "function") throw new Error();
+ return this.each(Cv(this._id, e));
+}
+function $v(e) {
+ typeof e != "function" && (e = $u(e));
+ for (var t = this._groups, n = t.length, r = new Array(n), o = 0; o < n; ++o)
+ for (var i = t[o], s = i.length, a = r[o] = [], l, u = 0; u < s; ++u)
+ (l = i[u]) && e.call(l, l.__data__, u, i) && a.push(l);
+ return new Ln(r, this._parents, this._name, this._id);
+}
+function Ev(e) {
+ if (e._id !== this._id) throw new Error();
+ for (var t = this._groups, n = e._groups, r = t.length, o = n.length, i = Math.min(r, o), s = new Array(r), a = 0; a < i; ++a)
+ for (var l = t[a], u = n[a], c = l.length, f = s[a] = new Array(c), d, g = 0; g < c; ++g)
+ (d = l[g] || u[g]) && (f[g] = d);
+ for (; a < r; ++a)
+ s[a] = t[a];
+ return new Ln(s, this._parents, this._name, this._id);
+}
+function Sv(e) {
+ return (e + "").trim().split(/^|\s+/).every(function(t) {
+ var n = t.indexOf(".");
+ return n >= 0 && (t = t.slice(0, n)), !t || t === "start";
+ });
+}
+function Pv(e, t, n) {
+ var r, o, i = Sv(t) ? ia : Cn;
+ return function() {
+ var s = i(this, e), a = s.on;
+ a !== r && (o = (r = a).copy()).on(t, n), s.on = o;
+ };
+}
+function Nv(e, t) {
+ var n = this._id;
+ return arguments.length < 2 ? cn(this.node(), n).on.on(e) : this.each(Pv(n, e, t));
+}
+function Mv(e) {
+ return function() {
+ var t = this.parentNode;
+ for (var n in this.__transition) if (+n !== e) return;
+ t && t.removeChild(this);
+ };
+}
+function Tv() {
+ return this.on("end.remove", Mv(this._id));
+}
+function Hv(e) {
+ var t = this._name, n = this._id;
+ typeof e != "function" && (e = ta(e));
+ for (var r = this._groups, o = r.length, i = new Array(o), s = 0; s < o; ++s)
+ for (var a = r[s], l = a.length, u = i[s] = new Array(l), c, f, d = 0; d < l; ++d)
+ (c = a[d]) && (f = e.call(c, c.__data__, d, a)) && ("__data__" in c && (f.__data__ = c.__data__), u[d] = f, Bi(u[d], t, n, d, u, cn(c, n)));
+ return new Ln(i, this._parents, t, n);
+}
+function Vv(e) {
+ var t = this._name, n = this._id;
+ typeof e != "function" && (e = ku(e));
+ for (var r = this._groups, o = r.length, i = [], s = [], a = 0; a < o; ++a)
+ for (var l = r[a], u = l.length, c, f = 0; f < u; ++f)
+ if (c = l[f]) {
+ for (var d = e.call(c, c.__data__, f, l), g, p = cn(c, n), x = 0, C = d.length; x < C; ++x)
+ (g = d[x]) && Bi(g, t, n, x, d, p);
+ i.push(d), s.push(c);
+ }
+ return new Ln(i, s, t, n);
+}
+var Dv = Eo.prototype.constructor;
+function Av() {
+ return new Dv(this._groups, this._parents);
+}
+function Lv(e, t) {
+ var n, r, o;
+ return function() {
+ var i = Hr(this, e), s = (this.style.removeProperty(e), Hr(this, e));
+ return i === s ? null : i === n && s === r ? o : o = t(n = i, r = s);
+ };
+}
+function qu(e) {
+ return function() {
+ this.style.removeProperty(e);
+ };
+}
+function Ov(e, t, n) {
+ var r, o = n + "", i;
+ return function() {
+ var s = Hr(this, e);
+ return s === o ? null : s === r ? i : i = t(r = s, n);
+ };
+}
+function Iv(e, t, n) {
+ var r, o, i;
+ return function() {
+ var s = Hr(this, e), a = n(this), l = a + "";
+ return a == null && (l = a = (this.style.removeProperty(e), Hr(this, e))), s === l ? null : s === r && l === o ? i : (o = l, i = t(r = s, a));
+ };
+}
+function zv(e, t) {
+ var n, r, o, i = "style." + t, s = "end." + i, a;
+ return function() {
+ var l = Cn(this, e), u = l.on, c = l.value[i] == null ? a || (a = qu(t)) : void 0;
+ (u !== n || o !== c) && (r = (n = u).copy()).on(s, o = c), l.on = r;
+ };
+}
+function Rv(e, t, n) {
+ var r = (e += "") == "transform" ? Rh : Ku;
+ return t == null ? this.styleTween(e, Lv(e, r)).on("end.style." + e, qu(e)) : typeof t == "function" ? this.styleTween(e, Iv(e, r, sa(this, "style." + e, t))).each(zv(this._id, e)) : this.styleTween(e, Ov(e, r, t), n).on("end.style." + e, null);
+}
+function Bv(e, t, n) {
+ return function(r) {
+ this.style.setProperty(e, t.call(this, r), n);
+ };
+}
+function Yv(e, t, n) {
+ var r, o;
+ function i() {
+ var s = t.apply(this, arguments);
+ return s !== o && (r = (o = s) && Bv(e, s, n)), r;
+ }
+ return i._value = t, i;
+}
+function Zv(e, t, n) {
+ var r = "style." + (e += "");
+ if (arguments.length < 2) return (r = this.tween(r)) && r._value;
+ if (t == null) return this.tween(r, null);
+ if (typeof t != "function") throw new Error();
+ return this.tween(r, Yv(e, t, n ?? ""));
+}
+function Xv(e) {
+ return function() {
+ this.textContent = e;
+ };
+}
+function Fv(e) {
+ return function() {
+ var t = e(this);
+ this.textContent = t ?? "";
+ };
+}
+function Wv(e) {
+ return this.tween("text", typeof e == "function" ? Fv(sa(this, "text", e)) : Xv(e == null ? "" : e + ""));
+}
+function Kv(e) {
+ return function(t) {
+ this.textContent = e.call(this, t);
+ };
+}
+function qv(e) {
+ var t, n;
+ function r() {
+ var o = e.apply(this, arguments);
+ return o !== n && (t = (n = o) && Kv(o)), t;
+ }
+ return r._value = e, r;
+}
+function Gv(e) {
+ var t = "text";
+ if (arguments.length < 1) return (t = this.tween(t)) && t._value;
+ if (e == null) return this.tween(t, null);
+ if (typeof e != "function") throw new Error();
+ return this.tween(t, qv(e));
+}
+function Uv() {
+ for (var e = this._name, t = this._id, n = Gu(), r = this._groups, o = r.length, i = 0; i < o; ++i)
+ for (var s = r[i], a = s.length, l, u = 0; u < a; ++u)
+ if (l = s[u]) {
+ var c = cn(l, t);
+ Bi(l, e, n, u, s, {
+ time: c.time + c.delay + c.duration,
+ delay: 0,
+ duration: c.duration,
+ ease: c.ease
+ });
+ }
+ return new Ln(r, this._parents, e, n);
+}
+function jv() {
+ var e, t, n = this, r = n._id, o = n.size();
+ return new Promise(function(i, s) {
+ var a = { value: s }, l = { value: function() {
+ --o === 0 && i();
+ } };
+ n.each(function() {
+ var u = Cn(this, r), c = u.on;
+ c !== e && (t = (e = c).copy(), t._.cancel.push(a), t._.interrupt.push(a), t._.end.push(l)), u.on = t;
+ }), o === 0 && i();
+ });
+}
+var Jv = 0;
+function Ln(e, t, n, r) {
+ this._groups = e, this._parents = t, this._name = n, this._id = r;
+}
+function Gu() {
+ return ++Jv;
+}
+var $n = Eo.prototype;
+Ln.prototype = {
+ constructor: Ln,
+ select: Hv,
+ selectAll: Vv,
+ selectChild: $n.selectChild,
+ selectChildren: $n.selectChildren,
+ filter: $v,
+ merge: Ev,
+ selection: Av,
+ transition: Uv,
+ call: $n.call,
+ nodes: $n.nodes,
+ node: $n.node,
+ size: $n.size,
+ empty: $n.empty,
+ each: $n.each,
+ on: Nv,
+ attr: uv,
+ attrTween: hv,
+ style: Rv,
+ styleTween: Zv,
+ text: Wv,
+ textTween: Gv,
+ remove: Tv,
+ tween: nv,
+ delay: mv,
+ duration: _v,
+ ease: bv,
+ easeVarying: kv,
+ end: jv,
+ [Symbol.iterator]: $n[Symbol.iterator]
+};
+function Qv(e) {
+ return ((e *= 2) <= 1 ? e * e * e : (e -= 2) * e * e + 2) / 2;
+}
+var e0 = {
+ time: null,
+ // Set on use.
+ delay: 0,
+ duration: 250,
+ ease: Qv
+};
+function t0(e, t) {
+ for (var n; !(n = e.__transition) || !(n = n[t]); )
+ if (!(e = e.parentNode))
+ throw new Error(`transition ${t} not found`);
+ return n;
+}
+function n0(e) {
+ var t, n;
+ e instanceof Ln ? (t = e._id, e = e._name) : (t = Gu(), (n = e0).time = oa(), e = e == null ? null : e + "");
+ for (var r = this._groups, o = r.length, i = 0; i < o; ++i)
+ for (var s = r[i], a = s.length, l, u = 0; u < a; ++u)
+ (l = s[u]) && Bi(l, e, t, u, s, n || t0(l, t));
+ return new Ln(r, this._parents, e, t);
+}
+Eo.prototype.interrupt = Qh;
+Eo.prototype.transition = n0;
+const Wo = (e) => () => e;
+function r0(e, {
+ sourceEvent: t,
+ target: n,
+ transform: r,
+ dispatch: o
+}) {
+ Object.defineProperties(this, {
+ type: { value: e, enumerable: !0, configurable: !0 },
+ sourceEvent: { value: t, enumerable: !0, configurable: !0 },
+ target: { value: n, enumerable: !0, configurable: !0 },
+ transform: { value: r, enumerable: !0, configurable: !0 },
+ _: { value: o }
+ });
+}
+function Pn(e, t, n) {
+ this.k = e, this.x = t, this.y = n;
+}
+Pn.prototype = {
+ constructor: Pn,
+ scale: function(e) {
+ return e === 1 ? this : new Pn(this.k * e, this.x, this.y);
+ },
+ translate: function(e, t) {
+ return e === 0 & t === 0 ? this : new Pn(this.k, this.x + this.k * e, this.y + this.k * t);
+ },
+ apply: function(e) {
+ return [e[0] * this.k + this.x, e[1] * this.k + this.y];
+ },
+ applyX: function(e) {
+ return e * this.k + this.x;
+ },
+ applyY: function(e) {
+ return e * this.k + this.y;
+ },
+ invert: function(e) {
+ return [(e[0] - this.x) / this.k, (e[1] - this.y) / this.k];
+ },
+ invertX: function(e) {
+ return (e - this.x) / this.k;
+ },
+ invertY: function(e) {
+ return (e - this.y) / this.k;
+ },
+ rescaleX: function(e) {
+ return e.copy().domain(e.range().map(this.invertX, this).map(e.invert, e));
+ },
+ rescaleY: function(e) {
+ return e.copy().domain(e.range().map(this.invertY, this).map(e.invert, e));
+ },
+ toString: function() {
+ return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")";
+ }
+};
+var Yi = new Pn(1, 0, 0);
+Uu.prototype = Pn.prototype;
+function Uu(e) {
+ for (; !e.__zoom; ) if (!(e = e.parentNode)) return Yi;
+ return e.__zoom;
+}
+function os(e) {
+ e.stopImmediatePropagation();
+}
+function Qr(e) {
+ e.preventDefault(), e.stopImmediatePropagation();
+}
+function o0(e) {
+ return (!e.ctrlKey || e.type === "wheel") && !e.button;
+}
+function i0() {
+ var e = this;
+ return e instanceof SVGElement ? (e = e.ownerSVGElement || e, e.hasAttribute("viewBox") ? (e = e.viewBox.baseVal, [[e.x, e.y], [e.x + e.width, e.y + e.height]]) : [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]]) : [[0, 0], [e.clientWidth, e.clientHeight]];
+}
+function sl() {
+ return this.__zoom || Yi;
+}
+function s0(e) {
+ return -e.deltaY * (e.deltaMode === 1 ? 0.05 : e.deltaMode ? 1 : 2e-3) * (e.ctrlKey ? 10 : 1);
+}
+function a0() {
+ return navigator.maxTouchPoints || "ontouchstart" in this;
+}
+function l0(e, t, n) {
+ var r = e.invertX(t[0][0]) - n[0][0], o = e.invertX(t[1][0]) - n[1][0], i = e.invertY(t[0][1]) - n[0][1], s = e.invertY(t[1][1]) - n[1][1];
+ return e.translate(
+ o > r ? (r + o) / 2 : Math.min(0, r) || Math.max(0, o),
+ s > i ? (i + s) / 2 : Math.min(0, i) || Math.max(0, s)
+ );
+}
+function ju() {
+ var e = o0, t = i0, n = l0, r = s0, o = a0, i = [0, 1 / 0], s = [[-1 / 0, -1 / 0], [1 / 0, 1 / 0]], a = 250, l = Fh, u = Ii("start", "zoom", "end"), c, f, d, g = 500, p = 150, x = 0, C = 10;
+ function $(S) {
+ S.property("__zoom", sl).on("wheel.zoom", M, { passive: !1 }).on("mousedown.zoom", D).on("dblclick.zoom", V).filter(o).on("touchstart.zoom", A).on("touchmove.zoom", O).on("touchend.zoom touchcancel.zoom", R).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)");
+ }
+ $.transform = function(S, T, k, P) {
+ var H = S.selection ? S.selection() : S;
+ H.property("__zoom", sl), S !== H ? b(S, T, k, P) : H.interrupt().each(function() {
+ N(this, arguments).event(P).start().zoom(null, typeof T == "function" ? T.apply(this, arguments) : T).end();
+ });
+ }, $.scaleBy = function(S, T, k, P) {
+ $.scaleTo(S, function() {
+ var H = this.__zoom.k, I = typeof T == "function" ? T.apply(this, arguments) : T;
+ return H * I;
+ }, k, P);
+ }, $.scaleTo = function(S, T, k, P) {
+ $.transform(S, function() {
+ var H = t.apply(this, arguments), I = this.__zoom, B = k == null ? v(H) : typeof k == "function" ? k.apply(this, arguments) : k, F = I.invert(B), K = typeof T == "function" ? T.apply(this, arguments) : T;
+ return n(_(m(I, K), B, F), H, s);
+ }, k, P);
+ }, $.translateBy = function(S, T, k, P) {
+ $.transform(S, function() {
+ return n(this.__zoom.translate(
+ typeof T == "function" ? T.apply(this, arguments) : T,
+ typeof k == "function" ? k.apply(this, arguments) : k
+ ), t.apply(this, arguments), s);
+ }, null, P);
+ }, $.translateTo = function(S, T, k, P, H) {
+ $.transform(S, function() {
+ var I = t.apply(this, arguments), B = this.__zoom, F = P == null ? v(I) : typeof P == "function" ? P.apply(this, arguments) : P;
+ return n(Yi.translate(F[0], F[1]).scale(B.k).translate(
+ typeof T == "function" ? -T.apply(this, arguments) : -T,
+ typeof k == "function" ? -k.apply(this, arguments) : -k
+ ), I, s);
+ }, P, H);
+ };
+ function m(S, T) {
+ return T = Math.max(i[0], Math.min(i[1], T)), T === S.k ? S : new Pn(T, S.x, S.y);
+ }
+ function _(S, T, k) {
+ var P = T[0] - k[0] * S.k, H = T[1] - k[1] * S.k;
+ return P === S.x && H === S.y ? S : new Pn(S.k, P, H);
+ }
+ function v(S) {
+ return [(+S[0][0] + +S[1][0]) / 2, (+S[0][1] + +S[1][1]) / 2];
+ }
+ function b(S, T, k, P) {
+ S.on("start.zoom", function() {
+ N(this, arguments).event(P).start();
+ }).on("interrupt.zoom end.zoom", function() {
+ N(this, arguments).event(P).end();
+ }).tween("zoom", function() {
+ var H = this, I = arguments, B = N(H, I).event(P), F = t.apply(H, I), K = k == null ? v(F) : typeof k == "function" ? k.apply(H, I) : k, ie = Math.max(F[1][0] - F[0][0], F[1][1] - F[0][1]), ee = H.__zoom, W = typeof T == "function" ? T.apply(H, I) : T, ue = l(ee.invert(K).concat(ie / ee.k), W.invert(K).concat(ie / W.k));
+ return function(me) {
+ if (me === 1) me = W;
+ else {
+ var Ce = ue(me), ge = ie / Ce[2];
+ me = new Pn(ge, K[0] - Ce[0] * ge, K[1] - Ce[1] * ge);
+ }
+ B.zoom(null, me);
+ };
+ });
+ }
+ function N(S, T, k) {
+ return !k && S.__zooming || new E(S, T);
+ }
+ function E(S, T) {
+ this.that = S, this.args = T, this.active = 0, this.sourceEvent = null, this.extent = t.apply(S, T), this.taps = 0;
+ }
+ E.prototype = {
+ event: function(S) {
+ return S && (this.sourceEvent = S), this;
+ },
+ start: function() {
+ return ++this.active === 1 && (this.that.__zooming = this, this.emit("start")), this;
+ },
+ zoom: function(S, T) {
+ return this.mouse && S !== "mouse" && (this.mouse[1] = T.invert(this.mouse[0])), this.touch0 && S !== "touch" && (this.touch0[1] = T.invert(this.touch0[0])), this.touch1 && S !== "touch" && (this.touch1[1] = T.invert(this.touch1[0])), this.that.__zoom = T, this.emit("zoom"), this;
+ },
+ end: function() {
+ return --this.active === 0 && (delete this.that.__zooming, this.emit("end")), this;
+ },
+ emit: function(S) {
+ var T = Kt(this.that).datum();
+ u.call(
+ S,
+ this.that,
+ new r0(S, {
+ sourceEvent: this.sourceEvent,
+ target: $,
+ transform: this.that.__zoom,
+ dispatch: u
+ }),
+ T
+ );
+ }
+ };
+ function M(S, ...T) {
+ if (!e.apply(this, arguments)) return;
+ var k = N(this, T).event(S), P = this.__zoom, H = Math.max(i[0], Math.min(i[1], P.k * Math.pow(2, r.apply(this, arguments)))), I = Qt(S);
+ if (k.wheel)
+ (k.mouse[0][0] !== I[0] || k.mouse[0][1] !== I[1]) && (k.mouse[1] = P.invert(k.mouse[0] = I)), clearTimeout(k.wheel);
+ else {
+ if (P.k === H) return;
+ k.mouse = [I, P.invert(I)], Qo(this), k.start();
+ }
+ Qr(S), k.wheel = setTimeout(B, p), k.zoom("mouse", n(_(m(P, H), k.mouse[0], k.mouse[1]), k.extent, s));
+ function B() {
+ k.wheel = null, k.end();
+ }
+ }
+ function D(S, ...T) {
+ if (d || !e.apply(this, arguments)) return;
+ var k = S.currentTarget, P = N(this, T, !0).event(S), H = Kt(S.view).on("mousemove.zoom", K, !0).on("mouseup.zoom", ie, !0), I = Qt(S, k), B = S.clientX, F = S.clientY;
+ Au(S.view), os(S), P.mouse = [I, this.__zoom.invert(I)], Qo(this), P.start();
+ function K(ee) {
+ if (Qr(ee), !P.moved) {
+ var W = ee.clientX - B, ue = ee.clientY - F;
+ P.moved = W * W + ue * ue > x;
+ }
+ P.event(ee).zoom("mouse", n(_(P.that.__zoom, P.mouse[0] = Qt(ee, k), P.mouse[1]), P.extent, s));
+ }
+ function ie(ee) {
+ H.on("mousemove.zoom mouseup.zoom", null), Lu(ee.view, P.moved), Qr(ee), P.event(ee).end();
+ }
+ }
+ function V(S, ...T) {
+ if (e.apply(this, arguments)) {
+ var k = this.__zoom, P = Qt(S.changedTouches ? S.changedTouches[0] : S, this), H = k.invert(P), I = k.k * (S.shiftKey ? 0.5 : 2), B = n(_(m(k, I), P, H), t.apply(this, T), s);
+ Qr(S), a > 0 ? Kt(this).transition().duration(a).call(b, B, P, S) : Kt(this).call($.transform, B, P, S);
+ }
+ }
+ function A(S, ...T) {
+ if (e.apply(this, arguments)) {
+ var k = S.touches, P = k.length, H = N(this, T, S.changedTouches.length === P).event(S), I, B, F, K;
+ for (os(S), B = 0; B < P; ++B)
+ F = k[B], K = Qt(F, this), K = [K, this.__zoom.invert(K), F.identifier], H.touch0 ? !H.touch1 && H.touch0[2] !== K[2] && (H.touch1 = K, H.taps = 0) : (H.touch0 = K, I = !0, H.taps = 1 + !!c);
+ c && (c = clearTimeout(c)), I && (H.taps < 2 && (f = K[0], c = setTimeout(function() {
+ c = null;
+ }, g)), Qo(this), H.start());
+ }
+ }
+ function O(S, ...T) {
+ if (this.__zooming) {
+ var k = N(this, T).event(S), P = S.changedTouches, H = P.length, I, B, F, K;
+ for (Qr(S), I = 0; I < H; ++I)
+ B = P[I], F = Qt(B, this), k.touch0 && k.touch0[2] === B.identifier ? k.touch0[0] = F : k.touch1 && k.touch1[2] === B.identifier && (k.touch1[0] = F);
+ if (B = k.that.__zoom, k.touch1) {
+ var ie = k.touch0[0], ee = k.touch0[1], W = k.touch1[0], ue = k.touch1[1], me = (me = W[0] - ie[0]) * me + (me = W[1] - ie[1]) * me, Ce = (Ce = ue[0] - ee[0]) * Ce + (Ce = ue[1] - ee[1]) * Ce;
+ B = m(B, Math.sqrt(me / Ce)), F = [(ie[0] + W[0]) / 2, (ie[1] + W[1]) / 2], K = [(ee[0] + ue[0]) / 2, (ee[1] + ue[1]) / 2];
+ } else if (k.touch0) F = k.touch0[0], K = k.touch0[1];
+ else return;
+ k.zoom("touch", n(_(B, F, K), k.extent, s));
+ }
+ }
+ function R(S, ...T) {
+ if (this.__zooming) {
+ var k = N(this, T).event(S), P = S.changedTouches, H = P.length, I, B;
+ for (os(S), d && clearTimeout(d), d = setTimeout(function() {
+ d = null;
+ }, g), I = 0; I < H; ++I)
+ B = P[I], k.touch0 && k.touch0[2] === B.identifier ? delete k.touch0 : k.touch1 && k.touch1[2] === B.identifier && delete k.touch1;
+ if (k.touch1 && !k.touch0 && (k.touch0 = k.touch1, delete k.touch1), k.touch0) k.touch0[1] = this.__zoom.invert(k.touch0[0]);
+ else if (k.end(), k.taps === 2 && (B = Qt(B, this), Math.hypot(f[0] - B[0], f[1] - B[1]) < C)) {
+ var F = Kt(this).on("dblclick.zoom");
+ F && F.apply(this, arguments);
+ }
+ }
+ }
+ return $.wheelDelta = function(S) {
+ return arguments.length ? (r = typeof S == "function" ? S : Wo(+S), $) : r;
+ }, $.filter = function(S) {
+ return arguments.length ? (e = typeof S == "function" ? S : Wo(!!S), $) : e;
+ }, $.touchable = function(S) {
+ return arguments.length ? (o = typeof S == "function" ? S : Wo(!!S), $) : o;
+ }, $.extent = function(S) {
+ return arguments.length ? (t = typeof S == "function" ? S : Wo([[+S[0][0], +S[0][1]], [+S[1][0], +S[1][1]]]), $) : t;
+ }, $.scaleExtent = function(S) {
+ return arguments.length ? (i[0] = +S[0], i[1] = +S[1], $) : [i[0], i[1]];
+ }, $.translateExtent = function(S) {
+ return arguments.length ? (s[0][0] = +S[0][0], s[1][0] = +S[1][0], s[0][1] = +S[0][1], s[1][1] = +S[1][1], $) : [[s[0][0], s[0][1]], [s[1][0], s[1][1]]];
+ }, $.constrain = function(S) {
+ return arguments.length ? (n = S, $) : n;
+ }, $.duration = function(S) {
+ return arguments.length ? (a = +S, $) : a;
+ }, $.interpolate = function(S) {
+ return arguments.length ? (l = S, $) : l;
+ }, $.on = function() {
+ var S = u.on.apply(u, arguments);
+ return S === u ? $ : S;
+ }, $.clickDistance = function(S) {
+ return arguments.length ? (x = (S = +S) * S, $) : Math.sqrt(x);
+ }, $.tapDistance = function(S) {
+ return arguments.length ? (C = +S, $) : C;
+ }, $;
+}
+const Dr = {
+ error001: () => "[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001",
+ error002: () => "It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",
+ error003: (e) => `Node type "${e}" not found. Using fallback type "default".`,
+ error004: () => "The React Flow parent container needs a width and a height to render the graph.",
+ error005: () => "Only child nodes can use a parent extent.",
+ error006: () => "Can't create edge. An edge needs a source and a target.",
+ error007: (e) => `The old edge with id=${e} does not exist.`,
+ error009: (e) => `Marker type "${e}" doesn't exist.`,
+ error008: (e, { id: t, sourceHandle: n, targetHandle: r }) => `Couldn't create edge for ${e} handle id: "${e === "source" ? n : r}", edge id: ${t}.`,
+ error010: () => "Handle: No node id found. Make sure to only use a Handle inside a custom Node.",
+ error011: (e) => `Edge type "${e}" not found. Using fallback type "default".`,
+ error012: (e) => `Node with id "${e}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
+ error013: (e = "react") => `It seems that you haven't loaded the styles. Please import '@xyflow/${e}/dist/style.css' or base.css to make sure everything is working properly.`,
+ error014: () => "useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.",
+ error015: () => "It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs."
+}, vi = [
+ [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
+ [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
+];
+var cr;
+(function(e) {
+ e.Strict = "strict", e.Loose = "loose";
+})(cr || (cr = {}));
+var qn;
+(function(e) {
+ e.Free = "free", e.Vertical = "vertical", e.Horizontal = "horizontal";
+})(qn || (qn = {}));
+var pi;
+(function(e) {
+ e.Partial = "partial", e.Full = "full";
+})(pi || (pi = {}));
+const Ns = {
+ inProgress: !1,
+ isValid: null,
+ from: null,
+ fromHandle: null,
+ fromPosition: null,
+ fromNode: null,
+ to: null,
+ toHandle: null,
+ toPosition: null,
+ toNode: null
+};
+var Cr;
+(function(e) {
+ e.Bezier = "default", e.Straight = "straight", e.Step = "step", e.SmoothStep = "smoothstep", e.SimpleBezier = "simplebezier";
+})(Cr || (Cr = {}));
+var mo;
+(function(e) {
+ e.Arrow = "arrow", e.ArrowClosed = "arrowclosed";
+})(mo || (mo = {}));
+var $e;
+(function(e) {
+ e.Left = "left", e.Top = "top", e.Right = "right", e.Bottom = "bottom";
+})($e || ($e = {}));
+const al = {
+ [$e.Left]: $e.Right,
+ [$e.Right]: $e.Left,
+ [$e.Top]: $e.Bottom,
+ [$e.Bottom]: $e.Top
+};
+function u0(e, t) {
+ if (!e && !t)
+ return !0;
+ if (!e || !t || e.size !== t.size)
+ return !1;
+ if (!e.size && !t.size)
+ return !0;
+ for (const n of e.keys())
+ if (!t.has(n))
+ return !1;
+ return !0;
+}
+function ll(e, t, n) {
+ if (!n)
+ return;
+ const r = [];
+ e.forEach((o, i) => {
+ t != null && t.has(i) || r.push(o);
+ }), r.length && n(r);
+}
+function c0(e) {
+ return e === null ? null : e ? "valid" : "invalid";
+}
+const d0 = (e) => "id" in e && "source" in e && "target" in e, f0 = (e) => "id" in e && "position" in e && !("source" in e) && !("target" in e), aa = (e) => "id" in e && "internals" in e && !("source" in e) && !("target" in e), Po = (e, t = [0, 0]) => {
+ const { width: n, height: r } = tr(e), o = e.origin ?? t, i = n * o[0], s = r * o[1];
+ return {
+ x: e.position.x - i,
+ y: e.position.y - s
+ };
+}, g0 = (e, t = { nodeOrigin: [0, 0], nodeLookup: void 0 }) => {
+ if (e.length === 0)
+ return { x: 0, y: 0, width: 0, height: 0 };
+ const n = e.reduce((r, o) => {
+ const i = typeof o == "string";
+ let s = !t.nodeLookup && !i ? o : void 0;
+ t.nodeLookup && (s = i ? t.nodeLookup.get(o) : aa(o) ? o : t.nodeLookup.get(o.id));
+ const a = s ? mi(s, t.nodeOrigin) : { x: 0, y: 0, x2: 0, y2: 0 };
+ return Zi(r, a);
+ }, { x: 1 / 0, y: 1 / 0, x2: -1 / 0, y2: -1 / 0 });
+ return Xi(n);
+}, No = (e, t = {}) => {
+ if (e.size === 0)
+ return { x: 0, y: 0, width: 0, height: 0 };
+ let n = { x: 1 / 0, y: 1 / 0, x2: -1 / 0, y2: -1 / 0 };
+ return e.forEach((r) => {
+ if (t.filter === void 0 || t.filter(r)) {
+ const o = mi(r);
+ n = Zi(n, o);
+ }
+ }), Xi(n);
+}, Ju = (e, t, [n, r, o] = [0, 0, 1], i = !1, s = !1) => {
+ const a = {
+ ...Mo(t, [n, r, o]),
+ width: t.width / o,
+ height: t.height / o
+ }, l = [];
+ for (const u of e.values()) {
+ const { measured: c, selectable: f = !0, hidden: d = !1 } = u;
+ if (s && !f || d)
+ continue;
+ const g = c.width ?? u.width ?? u.initialWidth ?? null, p = c.height ?? u.height ?? u.initialHeight ?? null, x = yo(a, Lr(u)), C = (g ?? 0) * (p ?? 0), $ = i && x > 0;
+ (!u.internals.handleBounds || $ || x >= C || u.dragging) && l.push(u);
+ }
+ return l;
+}, Ms = (e, t) => {
+ const n = /* @__PURE__ */ new Set();
+ return e.forEach((r) => {
+ n.add(r.id);
+ }), t.filter((r) => n.has(r.source) || n.has(r.target));
+};
+function ul(e, t) {
+ const n = /* @__PURE__ */ new Map(), r = t != null && t.nodes ? new Set(t.nodes.map((o) => o.id)) : null;
+ return e.forEach((o) => {
+ o.measured.width && o.measured.height && ((t == null ? void 0 : t.includeHiddenNodes) || !o.hidden) && (!r || r.has(o.id)) && n.set(o.id, o);
+ }), n;
+}
+async function cl({ nodes: e, width: t, height: n, panZoom: r, minZoom: o, maxZoom: i }, s) {
+ if (e.size === 0)
+ return Promise.resolve(!1);
+ const a = No(e), l = ua(a, t, n, (s == null ? void 0 : s.minZoom) ?? o, (s == null ? void 0 : s.maxZoom) ?? i, (s == null ? void 0 : s.padding) ?? 0.1);
+ return await r.setViewport(l, { duration: s == null ? void 0 : s.duration }), Promise.resolve(!0);
+}
+function h0({ nodeId: e, nextPosition: t, nodeLookup: n, nodeOrigin: r = [0, 0], nodeExtent: o, onError: i }) {
+ const s = n.get(e), a = s.parentId ? n.get(s.parentId) : void 0, { x: l, y: u } = a ? a.internals.positionAbsolute : { x: 0, y: 0 }, c = s.origin ?? r;
+ let f = o;
+ if (s.extent === "parent" && !s.expandParent)
+ if (!a)
+ i == null || i("005", Dr.error005());
+ else {
+ const g = a.measured.width, p = a.measured.height;
+ g && p && (f = [
+ [l, u],
+ [l + g, u + p]
+ ]);
+ }
+ else a && Or(s.extent) && (f = [
+ [s.extent[0][0] + l, s.extent[0][1] + u],
+ [s.extent[1][0] + l, s.extent[1][1] + u]
+ ]);
+ const d = Or(f) ? dr(t, f, s.measured) : t;
+ return (s.measured.width === void 0 || s.measured.height === void 0) && (i == null || i("015", Dr.error015())), {
+ position: {
+ x: d.x - l + (s.measured.width ?? 0) * c[0],
+ y: d.y - u + (s.measured.height ?? 0) * c[1]
+ },
+ positionAbsolute: d
+ };
+}
+async function Qu({ nodesToRemove: e = [], edgesToRemove: t = [], nodes: n, edges: r, onBeforeDelete: o }) {
+ const i = new Set(e.map((d) => d.id)), s = [];
+ for (const d of n) {
+ if (d.deletable === !1)
+ continue;
+ const g = i.has(d.id), p = !g && d.parentId && s.find((x) => x.id === d.parentId);
+ (g || p) && s.push(d);
+ }
+ const a = new Set(t.map((d) => d.id)), l = r.filter((d) => d.deletable !== !1), c = Ms(s, l);
+ for (const d of l)
+ a.has(d.id) && !c.find((p) => p.id === d.id) && c.push(d);
+ if (!o)
+ return {
+ edges: c,
+ nodes: s
+ };
+ const f = await o({
+ nodes: s,
+ edges: c
+ });
+ return typeof f == "boolean" ? f ? { edges: c, nodes: s } : { edges: [], nodes: [] } : f;
+}
+const Ar = (e, t = 0, n = 1) => Math.min(Math.max(e, t), n), dr = (e = { x: 0, y: 0 }, t, n) => ({
+ x: Ar(e.x, t[0][0], t[1][0] - ((n == null ? void 0 : n.width) ?? 0)),
+ y: Ar(e.y, t[0][1], t[1][1] - ((n == null ? void 0 : n.height) ?? 0))
+});
+function ec(e, t, n) {
+ const { width: r, height: o } = tr(n), { x: i, y: s } = n.internals.positionAbsolute;
+ return dr(e, [
+ [i, s],
+ [i + r, s + o]
+ ], t);
+}
+const dl = (e, t, n) => e < t ? Ar(Math.abs(e - t), 1, t) / t : e > n ? -Ar(Math.abs(e - n), 1, t) / t : 0, tc = (e, t, n = 15, r = 40) => {
+ const o = dl(e.x, r, t.width - r) * n, i = dl(e.y, r, t.height - r) * n;
+ return [o, i];
+}, Zi = (e, t) => ({
+ x: Math.min(e.x, t.x),
+ y: Math.min(e.y, t.y),
+ x2: Math.max(e.x2, t.x2),
+ y2: Math.max(e.y2, t.y2)
+}), Ts = ({ x: e, y: t, width: n, height: r }) => ({
+ x: e,
+ y: t,
+ x2: e + n,
+ y2: t + r
+}), Xi = ({ x: e, y: t, x2: n, y2: r }) => ({
+ x: e,
+ y: t,
+ width: n - e,
+ height: r - t
+}), Lr = (e, t = [0, 0]) => {
+ var o, i;
+ const { x: n, y: r } = aa(e) ? e.internals.positionAbsolute : Po(e, t);
+ return {
+ x: n,
+ y: r,
+ width: ((o = e.measured) == null ? void 0 : o.width) ?? e.width ?? e.initialWidth ?? 0,
+ height: ((i = e.measured) == null ? void 0 : i.height) ?? e.height ?? e.initialHeight ?? 0
+ };
+}, mi = (e, t = [0, 0]) => {
+ var o, i;
+ const { x: n, y: r } = aa(e) ? e.internals.positionAbsolute : Po(e, t);
+ return {
+ x: n,
+ y: r,
+ x2: n + (((o = e.measured) == null ? void 0 : o.width) ?? e.width ?? e.initialWidth ?? 0),
+ y2: r + (((i = e.measured) == null ? void 0 : i.height) ?? e.height ?? e.initialHeight ?? 0)
+ };
+}, nc = (e, t) => Xi(Zi(Ts(e), Ts(t))), yo = (e, t) => {
+ const n = Math.max(0, Math.min(e.x + e.width, t.x + t.width) - Math.max(e.x, t.x)), r = Math.max(0, Math.min(e.y + e.height, t.y + t.height) - Math.max(e.y, t.y));
+ return Math.ceil(n * r);
+}, fl = (e) => Nn(e.width) && Nn(e.height) && Nn(e.x) && Nn(e.y), Nn = (e) => !isNaN(e) && isFinite(e), v0 = (e, t) => {
+}, la = (e, t = [1, 1]) => ({
+ x: t[0] * Math.round(e.x / t[0]),
+ y: t[1] * Math.round(e.y / t[1])
+}), Mo = ({ x: e, y: t }, [n, r, o], i = !1, s = [1, 1]) => {
+ const a = {
+ x: (e - n) / o,
+ y: (t - r) / o
+ };
+ return i ? la(a, s) : a;
+}, rc = ({ x: e, y: t }, [n, r, o]) => ({
+ x: e * o + n,
+ y: t * o + r
+}), ua = (e, t, n, r, o, i) => {
+ const s = t / (e.width * (1 + i)), a = n / (e.height * (1 + i)), l = Math.min(s, a), u = Ar(l, r, o), c = e.x + e.width / 2, f = e.y + e.height / 2, d = t / 2 - c * u, g = n / 2 - f * u;
+ return { x: d, y: g, zoom: u };
+}, yi = () => {
+ var e;
+ return typeof navigator < "u" && ((e = navigator == null ? void 0 : navigator.userAgent) == null ? void 0 : e.indexOf("Mac")) >= 0;
+};
+function Or(e) {
+ return e !== void 0 && e !== "parent";
+}
+function tr(e) {
+ var t, n;
+ return {
+ width: ((t = e.measured) == null ? void 0 : t.width) ?? e.width ?? e.initialWidth ?? 0,
+ height: ((n = e.measured) == null ? void 0 : n.height) ?? e.height ?? e.initialHeight ?? 0
+ };
+}
+function oc(e) {
+ var t, n;
+ return (((t = e.measured) == null ? void 0 : t.width) ?? e.width ?? e.initialWidth) !== void 0 && (((n = e.measured) == null ? void 0 : n.height) ?? e.height ?? e.initialHeight) !== void 0;
+}
+function p0(e, t = { width: 0, height: 0 }, n, r, o) {
+ const i = { ...e }, s = r.get(n);
+ if (s) {
+ const a = s.origin || o;
+ i.x += s.internals.positionAbsolute.x - (t.width ?? 0) * a[0], i.y += s.internals.positionAbsolute.y - (t.height ?? 0) * a[1];
+ }
+ return i;
+}
+function is(e, { snapGrid: t = [0, 0], snapToGrid: n = !1, transform: r, containerBounds: o }) {
+ const { x: i, y: s } = Hn(e), a = Mo({ x: i - ((o == null ? void 0 : o.left) ?? 0), y: s - ((o == null ? void 0 : o.top) ?? 0) }, r), { x: l, y: u } = n ? la(a, t) : a;
+ return {
+ xSnapped: l,
+ ySnapped: u,
+ ...a
+ };
+}
+const ca = (e) => ({
+ width: e.offsetWidth,
+ height: e.offsetHeight
+}), m0 = (e) => {
+ var t;
+ return ((t = e == null ? void 0 : e.getRootNode) == null ? void 0 : t.call(e)) || (window == null ? void 0 : window.document);
+}, y0 = ["INPUT", "SELECT", "TEXTAREA"];
+function w0(e) {
+ var r, o;
+ const t = ((o = (r = e.composedPath) == null ? void 0 : r.call(e)) == null ? void 0 : o[0]) || e.target;
+ return (t == null ? void 0 : t.nodeType) !== 1 ? !1 : y0.includes(t.nodeName) || t.hasAttribute("contenteditable") || !!t.closest(".nokey");
+}
+const ic = (e) => "clientX" in e, Hn = (e, t) => {
+ var i, s;
+ const n = ic(e), r = n ? e.clientX : (i = e.touches) == null ? void 0 : i[0].clientX, o = n ? e.clientY : (s = e.touches) == null ? void 0 : s[0].clientY;
+ return {
+ x: r - ((t == null ? void 0 : t.left) ?? 0),
+ y: o - ((t == null ? void 0 : t.top) ?? 0)
+ };
+}, gl = (e, t, n, r, o) => {
+ const i = t.querySelectorAll(`.${e}`);
+ return !i || !i.length ? null : Array.from(i).map((s) => {
+ const a = s.getBoundingClientRect();
+ return {
+ id: s.getAttribute("data-handleid"),
+ type: e,
+ nodeId: o,
+ position: s.getAttribute("data-handlepos"),
+ x: (a.left - n.left) / r,
+ y: (a.top - n.top) / r,
+ ...ca(s)
+ };
+ });
+};
+function _0({ sourceX: e, sourceY: t, targetX: n, targetY: r, sourceControlX: o, sourceControlY: i, targetControlX: s, targetControlY: a }) {
+ const l = e * 0.125 + o * 0.375 + s * 0.375 + n * 0.125, u = t * 0.125 + i * 0.375 + a * 0.375 + r * 0.125, c = Math.abs(l - e), f = Math.abs(u - t);
+ return [l, u, c, f];
+}
+function Ko(e, t) {
+ return e >= 0 ? 0.5 * e : t * 25 * Math.sqrt(-e);
+}
+function hl({ pos: e, x1: t, y1: n, x2: r, y2: o, c: i }) {
+ switch (e) {
+ case $e.Left:
+ return [t - Ko(t - r, i), n];
+ case $e.Right:
+ return [t + Ko(r - t, i), n];
+ case $e.Top:
+ return [t, n - Ko(n - o, i)];
+ case $e.Bottom:
+ return [t, n + Ko(o - n, i)];
+ }
+}
+function sc({ sourceX: e, sourceY: t, sourcePosition: n = $e.Bottom, targetX: r, targetY: o, targetPosition: i = $e.Top, curvature: s = 0.25 }) {
+ const [a, l] = hl({
+ pos: n,
+ x1: e,
+ y1: t,
+ x2: r,
+ y2: o,
+ c: s
+ }), [u, c] = hl({
+ pos: i,
+ x1: r,
+ y1: o,
+ x2: e,
+ y2: t,
+ c: s
+ }), [f, d, g, p] = _0({
+ sourceX: e,
+ sourceY: t,
+ targetX: r,
+ targetY: o,
+ sourceControlX: a,
+ sourceControlY: l,
+ targetControlX: u,
+ targetControlY: c
+ });
+ return [
+ `M${e},${t} C${a},${l} ${u},${c} ${r},${o}`,
+ f,
+ d,
+ g,
+ p
+ ];
+}
+function ac({ sourceX: e, sourceY: t, targetX: n, targetY: r }) {
+ const o = Math.abs(n - e) / 2, i = n < e ? n + o : n - o, s = Math.abs(r - t) / 2, a = r < t ? r + s : r - s;
+ return [i, a, o, s];
+}
+function x0({ sourceNode: e, targetNode: t, selected: n = !1, zIndex: r = 0, elevateOnSelect: o = !1 }) {
+ if (!o)
+ return r;
+ const i = n || t.selected || e.selected, s = Math.max(e.internals.z || 0, t.internals.z || 0, 1e3);
+ return r + (i ? s : 0);
+}
+function b0({ sourceNode: e, targetNode: t, width: n, height: r, transform: o }) {
+ const i = Zi(mi(e), mi(t));
+ i.x === i.x2 && (i.x2 += 1), i.y === i.y2 && (i.y2 += 1);
+ const s = {
+ x: -o[0] / o[2],
+ y: -o[1] / o[2],
+ width: n / o[2],
+ height: r / o[2]
+ };
+ return yo(s, Xi(i)) > 0;
+}
+const C0 = ({ source: e, sourceHandle: t, target: n, targetHandle: r }) => `xy-edge__${e}${t || ""}-${n}${r || ""}`, k0 = (e, t) => t.some((n) => n.source === e.source && n.target === e.target && (n.sourceHandle === e.sourceHandle || !n.sourceHandle && !e.sourceHandle) && (n.targetHandle === e.targetHandle || !n.targetHandle && !e.targetHandle)), $0 = (e, t) => {
+ if (!e.source || !e.target)
+ return t;
+ let n;
+ return d0(e) ? n = { ...e } : n = {
+ ...e,
+ id: C0(e)
+ }, k0(n, t) ? t : (n.sourceHandle === null && delete n.sourceHandle, n.targetHandle === null && delete n.targetHandle, t.concat(n));
+};
+function Hs({ sourceX: e, sourceY: t, targetX: n, targetY: r }) {
+ const [o, i, s, a] = ac({
+ sourceX: e,
+ sourceY: t,
+ targetX: n,
+ targetY: r
+ });
+ return [`M ${e},${t}L ${n},${r}`, o, i, s, a];
+}
+const vl = {
+ [$e.Left]: { x: -1, y: 0 },
+ [$e.Right]: { x: 1, y: 0 },
+ [$e.Top]: { x: 0, y: -1 },
+ [$e.Bottom]: { x: 0, y: 1 }
+}, E0 = ({ source: e, sourcePosition: t = $e.Bottom, target: n }) => t === $e.Left || t === $e.Right ? e.x < n.x ? { x: 1, y: 0 } : { x: -1, y: 0 } : e.y < n.y ? { x: 0, y: 1 } : { x: 0, y: -1 }, pl = (e, t) => Math.sqrt(Math.pow(t.x - e.x, 2) + Math.pow(t.y - e.y, 2));
+function S0({ source: e, sourcePosition: t = $e.Bottom, target: n, targetPosition: r = $e.Top, center: o, offset: i }) {
+ const s = vl[t], a = vl[r], l = { x: e.x + s.x * i, y: e.y + s.y * i }, u = { x: n.x + a.x * i, y: n.y + a.y * i }, c = E0({
+ source: l,
+ sourcePosition: t,
+ target: u
+ }), f = c.x !== 0 ? "x" : "y", d = c[f];
+ let g = [], p, x;
+ const C = { x: 0, y: 0 }, $ = { x: 0, y: 0 }, [m, _, v, b] = ac({
+ sourceX: e.x,
+ sourceY: e.y,
+ targetX: n.x,
+ targetY: n.y
+ });
+ if (s[f] * a[f] === -1) {
+ p = o.x ?? m, x = o.y ?? _;
+ const E = [
+ { x: p, y: l.y },
+ { x: p, y: u.y }
+ ], M = [
+ { x: l.x, y: x },
+ { x: u.x, y: x }
+ ];
+ s[f] === d ? g = f === "x" ? E : M : g = f === "x" ? M : E;
+ } else {
+ const E = [{ x: l.x, y: u.y }], M = [{ x: u.x, y: l.y }];
+ if (f === "x" ? g = s.x === d ? M : E : g = s.y === d ? E : M, t === r) {
+ const R = Math.abs(e[f] - n[f]);
+ if (R <= i) {
+ const S = Math.min(i - 1, i - R);
+ s[f] === d ? C[f] = (l[f] > e[f] ? -1 : 1) * S : $[f] = (u[f] > n[f] ? -1 : 1) * S;
+ }
+ }
+ if (t !== r) {
+ const R = f === "x" ? "y" : "x", S = s[f] === a[R], T = l[R] > u[R], k = l[R] < u[R];
+ (s[f] === 1 && (!S && T || S && k) || s[f] !== 1 && (!S && k || S && T)) && (g = f === "x" ? E : M);
+ }
+ const D = { x: l.x + C.x, y: l.y + C.y }, V = { x: u.x + $.x, y: u.y + $.y }, A = Math.max(Math.abs(D.x - g[0].x), Math.abs(V.x - g[0].x)), O = Math.max(Math.abs(D.y - g[0].y), Math.abs(V.y - g[0].y));
+ A >= O ? (p = (D.x + V.x) / 2, x = g[0].y) : (p = g[0].x, x = (D.y + V.y) / 2);
+ }
+ return [[
+ e,
+ { x: l.x + C.x, y: l.y + C.y },
+ ...g,
+ { x: u.x + $.x, y: u.y + $.y },
+ n
+ ], p, x, v, b];
+}
+function P0(e, t, n, r) {
+ const o = Math.min(pl(e, t) / 2, pl(t, n) / 2, r), { x: i, y: s } = t;
+ if (e.x === i && i === n.x || e.y === s && s === n.y)
+ return `L${i} ${s}`;
+ if (e.y === s) {
+ const u = e.x < n.x ? -1 : 1, c = e.y < n.y ? 1 : -1;
+ return `L ${i + o * u},${s}Q ${i},${s} ${i},${s + o * c}`;
+ }
+ const a = e.x < n.x ? 1 : -1, l = e.y < n.y ? -1 : 1;
+ return `L ${i},${s + o * l}Q ${i},${s} ${i + o * a},${s}`;
+}
+function wi({ sourceX: e, sourceY: t, sourcePosition: n = $e.Bottom, targetX: r, targetY: o, targetPosition: i = $e.Top, borderRadius: s = 5, centerX: a, centerY: l, offset: u = 20 }) {
+ const [c, f, d, g, p] = S0({
+ source: { x: e, y: t },
+ sourcePosition: n,
+ target: { x: r, y: o },
+ targetPosition: i,
+ center: { x: a, y: l },
+ offset: u
+ });
+ return [c.reduce((C, $, m) => {
+ let _ = "";
+ return m > 0 && m < c.length - 1 ? _ = P0(c[m - 1], $, c[m + 1], s) : _ = `${m === 0 ? "M" : "L"}${$.x} ${$.y}`, C += _, C;
+ }, ""), f, d, g, p];
+}
+function ml(e) {
+ var t;
+ return e && !!(e.internals.handleBounds || (t = e.handles) != null && t.length) && !!(e.measured.width || e.width || e.initialWidth);
+}
+function N0(e) {
+ var f;
+ const { sourceNode: t, targetNode: n } = e;
+ if (!ml(t) || !ml(n))
+ return null;
+ const r = t.internals.handleBounds || yl(t.handles), o = n.internals.handleBounds || yl(n.handles), i = wl((r == null ? void 0 : r.source) ?? [], e.sourceHandle), s = wl(
+ // when connection type is loose we can define all handles as sources and connect source -> source
+ e.connectionMode === cr.Strict ? (o == null ? void 0 : o.target) ?? [] : ((o == null ? void 0 : o.target) ?? []).concat((o == null ? void 0 : o.source) ?? []),
+ e.targetHandle
+ );
+ if (!i || !s)
+ return (f = e.onError) == null || f.call(e, "008", Dr.error008(i ? "target" : "source", {
+ id: e.id,
+ sourceHandle: e.sourceHandle,
+ targetHandle: e.targetHandle
+ })), null;
+ const a = (i == null ? void 0 : i.position) || $e.Bottom, l = (s == null ? void 0 : s.position) || $e.Top, u = wo(t, i, a), c = wo(n, s, l);
+ return {
+ sourceX: u.x,
+ sourceY: u.y,
+ targetX: c.x,
+ targetY: c.y,
+ sourcePosition: a,
+ targetPosition: l
+ };
+}
+function yl(e) {
+ if (!e)
+ return null;
+ const t = [], n = [];
+ for (const r of e)
+ r.width = r.width ?? 1, r.height = r.height ?? 1, r.type === "source" ? t.push(r) : r.type === "target" && n.push(r);
+ return {
+ source: t,
+ target: n
+ };
+}
+function wo(e, t, n = $e.Left, r = !1) {
+ const o = ((t == null ? void 0 : t.x) ?? 0) + e.internals.positionAbsolute.x, i = ((t == null ? void 0 : t.y) ?? 0) + e.internals.positionAbsolute.y, { width: s, height: a } = t ?? tr(e);
+ if (r)
+ return { x: o + s / 2, y: i + a / 2 };
+ switch ((t == null ? void 0 : t.position) ?? n) {
+ case $e.Top:
+ return { x: o + s / 2, y: i };
+ case $e.Right:
+ return { x: o + s, y: i + a / 2 };
+ case $e.Bottom:
+ return { x: o + s / 2, y: i + a };
+ case $e.Left:
+ return { x: o, y: i + a / 2 };
+ }
+}
+function wl(e, t) {
+ return e && (t ? e.find((n) => n.id === t) : e[0]) || null;
+}
+function Vs(e, t) {
+ return e ? typeof e == "string" ? e : `${t ? `${t}__` : ""}${Object.keys(e).sort().map((r) => `${r}=${e[r]}`).join("&")}` : "";
+}
+function M0(e, { id: t, defaultColor: n, defaultMarkerStart: r, defaultMarkerEnd: o }) {
+ const i = /* @__PURE__ */ new Set();
+ return e.reduce((s, a) => ([a.markerStart || r, a.markerEnd || o].forEach((l) => {
+ if (l && typeof l == "object") {
+ const u = Vs(l, t);
+ i.has(u) || (s.push({ id: u, color: l.color || n, ...l }), i.add(u));
+ }
+ }), s), []).sort((s, a) => s.id.localeCompare(a.id));
+}
+function T0(e, t, n, r, o) {
+ let i = 0.5;
+ o === "start" ? i = 0 : o === "end" && (i = 1);
+ let s = [
+ (e.x + e.width * i) * t.zoom + t.x,
+ e.y * t.zoom + t.y - r
+ ], a = [-100 * i, -100];
+ switch (n) {
+ case $e.Right:
+ s = [
+ (e.x + e.width) * t.zoom + t.x + r,
+ (e.y + e.height * i) * t.zoom + t.y
+ ], a = [0, -100 * i];
+ break;
+ case $e.Bottom:
+ s[1] = (e.y + e.height) * t.zoom + t.y + r, a[1] = 0;
+ break;
+ case $e.Left:
+ s = [
+ e.x * t.zoom + t.x - r,
+ (e.y + e.height * i) * t.zoom + t.y
+ ], a = [-100, -100 * i];
+ break;
+ }
+ return `translate(${s[0]}px, ${s[1]}px) translate(${a[0]}%, ${a[1]}%)`;
+}
+const da = {
+ nodeOrigin: [0, 0],
+ nodeExtent: vi,
+ elevateNodesOnSelect: !0,
+ defaults: {}
+}, H0 = {
+ ...da,
+ checkEquality: !0
+};
+function fa(e, t) {
+ const n = { ...e };
+ for (const r in t)
+ t[r] !== void 0 && (n[r] = t[r]);
+ return n;
+}
+function V0(e, t, n) {
+ const r = fa(da, n);
+ for (const o of e.values())
+ if (o.parentId)
+ ga(o, e, t, r);
+ else {
+ const i = Po(o, r.nodeOrigin), s = Or(o.extent) ? o.extent : r.nodeExtent, a = dr(i, s, tr(o));
+ o.internals.positionAbsolute = a;
+ }
+}
+function lc(e, t, n, r) {
+ var a, l;
+ const o = fa(H0, r), i = new Map(t), s = o != null && o.elevateNodesOnSelect ? 1e3 : 0;
+ t.clear(), n.clear();
+ for (const u of e) {
+ let c = i.get(u.id);
+ if (o.checkEquality && u === (c == null ? void 0 : c.internals.userNode))
+ t.set(u.id, c);
+ else {
+ const f = Po(u, o.nodeOrigin), d = Or(u.extent) ? u.extent : o.nodeExtent, g = dr(f, d, tr(u));
+ c = {
+ ...o.defaults,
+ ...u,
+ measured: {
+ width: (a = u.measured) == null ? void 0 : a.width,
+ height: (l = u.measured) == null ? void 0 : l.height
+ },
+ internals: {
+ positionAbsolute: g,
+ // if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured
+ handleBounds: u.measured ? c == null ? void 0 : c.internals.handleBounds : void 0,
+ z: uc(u, s),
+ userNode: u
+ }
+ }, t.set(u.id, c);
+ }
+ u.parentId && ga(c, t, n, r);
+ }
+}
+function D0(e, t) {
+ if (!e.parentId)
+ return;
+ const n = t.get(e.parentId);
+ n ? n.set(e.id, e) : t.set(e.parentId, /* @__PURE__ */ new Map([[e.id, e]]));
+}
+function ga(e, t, n, r) {
+ const { elevateNodesOnSelect: o, nodeOrigin: i, nodeExtent: s } = fa(da, r), a = e.parentId, l = t.get(a);
+ if (!l) {
+ console.warn(`Parent node ${a} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);
+ return;
+ }
+ D0(e, n);
+ const u = o ? 1e3 : 0, { x: c, y: f, z: d } = A0(e, l, i, s, u), { positionAbsolute: g } = e.internals, p = c !== g.x || f !== g.y;
+ (p || d !== e.internals.z) && t.set(e.id, {
+ ...e,
+ internals: {
+ ...e.internals,
+ positionAbsolute: p ? { x: c, y: f } : g,
+ z: d
+ }
+ });
+}
+function uc(e, t) {
+ return (Nn(e.zIndex) ? e.zIndex : 0) + (e.selected ? t : 0);
+}
+function A0(e, t, n, r, o) {
+ const { x: i, y: s } = t.internals.positionAbsolute, a = tr(e), l = Po(e, n), u = Or(e.extent) ? dr(l, e.extent, a) : l;
+ let c = dr({ x: i + u.x, y: s + u.y }, r, a);
+ e.extent === "parent" && (c = ec(c, a, t));
+ const f = uc(e, o), d = t.internals.z ?? 0;
+ return {
+ x: c.x,
+ y: c.y,
+ z: d > f ? d : f
+ };
+}
+function L0(e, t, n, r = [0, 0]) {
+ var s;
+ const o = [], i = /* @__PURE__ */ new Map();
+ for (const a of e) {
+ const l = t.get(a.parentId);
+ if (!l)
+ continue;
+ const u = ((s = i.get(a.parentId)) == null ? void 0 : s.expandedRect) ?? Lr(l), c = nc(u, a.rect);
+ i.set(a.parentId, { expandedRect: c, parent: l });
+ }
+ return i.size > 0 && i.forEach(({ expandedRect: a, parent: l }, u) => {
+ var _;
+ const c = l.internals.positionAbsolute, f = tr(l), d = l.origin ?? r, g = a.x < c.x ? Math.round(Math.abs(c.x - a.x)) : 0, p = a.y < c.y ? Math.round(Math.abs(c.y - a.y)) : 0, x = Math.max(f.width, Math.round(a.width)), C = Math.max(f.height, Math.round(a.height)), $ = (x - f.width) * d[0], m = (C - f.height) * d[1];
+ (g > 0 || p > 0 || $ || m) && (o.push({
+ id: u,
+ type: "position",
+ position: {
+ x: l.position.x - g + $,
+ y: l.position.y - p + m
+ }
+ }), (_ = n.get(u)) == null || _.forEach((v) => {
+ e.some((b) => b.id === v.id) || o.push({
+ id: v.id,
+ type: "position",
+ position: {
+ x: v.position.x + g,
+ y: v.position.y + p
+ }
+ });
+ })), (f.width < a.width || f.height < a.height || g || p) && o.push({
+ id: u,
+ type: "dimensions",
+ setAttributes: !0,
+ dimensions: {
+ width: x + (g ? d[0] * g - $ : 0),
+ height: C + (p ? d[1] * p - m : 0)
+ }
+ });
+ }), o;
+}
+function O0(e, t, n, r, o, i) {
+ const s = r == null ? void 0 : r.querySelector(".xyflow__viewport");
+ let a = !1;
+ if (!s)
+ return { changes: [], updatedInternals: a };
+ const l = [], u = window.getComputedStyle(s), { m22: c } = new window.DOMMatrixReadOnly(u.transform), f = [];
+ for (const d of e.values()) {
+ const g = t.get(d.id);
+ if (!g)
+ continue;
+ if (g.hidden) {
+ t.set(g.id, {
+ ...g,
+ internals: {
+ ...g.internals,
+ handleBounds: void 0
+ }
+ }), a = !0;
+ continue;
+ }
+ const p = ca(d.nodeElement), x = g.measured.width !== p.width || g.measured.height !== p.height;
+ if (!!(p.width && p.height && (x || !g.internals.handleBounds || d.force))) {
+ const $ = d.nodeElement.getBoundingClientRect(), m = Or(g.extent) ? g.extent : i;
+ let { positionAbsolute: _ } = g.internals;
+ g.parentId && g.extent === "parent" ? _ = ec(_, p, t.get(g.parentId)) : m && (_ = dr(_, m, p));
+ const v = {
+ ...g,
+ measured: p,
+ internals: {
+ ...g.internals,
+ positionAbsolute: _,
+ handleBounds: {
+ source: gl("source", d.nodeElement, $, c, g.id),
+ target: gl("target", d.nodeElement, $, c, g.id)
+ }
+ }
+ };
+ t.set(g.id, v), g.parentId && ga(v, t, n, { nodeOrigin: o }), a = !0, x && (l.push({
+ id: g.id,
+ type: "dimensions",
+ dimensions: p
+ }), g.expandParent && g.parentId && f.push({
+ id: g.id,
+ parentId: g.parentId,
+ rect: Lr(v, o)
+ }));
+ }
+ }
+ if (f.length > 0) {
+ const d = L0(f, t, n, o);
+ l.push(...d);
+ }
+ return { changes: l, updatedInternals: a };
+}
+async function I0({ delta: e, panZoom: t, transform: n, translateExtent: r, width: o, height: i }) {
+ if (!t || !e.x && !e.y)
+ return Promise.resolve(!1);
+ const s = await t.setViewportConstrained({
+ x: n[0] + e.x,
+ y: n[1] + e.y,
+ zoom: n[2]
+ }, [
+ [0, 0],
+ [o, i]
+ ], r), a = !!s && (s.x !== n[0] || s.y !== n[1] || s.k !== n[2]);
+ return Promise.resolve(a);
+}
+function _l(e, t, n, r, o, i) {
+ let s = o;
+ const a = r.get(s) || /* @__PURE__ */ new Map();
+ r.set(s, a.set(n, t)), s = `${o}-${e}`;
+ const l = r.get(s) || /* @__PURE__ */ new Map();
+ if (r.set(s, l.set(n, t)), i) {
+ s = `${o}-${e}-${i}`;
+ const u = r.get(s) || /* @__PURE__ */ new Map();
+ r.set(s, u.set(n, t));
+ }
+}
+function cc(e, t, n) {
+ e.clear(), t.clear();
+ for (const r of n) {
+ const { source: o, target: i, sourceHandle: s = null, targetHandle: a = null } = r, l = { edgeId: r.id, source: o, target: i, sourceHandle: s, targetHandle: a }, u = `${o}-${s}--${i}-${a}`, c = `${i}-${a}--${o}-${s}`;
+ _l("source", l, c, e, o, s), _l("target", l, u, e, i, a), t.set(r.id, r);
+ }
+}
+function z0(e, t) {
+ if (e === null || t === null)
+ return !1;
+ const n = Array.isArray(e) ? e : [e], r = Array.isArray(t) ? t : [t];
+ if (n.length !== r.length)
+ return !1;
+ for (let o = 0; o < n.length; o++)
+ if (n[o].id !== r[o].id || n[o].type !== r[o].type || !Object.is(n[o].data, r[o].data))
+ return !1;
+ return !0;
+}
+function dc(e, t) {
+ if (!e.parentId)
+ return !1;
+ const n = t.get(e.parentId);
+ return n ? n.selected ? !0 : dc(n, t) : !1;
+}
+function xl(e, t, n) {
+ var o;
+ let r = e;
+ do {
+ if ((o = r == null ? void 0 : r.matches) != null && o.call(r, t))
+ return !0;
+ if (r === n)
+ return !1;
+ r = r == null ? void 0 : r.parentElement;
+ } while (r);
+ return !1;
+}
+function R0(e, t, n, r) {
+ const o = /* @__PURE__ */ new Map();
+ for (const [i, s] of e)
+ if ((s.selected || s.id === r) && (!s.parentId || !dc(s, e)) && (s.draggable || t && typeof s.draggable > "u")) {
+ const a = e.get(i);
+ a && o.set(i, {
+ id: i,
+ position: a.position || { x: 0, y: 0 },
+ distance: {
+ x: n.x - a.internals.positionAbsolute.x,
+ y: n.y - a.internals.positionAbsolute.y
+ },
+ extent: a.extent,
+ parentId: a.parentId,
+ origin: a.origin,
+ expandParent: a.expandParent,
+ internals: {
+ positionAbsolute: a.internals.positionAbsolute || { x: 0, y: 0 }
+ },
+ measured: {
+ width: a.measured.width ?? 0,
+ height: a.measured.height ?? 0
+ }
+ });
+ }
+ return o;
+}
+function ss({ nodeId: e, dragItems: t, nodeLookup: n, dragging: r = !0 }) {
+ var s, a, l;
+ const o = [];
+ for (const [u, c] of t) {
+ const f = (s = n.get(u)) == null ? void 0 : s.internals.userNode;
+ f && o.push({
+ ...f,
+ position: c.position,
+ dragging: r
+ });
+ }
+ if (!e)
+ return [o[0], o];
+ const i = (a = n.get(e)) == null ? void 0 : a.internals.userNode;
+ return [
+ i ? {
+ ...i,
+ position: ((l = t.get(e)) == null ? void 0 : l.position) || i.position,
+ dragging: r
+ } : o[0],
+ o
+ ];
+}
+function B0({ onNodeMouseDown: e, getStoreItems: t, onDragStart: n, onDrag: r, onDragStop: o }) {
+ let i = { x: null, y: null }, s = 0, a = /* @__PURE__ */ new Map(), l = !1, u = { x: 0, y: 0 }, c = null, f = !1, d = null, g = !1;
+ function p({ noDragClassName: C, handleSelector: $, domNode: m, isSelectable: _, nodeId: v, nodeClickDistance: b = 0 }) {
+ d = Kt(m);
+ function N({ x: V, y: A }, O) {
+ const { nodeLookup: R, nodeExtent: S, snapGrid: T, snapToGrid: k, nodeOrigin: P, onNodeDrag: H, onSelectionDrag: I, onError: B, updateNodePositions: F } = t();
+ i = { x: V, y: A };
+ let K = !1, ie = { x: 0, y: 0, x2: 0, y2: 0 };
+ if (a.size > 1 && S) {
+ const ee = No(a);
+ ie = Ts(ee);
+ }
+ for (const [ee, W] of a) {
+ if (!R.has(ee))
+ continue;
+ let ue = { x: V - W.distance.x, y: A - W.distance.y };
+ k && (ue = la(ue, T));
+ let me = [
+ [S[0][0], S[0][1]],
+ [S[1][0], S[1][1]]
+ ];
+ if (a.size > 1 && S && !W.extent) {
+ const { positionAbsolute: ze } = W.internals, G = ze.x - ie.x + S[0][0], se = ze.x + W.measured.width - ie.x2 + S[1][0], Te = ze.y - ie.y + S[0][1], Ae = ze.y + W.measured.height - ie.y2 + S[1][1];
+ me = [
+ [G, Te],
+ [se, Ae]
+ ];
+ }
+ const { position: Ce, positionAbsolute: ge } = h0({
+ nodeId: ee,
+ nextPosition: ue,
+ nodeLookup: R,
+ nodeExtent: me,
+ nodeOrigin: P,
+ onError: B
+ });
+ K = K || W.position.x !== Ce.x || W.position.y !== Ce.y, W.position = Ce, W.internals.positionAbsolute = ge;
+ }
+ if (K && (F(a, !0), O && (r || H || !v && I))) {
+ const [ee, W] = ss({
+ nodeId: v,
+ dragItems: a,
+ nodeLookup: R
+ });
+ r == null || r(O, a, ee, W), H == null || H(O, ee, W), v || I == null || I(O, W);
+ }
+ }
+ async function E() {
+ if (!c)
+ return;
+ const { transform: V, panBy: A, autoPanSpeed: O, autoPanOnNodeDrag: R } = t();
+ if (!R) {
+ l = !1, cancelAnimationFrame(s);
+ return;
+ }
+ const [S, T] = tc(u, c, O);
+ (S !== 0 || T !== 0) && (i.x = (i.x ?? 0) - S / V[2], i.y = (i.y ?? 0) - T / V[2], await A({ x: S, y: T }) && N(i, null)), s = requestAnimationFrame(E);
+ }
+ function M(V) {
+ var K;
+ const { nodeLookup: A, multiSelectionActive: O, nodesDraggable: R, transform: S, snapGrid: T, snapToGrid: k, selectNodesOnDrag: P, onNodeDragStart: H, onSelectionDragStart: I, unselectNodesAndEdges: B } = t();
+ f = !0, (!P || !_) && !O && v && ((K = A.get(v)) != null && K.selected || B()), _ && P && v && (e == null || e(v));
+ const F = is(V.sourceEvent, { transform: S, snapGrid: T, snapToGrid: k, containerBounds: c });
+ if (i = F, a = R0(A, R, F, v), a.size > 0 && (n || H || !v && I)) {
+ const [ie, ee] = ss({
+ nodeId: v,
+ dragItems: a,
+ nodeLookup: A
+ });
+ n == null || n(V.sourceEvent, a, ie, ee), H == null || H(V.sourceEvent, ie, ee), v || I == null || I(V.sourceEvent, ee);
+ }
+ }
+ const D = wh().clickDistance(b).on("start", (V) => {
+ const { domNode: A, nodeDragThreshold: O, transform: R, snapGrid: S, snapToGrid: T } = t();
+ c = (A == null ? void 0 : A.getBoundingClientRect()) || null, g = !1, O === 0 && M(V), i = is(V.sourceEvent, { transform: R, snapGrid: S, snapToGrid: T, containerBounds: c }), u = Hn(V.sourceEvent, c);
+ }).on("drag", (V) => {
+ const { autoPanOnNodeDrag: A, transform: O, snapGrid: R, snapToGrid: S, nodeDragThreshold: T, nodeLookup: k } = t(), P = is(V.sourceEvent, { transform: O, snapGrid: R, snapToGrid: S, containerBounds: c });
+ if ((V.sourceEvent.type === "touchmove" && V.sourceEvent.touches.length > 1 || // if user deletes a node while dragging, we need to abort the drag to prevent errors
+ v && !k.has(v)) && (g = !0), !g) {
+ if (!l && A && f && (l = !0, E()), !f) {
+ const H = P.xSnapped - (i.x ?? 0), I = P.ySnapped - (i.y ?? 0);
+ Math.sqrt(H * H + I * I) > T && M(V);
+ }
+ (i.x !== P.xSnapped || i.y !== P.ySnapped) && a && f && (u = Hn(V.sourceEvent, c), N(P, V.sourceEvent));
+ }
+ }).on("end", (V) => {
+ if (!(!f || g) && (l = !1, f = !1, cancelAnimationFrame(s), a.size > 0)) {
+ const { nodeLookup: A, updateNodePositions: O, onNodeDragStop: R, onSelectionDragStop: S } = t();
+ if (O(a, !1), o || R || !v && S) {
+ const [T, k] = ss({
+ nodeId: v,
+ dragItems: a,
+ nodeLookup: A,
+ dragging: !1
+ });
+ o == null || o(V.sourceEvent, a, T, k), R == null || R(V.sourceEvent, T, k), v || S == null || S(V.sourceEvent, k);
+ }
+ }
+ }).filter((V) => {
+ const A = V.target;
+ return !V.button && (!C || !xl(A, `.${C}`, m)) && (!$ || xl(A, $, m));
+ });
+ d.call(D);
+ }
+ function x() {
+ d == null || d.on(".drag", null);
+ }
+ return {
+ update: p,
+ destroy: x
+ };
+}
+function Y0(e, t, n) {
+ const r = [], o = {
+ x: e.x - n,
+ y: e.y - n,
+ width: n * 2,
+ height: n * 2
+ };
+ for (const i of t.values())
+ yo(o, Lr(i)) > 0 && r.push(i);
+ return r;
+}
+const Z0 = 250;
+function X0(e, t, n, r) {
+ var a, l;
+ let o = [], i = 1 / 0;
+ const s = Y0(e, n, t + Z0);
+ for (const u of s) {
+ const c = [...((a = u.internals.handleBounds) == null ? void 0 : a.source) ?? [], ...((l = u.internals.handleBounds) == null ? void 0 : l.target) ?? []];
+ for (const f of c) {
+ if (r.nodeId === f.nodeId && r.type === f.type && r.id === f.id)
+ continue;
+ const { x: d, y: g } = wo(u, f, f.position, !0), p = Math.sqrt(Math.pow(d - e.x, 2) + Math.pow(g - e.y, 2));
+ p > t || (p < i ? (o = [{ ...f, x: d, y: g }], i = p) : p === i && o.push({ ...f, x: d, y: g }));
+ }
+ }
+ if (!o.length)
+ return null;
+ if (o.length > 1) {
+ const u = r.type === "source" ? "target" : "source";
+ return o.find((c) => c.type === u) ?? o[0];
+ }
+ return o[0];
+}
+function fc(e, t, n, r, o, i = !1) {
+ var u, c, f;
+ const s = r.get(e);
+ if (!s)
+ return null;
+ const a = o === "strict" ? (u = s.internals.handleBounds) == null ? void 0 : u[t] : [...((c = s.internals.handleBounds) == null ? void 0 : c.source) ?? [], ...((f = s.internals.handleBounds) == null ? void 0 : f.target) ?? []], l = (n ? a == null ? void 0 : a.find((d) => d.id === n) : a == null ? void 0 : a[0]) ?? null;
+ return l && i ? { ...l, ...wo(s, l, l.position, !0) } : l;
+}
+function gc(e, t) {
+ return e || (t != null && t.classList.contains("target") ? "target" : t != null && t.classList.contains("source") ? "source" : null);
+}
+function F0(e, t) {
+ let n = null;
+ return t ? n = !0 : e && !t && (n = !1), n;
+}
+const hc = () => !0;
+function W0(e, { connectionMode: t, connectionRadius: n, handleId: r, nodeId: o, edgeUpdaterType: i, isTarget: s, domNode: a, nodeLookup: l, lib: u, autoPanOnConnect: c, flowId: f, panBy: d, cancelConnection: g, onConnectStart: p, onConnect: x, onConnectEnd: C, isValidConnection: $ = hc, onReconnectEnd: m, updateConnection: _, getTransform: v, getFromHandle: b, autoPanSpeed: N }) {
+ const E = m0(e.target);
+ let M = 0, D;
+ const { x: V, y: A } = Hn(e), O = E == null ? void 0 : E.elementFromPoint(V, A), R = gc(i, O), S = a == null ? void 0 : a.getBoundingClientRect();
+ if (!S || !R)
+ return;
+ const T = fc(o, R, r, l, t);
+ if (!T)
+ return;
+ let k = Hn(e, S), P = !1, H = null, I = !1, B = null;
+ function F() {
+ if (!c || !S)
+ return;
+ const [ge, ze] = tc(k, S, N);
+ d({ x: ge, y: ze }), M = requestAnimationFrame(F);
+ }
+ const K = {
+ ...T,
+ nodeId: o,
+ type: R,
+ position: T.position
+ }, ie = l.get(o), W = {
+ inProgress: !0,
+ isValid: null,
+ from: wo(ie, K, $e.Left, !0),
+ fromHandle: K,
+ fromPosition: K.position,
+ fromNode: ie,
+ to: k,
+ toHandle: null,
+ toPosition: al[K.position],
+ toNode: null
+ };
+ _(W);
+ let ue = W;
+ p == null || p(e, { nodeId: o, handleId: r, handleType: R });
+ function me(ge) {
+ if (!b() || !K) {
+ Ce(ge);
+ return;
+ }
+ const ze = v();
+ k = Hn(ge, S), D = X0(Mo(k, ze, !1, [1, 1]), n, l, K), P || (F(), P = !0);
+ const G = vc(ge, {
+ handle: D,
+ connectionMode: t,
+ fromNodeId: o,
+ fromHandleId: r,
+ fromType: s ? "target" : "source",
+ isValidConnection: $,
+ doc: E,
+ lib: u,
+ flowId: f,
+ nodeLookup: l
+ });
+ B = G.handleDomNode, H = G.connection, I = F0(!!D, G.isValid);
+ const se = {
+ // from stays the same
+ ...ue,
+ isValid: I,
+ to: D && I ? rc({ x: D.x, y: D.y }, ze) : k,
+ toHandle: G.toHandle,
+ toPosition: I && G.toHandle ? G.toHandle.position : al[K.position],
+ toNode: G.toHandle ? l.get(G.toHandle.nodeId) : null
+ };
+ I && D && ue.toHandle && se.toHandle && ue.toHandle.type === se.toHandle.type && ue.toHandle.nodeId === se.toHandle.nodeId && ue.toHandle.id === se.toHandle.id && ue.to.x === se.to.x && ue.to.y === se.to.y || (_(se), ue = se);
+ }
+ function Ce(ge) {
+ (D || B) && H && I && (x == null || x(H));
+ const { inProgress: ze, ...G } = ue, se = {
+ ...G,
+ toPosition: ue.toHandle ? ue.toPosition : null
+ };
+ C == null || C(ge, se), i && (m == null || m(ge, se)), g(), cancelAnimationFrame(M), P = !1, I = !1, H = null, B = null, E.removeEventListener("mousemove", me), E.removeEventListener("mouseup", Ce), E.removeEventListener("touchmove", me), E.removeEventListener("touchend", Ce);
+ }
+ E.addEventListener("mousemove", me), E.addEventListener("mouseup", Ce), E.addEventListener("touchmove", me), E.addEventListener("touchend", Ce);
+}
+function vc(e, { handle: t, connectionMode: n, fromNodeId: r, fromHandleId: o, fromType: i, doc: s, lib: a, flowId: l, isValidConnection: u = hc, nodeLookup: c }) {
+ const f = i === "target", d = t ? s.querySelector(`.${a}-flow__handle[data-id="${l}-${t == null ? void 0 : t.nodeId}-${t == null ? void 0 : t.id}-${t == null ? void 0 : t.type}"]`) : null, { x: g, y: p } = Hn(e), x = s.elementFromPoint(g, p), C = x != null && x.classList.contains(`${a}-flow__handle`) ? x : d, $ = {
+ handleDomNode: C,
+ isValid: !1,
+ connection: null,
+ toHandle: null
+ };
+ if (C) {
+ const m = gc(void 0, C), _ = C.getAttribute("data-nodeid"), v = C.getAttribute("data-handleid"), b = C.classList.contains("connectable"), N = C.classList.contains("connectableend");
+ if (!_ || !m)
+ return $;
+ const E = {
+ source: f ? _ : r,
+ sourceHandle: f ? v : o,
+ target: f ? r : _,
+ targetHandle: f ? o : v
+ };
+ $.connection = E;
+ const D = b && N && (n === cr.Strict ? f && m === "source" || !f && m === "target" : _ !== r || v !== o);
+ $.isValid = D && u(E), $.toHandle = fc(_, m, v, c, n, !1);
+ }
+ return $;
+}
+const K0 = {
+ onPointerDown: W0,
+ isValid: vc
+};
+function q0({ domNode: e, panZoom: t, getTransform: n, getViewScale: r }) {
+ const o = Kt(e);
+ function i({ translateExtent: a, width: l, height: u, zoomStep: c = 10, pannable: f = !0, zoomable: d = !0, inversePan: g = !1 }) {
+ const p = (_) => {
+ const v = n();
+ if (_.sourceEvent.type !== "wheel" || !t)
+ return;
+ const b = -_.sourceEvent.deltaY * (_.sourceEvent.deltaMode === 1 ? 0.05 : _.sourceEvent.deltaMode ? 1 : 2e-3) * c, N = v[2] * Math.pow(2, b);
+ t.scaleTo(N);
+ };
+ let x = [0, 0];
+ const C = (_) => {
+ (_.sourceEvent.type === "mousedown" || _.sourceEvent.type === "touchstart") && (x = [
+ _.sourceEvent.clientX ?? _.sourceEvent.touches[0].clientX,
+ _.sourceEvent.clientY ?? _.sourceEvent.touches[0].clientY
+ ]);
+ }, $ = (_) => {
+ const v = n();
+ if (_.sourceEvent.type !== "mousemove" && _.sourceEvent.type !== "touchmove" || !t)
+ return;
+ const b = [
+ _.sourceEvent.clientX ?? _.sourceEvent.touches[0].clientX,
+ _.sourceEvent.clientY ?? _.sourceEvent.touches[0].clientY
+ ], N = [b[0] - x[0], b[1] - x[1]];
+ x = b;
+ const E = r() * Math.max(v[2], Math.log(v[2])) * (g ? -1 : 1), M = {
+ x: v[0] - N[0] * E,
+ y: v[1] - N[1] * E
+ }, D = [
+ [0, 0],
+ [l, u]
+ ];
+ t.setViewportConstrained({
+ x: M.x,
+ y: M.y,
+ zoom: v[2]
+ }, D, a);
+ }, m = ju().on("start", C).on("zoom", f ? $ : null).on("zoom.wheel", d ? p : null);
+ o.call(m, {});
+ }
+ function s() {
+ o.on("zoom", null);
+ }
+ return {
+ update: i,
+ destroy: s,
+ pointer: Qt
+ };
+}
+const G0 = (e, t) => e.x !== t.x || e.y !== t.y || e.zoom !== t.k, Fi = (e) => ({
+ x: e.x,
+ y: e.y,
+ zoom: e.k
+}), as = ({ x: e, y: t, zoom: n }) => Yi.translate(e, t).scale(n), wr = (e, t) => e.target.closest(`.${t}`), pc = (e, t) => t === 2 && Array.isArray(e) && e.includes(2), ls = (e, t = 0, n = () => {
+}) => {
+ const r = typeof t == "number" && t > 0;
+ return r || n(), r ? e.transition().duration(t).on("end", n) : e;
+}, mc = (e) => {
+ const t = e.ctrlKey && yi() ? 10 : 1;
+ return -e.deltaY * (e.deltaMode === 1 ? 0.05 : e.deltaMode ? 1 : 2e-3) * t;
+};
+function U0({ zoomPanValues: e, noWheelClassName: t, d3Selection: n, d3Zoom: r, panOnScrollMode: o, panOnScrollSpeed: i, zoomOnPinch: s, onPanZoomStart: a, onPanZoom: l, onPanZoomEnd: u }) {
+ return (c) => {
+ if (wr(c, t))
+ return !1;
+ c.preventDefault(), c.stopImmediatePropagation();
+ const f = n.property("__zoom").k || 1;
+ if (c.ctrlKey && s) {
+ const C = Qt(c), $ = mc(c), m = f * Math.pow(2, $);
+ r.scaleTo(n, m, C, c);
+ return;
+ }
+ const d = c.deltaMode === 1 ? 20 : 1;
+ let g = o === qn.Vertical ? 0 : c.deltaX * d, p = o === qn.Horizontal ? 0 : c.deltaY * d;
+ !yi() && c.shiftKey && o !== qn.Vertical && (g = c.deltaY * d, p = 0), r.translateBy(
+ n,
+ -(g / f) * i,
+ -(p / f) * i,
+ // @ts-ignore
+ { internal: !0 }
+ );
+ const x = Fi(n.property("__zoom"));
+ clearTimeout(e.panScrollTimeout), e.isPanScrolling || (e.isPanScrolling = !0, a == null || a(c, x)), e.isPanScrolling && (l == null || l(c, x), e.panScrollTimeout = setTimeout(() => {
+ u == null || u(c, x), e.isPanScrolling = !1;
+ }, 150));
+ };
+}
+function j0({ noWheelClassName: e, preventScrolling: t, d3ZoomHandler: n }) {
+ return function(r, o) {
+ if (!t && r.type === "wheel" && !r.ctrlKey || wr(r, e))
+ return null;
+ r.preventDefault(), n.call(this, r, o);
+ };
+}
+function J0({ zoomPanValues: e, onDraggingChange: t, onPanZoomStart: n }) {
+ return (r) => {
+ var i, s, a;
+ if ((i = r.sourceEvent) != null && i.internal)
+ return;
+ const o = Fi(r.transform);
+ e.mouseButton = ((s = r.sourceEvent) == null ? void 0 : s.button) || 0, e.isZoomingOrPanning = !0, e.prevViewport = o, ((a = r.sourceEvent) == null ? void 0 : a.type) === "mousedown" && t(!0), n && (n == null || n(r.sourceEvent, o));
+ };
+}
+function Q0({ zoomPanValues: e, panOnDrag: t, onPaneContextMenu: n, onTransformChange: r, onPanZoom: o }) {
+ return (i) => {
+ var s, a;
+ e.usedRightMouseButton = !!(n && pc(t, e.mouseButton ?? 0)), (s = i.sourceEvent) != null && s.sync || r([i.transform.x, i.transform.y, i.transform.k]), o && !((a = i.sourceEvent) != null && a.internal) && (o == null || o(i.sourceEvent, Fi(i.transform)));
+ };
+}
+function e2({ zoomPanValues: e, panOnDrag: t, panOnScroll: n, onDraggingChange: r, onPanZoomEnd: o, onPaneContextMenu: i }) {
+ return (s) => {
+ var a;
+ if (!((a = s.sourceEvent) != null && a.internal) && (e.isZoomingOrPanning = !1, i && pc(t, e.mouseButton ?? 0) && !e.usedRightMouseButton && s.sourceEvent && i(s.sourceEvent), e.usedRightMouseButton = !1, r(!1), o && G0(e.prevViewport, s.transform))) {
+ const l = Fi(s.transform);
+ e.prevViewport = l, clearTimeout(e.timerId), e.timerId = setTimeout(
+ () => {
+ o == null || o(s.sourceEvent, l);
+ },
+ // we need a setTimeout for panOnScroll to supress multiple end events fired during scroll
+ n ? 150 : 0
+ );
+ }
+ };
+}
+function t2({ zoomActivationKeyPressed: e, zoomOnScroll: t, zoomOnPinch: n, panOnDrag: r, panOnScroll: o, zoomOnDoubleClick: i, userSelectionActive: s, noWheelClassName: a, noPanClassName: l, lib: u }) {
+ return (c) => {
+ var p;
+ const f = e || t, d = n && c.ctrlKey;
+ if (c.button === 1 && c.type === "mousedown" && (wr(c, `${u}-flow__node`) || wr(c, `${u}-flow__edge`)))
+ return !0;
+ if (!r && !f && !o && !i && !n || s || wr(c, a) && c.type === "wheel" || wr(c, l) && (c.type !== "wheel" || o && c.type === "wheel" && !e) || !n && c.ctrlKey && c.type === "wheel")
+ return !1;
+ if (!n && c.type === "touchstart" && ((p = c.touches) == null ? void 0 : p.length) > 1)
+ return c.preventDefault(), !1;
+ if (!f && !o && !d && c.type === "wheel" || !r && (c.type === "mousedown" || c.type === "touchstart") || Array.isArray(r) && !r.includes(c.button) && c.type === "mousedown")
+ return !1;
+ const g = Array.isArray(r) && r.includes(c.button) || !c.button || c.button <= 1;
+ return (!c.ctrlKey || c.type === "wheel") && g;
+ };
+}
+function n2({ domNode: e, minZoom: t, maxZoom: n, paneClickDistance: r, translateExtent: o, viewport: i, onPanZoom: s, onPanZoomStart: a, onPanZoomEnd: l, onDraggingChange: u }) {
+ const c = {
+ isZoomingOrPanning: !1,
+ usedRightMouseButton: !1,
+ prevViewport: { x: 0, y: 0, zoom: 0 },
+ mouseButton: 0,
+ timerId: void 0,
+ panScrollTimeout: void 0,
+ isPanScrolling: !1
+ }, f = e.getBoundingClientRect(), d = ju().clickDistance(!Nn(r) || r < 0 ? 0 : r).scaleExtent([t, n]).translateExtent(o), g = Kt(e).call(d);
+ _({
+ x: i.x,
+ y: i.y,
+ zoom: Ar(i.zoom, t, n)
+ }, [
+ [0, 0],
+ [f.width, f.height]
+ ], o);
+ const p = g.on("wheel.zoom"), x = g.on("dblclick.zoom");
+ d.wheelDelta(mc);
+ function C(O, R) {
+ return g ? new Promise((S) => {
+ d == null || d.transform(ls(g, R == null ? void 0 : R.duration, () => S(!0)), O);
+ }) : Promise.resolve(!1);
+ }
+ function $({ noWheelClassName: O, noPanClassName: R, onPaneContextMenu: S, userSelectionActive: T, panOnScroll: k, panOnDrag: P, panOnScrollMode: H, panOnScrollSpeed: I, preventScrolling: B, zoomOnPinch: F, zoomOnScroll: K, zoomOnDoubleClick: ie, zoomActivationKeyPressed: ee, lib: W, onTransformChange: ue }) {
+ T && !c.isZoomingOrPanning && m();
+ const Ce = k && !ee && !T ? U0({
+ zoomPanValues: c,
+ noWheelClassName: O,
+ d3Selection: g,
+ d3Zoom: d,
+ panOnScrollMode: H,
+ panOnScrollSpeed: I,
+ zoomOnPinch: F,
+ onPanZoomStart: a,
+ onPanZoom: s,
+ onPanZoomEnd: l
+ }) : j0({
+ noWheelClassName: O,
+ preventScrolling: B,
+ d3ZoomHandler: p
+ });
+ if (g.on("wheel.zoom", Ce, { passive: !1 }), !T) {
+ const ze = J0({
+ zoomPanValues: c,
+ onDraggingChange: u,
+ onPanZoomStart: a
+ });
+ d.on("start", ze);
+ const G = Q0({
+ zoomPanValues: c,
+ panOnDrag: P,
+ onPaneContextMenu: !!S,
+ onPanZoom: s,
+ onTransformChange: ue
+ });
+ d.on("zoom", G);
+ const se = e2({
+ zoomPanValues: c,
+ panOnDrag: P,
+ panOnScroll: k,
+ onPaneContextMenu: S,
+ onPanZoomEnd: l,
+ onDraggingChange: u
+ });
+ d.on("end", se);
+ }
+ const ge = t2({
+ zoomActivationKeyPressed: ee,
+ panOnDrag: P,
+ zoomOnScroll: K,
+ panOnScroll: k,
+ zoomOnDoubleClick: ie,
+ zoomOnPinch: F,
+ userSelectionActive: T,
+ noPanClassName: R,
+ noWheelClassName: O,
+ lib: W
+ });
+ d.filter(ge), ie ? g.on("dblclick.zoom", x) : g.on("dblclick.zoom", null);
+ }
+ function m() {
+ d.on("zoom", null);
+ }
+ async function _(O, R, S) {
+ const T = as(O), k = d == null ? void 0 : d.constrain()(T, R, S);
+ return k && await C(k), new Promise((P) => P(k));
+ }
+ async function v(O, R) {
+ const S = as(O);
+ return await C(S, R), new Promise((T) => T(S));
+ }
+ function b(O) {
+ if (g) {
+ const R = as(O), S = g.property("__zoom");
+ (S.k !== O.zoom || S.x !== O.x || S.y !== O.y) && (d == null || d.transform(g, R, null, { sync: !0 }));
+ }
+ }
+ function N() {
+ const O = g ? Uu(g.node()) : { x: 0, y: 0, k: 1 };
+ return { x: O.x, y: O.y, zoom: O.k };
+ }
+ function E(O, R) {
+ return g ? new Promise((S) => {
+ d == null || d.scaleTo(ls(g, R == null ? void 0 : R.duration, () => S(!0)), O);
+ }) : Promise.resolve(!1);
+ }
+ function M(O, R) {
+ return g ? new Promise((S) => {
+ d == null || d.scaleBy(ls(g, R == null ? void 0 : R.duration, () => S(!0)), O);
+ }) : Promise.resolve(!1);
+ }
+ function D(O) {
+ d == null || d.scaleExtent(O);
+ }
+ function V(O) {
+ d == null || d.translateExtent(O);
+ }
+ function A(O) {
+ const R = !Nn(O) || O < 0 ? 0 : O;
+ d == null || d.clickDistance(R);
+ }
+ return {
+ update: $,
+ destroy: m,
+ setViewport: v,
+ setViewportConstrained: _,
+ getViewport: N,
+ scaleTo: E,
+ scaleBy: M,
+ setScaleExtent: D,
+ setTranslateExtent: V,
+ syncViewport: b,
+ setClickDistance: A
+ };
+}
+var bl;
+(function(e) {
+ e.Line = "line", e.Handle = "handle";
+})(bl || (bl = {}));
+var r2 = /* @__PURE__ */ ne('<div role="button" tabindex="-1"><!></div>');
+function Qn(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(ie, "$connectable", n), i = () => Q(Ce, "$connectionRadius", n), s = () => Q(ue, "$domNode", n), a = () => Q(me, "$nodeLookup", n), l = () => Q(W, "$connectionMode", n), u = () => Q(G, "$lib", n), c = () => Q(Fe, "$autoPanOnConnect", n), f = () => Q(Oe, "$flowId", n), d = () => Q(ze, "$isValidConnectionStore", n), g = () => Q(Te, "$onedgecreate", n), p = () => Q(oe, "$onConnectAction", n), x = () => Q(ve, "$onConnectStartAction", n), C = () => Q(xe, "$onConnectEndAction", n), $ = () => Q(ge, "$viewport", n), m = () => Q(ct, "$connection", n), _ = () => Q(Le, "$edges", n), v = () => Q(Qe, "$connectionLookup", n), b = re(), N = re(), E = re(), M = re(), D = re(), V = re(), A = re(), O = re();
+ let R = w(t, "id", 12, void 0), S = w(t, "type", 12, "source"), T = w(t, "position", 28, () => $e.Top), k = w(t, "style", 12, void 0), P = w(t, "isValidConnection", 12, void 0), H = w(t, "onconnect", 12, void 0), I = w(t, "ondisconnect", 12, void 0), B = w(t, "isConnectable", 12, void 0), F = w(t, "class", 12, void 0);
+ const K = ar("svelteflow__node_id"), ie = ar("svelteflow__node_connectable"), ee = Ue(), {
+ connectionMode: W,
+ domNode: ue,
+ nodeLookup: me,
+ connectionRadius: Ce,
+ viewport: ge,
+ isValidConnection: ze,
+ lib: G,
+ addEdge: se,
+ onedgecreate: Te,
+ panBy: Ae,
+ cancelConnection: Xe,
+ updateConnection: te,
+ autoPanOnConnect: Fe,
+ edges: Le,
+ connectionLookup: Qe,
+ onconnect: oe,
+ onconnectstart: ve,
+ onconnectend: xe,
+ flowId: Oe,
+ connection: ct
+ } = ee;
+ function lt(Ne) {
+ const rt = ic(Ne);
+ (rt && Ne.button === 0 || !rt) && K0.onPointerDown(Ne, {
+ handleId: h(E),
+ nodeId: K,
+ isTarget: h(b),
+ connectionRadius: i(),
+ domNode: s(),
+ nodeLookup: a(),
+ connectionMode: l(),
+ lib: u(),
+ autoPanOnConnect: c(),
+ flowId: f(),
+ isValidConnection: P() ?? d(),
+ updateConnection: te,
+ cancelConnection: Xe,
+ panBy: Ae,
+ onConnect: (ye) => {
+ var at;
+ const ot = g() ? g()(ye) : ye;
+ ot && (se(ot), (at = p()) == null || at(ye));
+ },
+ onConnectStart: (ye, ot) => {
+ var at;
+ (at = x()) == null || at(ye, {
+ nodeId: ot.nodeId,
+ handleId: ot.handleId,
+ handleType: ot.handleType
+ });
+ },
+ onConnectEnd: (ye, ot) => {
+ var at;
+ (at = C()) == null || at(ye, ot);
+ },
+ getTransform: () => [
+ $().x,
+ $().y,
+ $().zoom
+ ],
+ getFromHandle: () => m().fromHandle
+ });
+ }
+ let J = re(null), Re = re();
+ he(() => j(S()), () => {
+ U(b, S() === "target");
+ }), he(
+ () => (j(B()), o()),
+ () => {
+ U(N, B() !== void 0 ? B() : o());
+ }
+ ), he(() => j(R()), () => {
+ U(E, R() || null);
+ }), he(
+ () => (j(H()), j(I()), _(), v(), j(S()), j(R())),
+ () => {
+ (H() || I()) && (_(), U(Re, v().get(`${K}-${S()}${R() ? `-${R()}` : ""}`)));
+ }
+ ), he(
+ () => (h(J), h(Re), j(I()), j(H())),
+ () => {
+ if (h(J) && !u0(h(Re), h(J))) {
+ const Ne = h(Re) ?? /* @__PURE__ */ new Map();
+ ll(h(J), Ne, I()), ll(Ne, h(J), H());
+ }
+ U(J, h(Re) ?? /* @__PURE__ */ new Map());
+ }
+ ), he(() => m(), () => {
+ U(M, !!m().fromHandle);
+ }), he(
+ () => (m(), j(S()), h(E)),
+ () => {
+ var Ne, rt, ye;
+ U(D, ((Ne = m().fromHandle) == null ? void 0 : Ne.nodeId) === K && ((rt = m().fromHandle) == null ? void 0 : rt.type) === S() && ((ye = m().fromHandle) == null ? void 0 : ye.id) === h(E));
+ }
+ ), he(
+ () => (m(), j(S()), h(E)),
+ () => {
+ var Ne, rt, ye;
+ U(V, ((Ne = m().toHandle) == null ? void 0 : Ne.nodeId) === K && ((rt = m().toHandle) == null ? void 0 : rt.type) === S() && ((ye = m().toHandle) == null ? void 0 : ye.id) === h(E));
+ }
+ ), he(
+ () => (l(), m(), j(S()), h(E)),
+ () => {
+ var Ne, rt, ye;
+ U(A, l() === cr.Strict ? ((Ne = m().fromHandle) == null ? void 0 : Ne.type) !== S() : K !== ((rt = m().fromHandle) == null ? void 0 : rt.nodeId) || h(E) !== ((ye = m().fromHandle) == null ? void 0 : ye.id));
+ }
+ ), he(() => (h(V), m()), () => {
+ U(O, h(V) && m().isValid);
+ }), gt(), He();
+ var le = r2();
+ ce(le, "data-nodeid", K);
+ let fn;
+ var Ut = X(le);
+ pt(Ut, t, "default", {}), Z(le), Ee(
+ (Ne) => {
+ ce(le, "data-handleid", h(E)), ce(le, "data-handlepos", T()), ce(le, "data-id", `${f() ?? ""}-${K ?? ""}-${R() || ""}-${S() ?? ""}`), fn = kt(le, 1, bn(Ne), null, fn, {
+ valid: h(O),
+ connectingto: h(V),
+ connectingfrom: h(D),
+ source: !h(b),
+ target: h(b),
+ connectablestart: h(N),
+ connectableend: h(N),
+ connectable: h(N),
+ connectionindicator: h(N) && (!h(M) || h(A))
+ }), ce(le, "style", k());
+ },
+ [
+ () => Et([
+ "svelte-flow__handle",
+ `svelte-flow__handle-${T()}`,
+ "nodrag",
+ "nopan",
+ T(),
+ F()
+ ])
+ ],
+ pe
+ ), Ye("mousedown", le, lt), Ye("touchstart", le, lt), L(e, le);
+ var gn = fe({
+ get id() {
+ return R();
+ },
+ set id(Ne) {
+ R(Ne), y();
+ },
+ get type() {
+ return S();
+ },
+ set type(Ne) {
+ S(Ne), y();
+ },
+ get position() {
+ return T();
+ },
+ set position(Ne) {
+ T(Ne), y();
+ },
+ get style() {
+ return k();
+ },
+ set style(Ne) {
+ k(Ne), y();
+ },
+ get isValidConnection() {
+ return P();
+ },
+ set isValidConnection(Ne) {
+ P(Ne), y();
+ },
+ get onconnect() {
+ return H();
+ },
+ set onconnect(Ne) {
+ H(Ne), y();
+ },
+ get ondisconnect() {
+ return I();
+ },
+ set ondisconnect(Ne) {
+ I(Ne), y();
+ },
+ get isConnectable() {
+ return B();
+ },
+ set isConnectable(Ne) {
+ B(Ne), y();
+ },
+ get class() {
+ return F();
+ },
+ set class(Ne) {
+ F(Ne), y();
+ }
+ });
+ return r(), gn;
+}
+ae(
+ Qn,
+ {
+ id: {},
+ type: {},
+ position: {},
+ style: {},
+ isValidConnection: {},
+ onconnect: {},
+ ondisconnect: {},
+ isConnectable: {},
+ class: {}
+ },
+ ["default"],
+ [],
+ !0
+);
+var o2 = /* @__PURE__ */ ne("<!> <!>", 1);
+function _i(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, ["data", "targetPosition", "sourcePosition"]), de(t, !1);
+ let r = w(t, "data", 28, () => ({ label: "Node" })), o = w(t, "targetPosition", 12, void 0), i = w(t, "sourcePosition", 12, void 0);
+ He();
+ var s = o2(), a = be(s);
+ const l = /* @__PURE__ */ pe(() => o() ?? $e.Top);
+ Qn(a, {
+ type: "target",
+ get position() {
+ return h(l);
+ }
+ });
+ var u = z(a), c = z(u);
+ const f = /* @__PURE__ */ pe(() => i() ?? $e.Bottom);
+ return Qn(c, {
+ type: "source",
+ get position() {
+ return h(f);
+ }
+ }), Ee(() => {
+ var d;
+ return Rt(u, ` ${((d = r()) == null ? void 0 : d.label) ?? ""} `);
+ }), L(e, s), fe({
+ get data() {
+ return r();
+ },
+ set data(d) {
+ r(d), y();
+ },
+ get targetPosition() {
+ return o();
+ },
+ set targetPosition(d) {
+ o(d), y();
+ },
+ get sourcePosition() {
+ return i();
+ },
+ set sourcePosition(d) {
+ i(d), y();
+ }
+ });
+}
+ae(
+ _i,
+ {
+ data: {},
+ targetPosition: {},
+ sourcePosition: {}
+ },
+ [],
+ [],
+ !0
+);
+var i2 = /* @__PURE__ */ ne(" <!>", 1);
+function yc(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, ["data", "sourcePosition"]), de(t, !1);
+ let r = w(t, "data", 28, () => ({ label: "Node" })), o = w(t, "sourcePosition", 12, void 0);
+ He(), Se();
+ var i = i2(), s = be(i), a = z(s);
+ const l = /* @__PURE__ */ pe(() => o() ?? $e.Bottom);
+ return Qn(a, {
+ type: "source",
+ get position() {
+ return h(l);
+ }
+ }), Ee(() => {
+ var u;
+ return Rt(s, `${((u = r()) == null ? void 0 : u.label) ?? ""} `);
+ }), L(e, i), fe({
+ get data() {
+ return r();
+ },
+ set data(u) {
+ r(u), y();
+ },
+ get sourcePosition() {
+ return o();
+ },
+ set sourcePosition(u) {
+ o(u), y();
+ }
+ });
+}
+ae(yc, { data: {}, sourcePosition: {} }, [], [], !0);
+var s2 = /* @__PURE__ */ ne(" <!>", 1);
+function wc(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, ["data", "targetPosition"]), de(t, !1);
+ let r = w(t, "data", 28, () => ({ label: "Node" })), o = w(t, "targetPosition", 12, void 0);
+ He(), Se();
+ var i = s2(), s = be(i), a = z(s);
+ const l = /* @__PURE__ */ pe(() => o() ?? $e.Top);
+ return Qn(a, {
+ type: "target",
+ get position() {
+ return h(l);
+ }
+ }), Ee(() => {
+ var u;
+ return Rt(s, `${((u = r()) == null ? void 0 : u.label) ?? ""} `);
+ }), L(e, i), fe({
+ get data() {
+ return r();
+ },
+ set data(u) {
+ r(u), y();
+ },
+ get targetPosition() {
+ return o();
+ },
+ set targetPosition(u) {
+ o(u), y();
+ }
+ });
+}
+ae(wc, { data: {}, targetPosition: {} }, [], [], !0);
+function _c(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, []);
+}
+ae(_c, {}, [], [], !0);
+function Cl(e, t, n) {
+ if (!t)
+ return;
+ const r = n ? t.querySelector(n) : t;
+ r && r.appendChild(e);
+}
+function kr(e, { target: t, domNode: n }) {
+ return Cl(e, n, t), {
+ async update({ target: r, domNode: o }) {
+ Cl(e, o, r);
+ },
+ destroy() {
+ e.parentNode && e.parentNode.removeChild(e);
+ }
+ };
+}
+var a2 = /* @__PURE__ */ ne("<div><!></div>");
+function xc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(i, "$domNode", n), { domNode: i } = Ue();
+ He();
+ var s = a2(), a = X(s);
+ pt(a, t, "default", {}), Z(s), vt(s, (l, u) => kr == null ? void 0 : kr(l, u), () => ({
+ target: ".svelte-flow__edgelabel-renderer",
+ domNode: o()
+ })), L(e, s), fe(), r();
+}
+ae(xc, {}, ["default"], [], !0);
+function bc() {
+ const { edgeLookup: e, selectionRect: t, selectionRectMode: n, multiselectionKeyPressed: r, addSelectedEdges: o, unselectNodesAndEdges: i, elementsSelectable: s } = Ue();
+ return (a) => {
+ const l = q(e).get(a);
+ if (!l) {
+ console.warn("012", Dr.error012(a));
+ return;
+ }
+ (l.selectable || q(s) && typeof l.selectable > "u") && (t.set(null), n.set(null), l.selected ? l.selected && q(r) && i({ nodes: [], edges: [l] }) : o([a]));
+ };
+}
+var l2 = /* @__PURE__ */ ne('<div class="svelte-flow__edge-label" role="button" tabindex="-1"><!></div>');
+function Cc(e, t) {
+ de(t, !1);
+ let n = w(t, "style", 12, void 0), r = w(t, "x", 12, void 0), o = w(t, "y", 12, void 0);
+ const i = bc(), s = ar("svelteflow__edge_id");
+ return He(), xc(e, {
+ children: (a, l) => {
+ var u = l2(), c = X(u);
+ pt(c, t, "default", {}), Z(u), Ee(() => {
+ ce(u, "style", "pointer-events: all;" + n()), st(u, "transform", `translate(-50%, -50%) translate(${r() ?? ""}px,${o() ?? ""}px)`);
+ }), Ye("keyup", u, () => {
+ }), Ye("click", u, () => {
+ s && i(s);
+ }), L(a, u);
+ },
+ $$slots: { default: !0 }
+ }), fe({
+ get style() {
+ return n();
+ },
+ set style(a) {
+ n(a), y();
+ },
+ get x() {
+ return r();
+ },
+ set x(a) {
+ r(a), y();
+ },
+ get y() {
+ return o();
+ },
+ set y(a) {
+ o(a), y();
+ }
+ });
+}
+ae(Cc, { style: {}, x: {}, y: {} }, ["default"], [], !0);
+var u2 = /* @__PURE__ */ _e('<path fill="none" class="svelte-flow__edge-interaction"></path>'), c2 = /* @__PURE__ */ _e('<path fill="none"></path><!><!>', 1);
+function To(e, t) {
+ de(t, !1);
+ let n = w(t, "id", 12, void 0), r = w(t, "path", 12), o = w(t, "label", 12, void 0), i = w(t, "labelX", 12, void 0), s = w(t, "labelY", 12, void 0), a = w(t, "labelStyle", 12, void 0), l = w(t, "markerStart", 12, void 0), u = w(t, "markerEnd", 12, void 0), c = w(t, "style", 12, void 0), f = w(t, "interactionWidth", 12, 20), d = w(t, "class", 12, void 0), g = f() === void 0 ? 20 : f();
+ He();
+ var p = c2(), x = be(p), C = z(x);
+ {
+ var $ = (v) => {
+ var b = u2();
+ ce(b, "stroke-opacity", 0), ce(b, "stroke-width", g), Ee(() => ce(b, "d", r())), L(v, b);
+ };
+ ke(C, (v) => {
+ g && v($);
+ });
+ }
+ var m = z(C);
+ {
+ var _ = (v) => {
+ Cc(v, {
+ get x() {
+ return i();
+ },
+ get y() {
+ return s();
+ },
+ get style() {
+ return a();
+ },
+ children: (b, N) => {
+ Se();
+ var E = Ie();
+ Ee(() => Rt(E, o())), L(b, E);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(m, (v) => {
+ o() && v(_);
+ });
+ }
+ return Ee(
+ (v) => {
+ ce(x, "id", n()), ce(x, "d", r()), kt(x, 0, bn(v)), ce(x, "marker-start", l()), ce(x, "marker-end", u()), ce(x, "style", c());
+ },
+ [
+ () => Et(["svelte-flow__edge-path", d()])
+ ],
+ pe
+ ), L(e, p), fe({
+ get id() {
+ return n();
+ },
+ set id(v) {
+ n(v), y();
+ },
+ get path() {
+ return r();
+ },
+ set path(v) {
+ r(v), y();
+ },
+ get label() {
+ return o();
+ },
+ set label(v) {
+ o(v), y();
+ },
+ get labelX() {
+ return i();
+ },
+ set labelX(v) {
+ i(v), y();
+ },
+ get labelY() {
+ return s();
+ },
+ set labelY(v) {
+ s(v), y();
+ },
+ get labelStyle() {
+ return a();
+ },
+ set labelStyle(v) {
+ a(v), y();
+ },
+ get markerStart() {
+ return l();
+ },
+ set markerStart(v) {
+ l(v), y();
+ },
+ get markerEnd() {
+ return u();
+ },
+ set markerEnd(v) {
+ u(v), y();
+ },
+ get style() {
+ return c();
+ },
+ set style(v) {
+ c(v), y();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ set interactionWidth(v) {
+ f(v), y();
+ },
+ get class() {
+ return d();
+ },
+ set class(v) {
+ d(v), y();
+ }
+ });
+}
+ae(
+ To,
+ {
+ id: {},
+ path: {},
+ label: {},
+ labelX: {},
+ labelY: {},
+ labelStyle: {},
+ markerStart: {},
+ markerEnd: {},
+ style: {},
+ interactionWidth: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+function xi(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, [
+ "label",
+ "labelStyle",
+ "style",
+ "markerStart",
+ "markerEnd",
+ "interactionWidth",
+ "sourceX",
+ "sourceY",
+ "sourcePosition",
+ "targetX",
+ "targetY",
+ "targetPosition"
+ ]), de(t, !1);
+ const r = re(), o = re(), i = re();
+ let s = w(t, "label", 12, void 0), a = w(t, "labelStyle", 12, void 0), l = w(t, "style", 12, void 0), u = w(t, "markerStart", 12, void 0), c = w(t, "markerEnd", 12, void 0), f = w(t, "interactionWidth", 12, void 0), d = w(t, "sourceX", 12), g = w(t, "sourceY", 12), p = w(t, "sourcePosition", 12), x = w(t, "targetX", 12), C = w(t, "targetY", 12), $ = w(t, "targetPosition", 12);
+ return he(
+ () => (h(r), h(o), h(i), j(d()), j(g()), j(x()), j(C()), j(p()), j($())),
+ () => {
+ ((m) => (U(r, m[0]), U(o, m[1]), U(i, m[2])))(sc({
+ sourceX: d(),
+ sourceY: g(),
+ targetX: x(),
+ targetY: C(),
+ sourcePosition: p(),
+ targetPosition: $()
+ }));
+ }
+ ), gt(), He(), To(e, {
+ get path() {
+ return h(r);
+ },
+ get labelX() {
+ return h(o);
+ },
+ get labelY() {
+ return h(i);
+ },
+ get label() {
+ return s();
+ },
+ get labelStyle() {
+ return a();
+ },
+ get markerStart() {
+ return u();
+ },
+ get markerEnd() {
+ return c();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ get style() {
+ return l();
+ }
+ }), fe({
+ get label() {
+ return s();
+ },
+ set label(m) {
+ s(m), y();
+ },
+ get labelStyle() {
+ return a();
+ },
+ set labelStyle(m) {
+ a(m), y();
+ },
+ get style() {
+ return l();
+ },
+ set style(m) {
+ l(m), y();
+ },
+ get markerStart() {
+ return u();
+ },
+ set markerStart(m) {
+ u(m), y();
+ },
+ get markerEnd() {
+ return c();
+ },
+ set markerEnd(m) {
+ c(m), y();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ set interactionWidth(m) {
+ f(m), y();
+ },
+ get sourceX() {
+ return d();
+ },
+ set sourceX(m) {
+ d(m), y();
+ },
+ get sourceY() {
+ return g();
+ },
+ set sourceY(m) {
+ g(m), y();
+ },
+ get sourcePosition() {
+ return p();
+ },
+ set sourcePosition(m) {
+ p(m), y();
+ },
+ get targetX() {
+ return x();
+ },
+ set targetX(m) {
+ x(m), y();
+ },
+ get targetY() {
+ return C();
+ },
+ set targetY(m) {
+ C(m), y();
+ },
+ get targetPosition() {
+ return $();
+ },
+ set targetPosition(m) {
+ $(m), y();
+ }
+ });
+}
+ae(
+ xi,
+ {
+ label: {},
+ labelStyle: {},
+ style: {},
+ markerStart: {},
+ markerEnd: {},
+ interactionWidth: {},
+ sourceX: {},
+ sourceY: {},
+ sourcePosition: {},
+ targetX: {},
+ targetY: {},
+ targetPosition: {}
+ },
+ [],
+ [],
+ !0
+);
+function kc(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, [
+ "label",
+ "labelStyle",
+ "style",
+ "markerStart",
+ "markerEnd",
+ "interactionWidth",
+ "sourceX",
+ "sourceY",
+ "sourcePosition",
+ "targetX",
+ "targetY",
+ "targetPosition"
+ ]), de(t, !1);
+ const r = re(), o = re(), i = re();
+ let s = w(t, "label", 12, void 0), a = w(t, "labelStyle", 12, void 0), l = w(t, "style", 12, void 0), u = w(t, "markerStart", 12, void 0), c = w(t, "markerEnd", 12, void 0), f = w(t, "interactionWidth", 12, void 0), d = w(t, "sourceX", 12), g = w(t, "sourceY", 12), p = w(t, "sourcePosition", 12), x = w(t, "targetX", 12), C = w(t, "targetY", 12), $ = w(t, "targetPosition", 12);
+ return he(
+ () => (h(r), h(o), h(i), j(d()), j(g()), j(x()), j(C()), j(p()), j($())),
+ () => {
+ ((m) => (U(r, m[0]), U(o, m[1]), U(i, m[2])))(wi({
+ sourceX: d(),
+ sourceY: g(),
+ targetX: x(),
+ targetY: C(),
+ sourcePosition: p(),
+ targetPosition: $()
+ }));
+ }
+ ), gt(), He(), To(e, {
+ get path() {
+ return h(r);
+ },
+ get labelX() {
+ return h(o);
+ },
+ get labelY() {
+ return h(i);
+ },
+ get label() {
+ return s();
+ },
+ get labelStyle() {
+ return a();
+ },
+ get markerStart() {
+ return u();
+ },
+ get markerEnd() {
+ return c();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ get style() {
+ return l();
+ }
+ }), fe({
+ get label() {
+ return s();
+ },
+ set label(m) {
+ s(m), y();
+ },
+ get labelStyle() {
+ return a();
+ },
+ set labelStyle(m) {
+ a(m), y();
+ },
+ get style() {
+ return l();
+ },
+ set style(m) {
+ l(m), y();
+ },
+ get markerStart() {
+ return u();
+ },
+ set markerStart(m) {
+ u(m), y();
+ },
+ get markerEnd() {
+ return c();
+ },
+ set markerEnd(m) {
+ c(m), y();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ set interactionWidth(m) {
+ f(m), y();
+ },
+ get sourceX() {
+ return d();
+ },
+ set sourceX(m) {
+ d(m), y();
+ },
+ get sourceY() {
+ return g();
+ },
+ set sourceY(m) {
+ g(m), y();
+ },
+ get sourcePosition() {
+ return p();
+ },
+ set sourcePosition(m) {
+ p(m), y();
+ },
+ get targetX() {
+ return x();
+ },
+ set targetX(m) {
+ x(m), y();
+ },
+ get targetY() {
+ return C();
+ },
+ set targetY(m) {
+ C(m), y();
+ },
+ get targetPosition() {
+ return $();
+ },
+ set targetPosition(m) {
+ $(m), y();
+ }
+ });
+}
+ae(
+ kc,
+ {
+ label: {},
+ labelStyle: {},
+ style: {},
+ markerStart: {},
+ markerEnd: {},
+ interactionWidth: {},
+ sourceX: {},
+ sourceY: {},
+ sourcePosition: {},
+ targetX: {},
+ targetY: {},
+ targetPosition: {}
+ },
+ [],
+ [],
+ !0
+);
+function $c(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, [
+ "label",
+ "labelStyle",
+ "style",
+ "markerStart",
+ "markerEnd",
+ "interactionWidth",
+ "sourceX",
+ "sourceY",
+ "targetX",
+ "targetY"
+ ]), de(t, !1);
+ const r = re(), o = re(), i = re();
+ let s = w(t, "label", 12, void 0), a = w(t, "labelStyle", 12, void 0), l = w(t, "style", 12, void 0), u = w(t, "markerStart", 12, void 0), c = w(t, "markerEnd", 12, void 0), f = w(t, "interactionWidth", 12, void 0), d = w(t, "sourceX", 12), g = w(t, "sourceY", 12), p = w(t, "targetX", 12), x = w(t, "targetY", 12);
+ return he(
+ () => (h(r), h(o), h(i), j(d()), j(g()), j(p()), j(x())),
+ () => {
+ ((C) => (U(r, C[0]), U(o, C[1]), U(i, C[2])))(Hs({
+ sourceX: d(),
+ sourceY: g(),
+ targetX: p(),
+ targetY: x()
+ }));
+ }
+ ), gt(), He(), To(e, {
+ get path() {
+ return h(r);
+ },
+ get labelX() {
+ return h(o);
+ },
+ get labelY() {
+ return h(i);
+ },
+ get label() {
+ return s();
+ },
+ get labelStyle() {
+ return a();
+ },
+ get markerStart() {
+ return u();
+ },
+ get markerEnd() {
+ return c();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ get style() {
+ return l();
+ }
+ }), fe({
+ get label() {
+ return s();
+ },
+ set label(C) {
+ s(C), y();
+ },
+ get labelStyle() {
+ return a();
+ },
+ set labelStyle(C) {
+ a(C), y();
+ },
+ get style() {
+ return l();
+ },
+ set style(C) {
+ l(C), y();
+ },
+ get markerStart() {
+ return u();
+ },
+ set markerStart(C) {
+ u(C), y();
+ },
+ get markerEnd() {
+ return c();
+ },
+ set markerEnd(C) {
+ c(C), y();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ set interactionWidth(C) {
+ f(C), y();
+ },
+ get sourceX() {
+ return d();
+ },
+ set sourceX(C) {
+ d(C), y();
+ },
+ get sourceY() {
+ return g();
+ },
+ set sourceY(C) {
+ g(C), y();
+ },
+ get targetX() {
+ return p();
+ },
+ set targetX(C) {
+ p(C), y();
+ },
+ get targetY() {
+ return x();
+ },
+ set targetY(C) {
+ x(C), y();
+ }
+ });
+}
+ae(
+ $c,
+ {
+ label: {},
+ labelStyle: {},
+ style: {},
+ markerStart: {},
+ markerEnd: {},
+ interactionWidth: {},
+ sourceX: {},
+ sourceY: {},
+ targetX: {},
+ targetY: {}
+ },
+ [],
+ [],
+ !0
+);
+function Ec(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]);
+ nt(n, [
+ "label",
+ "labelStyle",
+ "style",
+ "markerStart",
+ "markerEnd",
+ "interactionWidth",
+ "sourceX",
+ "sourceY",
+ "sourcePosition",
+ "targetX",
+ "targetY",
+ "targetPosition"
+ ]), de(t, !1);
+ const r = re(), o = re(), i = re();
+ let s = w(t, "label", 12, void 0), a = w(t, "labelStyle", 12, void 0), l = w(t, "style", 12, void 0), u = w(t, "markerStart", 12, void 0), c = w(t, "markerEnd", 12, void 0), f = w(t, "interactionWidth", 12, void 0), d = w(t, "sourceX", 12), g = w(t, "sourceY", 12), p = w(t, "sourcePosition", 12), x = w(t, "targetX", 12), C = w(t, "targetY", 12), $ = w(t, "targetPosition", 12);
+ return he(
+ () => (h(r), h(o), h(i), j(d()), j(g()), j(x()), j(C()), j(p()), j($())),
+ () => {
+ ((m) => (U(r, m[0]), U(o, m[1]), U(i, m[2])))(wi({
+ sourceX: d(),
+ sourceY: g(),
+ targetX: x(),
+ targetY: C(),
+ sourcePosition: p(),
+ targetPosition: $(),
+ borderRadius: 0
+ }));
+ }
+ ), gt(), He(), To(e, {
+ get path() {
+ return h(r);
+ },
+ get labelX() {
+ return h(o);
+ },
+ get labelY() {
+ return h(i);
+ },
+ get label() {
+ return s();
+ },
+ get labelStyle() {
+ return a();
+ },
+ get markerStart() {
+ return u();
+ },
+ get markerEnd() {
+ return c();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ get style() {
+ return l();
+ }
+ }), fe({
+ get label() {
+ return s();
+ },
+ set label(m) {
+ s(m), y();
+ },
+ get labelStyle() {
+ return a();
+ },
+ set labelStyle(m) {
+ a(m), y();
+ },
+ get style() {
+ return l();
+ },
+ set style(m) {
+ l(m), y();
+ },
+ get markerStart() {
+ return u();
+ },
+ set markerStart(m) {
+ u(m), y();
+ },
+ get markerEnd() {
+ return c();
+ },
+ set markerEnd(m) {
+ c(m), y();
+ },
+ get interactionWidth() {
+ return f();
+ },
+ set interactionWidth(m) {
+ f(m), y();
+ },
+ get sourceX() {
+ return d();
+ },
+ set sourceX(m) {
+ d(m), y();
+ },
+ get sourceY() {
+ return g();
+ },
+ set sourceY(m) {
+ g(m), y();
+ },
+ get sourcePosition() {
+ return p();
+ },
+ set sourcePosition(m) {
+ p(m), y();
+ },
+ get targetX() {
+ return x();
+ },
+ set targetX(m) {
+ x(m), y();
+ },
+ get targetY() {
+ return C();
+ },
+ set targetY(m) {
+ C(m), y();
+ },
+ get targetPosition() {
+ return $();
+ },
+ set targetPosition(m) {
+ $(m), y();
+ }
+ });
+}
+ae(
+ Ec,
+ {
+ label: {},
+ labelStyle: {},
+ style: {},
+ markerStart: {},
+ markerEnd: {},
+ interactionWidth: {},
+ sourceX: {},
+ sourceY: {},
+ sourcePosition: {},
+ targetX: {},
+ targetY: {},
+ targetPosition: {}
+ },
+ [],
+ [],
+ !0
+);
+function d2(e, t) {
+ const n = e.set, r = t.set, o = q(e), i = q(t);
+ let a = o.length === 0 && i.length > 0 ? i : o;
+ e.set(a);
+ const l = (u) => {
+ const c = n(u);
+ return a = c, r(a), c;
+ };
+ e.set = t.set = l, e.update = t.update = (u) => l(u(a));
+}
+function f2(e, t) {
+ const n = e.set, r = t.set;
+ let o = q(t);
+ e.set(o);
+ const i = (s) => {
+ n(s), r(s), o = s;
+ };
+ e.set = t.set = i, e.update = t.update = (s) => i(s(o));
+}
+const g2 = (e, t, n) => {
+ if (!n)
+ return;
+ const r = q(e), o = t.set, i = n.set;
+ let s = n ? q(n) : { x: 0, y: 0, zoom: 1 };
+ t.set(s), t.set = (a) => (o(a), i(a), s = a, a), n.set = (a) => (r == null || r.syncViewport(a), o(a), i(a), s = a, a), t.update = (a) => {
+ t.set(a(s));
+ }, n.update = (a) => {
+ n.set(a(s));
+ };
+}, h2 = (e, t, n, r = [0, 0], o = vi) => {
+ const { subscribe: i, set: s, update: a } = we([]);
+ let l = e, u = {}, c = !0;
+ const f = (x) => (lc(x, t, n, {
+ elevateNodesOnSelect: c,
+ nodeOrigin: r,
+ nodeExtent: o,
+ defaults: u,
+ checkEquality: !1
+ }), l = x, s(l), l), d = (x) => f(x(l)), g = (x) => {
+ u = x;
+ }, p = (x) => {
+ c = x.elevateNodesOnSelect ?? c;
+ };
+ return f(l), {
+ subscribe: i,
+ set: f,
+ update: d,
+ setDefaultOptions: g,
+ setOptions: p
+ };
+}, v2 = (e, t, n, r) => {
+ const { subscribe: o, set: i, update: s } = we([]);
+ let a = e, l = {};
+ const u = (d) => {
+ const g = l ? d.map((p) => ({ ...l, ...p })) : d;
+ cc(t, n, g), a = g, i(a);
+ }, c = (d) => u(d(a)), f = (d) => {
+ l = d;
+ };
+ return u(a), {
+ subscribe: o,
+ set: u,
+ update: c,
+ setDefaultOptions: f
+ };
+}, Sc = {
+ input: yc,
+ output: wc,
+ default: _i,
+ group: _c
+}, Pc = {
+ straight: $c,
+ smoothstep: kc,
+ default: xi,
+ step: Ec
+}, p2 = ({ nodes: e = [], edges: t = [], width: n, height: r, fitView: o, nodeOrigin: i, nodeExtent: s }) => {
+ const a = /* @__PURE__ */ new Map(), l = /* @__PURE__ */ new Map(), u = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(), f = i ?? [0, 0], d = s ?? vi;
+ lc(e, a, l, {
+ nodeExtent: d,
+ nodeOrigin: f,
+ elevateNodesOnSelect: !1,
+ checkEquality: !1
+ }), cc(u, c, t);
+ let g = { x: 0, y: 0, zoom: 1 };
+ if (o && n && r) {
+ const p = No(a, {
+ filter: (x) => !!((x.width || x.initialWidth) && (x.height || x.initialHeight))
+ });
+ g = ua(p, n, r, 0.5, 2, 0.1);
+ }
+ return {
+ flowId: we(null),
+ nodes: h2(e, a, l, f, d),
+ nodeLookup: Ft(a),
+ parentLookup: Ft(l),
+ edgeLookup: Ft(c),
+ visibleNodes: Ft([]),
+ edges: v2(t, u, c),
+ visibleEdges: Ft([]),
+ connectionLookup: Ft(u),
+ height: we(500),
+ width: we(500),
+ minZoom: we(0.5),
+ maxZoom: we(2),
+ nodeOrigin: we(f),
+ nodeDragThreshold: we(1),
+ nodeExtent: we(d),
+ translateExtent: we(vi),
+ autoPanOnNodeDrag: we(!0),
+ autoPanOnConnect: we(!0),
+ fitViewOnInit: we(!1),
+ fitViewOnInitDone: we(!1),
+ fitViewOptions: we(void 0),
+ panZoom: we(null),
+ snapGrid: we(null),
+ dragging: we(!1),
+ selectionRect: we(null),
+ selectionKeyPressed: we(!1),
+ multiselectionKeyPressed: we(!1),
+ deleteKeyPressed: we(!1),
+ panActivationKeyPressed: we(!1),
+ zoomActivationKeyPressed: we(!1),
+ selectionRectMode: we(null),
+ selectionMode: we(pi.Partial),
+ nodeTypes: we(Sc),
+ edgeTypes: we(Pc),
+ viewport: we(g),
+ connectionMode: we(cr.Strict),
+ domNode: we(null),
+ connection: Ft(Ns),
+ connectionLineType: we(Cr.Bezier),
+ connectionRadius: we(20),
+ isValidConnection: we(() => !0),
+ nodesDraggable: we(!0),
+ nodesConnectable: we(!0),
+ elementsSelectable: we(!0),
+ selectNodesOnDrag: we(!0),
+ markers: Ft([]),
+ defaultMarkerColor: we("#b1b1b7"),
+ lib: Ft("svelte"),
+ onlyRenderVisibleElements: we(!1),
+ onerror: we(v0),
+ ondelete: we(void 0),
+ onedgecreate: we(void 0),
+ onconnect: we(void 0),
+ onconnectstart: we(void 0),
+ onconnectend: we(void 0),
+ onbeforedelete: we(void 0),
+ nodesInitialized: we(!1),
+ edgesInitialized: we(!1),
+ viewportInitialized: we(!1),
+ initialized: Ft(!1)
+ };
+};
+function m2(e) {
+ const t = Kn([
+ e.edges,
+ e.nodes,
+ e.nodeLookup,
+ e.onlyRenderVisibleElements,
+ e.viewport,
+ e.width,
+ e.height
+ ], ([n, , r, o, i, s, a]) => o && s && a ? n.filter((u) => {
+ const c = r.get(u.source), f = r.get(u.target);
+ return c && f && b0({
+ sourceNode: c,
+ targetNode: f,
+ width: s,
+ height: a,
+ transform: [i.x, i.y, i.zoom]
+ });
+ }) : n);
+ return Kn([t, e.nodes, e.nodeLookup, e.connectionMode, e.onerror], ([n, , r, o, i]) => n.reduce((a, l) => {
+ const u = r.get(l.source), c = r.get(l.target);
+ if (!u || !c)
+ return a;
+ const f = N0({
+ id: l.id,
+ sourceNode: u,
+ targetNode: c,
+ sourceHandle: l.sourceHandle || null,
+ targetHandle: l.targetHandle || null,
+ connectionMode: o,
+ onError: i
+ });
+ return f && a.push({
+ ...l,
+ zIndex: x0({
+ selected: l.selected,
+ zIndex: l.zIndex,
+ sourceNode: u,
+ targetNode: c,
+ elevateOnSelect: !1
+ }),
+ ...f
+ }), a;
+ }, []));
+}
+function y2(e) {
+ return Kn([
+ e.nodeLookup,
+ e.onlyRenderVisibleElements,
+ e.width,
+ e.height,
+ e.viewport,
+ e.nodes
+ ], ([t, n, r, o, i]) => {
+ const s = [i.x, i.y, i.zoom];
+ return n ? Ju(t, { x: 0, y: 0, width: r, height: o }, s, !0) : Array.from(t.values());
+ });
+}
+const Wi = Symbol();
+function Nc({ nodes: e, edges: t, width: n, height: r, fitView: o, nodeOrigin: i, nodeExtent: s }) {
+ const a = p2({
+ nodes: e,
+ edges: t,
+ width: n,
+ height: r,
+ fitView: o,
+ nodeOrigin: i,
+ nodeExtent: s
+ });
+ function l(k) {
+ a.nodeTypes.set({
+ ...Sc,
+ ...k
+ });
+ }
+ function u(k) {
+ a.edgeTypes.set({
+ ...Pc,
+ ...k
+ });
+ }
+ function c(k) {
+ const P = q(a.edges);
+ a.edges.set($0(k, P));
+ }
+ const f = (k, P = !1) => {
+ var I;
+ const H = q(a.nodeLookup);
+ for (const [B, F] of k) {
+ const K = (I = H.get(B)) == null ? void 0 : I.internals.userNode;
+ K && (K.position = F.position, K.dragging = P);
+ }
+ a.nodes.update((B) => B);
+ };
+ function d(k) {
+ var F, K, ie;
+ const P = q(a.nodeLookup), H = q(a.parentLookup), { changes: I, updatedInternals: B } = O0(k, P, q(a.parentLookup), q(a.domNode), q(a.nodeOrigin));
+ if (B) {
+ if (V0(P, H, { nodeOrigin: i, nodeExtent: s }), !q(a.fitViewOnInitDone) && q(a.fitViewOnInit)) {
+ const ee = q(a.fitViewOptions), W = p({
+ ...ee,
+ nodes: ee == null ? void 0 : ee.nodes
+ });
+ a.fitViewOnInitDone.set(W);
+ }
+ for (const ee of I) {
+ const W = (F = P.get(ee.id)) == null ? void 0 : F.internals.userNode;
+ if (W)
+ switch (ee.type) {
+ case "dimensions": {
+ const ue = { ...W.measured, ...ee.dimensions };
+ ee.setAttributes && (W.width = ((K = ee.dimensions) == null ? void 0 : K.width) ?? W.width, W.height = ((ie = ee.dimensions) == null ? void 0 : ie.height) ?? W.height), W.measured = ue;
+ break;
+ }
+ case "position":
+ W.position = ee.position ?? W.position;
+ break;
+ }
+ }
+ a.nodes.update((ee) => ee), q(a.nodesInitialized) || a.nodesInitialized.set(!0);
+ }
+ }
+ function g(k) {
+ const P = q(a.panZoom), H = q(a.domNode);
+ if (!P || !H)
+ return Promise.resolve(!1);
+ const { width: I, height: B } = ca(H), F = ul(q(a.nodeLookup), k);
+ return cl({
+ nodes: F,
+ width: I,
+ height: B,
+ minZoom: q(a.minZoom),
+ maxZoom: q(a.maxZoom),
+ panZoom: P
+ }, k);
+ }
+ function p(k) {
+ const P = q(a.panZoom);
+ if (!P)
+ return !1;
+ const H = ul(q(a.nodeLookup), k);
+ return cl({
+ nodes: H,
+ width: q(a.width),
+ height: q(a.height),
+ minZoom: q(a.minZoom),
+ maxZoom: q(a.maxZoom),
+ panZoom: P
+ }, k), H.size > 0;
+ }
+ function x(k, P) {
+ const H = q(a.panZoom);
+ return H ? H.scaleBy(k, P) : Promise.resolve(!1);
+ }
+ function C(k) {
+ return x(1.2, k);
+ }
+ function $(k) {
+ return x(1 / 1.2, k);
+ }
+ function m(k) {
+ const P = q(a.panZoom);
+ P && (P.setScaleExtent([k, q(a.maxZoom)]), a.minZoom.set(k));
+ }
+ function _(k) {
+ const P = q(a.panZoom);
+ P && (P.setScaleExtent([q(a.minZoom), k]), a.maxZoom.set(k));
+ }
+ function v(k) {
+ const P = q(a.panZoom);
+ P && (P.setTranslateExtent(k), a.translateExtent.set(k));
+ }
+ function b(k) {
+ let P = !1;
+ return k.forEach((H) => {
+ H.selected && (H.selected = !1, P = !0);
+ }), P;
+ }
+ function N(k) {
+ var P;
+ (P = q(a.panZoom)) == null || P.setClickDistance(k);
+ }
+ function E(k) {
+ b((k == null ? void 0 : k.nodes) || q(a.nodes)) && a.nodes.set(q(a.nodes)), b((k == null ? void 0 : k.edges) || q(a.edges)) && a.edges.set(q(a.edges));
+ }
+ a.deleteKeyPressed.subscribe(async (k) => {
+ var P;
+ if (k) {
+ const H = q(a.nodes), I = q(a.edges), B = H.filter((ee) => ee.selected), F = I.filter((ee) => ee.selected), { nodes: K, edges: ie } = await Qu({
+ nodesToRemove: B,
+ edgesToRemove: F,
+ nodes: H,
+ edges: I,
+ onBeforeDelete: q(a.onbeforedelete)
+ });
+ (K.length || ie.length) && (a.nodes.update((ee) => ee.filter((W) => !K.some((ue) => ue.id === W.id))), a.edges.update((ee) => ee.filter((W) => !ie.some((ue) => ue.id === W.id))), (P = q(a.ondelete)) == null || P({
+ nodes: K,
+ edges: ie
+ }));
+ }
+ });
+ function M(k) {
+ const P = q(a.multiselectionKeyPressed);
+ a.nodes.update((H) => H.map((I) => {
+ const B = k.includes(I.id), F = P && I.selected || B;
+ return I.selected = F, I;
+ })), P || a.edges.update((H) => H.map((I) => (I.selected = !1, I)));
+ }
+ function D(k) {
+ const P = q(a.multiselectionKeyPressed);
+ a.edges.update((H) => H.map((I) => {
+ const B = k.includes(I.id), F = P && I.selected || B;
+ return I.selected = F, I;
+ })), P || a.nodes.update((H) => H.map((I) => (I.selected = !1, I)));
+ }
+ function V(k) {
+ var H;
+ const P = (H = q(a.nodes)) == null ? void 0 : H.find((I) => I.id === k);
+ if (!P) {
+ console.warn("012", Dr.error012(k));
+ return;
+ }
+ a.selectionRect.set(null), a.selectionRectMode.set(null), P.selected ? P.selected && q(a.multiselectionKeyPressed) && E({ nodes: [P], edges: [] }) : M([k]);
+ }
+ function A(k) {
+ const P = q(a.viewport);
+ return I0({
+ delta: k,
+ panZoom: q(a.panZoom),
+ transform: [P.x, P.y, P.zoom],
+ translateExtent: q(a.translateExtent),
+ width: q(a.width),
+ height: q(a.height)
+ });
+ }
+ const O = we(Ns), R = (k) => {
+ O.set({ ...k });
+ };
+ function S() {
+ O.set(Ns);
+ }
+ function T() {
+ a.fitViewOnInitDone.set(!1), a.selectionRect.set(null), a.selectionRectMode.set(null), a.snapGrid.set(null), a.isValidConnection.set(() => !0), E(), S();
+ }
+ return {
+ // state
+ ...a,
+ // derived state
+ visibleEdges: m2(a),
+ visibleNodes: y2(a),
+ connection: Kn([O, a.viewport], ([k, P]) => k.inProgress ? {
+ ...k,
+ to: Mo(k.to, [P.x, P.y, P.zoom])
+ } : { ...k }),
+ markers: Kn([a.edges, a.defaultMarkerColor, a.flowId], ([k, P, H]) => M0(k, { defaultColor: P, id: H })),
+ initialized: (() => {
+ let k = !1;
+ const P = q(a.nodes).length, H = q(a.edges).length;
+ return Kn([a.nodesInitialized, a.edgesInitialized, a.viewportInitialized], ([I, B, F]) => k || (P === 0 ? k = F : H === 0 ? k = F && I : k = F && I && B, k));
+ })(),
+ // actions
+ syncNodeStores: (k) => d2(a.nodes, k),
+ syncEdgeStores: (k) => f2(a.edges, k),
+ syncViewport: (k) => g2(a.panZoom, a.viewport, k),
+ setNodeTypes: l,
+ setEdgeTypes: u,
+ addEdge: c,
+ updateNodePositions: f,
+ updateNodeInternals: d,
+ zoomIn: C,
+ zoomOut: $,
+ fitView: (k) => g(k),
+ setMinZoom: m,
+ setMaxZoom: _,
+ setTranslateExtent: v,
+ setPaneClickDistance: N,
+ unselectNodesAndEdges: E,
+ addSelectedNodes: M,
+ addSelectedEdges: D,
+ handleNodeSelection: V,
+ panBy: A,
+ updateConnection: R,
+ cancelConnection: S,
+ reset: T
+ };
+}
+function Ue() {
+ const e = ar(Wi);
+ if (!e)
+ throw new Error("In order to use useStore you need to wrap your component in a <SvelteFlowProvider />");
+ return e.getStore();
+}
+function w2({ nodes: e, edges: t, width: n, height: r, fitView: o, nodeOrigin: i, nodeExtent: s }) {
+ const a = Nc({ nodes: e, edges: t, width: n, height: r, fitView: o, nodeOrigin: i, nodeExtent: s });
+ return Tr(Wi, {
+ getStore: () => a
+ }), a;
+}
+function us(e, t) {
+ const { panZoom: n, minZoom: r, maxZoom: o, initialViewport: i, viewport: s, dragging: a, translateExtent: l, paneClickDistance: u } = t, c = n2({
+ domNode: e,
+ minZoom: r,
+ maxZoom: o,
+ translateExtent: l,
+ viewport: i,
+ paneClickDistance: u,
+ onDraggingChange: a.set
+ }), f = c.getViewport();
+ return s.set(f), n.set(c), c.update(t), {
+ update(d) {
+ c.update(d);
+ }
+ };
+}
+var _2 = /* @__PURE__ */ ne('<div class="svelte-flow__zoom svelte-4xkw84"><!></div>');
+const x2 = {
+ hash: "svelte-4xkw84",
+ code: ".svelte-flow__zoom.svelte-4xkw84 {width:100%;height:100%;position:absolute;top:0;left:0;z-index:4;}"
+};
+function Mc(e, t) {
+ de(t, !1), Je(e, x2);
+ const [n, r] = tt(), o = () => Q(H, "$panActivationKeyPressed", n), i = () => Q(R, "$minZoom", n), s = () => Q(S, "$maxZoom", n), a = () => Q(I, "$zoomActivationKeyPressed", n), l = () => Q(O, "$selectionRect", n), u = () => Q(k, "$translateExtent", n), c = () => Q(P, "$lib", n), f = re(), d = re(), g = re();
+ let p = w(t, "initialViewport", 12, void 0), x = w(t, "onMoveStart", 12, void 0), C = w(t, "onMove", 12, void 0), $ = w(t, "onMoveEnd", 12, void 0), m = w(t, "panOnScrollMode", 12), _ = w(t, "preventScrolling", 12), v = w(t, "zoomOnScroll", 12), b = w(t, "zoomOnDoubleClick", 12), N = w(t, "zoomOnPinch", 12), E = w(t, "panOnDrag", 12), M = w(t, "panOnScroll", 12), D = w(t, "paneClickDistance", 12);
+ const {
+ viewport: V,
+ panZoom: A,
+ selectionRect: O,
+ minZoom: R,
+ maxZoom: S,
+ dragging: T,
+ translateExtent: k,
+ lib: P,
+ panActivationKeyPressed: H,
+ zoomActivationKeyPressed: I,
+ viewportInitialized: B
+ } = Ue(), F = (W) => V.set({
+ x: W[0],
+ y: W[1],
+ zoom: W[2]
+ });
+ un(() => {
+ li(B, !0);
+ }), he(() => j(p()), () => {
+ U(f, p() || { x: 0, y: 0, zoom: 1 });
+ }), he(
+ () => (o(), j(E())),
+ () => {
+ U(d, o() || E());
+ }
+ ), he(
+ () => (o(), j(M())),
+ () => {
+ U(g, o() || M());
+ }
+ ), gt(), He();
+ var K = _2(), ie = X(K);
+ pt(ie, t, "default", {}), Z(K), vt(K, (W, ue) => us == null ? void 0 : us(W, ue), () => ({
+ viewport: V,
+ minZoom: i(),
+ maxZoom: s(),
+ initialViewport: h(f),
+ dragging: T,
+ panZoom: A,
+ onPanZoomStart: x(),
+ onPanZoom: C(),
+ onPanZoomEnd: $(),
+ zoomOnScroll: v(),
+ zoomOnDoubleClick: b(),
+ zoomOnPinch: N(),
+ panOnScroll: h(g),
+ panOnDrag: h(d),
+ panOnScrollSpeed: 0.5,
+ panOnScrollMode: m() || qn.Free,
+ zoomActivationKeyPressed: a(),
+ preventScrolling: typeof _() == "boolean" ? _() : !0,
+ noPanClassName: "nopan",
+ noWheelClassName: "nowheel",
+ userSelectionActive: !!l(),
+ translateExtent: u(),
+ lib: c(),
+ paneClickDistance: D(),
+ onTransformChange: F
+ })), L(e, K);
+ var ee = fe({
+ get initialViewport() {
+ return p();
+ },
+ set initialViewport(W) {
+ p(W), y();
+ },
+ get onMoveStart() {
+ return x();
+ },
+ set onMoveStart(W) {
+ x(W), y();
+ },
+ get onMove() {
+ return C();
+ },
+ set onMove(W) {
+ C(W), y();
+ },
+ get onMoveEnd() {
+ return $();
+ },
+ set onMoveEnd(W) {
+ $(W), y();
+ },
+ get panOnScrollMode() {
+ return m();
+ },
+ set panOnScrollMode(W) {
+ m(W), y();
+ },
+ get preventScrolling() {
+ return _();
+ },
+ set preventScrolling(W) {
+ _(W), y();
+ },
+ get zoomOnScroll() {
+ return v();
+ },
+ set zoomOnScroll(W) {
+ v(W), y();
+ },
+ get zoomOnDoubleClick() {
+ return b();
+ },
+ set zoomOnDoubleClick(W) {
+ b(W), y();
+ },
+ get zoomOnPinch() {
+ return N();
+ },
+ set zoomOnPinch(W) {
+ N(W), y();
+ },
+ get panOnDrag() {
+ return E();
+ },
+ set panOnDrag(W) {
+ E(W), y();
+ },
+ get panOnScroll() {
+ return M();
+ },
+ set panOnScroll(W) {
+ M(W), y();
+ },
+ get paneClickDistance() {
+ return D();
+ },
+ set paneClickDistance(W) {
+ D(W), y();
+ }
+ });
+ return r(), ee;
+}
+ae(
+ Mc,
+ {
+ initialViewport: {},
+ onMoveStart: {},
+ onMove: {},
+ onMoveEnd: {},
+ panOnScrollMode: {},
+ preventScrolling: {},
+ zoomOnScroll: {},
+ zoomOnDoubleClick: {},
+ zoomOnPinch: {},
+ panOnDrag: {},
+ panOnScroll: {},
+ paneClickDistance: {}
+ },
+ ["default"],
+ [],
+ !0
+);
+function kl(e, t) {
+ return (n) => {
+ n.target === t && (e == null || e(n));
+ };
+}
+function $l(e) {
+ return (t) => {
+ const n = e.includes(t.id);
+ return t.selected !== n && (t.selected = n), t;
+ };
+}
+var b2 = /* @__PURE__ */ ne("<div><!></div>");
+const C2 = {
+ hash: "svelte-1esy7hx",
+ code: ".svelte-flow__pane.svelte-1esy7hx {position:absolute;top:0;left:0;width:100%;height:100%;}"
+};
+function Tc(e, t) {
+ de(t, !1), Je(e, C2);
+ const [n, r] = tt(), o = () => Q(S, "$panActivationKeyPressed", n), i = () => Q(O, "$selectionKeyPressed", n), s = () => Q(V, "$selectionRect", n), a = () => Q(D, "$elementsSelectable", n), l = () => Q(A, "$selectionRectMode", n), u = () => Q(N, "$edges", n), c = () => Q(b, "$nodeLookup", n), f = () => Q(E, "$viewport", n), d = () => Q(R, "$selectionMode", n), g = () => Q(M, "$dragging", n), p = re(), x = re(), C = re();
+ let $ = w(t, "panOnDrag", 12, void 0), m = w(t, "selectionOnDrag", 12, void 0);
+ const _ = Oi(), {
+ nodes: v,
+ nodeLookup: b,
+ edges: N,
+ viewport: E,
+ dragging: M,
+ elementsSelectable: D,
+ selectionRect: V,
+ selectionRectMode: A,
+ selectionKeyPressed: O,
+ selectionMode: R,
+ panActivationKeyPressed: S,
+ unselectNodesAndEdges: T
+ } = Ue();
+ let k = re(), P = null, H = [], I = !1;
+ function B(G) {
+ if (I) {
+ I = !1;
+ return;
+ }
+ _("paneclick", { event: G }), T(), A.set(null);
+ }
+ function F(G) {
+ var Ae, Xe;
+ if (P = h(k).getBoundingClientRect(), !D || !h(x) || G.button !== 0 || G.target !== h(k) || !P)
+ return;
+ (Xe = (Ae = G.target) == null ? void 0 : Ae.setPointerCapture) == null || Xe.call(Ae, G.pointerId);
+ const { x: se, y: Te } = Hn(G, P);
+ T(), V.set({
+ width: 0,
+ height: 0,
+ startX: se,
+ startY: Te,
+ x: se,
+ y: Te
+ });
+ }
+ function K(G) {
+ if (!h(x) || !P || !s())
+ return;
+ I = !0;
+ const se = Hn(G, P), Te = s().startX ?? 0, Ae = s().startY ?? 0, Xe = {
+ ...s(),
+ x: se.x < Te ? se.x : Te,
+ y: se.y < Ae ? se.y : Ae,
+ width: Math.abs(se.x - Te),
+ height: Math.abs(se.y - Ae)
+ }, te = H.map((oe) => oe.id), Fe = Ms(H, u()).map((oe) => oe.id);
+ H = Ju(
+ c(),
+ Xe,
+ [
+ f().x,
+ f().y,
+ f().zoom
+ ],
+ d() === pi.Partial,
+ !0
+ );
+ const Le = Ms(H, u()).map((oe) => oe.id), Qe = H.map((oe) => oe.id);
+ (te.length !== Qe.length || Qe.some((oe) => !te.includes(oe))) && v.update((oe) => oe.map($l(Qe))), (Fe.length !== Le.length || Le.some((oe) => !Fe.includes(oe))) && N.update((oe) => oe.map($l(Le))), A.set("user"), V.set(Xe);
+ }
+ function ie(G) {
+ var se, Te;
+ G.button === 0 && ((Te = (se = G.target) == null ? void 0 : se.releasePointerCapture) == null || Te.call(se, G.pointerId), !h(x) && l() === "user" && G.target === h(k) && (B == null || B(G)), V.set(null), H.length > 0 && li(A, "nodes"), i() && (I = !1));
+ }
+ const ee = (G) => {
+ var se;
+ if (Array.isArray(h(p)) && ((se = h(p)) != null && se.includes(2))) {
+ G.preventDefault();
+ return;
+ }
+ _("panecontextmenu", { event: G });
+ };
+ he(
+ () => (o(), j($())),
+ () => {
+ U(p, o() || $());
+ }
+ ), he(
+ () => (i(), s(), j(m()), h(p)),
+ () => {
+ U(x, i() || s() || m() && h(p) !== !0);
+ }
+ ), he(
+ () => (a(), h(x), l()),
+ () => {
+ U(C, a() && (h(x) || l() === "user"));
+ }
+ ), gt(), He();
+ var W = b2(), ue = /* @__PURE__ */ Me(() => h(C) ? void 0 : kl(B, h(k))), me = /* @__PURE__ */ Me(() => kl(ee, h(k)));
+ let Ce;
+ var ge = X(W);
+ pt(ge, t, "default", {}), Z(W), An(W, (G) => U(k, G), () => h(k)), Ee(
+ (G) => Ce = kt(W, 1, "svelte-flow__pane svelte-1esy7hx", null, Ce, {
+ draggable: G,
+ dragging: g(),
+ selection: h(x)
+ }),
+ [
+ () => $() === !0 || Array.isArray($()) && $().includes(0)
+ ],
+ pe
+ ), Ye("click", W, function(...G) {
+ var se;
+ (se = h(ue)) == null || se.apply(this, G);
+ }), Ye("pointerdown", W, function(...G) {
+ var se;
+ (se = h(C) ? F : void 0) == null || se.apply(this, G);
+ }), Ye("pointermove", W, function(...G) {
+ var se;
+ (se = h(C) ? K : void 0) == null || se.apply(this, G);
+ }), Ye("pointerup", W, function(...G) {
+ var se;
+ (se = h(C) ? ie : void 0) == null || se.apply(this, G);
+ }), Ye("contextmenu", W, function(...G) {
+ var se;
+ (se = h(me)) == null || se.apply(this, G);
+ }), L(e, W);
+ var ze = fe({
+ get panOnDrag() {
+ return $();
+ },
+ set panOnDrag(G) {
+ $(G), y();
+ },
+ get selectionOnDrag() {
+ return m();
+ },
+ set selectionOnDrag(G) {
+ m(G), y();
+ }
+ });
+ return r(), ze;
+}
+ae(Tc, { panOnDrag: {}, selectionOnDrag: {} }, ["default"], [], !0);
+var k2 = /* @__PURE__ */ ne('<div class="svelte-flow__viewport xyflow__viewport svelte-1floaup"><!></div>');
+const $2 = {
+ hash: "svelte-1floaup",
+ code: ".svelte-flow__viewport.svelte-1floaup {width:100%;height:100%;position:absolute;top:0;left:0;}"
+};
+function Hc(e, t) {
+ de(t, !1), Je(e, $2);
+ const [n, r] = tt(), o = () => Q(i, "$viewport", n), { viewport: i } = Ue();
+ He();
+ var s = k2(), a = X(s);
+ pt(a, t, "default", {}), Z(s), Ee(() => ce(s, "style", `transform: translate(${o().x ?? ""}px, ${o().y ?? ""}px) scale(${o().zoom ?? ""})`)), L(e, s), fe(), r();
+}
+ae(Hc, {}, ["default"], [], !0);
+function $r(e, t) {
+ const { store: n, onDrag: r, onDragStart: o, onDragStop: i, onNodeMouseDown: s } = t, a = B0({
+ onDrag: r,
+ onDragStart: o,
+ onDragStop: i,
+ onNodeMouseDown: s,
+ getStoreItems: () => {
+ const u = q(n.snapGrid), c = q(n.viewport);
+ return {
+ nodes: q(n.nodes),
+ nodeLookup: q(n.nodeLookup),
+ edges: q(n.edges),
+ nodeExtent: q(n.nodeExtent),
+ snapGrid: u || [0, 0],
+ snapToGrid: !!u,
+ nodeOrigin: q(n.nodeOrigin),
+ multiSelectionActive: q(n.multiselectionKeyPressed),
+ domNode: q(n.domNode),
+ transform: [c.x, c.y, c.zoom],
+ autoPanOnNodeDrag: q(n.autoPanOnNodeDrag),
+ nodesDraggable: q(n.nodesDraggable),
+ selectNodesOnDrag: q(n.selectNodesOnDrag),
+ nodeDragThreshold: q(n.nodeDragThreshold),
+ unselectNodesAndEdges: n.unselectNodesAndEdges,
+ updateNodePositions: n.updateNodePositions,
+ panBy: n.panBy
+ };
+ }
+ });
+ function l(u, c) {
+ if (c.disabled) {
+ a.destroy();
+ return;
+ }
+ a.update({
+ domNode: u,
+ noDragClassName: c.noDragClass,
+ handleSelector: c.handleSelector,
+ nodeId: c.nodeId,
+ isSelectable: c.isSelectable,
+ nodeClickDistance: c.nodeClickDistance
+ });
+ }
+ return l(e, t), {
+ update(u) {
+ l(e, u);
+ },
+ destroy() {
+ a.destroy();
+ }
+ };
+}
+function E2({ width: e, height: t, initialWidth: n, initialHeight: r, measuredWidth: o, measuredHeight: i }) {
+ if (o === void 0 && i === void 0) {
+ const s = e ?? n, a = t ?? r;
+ return {
+ width: s ? `width:${s}px;` : "",
+ height: a ? `height:${a}px;` : ""
+ };
+ }
+ return {
+ width: e ? `width:${e}px;` : "",
+ height: t ? `height:${t}px;` : ""
+ };
+}
+var S2 = /* @__PURE__ */ ne("<div><!></div>");
+function Vc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(me, "$nodeTypes", n), i = () => Q(se, "$elementsSelectable", n), s = () => Q(Te, "$nodesDraggable", n), a = () => Q(Fe, "$connectableStore", n), l = re(void 0, !0), u = re(void 0, !0), c = re(void 0, !0), f = re(void 0, !0);
+ let d = w(t, "node", 13), g = w(t, "id", 13), p = w(t, "data", 29, () => ({})), x = w(t, "selected", 13, !1), C = w(t, "draggable", 13, void 0), $ = w(t, "selectable", 13, void 0), m = w(t, "connectable", 13, !0), _ = w(t, "deletable", 13, !0), v = w(t, "hidden", 13, !1), b = w(t, "dragging", 13, !1), N = w(t, "resizeObserver", 13, null), E = w(t, "style", 13, void 0), M = w(t, "type", 13, "default"), D = w(t, "isParent", 13, !1), V = w(t, "positionX", 13), A = w(t, "positionY", 13), O = w(t, "sourcePosition", 13, void 0), R = w(t, "targetPosition", 13, void 0), S = w(t, "zIndex", 13), T = w(t, "measuredWidth", 13, void 0), k = w(t, "measuredHeight", 13, void 0), P = w(t, "initialWidth", 13, void 0), H = w(t, "initialHeight", 13, void 0), I = w(t, "width", 13, void 0), B = w(t, "height", 13, void 0), F = w(t, "dragHandle", 13, void 0), K = w(t, "initialized", 13, !1), ie = w(t, "parentId", 13, void 0), ee = w(t, "nodeClickDistance", 13, void 0), W = w(t, "class", 13, "");
+ const ue = Ue(), {
+ nodeTypes: me,
+ nodeDragThreshold: Ce,
+ selectNodesOnDrag: ge,
+ handleNodeSelection: ze,
+ updateNodeInternals: G,
+ elementsSelectable: se,
+ nodesDraggable: Te
+ } = ue;
+ let Ae = re(void 0, !0), Xe = re(null, !0);
+ const te = Oi(), Fe = we(m());
+ let Le = re(void 0, !0), Qe = re(void 0, !0), oe = re(void 0, !0);
+ Tr("svelteflow__node_id", g()), Tr("svelteflow__node_connectable", Fe), Qs(() => {
+ var J;
+ h(Xe) && ((J = N()) == null || J.unobserve(h(Xe)));
+ });
+ function ve(J) {
+ $() && (!q(ge) || !C() || q(Ce) > 0) && ze(g()), te("nodeclick", { node: d().internals.userNode, event: J });
+ }
+ he(() => j(M()), () => {
+ U(l, M() || "default");
+ }), he(() => (o(), h(l)), () => {
+ U(u, !!o()[h(l)]);
+ }), he(
+ () => (o(), h(l), _i),
+ () => {
+ U(c, o()[h(l)] || _i);
+ }
+ ), he(
+ () => (h(u), j(M())),
+ () => {
+ h(u) || console.warn("003", Dr.error003(M()));
+ }
+ ), he(
+ () => (j(I()), j(B()), j(P()), j(H()), j(T()), j(k())),
+ () => {
+ U(f, E2({
+ width: I(),
+ height: B(),
+ initialWidth: P(),
+ initialHeight: H(),
+ measuredWidth: T(),
+ measuredHeight: k()
+ }));
+ }
+ ), he(() => j(m()), () => {
+ Fe.set(!!m());
+ }), he(
+ () => (h(Le), h(l), h(Qe), j(O()), h(oe), j(R()), j(g()), h(Ae)),
+ () => {
+ (h(Le) && h(l) !== h(Le) || h(Qe) && O() !== h(Qe) || h(oe) && R() !== h(oe)) && requestAnimationFrame(() => G(/* @__PURE__ */ new Map([
+ [
+ g(),
+ {
+ id: g(),
+ nodeElement: h(Ae),
+ force: !0
+ }
+ ]
+ ]))), U(Le, h(l)), U(Qe, O()), U(oe, R());
+ }
+ ), he(
+ () => (j(N()), h(Ae), h(Xe), j(K())),
+ () => {
+ N() && (h(Ae) !== h(Xe) || !K()) && (h(Xe) && N().unobserve(h(Xe)), h(Ae) && N().observe(h(Ae)), U(Xe, h(Ae)));
+ }
+ ), gt(), He(!0);
+ var xe = et(), Oe = be(xe);
+ {
+ var ct = (J) => {
+ var Re = S2();
+ let le;
+ var fn = X(Re);
+ const Ut = /* @__PURE__ */ pe(() => x() ?? !1), gn = /* @__PURE__ */ pe(() => $() ?? i() ?? !0), Ne = /* @__PURE__ */ pe(() => _() ?? !0), rt = /* @__PURE__ */ pe(() => C() ?? s() ?? !0);
+ yu(fn, () => h(c), (ye, ot) => {
+ ot(ye, {
+ get data() {
+ return p();
+ },
+ get id() {
+ return g();
+ },
+ get selected() {
+ return h(Ut);
+ },
+ get selectable() {
+ return h(gn);
+ },
+ get deletable() {
+ return h(Ne);
+ },
+ get sourcePosition() {
+ return O();
+ },
+ get targetPosition() {
+ return R();
+ },
+ get zIndex() {
+ return S();
+ },
+ get dragging() {
+ return b();
+ },
+ get draggable() {
+ return h(rt);
+ },
+ get dragHandle() {
+ return F();
+ },
+ get parentId() {
+ return ie();
+ },
+ get type() {
+ return h(l);
+ },
+ get isConnectable() {
+ return a();
+ },
+ get positionAbsoluteX() {
+ return V();
+ },
+ get positionAbsoluteY() {
+ return A();
+ },
+ get width() {
+ return I();
+ },
+ get height() {
+ return B();
+ }
+ });
+ }), Z(Re), vt(Re, (ye, ot) => $r == null ? void 0 : $r(ye, ot), () => ({
+ nodeId: g(),
+ isSelectable: $(),
+ disabled: !1,
+ handleSelector: F(),
+ noDragClass: "nodrag",
+ nodeClickDistance: ee(),
+ onNodeMouseDown: ze,
+ onDrag: (ye, ot, at, Xt) => {
+ te("nodedrag", { event: ye, targetNode: at, nodes: Xt });
+ },
+ onDragStart: (ye, ot, at, Xt) => {
+ te("nodedragstart", { event: ye, targetNode: at, nodes: Xt });
+ },
+ onDragStop: (ye, ot, at, Xt) => {
+ te("nodedragstop", { event: ye, targetNode: at, nodes: Xt });
+ },
+ store: ue
+ })), An(Re, (ye) => U(Ae, ye), () => h(Ae)), Ot(() => Ye("click", Re, ve)), Ot(() => Ye("mouseenter", Re, (ye) => te("nodemouseenter", { node: d(), event: ye }))), Ot(() => Ye("mouseleave", Re, (ye) => te("nodemouseleave", { node: d(), event: ye }))), Ot(() => Ye("mousemove", Re, (ye) => te("nodemousemove", { node: d(), event: ye }))), Ot(() => Ye("contextmenu", Re, (ye) => te("nodecontextmenu", { node: d(), event: ye }))), Ee(
+ (ye) => {
+ ce(Re, "data-id", g()), le = kt(Re, 1, bn(ye), null, le, {
+ dragging: b(),
+ selected: x(),
+ draggable: C(),
+ connectable: m(),
+ selectable: $(),
+ nopan: C(),
+ parent: D()
+ }), ce(Re, "style", `${E() ?? ""};${h(f).width ?? ""}${h(f).height ?? ""}`), st(Re, "z-index", S()), st(Re, "transform", `translate(${V() ?? ""}px, ${A() ?? ""}px)`), st(Re, "visibility", K() ? "visible" : "hidden");
+ },
+ [
+ () => Et([
+ "svelte-flow__node",
+ `svelte-flow__node-${h(l)}`,
+ W()
+ ])
+ ],
+ pe
+ ), L(J, Re);
+ };
+ ke(Oe, (J) => {
+ v() || J(ct);
+ });
+ }
+ L(e, xe);
+ var lt = fe({
+ get node() {
+ return d();
+ },
+ set node(J) {
+ d(J), y();
+ },
+ get id() {
+ return g();
+ },
+ set id(J) {
+ g(J), y();
+ },
+ get data() {
+ return p();
+ },
+ set data(J) {
+ p(J), y();
+ },
+ get selected() {
+ return x();
+ },
+ set selected(J) {
+ x(J), y();
+ },
+ get draggable() {
+ return C();
+ },
+ set draggable(J) {
+ C(J), y();
+ },
+ get selectable() {
+ return $();
+ },
+ set selectable(J) {
+ $(J), y();
+ },
+ get connectable() {
+ return m();
+ },
+ set connectable(J) {
+ m(J), y();
+ },
+ get deletable() {
+ return _();
+ },
+ set deletable(J) {
+ _(J), y();
+ },
+ get hidden() {
+ return v();
+ },
+ set hidden(J) {
+ v(J), y();
+ },
+ get dragging() {
+ return b();
+ },
+ set dragging(J) {
+ b(J), y();
+ },
+ get resizeObserver() {
+ return N();
+ },
+ set resizeObserver(J) {
+ N(J), y();
+ },
+ get style() {
+ return E();
+ },
+ set style(J) {
+ E(J), y();
+ },
+ get type() {
+ return M();
+ },
+ set type(J) {
+ M(J), y();
+ },
+ get isParent() {
+ return D();
+ },
+ set isParent(J) {
+ D(J), y();
+ },
+ get positionX() {
+ return V();
+ },
+ set positionX(J) {
+ V(J), y();
+ },
+ get positionY() {
+ return A();
+ },
+ set positionY(J) {
+ A(J), y();
+ },
+ get sourcePosition() {
+ return O();
+ },
+ set sourcePosition(J) {
+ O(J), y();
+ },
+ get targetPosition() {
+ return R();
+ },
+ set targetPosition(J) {
+ R(J), y();
+ },
+ get zIndex() {
+ return S();
+ },
+ set zIndex(J) {
+ S(J), y();
+ },
+ get measuredWidth() {
+ return T();
+ },
+ set measuredWidth(J) {
+ T(J), y();
+ },
+ get measuredHeight() {
+ return k();
+ },
+ set measuredHeight(J) {
+ k(J), y();
+ },
+ get initialWidth() {
+ return P();
+ },
+ set initialWidth(J) {
+ P(J), y();
+ },
+ get initialHeight() {
+ return H();
+ },
+ set initialHeight(J) {
+ H(J), y();
+ },
+ get width() {
+ return I();
+ },
+ set width(J) {
+ I(J), y();
+ },
+ get height() {
+ return B();
+ },
+ set height(J) {
+ B(J), y();
+ },
+ get dragHandle() {
+ return F();
+ },
+ set dragHandle(J) {
+ F(J), y();
+ },
+ get initialized() {
+ return K();
+ },
+ set initialized(J) {
+ K(J), y();
+ },
+ get parentId() {
+ return ie();
+ },
+ set parentId(J) {
+ ie(J), y();
+ },
+ get nodeClickDistance() {
+ return ee();
+ },
+ set nodeClickDistance(J) {
+ ee(J), y();
+ },
+ get class() {
+ return W();
+ },
+ set class(J) {
+ W(J), y();
+ }
+ });
+ return r(), lt;
+}
+ae(
+ Vc,
+ {
+ node: {},
+ id: {},
+ data: {},
+ selected: {},
+ draggable: {},
+ selectable: {},
+ connectable: {},
+ deletable: {},
+ hidden: {},
+ dragging: {},
+ resizeObserver: {},
+ style: {},
+ type: {},
+ isParent: {},
+ positionX: {},
+ positionY: {},
+ sourcePosition: {},
+ targetPosition: {},
+ zIndex: {},
+ measuredWidth: {},
+ measuredHeight: {},
+ initialWidth: {},
+ initialHeight: {},
+ width: {},
+ height: {},
+ dragHandle: {},
+ initialized: {},
+ parentId: {},
+ nodeClickDistance: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+var P2 = /* @__PURE__ */ ne('<div class="svelte-flow__nodes svelte-tf4uy4"></div>');
+const N2 = {
+ hash: "svelte-tf4uy4",
+ code: ".svelte-flow__nodes.svelte-tf4uy4 {width:100%;height:100%;position:absolute;left:0;top:0;}"
+};
+function Dc(e, t) {
+ de(t, !1), Je(e, N2);
+ const [n, r] = tt(), o = () => Q(c, "$visibleNodes", n), i = () => Q(f, "$nodesDraggable", n), s = () => Q(g, "$elementsSelectable", n), a = () => Q(d, "$nodesConnectable", n), l = () => Q(x, "$parentLookup", n);
+ let u = w(t, "nodeClickDistance", 12, 0);
+ const {
+ visibleNodes: c,
+ nodesDraggable: f,
+ nodesConnectable: d,
+ elementsSelectable: g,
+ updateNodeInternals: p,
+ parentLookup: x
+ } = Ue(), C = typeof ResizeObserver > "u" ? null : new ResizeObserver((_) => {
+ const v = /* @__PURE__ */ new Map();
+ _.forEach((b) => {
+ const N = b.target.getAttribute("data-id");
+ v.set(N, { id: N, nodeElement: b.target, force: !0 });
+ }), p(v);
+ });
+ Qs(() => {
+ C == null || C.disconnect();
+ }), He();
+ var $ = P2();
+ Yt($, 5, o, (_) => _.id, (_, v) => {
+ const b = /* @__PURE__ */ pe(() => !!h(v).selected), N = /* @__PURE__ */ pe(() => !!h(v).hidden), E = /* @__PURE__ */ pe(() => !!(h(v).draggable || i() && typeof h(v).draggable > "u")), M = /* @__PURE__ */ pe(() => !!(h(v).selectable || s() && typeof h(v).selectable > "u")), D = /* @__PURE__ */ pe(() => !!(h(v).connectable || a() && typeof h(v).connectable > "u")), V = /* @__PURE__ */ pe(() => h(v).deletable ?? !0), A = /* @__PURE__ */ pe(() => l().has(h(v).id)), O = /* @__PURE__ */ pe(() => h(v).type ?? "default"), R = /* @__PURE__ */ pe(() => h(v).internals.z ?? 0), S = /* @__PURE__ */ pe(() => oc(h(v)));
+ Vc(_, {
+ get node() {
+ return h(v);
+ },
+ get id() {
+ return h(v).id;
+ },
+ get data() {
+ return h(v).data;
+ },
+ get selected() {
+ return h(b);
+ },
+ get hidden() {
+ return h(N);
+ },
+ get draggable() {
+ return h(E);
+ },
+ get selectable() {
+ return h(M);
+ },
+ get connectable() {
+ return h(D);
+ },
+ get deletable() {
+ return h(V);
+ },
+ get positionX() {
+ return h(v).internals.positionAbsolute.x;
+ },
+ get positionY() {
+ return h(v).internals.positionAbsolute.y;
+ },
+ get isParent() {
+ return h(A);
+ },
+ get style() {
+ return h(v).style;
+ },
+ get class() {
+ return h(v).class;
+ },
+ get type() {
+ return h(O);
+ },
+ get sourcePosition() {
+ return h(v).sourcePosition;
+ },
+ get targetPosition() {
+ return h(v).targetPosition;
+ },
+ get dragging() {
+ return h(v).dragging;
+ },
+ get zIndex() {
+ return h(R);
+ },
+ get dragHandle() {
+ return h(v).dragHandle;
+ },
+ get initialized() {
+ return h(S);
+ },
+ get width() {
+ return h(v).width;
+ },
+ get height() {
+ return h(v).height;
+ },
+ get initialWidth() {
+ return h(v).initialWidth;
+ },
+ get initialHeight() {
+ return h(v).initialHeight;
+ },
+ get measuredWidth() {
+ return h(v).measured.width;
+ },
+ get measuredHeight() {
+ return h(v).measured.height;
+ },
+ get parentId() {
+ return h(v).parentId;
+ },
+ resizeObserver: C,
+ get nodeClickDistance() {
+ return u();
+ },
+ $$events: {
+ nodeclick(T) {
+ Ve.call(this, t, T);
+ },
+ nodemouseenter(T) {
+ Ve.call(this, t, T);
+ },
+ nodemousemove(T) {
+ Ve.call(this, t, T);
+ },
+ nodemouseleave(T) {
+ Ve.call(this, t, T);
+ },
+ nodedrag(T) {
+ Ve.call(this, t, T);
+ },
+ nodedragstart(T) {
+ Ve.call(this, t, T);
+ },
+ nodedragstop(T) {
+ Ve.call(this, t, T);
+ },
+ nodecontextmenu(T) {
+ Ve.call(this, t, T);
+ }
+ }
+ });
+ }), Z($), L(e, $);
+ var m = fe({
+ get nodeClickDistance() {
+ return u();
+ },
+ set nodeClickDistance(_) {
+ u(_), y();
+ }
+ });
+ return r(), m;
+}
+ae(Dc, { nodeClickDistance: {} }, [], [], !0);
+var M2 = /* @__PURE__ */ _e('<svg><g role="img"><!></g></svg>');
+function Ac(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(W, "$edgeTypes", n), i = () => Q(ue, "$flowId", n), s = () => Q(me, "$elementsSelectable", n), a = () => Q(ee, "$edgeLookup", n), l = re(void 0, !0), u = re(void 0, !0), c = re(void 0, !0), f = re(void 0, !0), d = re(void 0, !0);
+ let g = w(t, "id", 13), p = w(t, "type", 13, "default"), x = w(t, "source", 13, ""), C = w(t, "target", 13, ""), $ = w(t, "data", 29, () => ({})), m = w(t, "style", 13, void 0), _ = w(t, "zIndex", 13, void 0), v = w(t, "animated", 13, !1), b = w(t, "selected", 13, !1), N = w(t, "selectable", 13, void 0), E = w(t, "deletable", 13, void 0), M = w(t, "hidden", 13, !1), D = w(t, "label", 13, void 0), V = w(t, "labelStyle", 13, void 0), A = w(t, "markerStart", 13, void 0), O = w(t, "markerEnd", 13, void 0), R = w(t, "sourceHandle", 13, void 0), S = w(t, "targetHandle", 13, void 0), T = w(t, "sourceX", 13), k = w(t, "sourceY", 13), P = w(t, "targetX", 13), H = w(t, "targetY", 13), I = w(t, "sourcePosition", 13), B = w(t, "targetPosition", 13), F = w(t, "ariaLabel", 13, void 0), K = w(t, "interactionWidth", 13, void 0), ie = w(t, "class", 13, "");
+ Tr("svelteflow__edge_id", g());
+ const {
+ edgeLookup: ee,
+ edgeTypes: W,
+ flowId: ue,
+ elementsSelectable: me
+ } = Ue(), Ce = Oi(), ge = bc();
+ function ze(te) {
+ const Fe = a().get(g());
+ Fe && (ge(g()), Ce("edgeclick", { event: te, edge: Fe }));
+ }
+ function G(te, Fe) {
+ const Le = a().get(g());
+ Le && Ce(Fe, { event: te, edge: Le });
+ }
+ he(() => j(p()), () => {
+ U(l, p() || "default");
+ }), he(
+ () => (o(), h(l), xi),
+ () => {
+ U(u, o()[h(l)] || xi);
+ }
+ ), he(
+ () => (j(A()), i()),
+ () => {
+ U(c, A() ? `url('#${Vs(A(), i())}')` : void 0);
+ }
+ ), he(
+ () => (j(O()), i()),
+ () => {
+ U(f, O() ? `url('#${Vs(O(), i())}')` : void 0);
+ }
+ ), he(
+ () => (j(N()), s()),
+ () => {
+ U(d, N() ?? s());
+ }
+ ), gt(), He(!0);
+ var se = et(), Te = be(se);
+ {
+ var Ae = (te) => {
+ var Fe = M2(), Le = X(Fe);
+ let Qe;
+ var oe = X(Le);
+ const ve = /* @__PURE__ */ pe(() => E() ?? !0);
+ yu(oe, () => h(u), (xe, Oe) => {
+ Oe(xe, {
+ get id() {
+ return g();
+ },
+ get source() {
+ return x();
+ },
+ get target() {
+ return C();
+ },
+ get sourceX() {
+ return T();
+ },
+ get sourceY() {
+ return k();
+ },
+ get targetX() {
+ return P();
+ },
+ get targetY() {
+ return H();
+ },
+ get sourcePosition() {
+ return I();
+ },
+ get targetPosition() {
+ return B();
+ },
+ get animated() {
+ return v();
+ },
+ get selected() {
+ return b();
+ },
+ get label() {
+ return D();
+ },
+ get labelStyle() {
+ return V();
+ },
+ get data() {
+ return $();
+ },
+ get style() {
+ return m();
+ },
+ get interactionWidth() {
+ return K();
+ },
+ get selectable() {
+ return h(d);
+ },
+ get deletable() {
+ return h(ve);
+ },
+ get type() {
+ return h(l);
+ },
+ get sourceHandleId() {
+ return R();
+ },
+ get targetHandleId() {
+ return S();
+ },
+ get markerStart() {
+ return h(c);
+ },
+ get markerEnd() {
+ return h(f);
+ }
+ });
+ }), Z(Le), Z(Fe), Ee(
+ (xe) => {
+ st(Fe, "z-index", _()), Qe = kt(Le, 0, bn(xe), null, Qe, {
+ animated: v(),
+ selected: b(),
+ selectable: h(d)
+ }), ce(Le, "data-id", g()), ce(Le, "aria-label", F() === null ? void 0 : F() ? F() : `Edge from ${x()} to ${C()}`);
+ },
+ [
+ () => Et(["svelte-flow__edge", ie()])
+ ],
+ pe
+ ), Ye("click", Le, ze), Ye("contextmenu", Le, (xe) => {
+ G(xe, "edgecontextmenu");
+ }), Ye("mouseenter", Le, (xe) => {
+ G(xe, "edgemouseenter");
+ }), Ye("mouseleave", Le, (xe) => {
+ G(xe, "edgemouseleave");
+ }), L(te, Fe);
+ };
+ ke(Te, (te) => {
+ M() || te(Ae);
+ });
+ }
+ L(e, se);
+ var Xe = fe({
+ get id() {
+ return g();
+ },
+ set id(te) {
+ g(te), y();
+ },
+ get type() {
+ return p();
+ },
+ set type(te) {
+ p(te), y();
+ },
+ get source() {
+ return x();
+ },
+ set source(te) {
+ x(te), y();
+ },
+ get target() {
+ return C();
+ },
+ set target(te) {
+ C(te), y();
+ },
+ get data() {
+ return $();
+ },
+ set data(te) {
+ $(te), y();
+ },
+ get style() {
+ return m();
+ },
+ set style(te) {
+ m(te), y();
+ },
+ get zIndex() {
+ return _();
+ },
+ set zIndex(te) {
+ _(te), y();
+ },
+ get animated() {
+ return v();
+ },
+ set animated(te) {
+ v(te), y();
+ },
+ get selected() {
+ return b();
+ },
+ set selected(te) {
+ b(te), y();
+ },
+ get selectable() {
+ return N();
+ },
+ set selectable(te) {
+ N(te), y();
+ },
+ get deletable() {
+ return E();
+ },
+ set deletable(te) {
+ E(te), y();
+ },
+ get hidden() {
+ return M();
+ },
+ set hidden(te) {
+ M(te), y();
+ },
+ get label() {
+ return D();
+ },
+ set label(te) {
+ D(te), y();
+ },
+ get labelStyle() {
+ return V();
+ },
+ set labelStyle(te) {
+ V(te), y();
+ },
+ get markerStart() {
+ return A();
+ },
+ set markerStart(te) {
+ A(te), y();
+ },
+ get markerEnd() {
+ return O();
+ },
+ set markerEnd(te) {
+ O(te), y();
+ },
+ get sourceHandle() {
+ return R();
+ },
+ set sourceHandle(te) {
+ R(te), y();
+ },
+ get targetHandle() {
+ return S();
+ },
+ set targetHandle(te) {
+ S(te), y();
+ },
+ get sourceX() {
+ return T();
+ },
+ set sourceX(te) {
+ T(te), y();
+ },
+ get sourceY() {
+ return k();
+ },
+ set sourceY(te) {
+ k(te), y();
+ },
+ get targetX() {
+ return P();
+ },
+ set targetX(te) {
+ P(te), y();
+ },
+ get targetY() {
+ return H();
+ },
+ set targetY(te) {
+ H(te), y();
+ },
+ get sourcePosition() {
+ return I();
+ },
+ set sourcePosition(te) {
+ I(te), y();
+ },
+ get targetPosition() {
+ return B();
+ },
+ set targetPosition(te) {
+ B(te), y();
+ },
+ get ariaLabel() {
+ return F();
+ },
+ set ariaLabel(te) {
+ F(te), y();
+ },
+ get interactionWidth() {
+ return K();
+ },
+ set interactionWidth(te) {
+ K(te), y();
+ },
+ get class() {
+ return ie();
+ },
+ set class(te) {
+ ie(te), y();
+ }
+ });
+ return r(), Xe;
+}
+ae(
+ Ac,
+ {
+ id: {},
+ type: {},
+ source: {},
+ target: {},
+ data: {},
+ style: {},
+ zIndex: {},
+ animated: {},
+ selected: {},
+ selectable: {},
+ deletable: {},
+ hidden: {},
+ label: {},
+ labelStyle: {},
+ markerStart: {},
+ markerEnd: {},
+ sourceHandle: {},
+ targetHandle: {},
+ sourceX: {},
+ sourceY: {},
+ targetX: {},
+ targetY: {},
+ sourcePosition: {},
+ targetPosition: {},
+ ariaLabel: {},
+ interactionWidth: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+function Lc(e, t) {
+ de(t, !1);
+ let n = w(t, "onMount", 12, void 0), r = w(t, "onDestroy", 12, void 0);
+ return un(() => {
+ var o;
+ return (o = n()) == null || o(), r();
+ }), He(), fe({
+ get onMount() {
+ return n();
+ },
+ set onMount(o) {
+ n(o), y();
+ },
+ get onDestroy() {
+ return r();
+ },
+ set onDestroy(o) {
+ r(o), y();
+ }
+ });
+}
+ae(Lc, { onMount: {}, onDestroy: {} }, [], [], !0);
+var T2 = /* @__PURE__ */ _e("<defs></defs>");
+function Oc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(i, "$markers", n), { markers: i } = Ue();
+ He();
+ var s = T2();
+ Yt(s, 5, o, (a) => a.id, (a, l) => {
+ Ic(a, ut(() => h(l)));
+ }), Z(s), L(e, s), fe(), r();
+}
+ae(Oc, {}, [], [], !0);
+var H2 = /* @__PURE__ */ _e('<polyline stroke-linecap="round" stroke-linejoin="round" fill="none" points="-5,-4 0,0 -5,4"></polyline>'), V2 = /* @__PURE__ */ _e('<polyline stroke-linecap="round" stroke-linejoin="round" points="-5,-4 0,0 -5,4 -5,-4"></polyline>'), D2 = /* @__PURE__ */ _e('<marker class="svelte-flow__arrowhead" viewBox="-10 -10 20 20" refX="0" refY="0"><!></marker>');
+function Ic(e, t) {
+ de(t, !1);
+ let n = w(t, "id", 12), r = w(t, "type", 12), o = w(t, "width", 12, 12.5), i = w(t, "height", 12, 12.5), s = w(t, "markerUnits", 12, "strokeWidth"), a = w(t, "orient", 12, "auto-start-reverse"), l = w(t, "color", 12, void 0), u = w(t, "strokeWidth", 12, void 0);
+ He();
+ var c = D2(), f = X(c);
+ {
+ var d = (p) => {
+ var x = H2();
+ Ee(() => {
+ ce(x, "stroke", l()), ce(x, "stroke-width", u());
+ }), L(p, x);
+ }, g = (p, x) => {
+ {
+ var C = ($) => {
+ var m = V2();
+ Ee(() => {
+ ce(m, "stroke", l()), ce(m, "stroke-width", u()), ce(m, "fill", l());
+ }), L($, m);
+ };
+ ke(
+ p,
+ ($) => {
+ r() === mo.ArrowClosed && $(C);
+ },
+ x
+ );
+ }
+ };
+ ke(f, (p) => {
+ r() === mo.Arrow ? p(d) : p(g, !1);
+ });
+ }
+ return Z(c), Ee(() => {
+ ce(c, "id", n()), ce(c, "markerWidth", `${o()}`), ce(c, "markerHeight", `${i()}`), ce(c, "markerUnits", s()), ce(c, "orient", a());
+ }), L(e, c), fe({
+ get id() {
+ return n();
+ },
+ set id(p) {
+ n(p), y();
+ },
+ get type() {
+ return r();
+ },
+ set type(p) {
+ r(p), y();
+ },
+ get width() {
+ return o();
+ },
+ set width(p) {
+ o(p), y();
+ },
+ get height() {
+ return i();
+ },
+ set height(p) {
+ i(p), y();
+ },
+ get markerUnits() {
+ return s();
+ },
+ set markerUnits(p) {
+ s(p), y();
+ },
+ get orient() {
+ return a();
+ },
+ set orient(p) {
+ a(p), y();
+ },
+ get color() {
+ return l();
+ },
+ set color(p) {
+ l(p), y();
+ },
+ get strokeWidth() {
+ return u();
+ },
+ set strokeWidth(p) {
+ u(p), y();
+ }
+ });
+}
+ae(
+ Ic,
+ {
+ id: {},
+ type: {},
+ width: {},
+ height: {},
+ markerUnits: {},
+ orient: {},
+ color: {},
+ strokeWidth: {}
+ },
+ [],
+ [],
+ !0
+);
+var A2 = /* @__PURE__ */ ne('<div class="svelte-flow__edges"><svg class="svelte-flow__marker"><!></svg> <!> <!></div>');
+function zc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(a, "$visibleEdges", n), i = () => Q(c, "$elementsSelectable", n);
+ let s = w(t, "defaultEdgeOptions", 12);
+ const {
+ visibleEdges: a,
+ edgesInitialized: l,
+ edges: { setDefaultOptions: u },
+ elementsSelectable: c
+ } = Ue();
+ un(() => {
+ s() && u(s());
+ }), He();
+ var f = A2(), d = X(f), g = X(d);
+ Oc(g, {}), Z(d);
+ var p = z(d, 2);
+ Yt(p, 1, o, (m) => m.id, (m, _) => {
+ const v = /* @__PURE__ */ pe(() => h(_).selectable ?? i()), b = /* @__PURE__ */ pe(() => h(_).type || "default");
+ Ac(m, {
+ get id() {
+ return h(_).id;
+ },
+ get source() {
+ return h(_).source;
+ },
+ get target() {
+ return h(_).target;
+ },
+ get data() {
+ return h(_).data;
+ },
+ get style() {
+ return h(_).style;
+ },
+ get animated() {
+ return h(_).animated;
+ },
+ get selected() {
+ return h(_).selected;
+ },
+ get selectable() {
+ return h(v);
+ },
+ get deletable() {
+ return h(_).deletable;
+ },
+ get hidden() {
+ return h(_).hidden;
+ },
+ get label() {
+ return h(_).label;
+ },
+ get labelStyle() {
+ return h(_).labelStyle;
+ },
+ get markerStart() {
+ return h(_).markerStart;
+ },
+ get markerEnd() {
+ return h(_).markerEnd;
+ },
+ get sourceHandle() {
+ return h(_).sourceHandle;
+ },
+ get targetHandle() {
+ return h(_).targetHandle;
+ },
+ get sourceX() {
+ return h(_).sourceX;
+ },
+ get sourceY() {
+ return h(_).sourceY;
+ },
+ get targetX() {
+ return h(_).targetX;
+ },
+ get targetY() {
+ return h(_).targetY;
+ },
+ get sourcePosition() {
+ return h(_).sourcePosition;
+ },
+ get targetPosition() {
+ return h(_).targetPosition;
+ },
+ get ariaLabel() {
+ return h(_).ariaLabel;
+ },
+ get interactionWidth() {
+ return h(_).interactionWidth;
+ },
+ get class() {
+ return h(_).class;
+ },
+ get type() {
+ return h(b);
+ },
+ get zIndex() {
+ return h(_).zIndex;
+ },
+ $$events: {
+ edgeclick(N) {
+ Ve.call(this, t, N);
+ },
+ edgecontextmenu(N) {
+ Ve.call(this, t, N);
+ },
+ edgemouseenter(N) {
+ Ve.call(this, t, N);
+ },
+ edgemouseleave(N) {
+ Ve.call(this, t, N);
+ }
+ }
+ });
+ });
+ var x = z(p, 2);
+ {
+ var C = (m) => {
+ Lc(m, {
+ onMount: () => {
+ li(l, !0);
+ },
+ onDestroy: () => {
+ li(l, !1);
+ }
+ });
+ };
+ ke(x, (m) => {
+ o().length > 0 && m(C);
+ });
+ }
+ Z(f), L(e, f);
+ var $ = fe({
+ get defaultEdgeOptions() {
+ return s();
+ },
+ set defaultEdgeOptions(m) {
+ s(m), y();
+ }
+ });
+ return r(), $;
+}
+ae(zc, { defaultEdgeOptions: {} }, [], [], !0);
+var L2 = /* @__PURE__ */ ne('<div class="svelte-flow__selection svelte-1iugwpu"></div>');
+const O2 = {
+ hash: "svelte-1iugwpu",
+ code: ".svelte-flow__selection.svelte-1iugwpu {position:absolute;top:0;left:0;}"
+};
+function ha(e, t) {
+ de(t, !1), Je(e, O2);
+ let n = w(t, "x", 12, 0), r = w(t, "y", 12, 0), o = w(t, "width", 12, 0), i = w(t, "height", 12, 0), s = w(t, "isVisible", 12, !0);
+ var a = et(), l = be(a);
+ {
+ var u = (c) => {
+ var f = L2();
+ Ee(() => {
+ st(f, "width", typeof o() == "string" ? o() : `${o()}px`), st(f, "height", typeof i() == "string" ? i() : `${i()}px`), st(f, "transform", `translate(${n()}px, ${r()}px)`);
+ }), L(c, f);
+ };
+ ke(l, (c) => {
+ s() && c(u);
+ });
+ }
+ return L(e, a), fe({
+ get x() {
+ return n();
+ },
+ set x(c) {
+ n(c), y();
+ },
+ get y() {
+ return r();
+ },
+ set y(c) {
+ r(c), y();
+ },
+ get width() {
+ return o();
+ },
+ set width(c) {
+ o(c), y();
+ },
+ get height() {
+ return i();
+ },
+ set height(c) {
+ i(c), y();
+ },
+ get isVisible() {
+ return s();
+ },
+ set isVisible(c) {
+ s(c), y();
+ }
+ });
+}
+ae(
+ ha,
+ {
+ x: {},
+ y: {},
+ width: {},
+ height: {},
+ isVisible: {}
+ },
+ [],
+ [],
+ !0
+);
+function Rc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(s, "$selectionRect", n), i = () => Q(a, "$selectionRectMode", n), { selectionRect: s, selectionRectMode: a } = Ue();
+ He();
+ const l = /* @__PURE__ */ pe(() => !!(o() && i() === "user")), u = /* @__PURE__ */ pe(() => {
+ var g;
+ return (g = o()) == null ? void 0 : g.width;
+ }), c = /* @__PURE__ */ pe(() => {
+ var g;
+ return (g = o()) == null ? void 0 : g.height;
+ }), f = /* @__PURE__ */ pe(() => {
+ var g;
+ return (g = o()) == null ? void 0 : g.x;
+ }), d = /* @__PURE__ */ pe(() => {
+ var g;
+ return (g = o()) == null ? void 0 : g.y;
+ });
+ ha(e, {
+ get isVisible() {
+ return h(l);
+ },
+ get width() {
+ return h(u);
+ },
+ get height() {
+ return h(c);
+ },
+ get x() {
+ return h(f);
+ },
+ get y() {
+ return h(d);
+ }
+ }), fe(), r();
+}
+ae(Rc, {}, [], [], !0);
+var I2 = /* @__PURE__ */ ne('<div class="selection-wrapper nopan svelte-5pxri" role="button" tabindex="-1"><!></div>');
+const z2 = {
+ hash: "svelte-5pxri",
+ code: ".selection-wrapper.svelte-5pxri {position:absolute;top:0;left:0;z-index:7;pointer-events:all;}"
+};
+function Bc(e, t) {
+ de(t, !1), Je(e, z2);
+ const [n, r] = tt(), o = () => Q(l, "$selectionRectMode", n), i = () => Q(c, "$nodeLookup", n), s = () => Q(u, "$nodes", n), a = Ue(), { selectionRectMode: l, nodes: u, nodeLookup: c } = a, f = Oi();
+ let d = re(null);
+ function g(m) {
+ const _ = s().filter((v) => v.selected);
+ f("selectioncontextmenu", { nodes: _, event: m });
+ }
+ function p(m) {
+ const _ = s().filter((v) => v.selected);
+ f("selectionclick", { nodes: _, event: m });
+ }
+ he(
+ () => (o(), i(), s()),
+ () => {
+ o() === "nodes" && (U(d, No(i(), { filter: (m) => !!m.selected })), s());
+ }
+ ), gt(), He();
+ var x = et(), C = be(x);
+ {
+ var $ = (m) => {
+ var _ = I2(), v = X(_);
+ ha(v, { width: "100%", height: "100%", x: 0, y: 0 }), Z(_), vt(_, (b, N) => $r == null ? void 0 : $r(b, N), () => ({
+ disabled: !1,
+ store: a,
+ onDrag: (b, N, E, M) => {
+ f("nodedrag", { event: b, targetNode: null, nodes: M });
+ },
+ onDragStart: (b, N, E, M) => {
+ f("nodedragstart", { event: b, targetNode: null, nodes: M });
+ },
+ onDragStop: (b, N, E, M) => {
+ f("nodedragstop", { event: b, targetNode: null, nodes: M });
+ }
+ })), Ot(() => Ye("contextmenu", _, g)), Ot(() => Ye("click", _, p)), Ot(() => Ye("keyup", _, () => {
+ })), Ee(() => ce(_, "style", `width: ${h(d).width ?? ""}px; height: ${h(d).height ?? ""}px; transform: translate(${h(d).x ?? ""}px, ${h(d).y ?? ""}px)`)), L(m, _);
+ };
+ ke(C, (m) => {
+ o() === "nodes" && h(d) && Nn(h(d).x) && Nn(h(d).y) && m($);
+ });
+ }
+ L(e, x), fe(), r();
+}
+ae(Bc, {}, [], [], !0);
+function We(e, t) {
+ let { enabled: n = !0, trigger: r, type: o = "keydown" } = t;
+ function i(s) {
+ const a = Array.isArray(r) ? r : [r], l = {
+ alt: s.altKey,
+ ctrl: s.ctrlKey,
+ shift: s.shiftKey,
+ meta: s.metaKey
+ };
+ for (const u of a) {
+ const c = {
+ modifier: [],
+ preventDefault: !1,
+ enabled: !0,
+ ...u
+ }, { modifier: f, key: d, callback: g, preventDefault: p, enabled: x } = c;
+ if (x) {
+ if (f.length && !(Array.isArray(f) ? f : [f]).map(
+ (m) => typeof m == "string" ? [m] : m
+ ).some(
+ (m) => m.every((_) => l[_])
+ ))
+ continue;
+ if (s.key === d) {
+ p && s.preventDefault();
+ const C = {
+ node: e,
+ trigger: c,
+ originalEvent: s
+ };
+ e.dispatchEvent(new CustomEvent("shortcut", { detail: C })), g == null || g(C);
+ }
+ }
+ }
+ }
+ return n && e.addEventListener(o, i), {
+ update: (s) => {
+ const { enabled: a = !0, type: l = "keydown" } = s;
+ n && (!a || o !== l) ? e.removeEventListener(o, i) : !n && a && e.addEventListener(l, i), n = a, o = l, r = s.trigger;
+ },
+ destroy: () => {
+ e.removeEventListener(o, i);
+ }
+ };
+}
+function Yc(e, t) {
+ de(t, !1);
+ let n = w(t, "selectionKey", 12, "Shift"), r = w(t, "multiSelectionKey", 28, () => yi() ? "Meta" : "Control"), o = w(t, "deleteKey", 12, "Backspace"), i = w(t, "panActivationKey", 12, " "), s = w(t, "zoomActivationKey", 28, () => yi() ? "Meta" : "Control");
+ const {
+ selectionKeyPressed: a,
+ multiselectionKeyPressed: l,
+ deleteKeyPressed: u,
+ panActivationKeyPressed: c,
+ zoomActivationKeyPressed: f,
+ selectionRect: d
+ } = Ue();
+ function g(m) {
+ return m !== null && typeof m == "object";
+ }
+ function p(m) {
+ return g(m) ? m.modifier || [] : [];
+ }
+ function x(m) {
+ return m == null ? "" : g(m) ? m.key : m;
+ }
+ function C(m, _) {
+ return (Array.isArray(m) ? m : [m]).map((b) => {
+ const N = x(b);
+ return {
+ key: N,
+ modifier: p(b),
+ enabled: N !== null,
+ callback: _
+ };
+ });
+ }
+ function $() {
+ d.set(null), a.set(!1), l.set(!1), u.set(!1), c.set(!1), f.set(!1);
+ }
+ return He(), Ye("blur", Nt, $), Ye("contextmenu", Nt, $), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(n(), () => a.set(!0)),
+ type: "keydown"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(n(), () => a.set(!1)),
+ type: "keyup"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(r(), () => l.set(!0)),
+ type: "keydown"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(r(), () => l.set(!1)),
+ type: "keyup"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(o(), (m) => {
+ !(m.originalEvent.ctrlKey || m.originalEvent.metaKey || m.originalEvent.shiftKey) && !w0(m.originalEvent) && u.set(!0);
+ }),
+ type: "keydown"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(o(), () => u.set(!1)),
+ type: "keyup"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(i(), () => c.set(!0)),
+ type: "keydown"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(i(), () => c.set(!1)),
+ type: "keyup"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(s(), () => f.set(!0)),
+ type: "keydown"
+ })), vt(Nt, (m, _) => We == null ? void 0 : We(m, _), () => ({
+ trigger: C(s(), () => f.set(!1)),
+ type: "keyup"
+ })), fe({
+ get selectionKey() {
+ return n();
+ },
+ set selectionKey(m) {
+ n(m), y();
+ },
+ get multiSelectionKey() {
+ return r();
+ },
+ set multiSelectionKey(m) {
+ r(m), y();
+ },
+ get deleteKey() {
+ return o();
+ },
+ set deleteKey(m) {
+ o(m), y();
+ },
+ get panActivationKey() {
+ return i();
+ },
+ set panActivationKey(m) {
+ i(m), y();
+ },
+ get zoomActivationKey() {
+ return s();
+ },
+ set zoomActivationKey(m) {
+ s(m), y();
+ }
+ });
+}
+ae(
+ Yc,
+ {
+ selectionKey: {},
+ multiSelectionKey: {},
+ deleteKey: {},
+ panActivationKey: {},
+ zoomActivationKey: {}
+ },
+ [],
+ [],
+ !0
+);
+var R2 = /* @__PURE__ */ _e('<path fill="none" class="svelte-flow__connection-path"></path>'), B2 = /* @__PURE__ */ _e('<svg class="svelte-flow__connectionline"><g><!><!></g></svg>');
+function Zc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(g, "$connection", n), i = () => Q(p, "$connectionLineType", n), s = () => Q(f, "$width", n), a = () => Q(d, "$height", n);
+ let l = w(t, "containerStyle", 12, ""), u = w(t, "style", 12, ""), c = w(t, "isCustomComponent", 12, !1);
+ const {
+ width: f,
+ height: d,
+ connection: g,
+ connectionLineType: p
+ } = Ue();
+ let x = re(null);
+ he(
+ () => (o(), j(c()), i(), h(x), Hs),
+ () => {
+ if (o().inProgress && !c()) {
+ const { from: v, to: b, fromPosition: N, toPosition: E } = o(), M = {
+ sourceX: v.x,
+ sourceY: v.y,
+ sourcePosition: N,
+ targetX: b.x,
+ targetY: b.y,
+ targetPosition: E
+ };
+ switch (i()) {
+ case Cr.Bezier:
+ ((D) => U(x, D[0]))(sc(M));
+ break;
+ case Cr.Step:
+ ((D) => U(x, D[0]))(wi({ ...M, borderRadius: 0 }));
+ break;
+ case Cr.SmoothStep:
+ ((D) => U(x, D[0]))(wi(M));
+ break;
+ default:
+ ((D) => U(x, D[0]))(Hs(M));
+ }
+ }
+ }
+ ), gt(), He();
+ var C = et(), $ = be(C);
+ {
+ var m = (v) => {
+ var b = B2(), N = X(b), E = X(N);
+ pt(E, t, "connectionLine", {});
+ var M = z(E);
+ {
+ var D = (V) => {
+ var A = R2();
+ Ee(() => {
+ ce(A, "d", h(x)), ce(A, "style", u());
+ }), L(V, A);
+ };
+ ke(M, (V) => {
+ c() || V(D);
+ });
+ }
+ Z(N), Z(b), Ee(
+ (V) => {
+ ce(b, "width", s()), ce(b, "height", a()), ce(b, "style", l()), kt(N, 0, bn(V));
+ },
+ [
+ () => Et([
+ "svelte-flow__connection",
+ c0(o().isValid)
+ ])
+ ],
+ pe
+ ), L(v, b);
+ };
+ ke($, (v) => {
+ o().inProgress && v(m);
+ });
+ }
+ L(e, C);
+ var _ = fe({
+ get containerStyle() {
+ return l();
+ },
+ set containerStyle(v) {
+ l(v), y();
+ },
+ get style() {
+ return u();
+ },
+ set style(v) {
+ u(v), y();
+ },
+ get isCustomComponent() {
+ return c();
+ },
+ set isCustomComponent(v) {
+ c(v), y();
+ }
+ });
+ return r(), _;
+}
+ae(
+ Zc,
+ {
+ containerStyle: {},
+ style: {},
+ isCustomComponent: {}
+ },
+ ["connectionLine"],
+ [],
+ !0
+);
+var Y2 = /* @__PURE__ */ ne("<div><!></div>");
+function Ho(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]), r = nt(n, ["position", "style", "class"]);
+ de(t, !1);
+ const [o, i] = tt(), s = () => Q(f, "$selectionRectMode", o), a = re();
+ let l = w(t, "position", 12, "top-right"), u = w(t, "style", 12, void 0), c = w(t, "class", 12, void 0);
+ const { selectionRectMode: f } = Ue();
+ he(() => j(l()), () => {
+ U(a, `${l()}`.split("-"));
+ }), gt(), He();
+ var d = Y2();
+ let g;
+ var p = X(d);
+ pt(p, t, "default", {}), Z(d), Ee(
+ (C) => {
+ g = on(d, g, {
+ class: C,
+ style: u(),
+ ...r
+ }), st(d, "pointer-events", s() ? "none" : "");
+ },
+ [
+ () => Et([
+ "svelte-flow__panel",
+ c(),
+ ...h(a)
+ ])
+ ],
+ pe
+ ), L(e, d);
+ var x = fe({
+ get position() {
+ return l();
+ },
+ set position(C) {
+ l(C), y();
+ },
+ get style() {
+ return u();
+ },
+ set style(C) {
+ u(C), y();
+ },
+ get class() {
+ return c();
+ },
+ set class(C) {
+ c(C), y();
+ }
+ });
+ return i(), x;
+}
+ae(Ho, { position: {}, style: {}, class: {} }, ["default"], [], !0);
+var Z2 = /* @__PURE__ */ ne('<a href="https://svelteflow.dev" target="_blank" rel="noopener noreferrer" aria-label="Svelte Flow attribution">Svelte Flow</a>');
+function Xc(e, t) {
+ de(t, !1);
+ let n = w(t, "proOptions", 12, void 0), r = w(t, "position", 12, "bottom-right");
+ He();
+ var o = et(), i = be(o);
+ {
+ var s = (a) => {
+ Ho(a, {
+ get position() {
+ return r();
+ },
+ class: "svelte-flow__attribution",
+ "data-message": "Feel free to remove the attribution or check out how you could support us: https://svelteflow.dev/support-us",
+ children: (l, u) => {
+ var c = Z2();
+ L(l, c);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(i, (a) => {
+ var l;
+ (l = n()) != null && l.hideAttribution || a(s);
+ });
+ }
+ return L(e, o), fe({
+ get proOptions() {
+ return n();
+ },
+ set proOptions(a) {
+ n(a), y();
+ },
+ get position() {
+ return r();
+ },
+ set position(a) {
+ r(a), y();
+ }
+ });
+}
+ae(Xc, { proOptions: {}, position: {} }, [], [], !0);
+function El(e, { nodeTypes: t, edgeTypes: n, minZoom: r, maxZoom: o, translateExtent: i, paneClickDistance: s }) {
+ t !== void 0 && e.setNodeTypes(t), n !== void 0 && e.setEdgeTypes(n), r !== void 0 && e.setMinZoom(r), o !== void 0 && e.setMaxZoom(o), i !== void 0 && e.setTranslateExtent(i), s !== void 0 && e.setPaneClickDistance(s);
+}
+const X2 = (e) => Object.keys(e);
+function Sl(e, t) {
+ X2(t).forEach((n) => {
+ const r = t[n];
+ r !== void 0 && e[n].set(r);
+ });
+}
+function F2() {
+ return typeof window > "u" || !window.matchMedia ? null : window.matchMedia("(prefers-color-scheme: dark)");
+}
+function W2(e = "light") {
+ return Ft("light", (n) => {
+ if (e !== "system") {
+ n(e);
+ return;
+ }
+ const r = F2(), o = () => n(r != null && r.matches ? "dark" : "light");
+ return n(r != null && r.matches ? "dark" : "light"), r == null || r.addEventListener("change", o), () => {
+ r == null || r.removeEventListener("change", o);
+ };
+ });
+}
+var K2 = /* @__PURE__ */ ne('<!> <!> <div class="svelte-flow__edgelabel-renderer"></div> <div class="svelte-flow__viewport-portal"></div> <!> <!>', 1), q2 = /* @__PURE__ */ ne("<!> <!>", 1), G2 = /* @__PURE__ */ ne("<div><!> <!> <!> <!></div>");
+const U2 = {
+ hash: "svelte-12wlba6",
+ code: ".svelte-flow.svelte-12wlba6 {width:100%;height:100%;overflow:hidden;position:relative;z-index:0;background-color:var(--background-color, var(--background-color-default));}:root {--background-color-default: #fff;--background-pattern-color-default: #ddd;--minimap-mask-color-default: rgb(240, 240, 240, 0.6);--minimap-mask-stroke-color-default: none;--minimap-mask-stroke-width-default: 1;--controls-button-background-color-default: #fefefe;--controls-button-background-color-hover-default: #f4f4f4;--controls-button-color-default: inherit;--controls-button-color-hover-default: inherit;--controls-button-border-color-default: #eee;}"
+};
+function Fc(e, t) {
+ const n = p1(t), r = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]), o = nt(r, [
+ "id",
+ "nodes",
+ "edges",
+ "fitView",
+ "fitViewOptions",
+ "minZoom",
+ "maxZoom",
+ "initialViewport",
+ "viewport",
+ "nodeTypes",
+ "edgeTypes",
+ "selectionKey",
+ "selectionMode",
+ "panActivationKey",
+ "multiSelectionKey",
+ "zoomActivationKey",
+ "nodesDraggable",
+ "nodesConnectable",
+ "nodeDragThreshold",
+ "elementsSelectable",
+ "snapGrid",
+ "deleteKey",
+ "connectionRadius",
+ "connectionLineType",
+ "connectionMode",
+ "connectionLineStyle",
+ "connectionLineContainerStyle",
+ "onMoveStart",
+ "onMove",
+ "onMoveEnd",
+ "isValidConnection",
+ "translateExtent",
+ "nodeExtent",
+ "onlyRenderVisibleElements",
+ "panOnScrollMode",
+ "preventScrolling",
+ "zoomOnScroll",
+ "zoomOnDoubleClick",
+ "zoomOnPinch",
+ "panOnScroll",
+ "panOnDrag",
+ "selectionOnDrag",
+ "autoPanOnConnect",
+ "autoPanOnNodeDrag",
+ "onerror",
+ "ondelete",
+ "onedgecreate",
+ "attributionPosition",
+ "proOptions",
+ "defaultEdgeOptions",
+ "width",
+ "height",
+ "colorMode",
+ "onconnect",
+ "onconnectstart",
+ "onconnectend",
+ "onbeforedelete",
+ "oninit",
+ "nodeOrigin",
+ "paneClickDistance",
+ "nodeClickDistance",
+ "defaultMarkerColor",
+ "style",
+ "class"
+ ]);
+ de(t, !1), Je(e, U2);
+ const [i, s] = tt(), a = () => Q(_(), "$viewport", i), l = () => Q(ji, "$initialized", i), u = () => Q(h(c), "$colorModeClass", i), c = re();
+ let f = w(t, "id", 12, "1"), d = w(t, "nodes", 12), g = w(t, "edges", 12), p = w(t, "fitView", 12, void 0), x = w(t, "fitViewOptions", 12, void 0), C = w(t, "minZoom", 12, void 0), $ = w(t, "maxZoom", 12, void 0), m = w(t, "initialViewport", 12, void 0), _ = w(t, "viewport", 12, void 0), v = w(t, "nodeTypes", 12, void 0), b = w(t, "edgeTypes", 12, void 0), N = w(t, "selectionKey", 12, void 0), E = w(t, "selectionMode", 12, void 0), M = w(t, "panActivationKey", 12, void 0), D = w(t, "multiSelectionKey", 12, void 0), V = w(t, "zoomActivationKey", 12, void 0), A = w(t, "nodesDraggable", 12, void 0), O = w(t, "nodesConnectable", 12, void 0), R = w(t, "nodeDragThreshold", 12, void 0), S = w(t, "elementsSelectable", 12, void 0), T = w(t, "snapGrid", 12, void 0), k = w(t, "deleteKey", 12, void 0), P = w(t, "connectionRadius", 12, void 0), H = w(t, "connectionLineType", 12, void 0), I = w(t, "connectionMode", 28, () => cr.Strict), B = w(t, "connectionLineStyle", 12, ""), F = w(t, "connectionLineContainerStyle", 12, ""), K = w(t, "onMoveStart", 12, void 0), ie = w(t, "onMove", 12, void 0), ee = w(t, "onMoveEnd", 12, void 0), W = w(t, "isValidConnection", 12, void 0), ue = w(t, "translateExtent", 12, void 0), me = w(t, "nodeExtent", 12, void 0), Ce = w(t, "onlyRenderVisibleElements", 12, void 0), ge = w(t, "panOnScrollMode", 28, () => qn.Free), ze = w(t, "preventScrolling", 12, !0), G = w(t, "zoomOnScroll", 12, !0), se = w(t, "zoomOnDoubleClick", 12, !0), Te = w(t, "zoomOnPinch", 12, !0), Ae = w(t, "panOnScroll", 12, !1), Xe = w(t, "panOnDrag", 12, !0), te = w(t, "selectionOnDrag", 12, void 0), Fe = w(t, "autoPanOnConnect", 12, !0), Le = w(t, "autoPanOnNodeDrag", 12, !0), Qe = w(t, "onerror", 12, void 0), oe = w(t, "ondelete", 12, void 0), ve = w(t, "onedgecreate", 12, void 0), xe = w(t, "attributionPosition", 12, void 0), Oe = w(t, "proOptions", 12, void 0), ct = w(t, "defaultEdgeOptions", 12, void 0), lt = w(t, "width", 12, void 0), J = w(t, "height", 12, void 0), Re = w(t, "colorMode", 12, "light"), le = w(t, "onconnect", 12, void 0), fn = w(t, "onconnectstart", 12, void 0), Ut = w(t, "onconnectend", 12, void 0), gn = w(t, "onbeforedelete", 12, void 0), Ne = w(t, "oninit", 12, void 0), rt = w(t, "nodeOrigin", 12, void 0), ye = w(t, "paneClickDistance", 12, 0), ot = w(t, "nodeClickDistance", 12, 0), at = w(t, "defaultMarkerColor", 12, "#b1b1b7"), Xt = w(t, "style", 12, void 0), Kr = w(t, "class", 12, void 0), At = re(), St = re(), hn = re();
+ const jt = a() || m(), ft = Uf(Wi) ? Ue() : w2({
+ nodes: q(d()),
+ edges: q(g()),
+ width: lt(),
+ height: J(),
+ fitView: p(),
+ nodeOrigin: rt(),
+ nodeExtent: me()
+ });
+ un(() => (ft.width.set(h(St)), ft.height.set(h(hn)), ft.domNode.set(h(At)), ft.syncNodeStores(d()), ft.syncEdgeStores(g()), ft.syncViewport(_()), p() !== void 0 && ft.fitViewOnInit.set(p()), x() && ft.fitViewOptions.set(x()), El(ft, {
+ nodeTypes: v(),
+ edgeTypes: b(),
+ minZoom: C(),
+ maxZoom: $(),
+ translateExtent: ue(),
+ paneClickDistance: ye()
+ }), () => {
+ ft.reset();
+ }));
+ const { initialized: ji } = ft;
+ let nr = re(!1);
+ he(
+ () => (h(St), h(hn)),
+ () => {
+ h(St) !== void 0 && h(hn) !== void 0 && (ft.width.set(h(St)), ft.height.set(h(hn)));
+ }
+ ), he(
+ () => (h(nr), l(), j(Ne())),
+ () => {
+ var Y;
+ !h(nr) && l() && ((Y = Ne()) == null || Y(), U(nr, !0));
+ }
+ ), he(
+ () => (j(f()), j(H()), j(P()), j(E()), j(T()), j(at()), j(A()), j(O()), j(S()), j(Ce()), j(W()), j(Fe()), j(Le()), j(Qe()), j(oe()), j(ve()), j(I()), j(R()), j(le()), j(fn()), j(Ut()), j(gn()), j(rt()), Sl),
+ () => {
+ const Y = {
+ flowId: f(),
+ connectionLineType: H(),
+ connectionRadius: P(),
+ selectionMode: E(),
+ snapGrid: T(),
+ defaultMarkerColor: at(),
+ nodesDraggable: A(),
+ nodesConnectable: O(),
+ elementsSelectable: S(),
+ onlyRenderVisibleElements: Ce(),
+ isValidConnection: W(),
+ autoPanOnConnect: Fe(),
+ autoPanOnNodeDrag: Le(),
+ onerror: Qe(),
+ ondelete: oe(),
+ onedgecreate: ve(),
+ connectionMode: I(),
+ nodeDragThreshold: R(),
+ onconnect: le(),
+ onconnectstart: fn(),
+ onconnectend: Ut(),
+ onbeforedelete: gn(),
+ nodeOrigin: rt()
+ };
+ Sl(ft, Y);
+ }
+ ), he(
+ () => (j(v()), j(b()), j(C()), j($()), j(ue()), j(ye())),
+ () => {
+ El(ft, {
+ nodeTypes: v(),
+ edgeTypes: b(),
+ minZoom: C(),
+ maxZoom: $(),
+ translateExtent: ue(),
+ paneClickDistance: ye()
+ });
+ }
+ ), he(
+ () => j(Re()),
+ () => {
+ k1(U(c, W2(Re())), "$colorModeClass", i);
+ }
+ ), gt(), He();
+ var Jt = G2();
+ let Io;
+ var zo = X(Jt);
+ Yc(zo, {
+ get selectionKey() {
+ return N();
+ },
+ get deleteKey() {
+ return k();
+ },
+ get panActivationKey() {
+ return M();
+ },
+ get multiSelectionKey() {
+ return D();
+ },
+ get zoomActivationKey() {
+ return V();
+ }
+ });
+ var Ro = z(zo, 2);
+ const Rd = /* @__PURE__ */ pe(() => ge() === void 0 ? qn.Free : ge()), Bd = /* @__PURE__ */ pe(() => ze() === void 0 ? !0 : ze()), Yd = /* @__PURE__ */ pe(() => G() === void 0 ? !0 : G()), Zd = /* @__PURE__ */ pe(() => se() === void 0 ? !0 : se()), Xd = /* @__PURE__ */ pe(() => Te() === void 0 ? !0 : Te()), Fd = /* @__PURE__ */ pe(() => Ae() === void 0 ? !1 : Ae()), Wd = /* @__PURE__ */ pe(() => Xe() === void 0 ? !0 : Xe()), Kd = /* @__PURE__ */ pe(() => ye() === void 0 ? 0 : ye());
+ Mc(Ro, {
+ initialViewport: jt,
+ get onMoveStart() {
+ return K();
+ },
+ get onMove() {
+ return ie();
+ },
+ get onMoveEnd() {
+ return ee();
+ },
+ get panOnScrollMode() {
+ return h(Rd);
+ },
+ get preventScrolling() {
+ return h(Bd);
+ },
+ get zoomOnScroll() {
+ return h(Yd);
+ },
+ get zoomOnDoubleClick() {
+ return h(Zd);
+ },
+ get zoomOnPinch() {
+ return h(Xd);
+ },
+ get panOnScroll() {
+ return h(Fd);
+ },
+ get panOnDrag() {
+ return h(Wd);
+ },
+ get paneClickDistance() {
+ return h(Kd);
+ },
+ children: (Y, gw) => {
+ const Ud = /* @__PURE__ */ pe(() => Xe() === void 0 ? !0 : Xe());
+ Tc(Y, {
+ get panOnDrag() {
+ return h(Ud);
+ },
+ get selectionOnDrag() {
+ return te();
+ },
+ $$events: {
+ paneclick(qr) {
+ Ve.call(this, t, qr);
+ },
+ panecontextmenu(qr) {
+ Ve.call(this, t, qr);
+ }
+ },
+ children: (qr, hw) => {
+ var xa = q2(), ba = be(xa);
+ Hc(ba, {
+ children: (Jd, vw) => {
+ var Ca = K2(), ka = be(Ca);
+ zc(ka, {
+ get defaultEdgeOptions() {
+ return ct();
+ },
+ $$events: {
+ edgeclick(Be) {
+ Ve.call(this, t, Be);
+ },
+ edgecontextmenu(Be) {
+ Ve.call(this, t, Be);
+ },
+ edgemouseenter(Be) {
+ Ve.call(this, t, Be);
+ },
+ edgemouseleave(Be) {
+ Ve.call(this, t, Be);
+ }
+ }
+ });
+ var $a = z(ka, 2);
+ Zc($a, {
+ get containerStyle() {
+ return F();
+ },
+ get style() {
+ return B();
+ },
+ isCustomComponent: n.connectionLine,
+ $$slots: {
+ connectionLine: (Be, pw) => {
+ var Sa = et(), ef = be(Sa);
+ pt(ef, t, "connectionLine", {}), L(Be, Sa);
+ }
+ }
+ });
+ var Ea = z($a, 6);
+ Dc(Ea, {
+ get nodeClickDistance() {
+ return ot();
+ },
+ $$events: {
+ nodeclick(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodemouseenter(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodemousemove(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodemouseleave(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodedragstart(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodedrag(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodedragstop(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodecontextmenu(Be) {
+ Ve.call(this, t, Be);
+ }
+ }
+ });
+ var Qd = z(Ea, 2);
+ Bc(Qd, {
+ $$events: {
+ selectionclick(Be) {
+ Ve.call(this, t, Be);
+ },
+ selectioncontextmenu(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodedragstart(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodedrag(Be) {
+ Ve.call(this, t, Be);
+ },
+ nodedragstop(Be) {
+ Ve.call(this, t, Be);
+ }
+ }
+ }), L(Jd, Ca);
+ },
+ $$slots: { default: !0 }
+ });
+ var jd = z(ba, 2);
+ Rc(jd, {}), L(qr, xa);
+ },
+ $$slots: { default: !0 }
+ });
+ },
+ $$slots: { default: !0 }
+ });
+ var _a = z(Ro, 2);
+ Xc(_a, {
+ get proOptions() {
+ return Oe();
+ },
+ get position() {
+ return xe();
+ }
+ });
+ var qd = z(_a, 2);
+ pt(qd, t, "default", {}), Z(Jt), An(Jt, (Y) => U(At, Y), () => h(At)), Ee(
+ (Y) => Io = on(
+ Jt,
+ Io,
+ {
+ style: Xt(),
+ class: Y,
+ "data-testid": "svelte-flow__wrapper",
+ ...o,
+ role: "application"
+ },
+ "svelte-12wlba6"
+ ),
+ [
+ () => Et([
+ "svelte-flow",
+ Kr(),
+ u()
+ ])
+ ],
+ pe
+ ), Ra(Jt, "clientWidth", (Y) => U(St, Y)), Ra(Jt, "clientHeight", (Y) => U(hn, Y)), Ye("dragover", Jt, function(Y) {
+ Ve.call(this, t, Y);
+ }), Ye("drop", Jt, function(Y) {
+ Ve.call(this, t, Y);
+ }), L(e, Jt);
+ var Gd = fe({
+ get id() {
+ return f();
+ },
+ set id(Y) {
+ f(Y), y();
+ },
+ get nodes() {
+ return d();
+ },
+ set nodes(Y) {
+ d(Y), y();
+ },
+ get edges() {
+ return g();
+ },
+ set edges(Y) {
+ g(Y), y();
+ },
+ get fitView() {
+ return p();
+ },
+ set fitView(Y) {
+ p(Y), y();
+ },
+ get fitViewOptions() {
+ return x();
+ },
+ set fitViewOptions(Y) {
+ x(Y), y();
+ },
+ get minZoom() {
+ return C();
+ },
+ set minZoom(Y) {
+ C(Y), y();
+ },
+ get maxZoom() {
+ return $();
+ },
+ set maxZoom(Y) {
+ $(Y), y();
+ },
+ get initialViewport() {
+ return m();
+ },
+ set initialViewport(Y) {
+ m(Y), y();
+ },
+ get viewport() {
+ return _();
+ },
+ set viewport(Y) {
+ _(Y), y();
+ },
+ get nodeTypes() {
+ return v();
+ },
+ set nodeTypes(Y) {
+ v(Y), y();
+ },
+ get edgeTypes() {
+ return b();
+ },
+ set edgeTypes(Y) {
+ b(Y), y();
+ },
+ get selectionKey() {
+ return N();
+ },
+ set selectionKey(Y) {
+ N(Y), y();
+ },
+ get selectionMode() {
+ return E();
+ },
+ set selectionMode(Y) {
+ E(Y), y();
+ },
+ get panActivationKey() {
+ return M();
+ },
+ set panActivationKey(Y) {
+ M(Y), y();
+ },
+ get multiSelectionKey() {
+ return D();
+ },
+ set multiSelectionKey(Y) {
+ D(Y), y();
+ },
+ get zoomActivationKey() {
+ return V();
+ },
+ set zoomActivationKey(Y) {
+ V(Y), y();
+ },
+ get nodesDraggable() {
+ return A();
+ },
+ set nodesDraggable(Y) {
+ A(Y), y();
+ },
+ get nodesConnectable() {
+ return O();
+ },
+ set nodesConnectable(Y) {
+ O(Y), y();
+ },
+ get nodeDragThreshold() {
+ return R();
+ },
+ set nodeDragThreshold(Y) {
+ R(Y), y();
+ },
+ get elementsSelectable() {
+ return S();
+ },
+ set elementsSelectable(Y) {
+ S(Y), y();
+ },
+ get snapGrid() {
+ return T();
+ },
+ set snapGrid(Y) {
+ T(Y), y();
+ },
+ get deleteKey() {
+ return k();
+ },
+ set deleteKey(Y) {
+ k(Y), y();
+ },
+ get connectionRadius() {
+ return P();
+ },
+ set connectionRadius(Y) {
+ P(Y), y();
+ },
+ get connectionLineType() {
+ return H();
+ },
+ set connectionLineType(Y) {
+ H(Y), y();
+ },
+ get connectionMode() {
+ return I();
+ },
+ set connectionMode(Y) {
+ I(Y), y();
+ },
+ get connectionLineStyle() {
+ return B();
+ },
+ set connectionLineStyle(Y) {
+ B(Y), y();
+ },
+ get connectionLineContainerStyle() {
+ return F();
+ },
+ set connectionLineContainerStyle(Y) {
+ F(Y), y();
+ },
+ get onMoveStart() {
+ return K();
+ },
+ set onMoveStart(Y) {
+ K(Y), y();
+ },
+ get onMove() {
+ return ie();
+ },
+ set onMove(Y) {
+ ie(Y), y();
+ },
+ get onMoveEnd() {
+ return ee();
+ },
+ set onMoveEnd(Y) {
+ ee(Y), y();
+ },
+ get isValidConnection() {
+ return W();
+ },
+ set isValidConnection(Y) {
+ W(Y), y();
+ },
+ get translateExtent() {
+ return ue();
+ },
+ set translateExtent(Y) {
+ ue(Y), y();
+ },
+ get nodeExtent() {
+ return me();
+ },
+ set nodeExtent(Y) {
+ me(Y), y();
+ },
+ get onlyRenderVisibleElements() {
+ return Ce();
+ },
+ set onlyRenderVisibleElements(Y) {
+ Ce(Y), y();
+ },
+ get panOnScrollMode() {
+ return ge();
+ },
+ set panOnScrollMode(Y) {
+ ge(Y), y();
+ },
+ get preventScrolling() {
+ return ze();
+ },
+ set preventScrolling(Y) {
+ ze(Y), y();
+ },
+ get zoomOnScroll() {
+ return G();
+ },
+ set zoomOnScroll(Y) {
+ G(Y), y();
+ },
+ get zoomOnDoubleClick() {
+ return se();
+ },
+ set zoomOnDoubleClick(Y) {
+ se(Y), y();
+ },
+ get zoomOnPinch() {
+ return Te();
+ },
+ set zoomOnPinch(Y) {
+ Te(Y), y();
+ },
+ get panOnScroll() {
+ return Ae();
+ },
+ set panOnScroll(Y) {
+ Ae(Y), y();
+ },
+ get panOnDrag() {
+ return Xe();
+ },
+ set panOnDrag(Y) {
+ Xe(Y), y();
+ },
+ get selectionOnDrag() {
+ return te();
+ },
+ set selectionOnDrag(Y) {
+ te(Y), y();
+ },
+ get autoPanOnConnect() {
+ return Fe();
+ },
+ set autoPanOnConnect(Y) {
+ Fe(Y), y();
+ },
+ get autoPanOnNodeDrag() {
+ return Le();
+ },
+ set autoPanOnNodeDrag(Y) {
+ Le(Y), y();
+ },
+ get onerror() {
+ return Qe();
+ },
+ set onerror(Y) {
+ Qe(Y), y();
+ },
+ get ondelete() {
+ return oe();
+ },
+ set ondelete(Y) {
+ oe(Y), y();
+ },
+ get onedgecreate() {
+ return ve();
+ },
+ set onedgecreate(Y) {
+ ve(Y), y();
+ },
+ get attributionPosition() {
+ return xe();
+ },
+ set attributionPosition(Y) {
+ xe(Y), y();
+ },
+ get proOptions() {
+ return Oe();
+ },
+ set proOptions(Y) {
+ Oe(Y), y();
+ },
+ get defaultEdgeOptions() {
+ return ct();
+ },
+ set defaultEdgeOptions(Y) {
+ ct(Y), y();
+ },
+ get width() {
+ return lt();
+ },
+ set width(Y) {
+ lt(Y), y();
+ },
+ get height() {
+ return J();
+ },
+ set height(Y) {
+ J(Y), y();
+ },
+ get colorMode() {
+ return Re();
+ },
+ set colorMode(Y) {
+ Re(Y), y();
+ },
+ get onconnect() {
+ return le();
+ },
+ set onconnect(Y) {
+ le(Y), y();
+ },
+ get onconnectstart() {
+ return fn();
+ },
+ set onconnectstart(Y) {
+ fn(Y), y();
+ },
+ get onconnectend() {
+ return Ut();
+ },
+ set onconnectend(Y) {
+ Ut(Y), y();
+ },
+ get onbeforedelete() {
+ return gn();
+ },
+ set onbeforedelete(Y) {
+ gn(Y), y();
+ },
+ get oninit() {
+ return Ne();
+ },
+ set oninit(Y) {
+ Ne(Y), y();
+ },
+ get nodeOrigin() {
+ return rt();
+ },
+ set nodeOrigin(Y) {
+ rt(Y), y();
+ },
+ get paneClickDistance() {
+ return ye();
+ },
+ set paneClickDistance(Y) {
+ ye(Y), y();
+ },
+ get nodeClickDistance() {
+ return ot();
+ },
+ set nodeClickDistance(Y) {
+ ot(Y), y();
+ },
+ get defaultMarkerColor() {
+ return at();
+ },
+ set defaultMarkerColor(Y) {
+ at(Y), y();
+ },
+ get style() {
+ return Xt();
+ },
+ set style(Y) {
+ Xt(Y), y();
+ },
+ get class() {
+ return Kr();
+ },
+ set class(Y) {
+ Kr(Y), y();
+ }
+ });
+ return s(), Gd;
+}
+ae(
+ Fc,
+ {
+ id: {},
+ nodes: {},
+ edges: {},
+ fitView: {},
+ fitViewOptions: {},
+ minZoom: {},
+ maxZoom: {},
+ initialViewport: {},
+ viewport: {},
+ nodeTypes: {},
+ edgeTypes: {},
+ selectionKey: {},
+ selectionMode: {},
+ panActivationKey: {},
+ multiSelectionKey: {},
+ zoomActivationKey: {},
+ nodesDraggable: {},
+ nodesConnectable: {},
+ nodeDragThreshold: {},
+ elementsSelectable: {},
+ snapGrid: {},
+ deleteKey: {},
+ connectionRadius: {},
+ connectionLineType: {},
+ connectionMode: {},
+ connectionLineStyle: {},
+ connectionLineContainerStyle: {},
+ onMoveStart: {},
+ onMove: {},
+ onMoveEnd: {},
+ isValidConnection: {},
+ translateExtent: {},
+ nodeExtent: {},
+ onlyRenderVisibleElements: {},
+ panOnScrollMode: {},
+ preventScrolling: {},
+ zoomOnScroll: {},
+ zoomOnDoubleClick: {},
+ zoomOnPinch: {},
+ panOnScroll: {},
+ panOnDrag: {},
+ selectionOnDrag: {},
+ autoPanOnConnect: {},
+ autoPanOnNodeDrag: {},
+ onerror: {},
+ ondelete: {},
+ onedgecreate: {},
+ attributionPosition: {},
+ proOptions: {},
+ defaultEdgeOptions: {},
+ width: {},
+ height: {},
+ colorMode: {},
+ onconnect: {},
+ onconnectstart: {},
+ onconnectend: {},
+ onbeforedelete: {},
+ oninit: {},
+ nodeOrigin: {},
+ paneClickDistance: {},
+ nodeClickDistance: {},
+ defaultMarkerColor: {},
+ style: {},
+ class: {}
+ },
+ ["connectionLine", "default"],
+ [],
+ !0
+);
+function Wc(e, t) {
+ de(t, !1);
+ let n = w(t, "initialNodes", 12, void 0), r = w(t, "initialEdges", 12, void 0), o = w(t, "initialWidth", 12, void 0), i = w(t, "initialHeight", 12, void 0), s = w(t, "fitView", 12, void 0), a = w(t, "nodeOrigin", 12, void 0);
+ const l = Nc({
+ nodes: n(),
+ edges: r(),
+ width: o(),
+ height: i(),
+ nodeOrigin: a(),
+ fitView: s()
+ });
+ Tr(Wi, { getStore: () => l }), Qs(() => {
+ l.reset();
+ }), He();
+ var u = et(), c = be(u);
+ return pt(c, t, "default", {}), L(e, u), fe({
+ get initialNodes() {
+ return n();
+ },
+ set initialNodes(f) {
+ n(f), y();
+ },
+ get initialEdges() {
+ return r();
+ },
+ set initialEdges(f) {
+ r(f), y();
+ },
+ get initialWidth() {
+ return o();
+ },
+ set initialWidth(f) {
+ o(f), y();
+ },
+ get initialHeight() {
+ return i();
+ },
+ set initialHeight(f) {
+ i(f), y();
+ },
+ get fitView() {
+ return s();
+ },
+ set fitView(f) {
+ s(f), y();
+ },
+ get nodeOrigin() {
+ return a();
+ },
+ set nodeOrigin(f) {
+ a(f), y();
+ }
+ });
+}
+ae(
+ Wc,
+ {
+ initialNodes: {},
+ initialEdges: {},
+ initialWidth: {},
+ initialHeight: {},
+ fitView: {},
+ nodeOrigin: {}
+ },
+ ["default"],
+ [],
+ !0
+);
+var j2 = /* @__PURE__ */ ne("<button><!></button>");
+function ro(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]), r = nt(n, [
+ "class",
+ "bgColor",
+ "bgColorHover",
+ "color",
+ "colorHover",
+ "borderColor"
+ ]);
+ de(t, !1);
+ let o = w(t, "class", 12, void 0), i = w(t, "bgColor", 12, void 0), s = w(t, "bgColorHover", 12, void 0), a = w(t, "color", 12, void 0), l = w(t, "colorHover", 12, void 0), u = w(t, "borderColor", 12, void 0);
+ He();
+ var c = j2();
+ let f;
+ var d = X(c);
+ return pt(d, t, "default", { class: "button-svg" }), Z(c), Ee(
+ (g) => {
+ f = on(c, f, { type: "button", class: g, ...r }), st(c, "--xy-controls-button-background-color-props", i()), st(c, "--xy-controls-button-background-color-hover-props", s()), st(c, "--xy-controls-button-color-props", a()), st(c, "--xy-controls-button-color-hover-props", l()), st(c, "--xy-controls-button-border-color-props", u());
+ },
+ [
+ () => Et([
+ "svelte-flow__controls-button",
+ o()
+ ])
+ ],
+ pe
+ ), Ye("click", c, function(g) {
+ Ve.call(this, t, g);
+ }), L(e, c), fe({
+ get class() {
+ return o();
+ },
+ set class(g) {
+ o(g), y();
+ },
+ get bgColor() {
+ return i();
+ },
+ set bgColor(g) {
+ i(g), y();
+ },
+ get bgColorHover() {
+ return s();
+ },
+ set bgColorHover(g) {
+ s(g), y();
+ },
+ get color() {
+ return a();
+ },
+ set color(g) {
+ a(g), y();
+ },
+ get colorHover() {
+ return l();
+ },
+ set colorHover(g) {
+ l(g), y();
+ },
+ get borderColor() {
+ return u();
+ },
+ set borderColor(g) {
+ u(g), y();
+ }
+ });
+}
+ae(
+ ro,
+ {
+ class: {},
+ bgColor: {},
+ bgColorHover: {},
+ color: {},
+ colorHover: {},
+ borderColor: {}
+ },
+ ["default"],
+ [],
+ !0
+);
+var J2 = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"></path></svg>');
+function Kc(e) {
+ var t = J2();
+ L(e, t);
+}
+ae(Kc, {}, [], [], !0);
+var Q2 = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5"><path d="M0 0h32v4.2H0z"></path></svg>');
+function qc(e) {
+ var t = Q2();
+ L(e, t);
+}
+ae(qc, {}, [], [], !0);
+var ep = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30"><path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z"></path></svg>');
+function Gc(e) {
+ var t = ep();
+ L(e, t);
+}
+ae(Gc, {}, [], [], !0);
+var tp = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32"><path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z"></path></svg>');
+function Uc(e) {
+ var t = tp();
+ L(e, t);
+}
+ae(Uc, {}, [], [], !0);
+var np = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32"><path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z"></path></svg>');
+function jc(e) {
+ var t = np();
+ L(e, t);
+}
+ae(jc, {}, [], [], !0);
+var rp = /* @__PURE__ */ ne("<!> <!>", 1), op = /* @__PURE__ */ ne("<!> <!> <!> <!> <!> <!>", 1);
+function Jc(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(H, "$nodesDraggable", n), i = () => Q(I, "$nodesConnectable", n), s = () => Q(B, "$elementsSelectable", n), a = () => Q(T, "$viewport", n), l = () => Q(k, "$minZoom", n), u = () => Q(P, "$maxZoom", n), c = re(), f = re(), d = re(), g = re();
+ let p = w(t, "position", 12, "bottom-left"), x = w(t, "showZoom", 12, !0), C = w(t, "showFitView", 12, !0), $ = w(t, "showLock", 12, !0), m = w(t, "buttonBgColor", 12, void 0), _ = w(t, "buttonBgColorHover", 12, void 0), v = w(t, "buttonColor", 12, void 0), b = w(t, "buttonColorHover", 12, void 0), N = w(t, "buttonBorderColor", 12, void 0), E = w(t, "ariaLabel", 12, void 0), M = w(t, "style", 12, void 0), D = w(t, "orientation", 12, "vertical"), V = w(t, "fitViewOptions", 12, void 0), A = w(t, "class", 12, "");
+ const {
+ zoomIn: O,
+ zoomOut: R,
+ fitView: S,
+ viewport: T,
+ minZoom: k,
+ maxZoom: P,
+ nodesDraggable: H,
+ nodesConnectable: I,
+ elementsSelectable: B
+ } = Ue(), F = {
+ bgColor: m(),
+ bgColorHover: _(),
+ color: v(),
+ colorHover: b(),
+ borderColor: N()
+ }, K = () => {
+ O();
+ }, ie = () => {
+ R();
+ }, ee = () => {
+ S(V());
+ }, W = () => {
+ U(c, !h(c)), H.set(h(c)), I.set(h(c)), B.set(h(c));
+ };
+ he(
+ () => (o(), i(), s()),
+ () => {
+ U(c, o() || i() || s());
+ }
+ ), he(() => (a(), l()), () => {
+ U(f, a().zoom <= l());
+ }), he(() => (a(), u()), () => {
+ U(d, a().zoom >= u());
+ }), he(() => j(D()), () => {
+ U(g, D() === "horizontal" ? "horizontal" : "vertical");
+ }), gt(), He();
+ const ue = /* @__PURE__ */ pe(() => Et([
+ "svelte-flow__controls",
+ h(g),
+ A()
+ ])), me = /* @__PURE__ */ pe(() => E() ?? "Svelte Flow controls");
+ Ho(e, {
+ get class() {
+ return h(ue);
+ },
+ get position() {
+ return p();
+ },
+ "data-testid": "svelte-flow__controls",
+ get "aria-label"() {
+ return h(me);
+ },
+ get style() {
+ return M();
+ },
+ children: (ge, ze) => {
+ var G = op(), se = be(G);
+ pt(se, t, "before", {});
+ var Te = z(se, 2);
+ {
+ var Ae = (ve) => {
+ var xe = rp(), Oe = be(xe);
+ ro(Oe, ut(
+ {
+ class: "svelte-flow__controls-zoomin",
+ title: "zoom in",
+ "aria-label": "zoom in",
+ get disabled() {
+ return h(d);
+ }
+ },
+ F,
+ {
+ $$events: { click: K },
+ children: (lt, J) => {
+ Kc(lt);
+ },
+ $$slots: { default: !0 }
+ }
+ ));
+ var ct = z(Oe, 2);
+ ro(ct, ut(
+ {
+ class: "svelte-flow__controls-zoomout",
+ title: "zoom out",
+ "aria-label": "zoom out",
+ get disabled() {
+ return h(f);
+ }
+ },
+ F,
+ {
+ $$events: { click: ie },
+ children: (lt, J) => {
+ qc(lt);
+ },
+ $$slots: { default: !0 }
+ }
+ )), L(ve, xe);
+ };
+ ke(Te, (ve) => {
+ x() && ve(Ae);
+ });
+ }
+ var Xe = z(Te, 2);
+ {
+ var te = (ve) => {
+ ro(ve, ut(
+ {
+ class: "svelte-flow__controls-fitview",
+ title: "fit view",
+ "aria-label": "fit view"
+ },
+ F,
+ {
+ $$events: { click: ee },
+ children: (xe, Oe) => {
+ Gc(xe);
+ },
+ $$slots: { default: !0 }
+ }
+ ));
+ };
+ ke(Xe, (ve) => {
+ C() && ve(te);
+ });
+ }
+ var Fe = z(Xe, 2);
+ {
+ var Le = (ve) => {
+ ro(ve, ut(
+ {
+ class: "svelte-flow__controls-interactive",
+ title: "toggle interactivity",
+ "aria-label": "toggle interactivity"
+ },
+ F,
+ {
+ $$events: { click: W },
+ children: (xe, Oe) => {
+ var ct = et(), lt = be(ct);
+ {
+ var J = (le) => {
+ jc(le);
+ }, Re = (le) => {
+ Uc(le);
+ };
+ ke(lt, (le) => {
+ h(c) ? le(J) : le(Re, !1);
+ });
+ }
+ L(xe, ct);
+ },
+ $$slots: { default: !0 }
+ }
+ ));
+ };
+ ke(Fe, (ve) => {
+ $() && ve(Le);
+ });
+ }
+ var Qe = z(Fe, 2);
+ pt(Qe, t, "default", {});
+ var oe = z(Qe, 2);
+ pt(oe, t, "after", {}), L(ge, G);
+ },
+ $$slots: { default: !0 }
+ });
+ var Ce = fe({
+ get position() {
+ return p();
+ },
+ set position(ge) {
+ p(ge), y();
+ },
+ get showZoom() {
+ return x();
+ },
+ set showZoom(ge) {
+ x(ge), y();
+ },
+ get showFitView() {
+ return C();
+ },
+ set showFitView(ge) {
+ C(ge), y();
+ },
+ get showLock() {
+ return $();
+ },
+ set showLock(ge) {
+ $(ge), y();
+ },
+ get buttonBgColor() {
+ return m();
+ },
+ set buttonBgColor(ge) {
+ m(ge), y();
+ },
+ get buttonBgColorHover() {
+ return _();
+ },
+ set buttonBgColorHover(ge) {
+ _(ge), y();
+ },
+ get buttonColor() {
+ return v();
+ },
+ set buttonColor(ge) {
+ v(ge), y();
+ },
+ get buttonColorHover() {
+ return b();
+ },
+ set buttonColorHover(ge) {
+ b(ge), y();
+ },
+ get buttonBorderColor() {
+ return N();
+ },
+ set buttonBorderColor(ge) {
+ N(ge), y();
+ },
+ get ariaLabel() {
+ return E();
+ },
+ set ariaLabel(ge) {
+ E(ge), y();
+ },
+ get style() {
+ return M();
+ },
+ set style(ge) {
+ M(ge), y();
+ },
+ get orientation() {
+ return D();
+ },
+ set orientation(ge) {
+ D(ge), y();
+ },
+ get fitViewOptions() {
+ return V();
+ },
+ set fitViewOptions(ge) {
+ V(ge), y();
+ },
+ get class() {
+ return A();
+ },
+ set class(ge) {
+ A(ge), y();
+ }
+ });
+ return r(), Ce;
+}
+ae(
+ Jc,
+ {
+ position: {},
+ showZoom: {},
+ showFitView: {},
+ showLock: {},
+ buttonBgColor: {},
+ buttonBgColorHover: {},
+ buttonColor: {},
+ buttonColorHover: {},
+ buttonBorderColor: {},
+ ariaLabel: {},
+ style: {},
+ orientation: {},
+ fitViewOptions: {},
+ class: {}
+ },
+ ["before", "default", "after"],
+ [],
+ !0
+);
+var Gn;
+(function(e) {
+ e.Lines = "lines", e.Dots = "dots", e.Cross = "cross";
+})(Gn || (Gn = {}));
+var ip = /* @__PURE__ */ _e("<circle></circle>");
+function Qc(e, t) {
+ de(t, !1);
+ let n = w(t, "radius", 12, 5), r = w(t, "class", 12, "");
+ He();
+ var o = ip();
+ return Ee(
+ (i) => {
+ ce(o, "cx", n()), ce(o, "cy", n()), ce(o, "r", n()), kt(o, 0, bn(i));
+ },
+ [
+ () => Et([
+ "svelte-flow__background-pattern",
+ "dots",
+ r()
+ ])
+ ],
+ pe
+ ), L(e, o), fe({
+ get radius() {
+ return n();
+ },
+ set radius(i) {
+ n(i), y();
+ },
+ get class() {
+ return r();
+ },
+ set class(i) {
+ r(i), y();
+ }
+ });
+}
+ae(Qc, { radius: {}, class: {} }, [], [], !0);
+var sp = /* @__PURE__ */ _e("<path></path>");
+function ed(e, t) {
+ de(t, !1);
+ let n = w(t, "lineWidth", 12, 1), r = w(t, "dimensions", 12), o = w(t, "variant", 12, void 0), i = w(t, "class", 12, "");
+ He();
+ var s = sp();
+ return Ee(
+ (a) => {
+ ce(s, "stroke-width", n()), ce(s, "d", `M${r()[0] / 2} 0 V${r()[1]} M0 ${r()[1] / 2} H${r()[0]}`), kt(s, 0, bn(a));
+ },
+ [
+ () => Et([
+ "svelte-flow__background-pattern",
+ o(),
+ i()
+ ])
+ ],
+ pe
+ ), L(e, s), fe({
+ get lineWidth() {
+ return n();
+ },
+ set lineWidth(a) {
+ n(a), y();
+ },
+ get dimensions() {
+ return r();
+ },
+ set dimensions(a) {
+ r(a), y();
+ },
+ get variant() {
+ return o();
+ },
+ set variant(a) {
+ o(a), y();
+ },
+ get class() {
+ return i();
+ },
+ set class(a) {
+ i(a), y();
+ }
+ });
+}
+ae(
+ ed,
+ {
+ lineWidth: {},
+ dimensions: {},
+ variant: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+const ap = {
+ [Gn.Dots]: 1,
+ [Gn.Lines]: 1,
+ [Gn.Cross]: 6
+};
+var lp = /* @__PURE__ */ _e('<svg data-testid="svelte-flow__background"><pattern patternUnits="userSpaceOnUse"><!></pattern><rect x="0" y="0" width="100%" height="100%"></rect></svg>');
+const up = {
+ hash: "svelte-1r7pe8d",
+ code: ".svelte-flow__background.svelte-1r7pe8d {position:absolute;width:100%;height:100%;top:0;left:0;}"
+};
+function td(e, t) {
+ de(t, !1), Je(e, up);
+ const [n, r] = tt(), o = () => Q(b, "$flowId", n), i = () => Q(v, "$viewport", n), s = re(), a = re(), l = re(), u = re(), c = re();
+ let f = w(t, "id", 12, void 0), d = w(t, "variant", 28, () => Gn.Dots), g = w(t, "gap", 12, 20), p = w(t, "size", 12, 1), x = w(t, "lineWidth", 12, 1), C = w(t, "bgColor", 12, void 0), $ = w(t, "patternColor", 12, void 0), m = w(t, "patternClass", 12, void 0), _ = w(t, "class", 12, "");
+ const { viewport: v, flowId: b } = Ue(), N = p() || ap[d()], E = d() === Gn.Dots, M = d() === Gn.Cross, D = Array.isArray(g()) ? g() : [g(), g()];
+ he(
+ () => (o(), j(f())),
+ () => {
+ U(s, `background-pattern-${o()}-${f() ? f() : ""}`);
+ }
+ ), he(() => i(), () => {
+ U(a, [
+ D[0] * i().zoom || 1,
+ D[1] * i().zoom || 1
+ ]);
+ }), he(() => i(), () => {
+ U(l, N * i().zoom);
+ }), he(() => (h(l), h(a)), () => {
+ U(u, M ? [h(l), h(l)] : h(a));
+ }), he(
+ () => (h(l), h(u)),
+ () => {
+ U(c, E ? [
+ h(l) / 2,
+ h(l) / 2
+ ] : [
+ h(u)[0] / 2,
+ h(u)[1] / 2
+ ]);
+ }
+ ), gt(), He();
+ var V = lp(), A = X(V), O = X(A);
+ {
+ var R = (P) => {
+ const H = /* @__PURE__ */ pe(() => h(l) / 2);
+ Qc(P, {
+ get radius() {
+ return h(H);
+ },
+ get class() {
+ return m();
+ }
+ });
+ }, S = (P) => {
+ ed(P, {
+ get dimensions() {
+ return h(u);
+ },
+ get variant() {
+ return d();
+ },
+ get lineWidth() {
+ return x();
+ },
+ get class() {
+ return m();
+ }
+ });
+ };
+ ke(O, (P) => {
+ E ? P(R) : P(S, !1);
+ });
+ }
+ Z(A);
+ var T = z(A);
+ Z(V), Ee(
+ (P) => {
+ kt(V, 0, bn(P), "svelte-1r7pe8d"), st(V, "--xy-background-color-props", C()), st(V, "--xy-background-pattern-color-props", $()), ce(A, "id", h(s)), ce(A, "x", i().x % h(a)[0]), ce(A, "y", i().y % h(a)[1]), ce(A, "width", h(a)[0]), ce(A, "height", h(a)[1]), ce(A, "patternTransform", `translate(-${h(c)[0]},-${h(c)[1]})`), ce(T, "fill", `url(#${h(s)})`);
+ },
+ [
+ () => Et(["svelte-flow__background", _()])
+ ],
+ pe
+ ), L(e, V);
+ var k = fe({
+ get id() {
+ return f();
+ },
+ set id(P) {
+ f(P), y();
+ },
+ get variant() {
+ return d();
+ },
+ set variant(P) {
+ d(P), y();
+ },
+ get gap() {
+ return g();
+ },
+ set gap(P) {
+ g(P), y();
+ },
+ get size() {
+ return p();
+ },
+ set size(P) {
+ p(P), y();
+ },
+ get lineWidth() {
+ return x();
+ },
+ set lineWidth(P) {
+ x(P), y();
+ },
+ get bgColor() {
+ return C();
+ },
+ set bgColor(P) {
+ C(P), y();
+ },
+ get patternColor() {
+ return $();
+ },
+ set patternColor(P) {
+ $(P), y();
+ },
+ get patternClass() {
+ return m();
+ },
+ set patternClass(P) {
+ m(P), y();
+ },
+ get class() {
+ return _();
+ },
+ set class(P) {
+ _(P), y();
+ }
+ });
+ return r(), k;
+}
+ae(
+ td,
+ {
+ id: {},
+ variant: {},
+ gap: {},
+ size: {},
+ lineWidth: {},
+ bgColor: {},
+ patternColor: {},
+ patternClass: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+var cp = /* @__PURE__ */ _e("<rect></rect>");
+function nd(e, t) {
+ de(t, !1);
+ let n = w(t, "x", 12), r = w(t, "y", 12), o = w(t, "width", 12, 0), i = w(t, "height", 12, 0), s = w(t, "borderRadius", 12, 5), a = w(t, "color", 12, void 0), l = w(t, "shapeRendering", 12), u = w(t, "strokeColor", 12, void 0), c = w(t, "strokeWidth", 12, 2), f = w(t, "selected", 12, !1), d = w(t, "class", 12, "");
+ He();
+ var g = cp();
+ let p;
+ return Ee(
+ (x) => {
+ p = kt(g, 0, bn(x), null, p, { selected: f() }), ce(g, "x", n()), ce(g, "y", r()), ce(g, "rx", s()), ce(g, "ry", s()), ce(g, "width", o()), ce(g, "height", i()), ce(g, "style", `${a() ? `fill: ${a()};` : ""}${u() ? `stroke: ${u()};` : ""}${c() ? `stroke-width: ${c()};` : ""}`), ce(g, "shape-rendering", l());
+ },
+ [
+ () => Et(["svelte-flow__minimap-node", d()])
+ ],
+ pe
+ ), L(e, g), fe({
+ get x() {
+ return n();
+ },
+ set x(x) {
+ n(x), y();
+ },
+ get y() {
+ return r();
+ },
+ set y(x) {
+ r(x), y();
+ },
+ get width() {
+ return o();
+ },
+ set width(x) {
+ o(x), y();
+ },
+ get height() {
+ return i();
+ },
+ set height(x) {
+ i(x), y();
+ },
+ get borderRadius() {
+ return s();
+ },
+ set borderRadius(x) {
+ s(x), y();
+ },
+ get color() {
+ return a();
+ },
+ set color(x) {
+ a(x), y();
+ },
+ get shapeRendering() {
+ return l();
+ },
+ set shapeRendering(x) {
+ l(x), y();
+ },
+ get strokeColor() {
+ return u();
+ },
+ set strokeColor(x) {
+ u(x), y();
+ },
+ get strokeWidth() {
+ return c();
+ },
+ set strokeWidth(x) {
+ c(x), y();
+ },
+ get selected() {
+ return f();
+ },
+ set selected(x) {
+ f(x), y();
+ },
+ get class() {
+ return d();
+ },
+ set class(x) {
+ d(x), y();
+ }
+ });
+}
+ae(
+ nd,
+ {
+ x: {},
+ y: {},
+ width: {},
+ height: {},
+ borderRadius: {},
+ color: {},
+ shapeRendering: {},
+ strokeColor: {},
+ strokeWidth: {},
+ selected: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+function cs(e, t) {
+ const n = q0({
+ domNode: e,
+ panZoom: t.panZoom,
+ getTransform: () => {
+ const o = q(t.viewport);
+ return [o.x, o.y, o.zoom];
+ },
+ getViewScale: t.getViewScale
+ });
+ function r(o) {
+ n.update({
+ translateExtent: o.translateExtent,
+ width: o.width,
+ height: o.height,
+ inversePan: o.inversePan,
+ zoomStep: o.zoomStep,
+ pannable: o.pannable,
+ zoomable: o.zoomable
+ });
+ }
+ return {
+ update: r,
+ destroy() {
+ n.destroy();
+ }
+ };
+}
+const ds = (e) => e instanceof Function ? e : () => e;
+var dp = /* @__PURE__ */ _e("<title> </title>"), fp = /* @__PURE__ */ _e('<svg class="svelte-flow__minimap-svg" role="img"><!><!><path class="svelte-flow__minimap-mask" fill-rule="evenodd" pointer-events="none"></path></svg>');
+function rd(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(Xe, "$flowId", n), i = () => Q(se, "$viewport", n), s = () => Q(Te, "$containerWidth", n), a = () => Q(Ae, "$containerHeight", n), l = () => Q(G, "$nodeLookup", n), u = () => Q(ze, "$nodes", n), c = () => Q(te, "$panZoom", n), f = () => Q(Fe, "$translateExtent", n), d = re(), g = re(), p = re(), x = re(), C = re(), $ = re(), m = re(), _ = re(), v = re(), b = re(), N = re(), E = re(), M = re();
+ let D = w(t, "position", 12, "bottom-right"), V = w(t, "ariaLabel", 12, "Mini map"), A = w(t, "nodeStrokeColor", 12, "transparent"), O = w(t, "nodeColor", 12, void 0), R = w(t, "nodeClass", 12, ""), S = w(t, "nodeBorderRadius", 12, 5), T = w(t, "nodeStrokeWidth", 12, 2), k = w(t, "bgColor", 12, void 0), P = w(t, "maskColor", 12, void 0), H = w(t, "maskStrokeColor", 12, void 0), I = w(t, "maskStrokeWidth", 12, void 0), B = w(t, "width", 12, void 0), F = w(t, "height", 12, void 0), K = w(t, "pannable", 12, !0), ie = w(t, "zoomable", 12, !0), ee = w(t, "inversePan", 12, void 0), W = w(t, "zoomStep", 12, void 0), ue = w(t, "style", 12, ""), me = w(t, "class", 12, "");
+ const Ce = 200, ge = 150, {
+ nodes: ze,
+ nodeLookup: G,
+ viewport: se,
+ width: Te,
+ height: Ae,
+ flowId: Xe,
+ panZoom: te,
+ translateExtent: Fe
+ } = Ue(), Le = O() === void 0 ? void 0 : ds(O()), Qe = ds(A()), oe = ds(R()), ve = (
+ // @ts-expect-error - TS doesn't know about chrome
+ typeof window > "u" || window.chrome ? "crispEdges" : "geometricPrecision"
+ ), xe = `svelte-flow__minimap-desc-${o()}`;
+ let Oe = re(h(d));
+ const ct = () => h($);
+ he(
+ () => (i(), s(), a()),
+ () => {
+ U(d, {
+ x: -i().x / i().zoom,
+ y: -i().y / i().zoom,
+ width: s() / i().zoom,
+ height: a() / i().zoom
+ });
+ }
+ ), he(
+ () => (l(), h(d), u()),
+ () => {
+ U(Oe, l().size > 0 ? nc(No(l()), h(d)) : h(d)), u();
+ }
+ ), he(() => j(B()), () => {
+ U(g, B() ?? Ce);
+ }), he(() => j(F()), () => {
+ U(p, F() ?? ge);
+ }), he(
+ () => (h(Oe), h(g)),
+ () => {
+ U(x, h(Oe).width / h(g));
+ }
+ ), he(
+ () => (h(Oe), h(p)),
+ () => {
+ U(C, h(Oe).height / h(p));
+ }
+ ), he(
+ () => (h(x), h(C)),
+ () => {
+ U($, Math.max(h(x), h(C)));
+ }
+ ), he(() => (h($), h(g)), () => {
+ U(m, h($) * h(g));
+ }), he(
+ () => (h($), h(p)),
+ () => {
+ U(_, h($) * h(p));
+ }
+ ), he(() => h($), () => {
+ U(v, 5 * h($));
+ }), he(
+ () => (h(Oe), h(m), h(v)),
+ () => {
+ U(b, h(Oe).x - (h(m) - h(Oe).width) / 2 - h(v));
+ }
+ ), he(
+ () => (h(Oe), h(_), h(v)),
+ () => {
+ U(N, h(Oe).y - (h(_) - h(Oe).height) / 2 - h(v));
+ }
+ ), he(() => (h(m), h(v)), () => {
+ U(E, h(m) + h(v) * 2);
+ }), he(() => (h(_), h(v)), () => {
+ U(M, h(_) + h(v) * 2);
+ }), gt(), He();
+ const lt = /* @__PURE__ */ pe(() => ue() + (k() ? `;--xy-minimap-background-color-props:${k()}` : "")), J = /* @__PURE__ */ pe(() => Et(["svelte-flow__minimap", me()]));
+ Ho(e, {
+ get position() {
+ return D();
+ },
+ get style() {
+ return h(lt);
+ },
+ get class() {
+ return h(J);
+ },
+ "data-testid": "svelte-flow__minimap",
+ children: (le, fn) => {
+ var Ut = et(), gn = be(Ut);
+ {
+ var Ne = (rt) => {
+ var ye = fp();
+ ce(ye, "aria-labelledby", xe);
+ var ot = X(ye);
+ {
+ var at = (At) => {
+ var St = dp();
+ ce(St, "id", xe);
+ var hn = X(St, !0);
+ Z(St), Ee(() => Rt(hn, V())), L(At, St);
+ };
+ ke(ot, (At) => {
+ V() && At(at);
+ });
+ }
+ var Xt = z(ot);
+ Yt(Xt, 1, u, (At) => At.id, (At, St) => {
+ var hn = et();
+ const jt = /* @__PURE__ */ pe(() => l().get(h(St).id));
+ var ft = be(hn);
+ {
+ var ji = (nr) => {
+ const Jt = /* @__PURE__ */ pe(() => tr(h(jt))), Io = /* @__PURE__ */ pe(() => Le == null ? void 0 : Le(h(jt))), zo = /* @__PURE__ */ pe(() => Qe(h(jt))), Ro = /* @__PURE__ */ pe(() => oe(h(jt)));
+ nd(nr, ut(
+ {
+ get x() {
+ return h(jt).internals.positionAbsolute.x;
+ },
+ get y() {
+ return h(jt).internals.positionAbsolute.y;
+ }
+ },
+ () => h(Jt),
+ {
+ get selected() {
+ return h(jt).selected;
+ },
+ get color() {
+ return h(Io);
+ },
+ get borderRadius() {
+ return S();
+ },
+ get strokeColor() {
+ return h(zo);
+ },
+ get strokeWidth() {
+ return T();
+ },
+ shapeRendering: ve,
+ get class() {
+ return h(Ro);
+ }
+ }
+ ));
+ };
+ ke(ft, (nr) => {
+ h(jt) && oc(h(jt)) && nr(ji);
+ });
+ }
+ L(At, hn);
+ });
+ var Kr = z(Xt);
+ Z(ye), vt(ye, (At, St) => cs == null ? void 0 : cs(At, St), () => ({
+ panZoom: c(),
+ viewport: se,
+ getViewScale: ct,
+ translateExtent: f(),
+ width: s(),
+ height: a(),
+ inversePan: ee(),
+ zoomStep: W(),
+ pannable: K(),
+ zoomable: ie()
+ })), Ee(() => {
+ ce(ye, "width", h(g)), ce(ye, "height", h(p)), ce(ye, "viewBox", `${h(b) ?? ""} ${h(N) ?? ""} ${h(E) ?? ""} ${h(M) ?? ""}`), st(ye, "--xy-minimap-mask-background-color-props", P()), st(ye, "--xy-minimap-mask-stroke-color-props", H()), st(ye, "--xy-minimap-mask-stroke-width-props", I() ? I() * h($) : void 0), ce(Kr, "d", `M${h(b) - h(v)},${h(N) - h(v)}h${h(E) + h(v) * 2}v${h(M) + h(v) * 2}h${-h(E) - h(v) * 2}z
+ M${h(d).x ?? ""},${h(d).y ?? ""}h${h(d).width ?? ""}v${h(d).height ?? ""}h${-h(d).width}z`);
+ }), L(rt, ye);
+ };
+ ke(gn, (rt) => {
+ c() && rt(Ne);
+ });
+ }
+ L(le, Ut);
+ },
+ $$slots: { default: !0 }
+ });
+ var Re = fe({
+ get position() {
+ return D();
+ },
+ set position(le) {
+ D(le), y();
+ },
+ get ariaLabel() {
+ return V();
+ },
+ set ariaLabel(le) {
+ V(le), y();
+ },
+ get nodeStrokeColor() {
+ return A();
+ },
+ set nodeStrokeColor(le) {
+ A(le), y();
+ },
+ get nodeColor() {
+ return O();
+ },
+ set nodeColor(le) {
+ O(le), y();
+ },
+ get nodeClass() {
+ return R();
+ },
+ set nodeClass(le) {
+ R(le), y();
+ },
+ get nodeBorderRadius() {
+ return S();
+ },
+ set nodeBorderRadius(le) {
+ S(le), y();
+ },
+ get nodeStrokeWidth() {
+ return T();
+ },
+ set nodeStrokeWidth(le) {
+ T(le), y();
+ },
+ get bgColor() {
+ return k();
+ },
+ set bgColor(le) {
+ k(le), y();
+ },
+ get maskColor() {
+ return P();
+ },
+ set maskColor(le) {
+ P(le), y();
+ },
+ get maskStrokeColor() {
+ return H();
+ },
+ set maskStrokeColor(le) {
+ H(le), y();
+ },
+ get maskStrokeWidth() {
+ return I();
+ },
+ set maskStrokeWidth(le) {
+ I(le), y();
+ },
+ get width() {
+ return B();
+ },
+ set width(le) {
+ B(le), y();
+ },
+ get height() {
+ return F();
+ },
+ set height(le) {
+ F(le), y();
+ },
+ get pannable() {
+ return K();
+ },
+ set pannable(le) {
+ K(le), y();
+ },
+ get zoomable() {
+ return ie();
+ },
+ set zoomable(le) {
+ ie(le), y();
+ },
+ get inversePan() {
+ return ee();
+ },
+ set inversePan(le) {
+ ee(le), y();
+ },
+ get zoomStep() {
+ return W();
+ },
+ set zoomStep(le) {
+ W(le), y();
+ },
+ get style() {
+ return ue();
+ },
+ set style(le) {
+ ue(le), y();
+ },
+ get class() {
+ return me();
+ },
+ set class(le) {
+ me(le), y();
+ }
+ });
+ return r(), Re;
+}
+ae(
+ rd,
+ {
+ position: {},
+ ariaLabel: {},
+ nodeStrokeColor: {},
+ nodeColor: {},
+ nodeClass: {},
+ nodeBorderRadius: {},
+ nodeStrokeWidth: {},
+ bgColor: {},
+ maskColor: {},
+ maskStrokeColor: {},
+ maskStrokeWidth: {},
+ width: {},
+ height: {},
+ pannable: {},
+ zoomable: {},
+ inversePan: {},
+ zoomStep: {},
+ style: {},
+ class: {}
+ },
+ [],
+ [],
+ !0
+);
+const Pl = (e) => f0(e);
+function Dt() {
+ const { zoomIn: e, zoomOut: t, fitView: n, onbeforedelete: r, snapGrid: o, viewport: i, width: s, height: a, minZoom: l, maxZoom: u, panZoom: c, nodes: f, edges: d, domNode: g, nodeLookup: p, nodeOrigin: x, edgeLookup: C, connectionLookup: $ } = Ue(), m = (b) => {
+ var V, A;
+ const N = q(p), E = Pl(b) ? b : N.get(b.id), M = E.parentId ? p0(E.position, E.measured, E.parentId, N, q(x)) : E.position, D = {
+ ...E,
+ position: M,
+ width: ((V = E.measured) == null ? void 0 : V.width) ?? E.width,
+ height: ((A = E.measured) == null ? void 0 : A.height) ?? E.height
+ };
+ return Lr(D);
+ }, _ = (b, N, E = { replace: !1 }) => {
+ var V;
+ const M = (V = q(p).get(b)) == null ? void 0 : V.internals.userNode;
+ if (!M)
+ return;
+ const D = typeof N == "function" ? N(M) : N;
+ E.replace ? f.update((A) => A.map((O) => O.id === b ? Pl(D) ? D : { ...O, ...D } : O)) : (Object.assign(M, D), f.update((A) => A));
+ }, v = (b) => q(p).get(b);
+ return {
+ zoomIn: e,
+ zoomOut: t,
+ getInternalNode: v,
+ getNode: (b) => {
+ var N;
+ return (N = v(b)) == null ? void 0 : N.internals.userNode;
+ },
+ getNodes: (b) => b === void 0 ? q(f) : Nl(q(p), b),
+ getEdge: (b) => q(C).get(b),
+ getEdges: (b) => b === void 0 ? q(d) : Nl(q(C), b),
+ setZoom: (b, N) => {
+ const E = q(c);
+ return E ? E.scaleTo(b, { duration: N == null ? void 0 : N.duration }) : Promise.resolve(!1);
+ },
+ getZoom: () => q(i).zoom,
+ setViewport: async (b, N) => {
+ const E = q(i), M = q(c);
+ return M ? (await M.setViewport({
+ x: b.x ?? E.x,
+ y: b.y ?? E.y,
+ zoom: b.zoom ?? E.zoom
+ }, { duration: N == null ? void 0 : N.duration }), Promise.resolve(!0)) : Promise.resolve(!1);
+ },
+ getViewport: () => q(i),
+ setCenter: async (b, N, E) => {
+ const M = typeof (E == null ? void 0 : E.zoom) < "u" ? E.zoom : q(u), D = q(c);
+ return D ? (await D.setViewport({
+ x: q(s) / 2 - b * M,
+ y: q(a) / 2 - N * M,
+ zoom: M
+ }, { duration: E == null ? void 0 : E.duration }), Promise.resolve(!0)) : Promise.resolve(!1);
+ },
+ fitView: n,
+ fitBounds: async (b, N) => {
+ const E = q(c);
+ if (!E)
+ return Promise.resolve(!1);
+ const M = ua(b, q(s), q(a), q(l), q(u), (N == null ? void 0 : N.padding) ?? 0.1);
+ return await E.setViewport(M, { duration: N == null ? void 0 : N.duration }), Promise.resolve(!0);
+ },
+ getIntersectingNodes: (b, N = !0, E) => {
+ const M = fl(b), D = M ? b : m(b);
+ return D ? (E || q(f)).filter((V) => {
+ const A = q(p).get(V.id);
+ if (!A || !M && V.id === b.id)
+ return !1;
+ const O = Lr(A), R = yo(O, D);
+ return N && R > 0 || R >= D.width * D.height;
+ }) : [];
+ },
+ isNodeIntersecting: (b, N, E = !0) => {
+ const D = fl(b) ? b : m(b);
+ if (!D)
+ return !1;
+ const V = yo(D, N);
+ return E && V > 0 || V >= D.width * D.height;
+ },
+ deleteElements: async ({ nodes: b = [], edges: N = [] }) => {
+ const { nodes: E, edges: M } = await Qu({
+ nodesToRemove: b,
+ edgesToRemove: N,
+ nodes: q(f),
+ edges: q(d),
+ onBeforeDelete: q(r)
+ });
+ return E && f.update((D) => D.filter((V) => !E.some(({ id: A }) => A === V.id))), M && d.update((D) => D.filter((V) => !M.some(({ id: A }) => A === V.id))), {
+ deletedNodes: E,
+ deletedEdges: M
+ };
+ },
+ screenToFlowPosition: (b, N = { snapToGrid: !0 }) => {
+ const E = q(g);
+ if (!E)
+ return b;
+ const M = N.snapToGrid ? q(o) : !1, { x: D, y: V, zoom: A } = q(i), { x: O, y: R } = E.getBoundingClientRect(), S = {
+ x: b.x - O,
+ y: b.y - R
+ };
+ return Mo(S, [D, V, A], M !== null, M || [1, 1]);
+ },
+ /**
+ *
+ * @param position
+ * @returns
+ */
+ flowToScreenPosition: (b) => {
+ const N = q(g);
+ if (!N)
+ return b;
+ const { x: E, y: M, zoom: D } = q(i), { x: V, y: A } = N.getBoundingClientRect(), O = rc(b, [E, M, D]);
+ return {
+ x: O.x + V,
+ y: O.y + A
+ };
+ },
+ toObject: () => ({
+ nodes: q(f).map((b) => ({
+ ...b,
+ // we want to make sure that changes to the nodes object that gets returned by toObject
+ // do not affect the nodes object
+ position: { ...b.position },
+ data: { ...b.data }
+ })),
+ edges: q(d).map((b) => ({ ...b })),
+ viewport: { ...q(i) }
+ }),
+ updateNode: _,
+ updateNodeData: (b, N, E) => {
+ var V;
+ const M = (V = q(p).get(b)) == null ? void 0 : V.internals.userNode;
+ if (!M)
+ return;
+ const D = typeof N == "function" ? N(M) : N;
+ M.data = E != null && E.replace ? D : { ...M.data, ...D }, f.update((A) => A);
+ },
+ getNodesBounds: (b) => {
+ const N = q(p), E = q(x);
+ return g0(b, { nodeLookup: N, nodeOrigin: E });
+ },
+ getHandleConnections: ({ type: b, id: N, nodeId: E }) => {
+ var M;
+ return Array.from(((M = q($).get(`${E}-${b}-${N ?? null}`)) == null ? void 0 : M.values()) ?? []);
+ },
+ viewport: i
+ };
+}
+function Nl(e, t) {
+ var r;
+ const n = [];
+ for (const o of t) {
+ const i = e.get(o);
+ if (i) {
+ const s = "internals" in i ? (r = i.internals) == null ? void 0 : r.userNode : i;
+ n.push(s);
+ }
+ }
+ return n;
+}
+var gp = /* @__PURE__ */ ne('<div class="svelte-flow__node-toolbar"><!></div>');
+function od(e, t) {
+ de(t, !1);
+ const [n, r] = tt(), o = () => Q(_, "$nodes", n), i = () => Q(m, "$nodeLookup", n), s = () => Q($, "$viewport", n), a = () => Q(C, "$domNode", n), l = re(), u = re(), c = re();
+ let f = w(t, "nodeId", 12, void 0), d = w(t, "position", 12, void 0), g = w(t, "align", 12, void 0), p = w(t, "offset", 12, void 0), x = w(t, "isVisible", 12, void 0);
+ const { domNode: C, viewport: $, nodeLookup: m, nodes: _ } = Ue(), { getNodesBounds: v } = Dt(), b = ar("svelteflow__node_id");
+ let N = re(), E = re([]), M = p() !== void 0 ? p() : 10, D = d() !== void 0 ? d() : $e.Top, V = g() !== void 0 ? g() : "center";
+ he(
+ () => (o(), j(f()), i()),
+ () => {
+ o();
+ const T = Array.isArray(f()) ? f() : [f() || b];
+ U(E, T.reduce(
+ (k, P) => {
+ const H = i().get(P);
+ return H && k.push(H), k;
+ },
+ []
+ ));
+ }
+ ), he(
+ () => (h(E), s()),
+ () => {
+ const T = v(h(E));
+ T && U(N, T0(T, s(), D, M, V));
+ }
+ ), he(() => h(E), () => {
+ U(l, h(E).length === 0 ? 1 : Math.max(...h(E).map((T) => (T.internals.z || 5) + 1)));
+ }), he(() => o(), () => {
+ U(u, o().filter((T) => T.selected).length);
+ }), he(
+ () => (j(x()), h(E), h(u)),
+ () => {
+ U(c, typeof x() == "boolean" ? x() : h(E).length === 1 && h(E)[0].selected && h(u) === 1);
+ }
+ ), gt(), He();
+ var A = et(), O = be(A);
+ {
+ var R = (T) => {
+ var k = gp(), P = X(k);
+ pt(P, t, "default", {}), Z(k), vt(k, (H, I) => kr == null ? void 0 : kr(H, I), () => ({ domNode: a() })), Ee(
+ (H) => {
+ ce(k, "data-id", H), st(k, "position", "absolute"), st(k, "transform", h(N)), st(k, "z-index", h(l));
+ },
+ [
+ () => h(E).reduce((H, I) => `${H}${I.id} `, "").trim()
+ ],
+ pe
+ ), L(T, k);
+ };
+ ke(O, (T) => {
+ a() && h(c) && h(E) && T(R);
+ });
+ }
+ L(e, A);
+ var S = fe({
+ get nodeId() {
+ return f();
+ },
+ set nodeId(T) {
+ f(T), y();
+ },
+ get position() {
+ return d();
+ },
+ set position(T) {
+ d(T), y();
+ },
+ get align() {
+ return g();
+ },
+ set align(T) {
+ g(T), y();
+ },
+ get offset() {
+ return p();
+ },
+ set offset(T) {
+ p(T), y();
+ },
+ get isVisible() {
+ return x();
+ },
+ set isVisible(T) {
+ x(T), y();
+ }
+ });
+ return r(), S;
+}
+ae(
+ od,
+ {
+ nodeId: {},
+ position: {},
+ align: {},
+ offset: {},
+ isVisible: {}
+ },
+ ["default"],
+ [],
+ !0
+);
+function pr(e) {
+ const { nodes: t, nodeLookup: n } = Ue();
+ let r = [], o = !0;
+ return Kn([t, n], ([, i], s) => {
+ var c;
+ const a = [], l = Array.isArray(e), u = l ? e : [e];
+ for (const f of u) {
+ const d = (c = i.get(f)) == null ? void 0 : c.internals.userNode;
+ d && a.push({
+ id: d.id,
+ type: d.type,
+ data: d.data
+ });
+ }
+ (!z0(a, r) || o) && (r = a, s(l ? a : a[0] ?? null), o = !1);
+ });
+}
+const Ml = "tinyflow-component";
+class yw {
+ constructor(t) {
+ wt(this, "options");
+ wt(this, "rootEl");
+ wt(this, "svelteFlowInstance");
+ if (typeof t.element != "string" && !(t.element instanceof Element))
+ throw new Error("element must be a string or Element");
+ this._setOptions(t), this._init();
+ }
+ _init() {
+ if (typeof this.options.element == "string") {
+ if (this.rootEl = document.querySelector(this.options.element), !this.rootEl)
+ throw new Error(
+ `element not found by document.querySelector('${this.options.element}')`
+ );
+ } else if (this.options.element instanceof Element)
+ this.rootEl = this.options.element;
+ else
+ throw new Error("element must be a string or Element");
+ const t = document.createElement(Ml);
+ t.style.display = "block", t.style.width = "100%", t.style.height = "100%", t.classList.add("tf-theme-light"), t.options = this.options, t.onInit = (n) => {
+ this.svelteFlowInstance = n;
+ }, this.rootEl.appendChild(t);
+ }
+ _setOptions(t) {
+ this.options = {
+ ...t
+ };
+ }
+ getOptions() {
+ return this.options;
+ }
+ getData() {
+ return this.svelteFlowInstance.toObject();
+ }
+ setData(t) {
+ this.options.data = t;
+ const n = document.createElement(Ml);
+ n.style.display = "block", n.style.width = "100%", n.style.height = "100%", n.classList.add("tf-theme-light"), n.options = this.options, n.onInit = (r) => {
+ this.svelteFlowInstance = r;
+ }, this.destroy(), this.rootEl.appendChild(n);
+ }
+ destroy() {
+ for (; this.rootEl.firstChild; )
+ this.rootEl.removeChild(this.rootEl.firstChild);
+ }
+}
+const hp = () => {
+ const e = we([]), t = we([]), n = we({ x: 250, y: 100, zoom: 1 });
+ return {
+ nodes: e,
+ edges: t,
+ viewport: n,
+ init: (r, o) => {
+ e.set(r), t.set(o);
+ },
+ addNode: (r) => {
+ e.update((o) => [...o, r]);
+ },
+ removeNode: (r) => {
+ e.update((o) => o.filter((i) => i.id !== r));
+ },
+ updateNode: (r, o) => {
+ e.update((i) => i.map((s) => s.id === r ? o : s));
+ },
+ updateNodeData: (r, o) => {
+ e.update(
+ (i) => i.map((s) => s.id === r ? { ...s, data: { ...s.data, ...o } } : s)
+ );
+ },
+ selectNodeOnly: (r) => {
+ e.update(
+ (o) => o.map(
+ (i) => i.id === r ? { ...i, selected: !0 } : { ...i, selected: !1 }
+ )
+ );
+ },
+ addEdge: (r) => {
+ t.update((o) => [...o, r]);
+ },
+ removeEdge: (r) => {
+ t.update((o) => o.filter((i) => i.id !== r));
+ },
+ updateEdge: (r, o) => {
+ t.update((i) => i.map((s) => s.id === r ? o : s));
+ },
+ updateEdgeData: (r, o) => {
+ t.update((i) => i.map((s) => s.id === r ? { ...s, data: o } : s));
+ }
+ };
+}, ei = hp();
+var vp = /* @__PURE__ */ ne("<button><!></button>");
+function Ke(e, t) {
+ de(t, !0);
+ const n = w(t, "children", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "children"
+ ]);
+ var o = vp();
+ let i;
+ var s = X(o);
+ return lr(s, () => n() ?? dt), Z(o), Ee(() => i = on(o, i, {
+ type: "button",
+ ...r,
+ class: `tf-btn nopan nodrag ${t.class ?? ""}`
+ })), L(e, o), fe({
+ get children() {
+ return n();
+ },
+ set children(a) {
+ n(a), y();
+ }
+ });
+}
+ae(Ke, { children: {} }, [], [], !0);
+var pp = /* @__PURE__ */ ne("<input>");
+function id(e, t) {
+ de(t, !0);
+ const n = /* @__PURE__ */ yt(t, ["$$slots", "$$events", "$$legacy", "$$host"]);
+ var r = pp();
+ io(r);
+ let o;
+ Ee(() => o = on(r, o, {
+ type: "checkbox",
+ ...n,
+ class: `tf-checkbox nopan nodrag ${t.class ?? ""}`
+ })), L(e, r), fe();
+}
+ae(id, {}, [], [], !0);
+var mp = /* @__PURE__ */ ne("<input>");
+function xt(e, t) {
+ de(t, !0);
+ const n = /* @__PURE__ */ yt(t, ["$$slots", "$$events", "$$legacy", "$$host"]);
+ var r = mp();
+ io(r);
+ let o;
+ Ee(() => o = on(r, o, {
+ type: "text",
+ ...n,
+ class: `tf-input nopan nodrag ${t.class ?? ""}`
+ })), L(e, r), fe();
+}
+ae(xt, {}, [], [], !0);
+var yp = /* @__PURE__ */ ne("<textarea></textarea>");
+function $t(e, t) {
+ de(t, !0);
+ const n = /* @__PURE__ */ yt(t, ["$$slots", "$$events", "$$legacy", "$$host"]);
+ var r = yp();
+ l1(r);
+ let o;
+ Ee(() => o = on(r, o, {
+ ...n,
+ class: `tf-textarea nodrag ${t.class ?? ""}`
+ })), L(e, r), fe();
+}
+ae($t, {}, [], [], !0);
+var wp = /* @__PURE__ */ ne('<div role="button"><!></div>'), _p = /* @__PURE__ */ ne("<div></div>");
+function sd(e, t) {
+ const n = nt(t, [
+ "children",
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host"
+ ]), r = nt(n, ["items", "onChange", "activeIndex"]);
+ de(t, !1);
+ let o = w(t, "items", 28, () => []), i = w(t, "onChange", 12, () => {
+ }), s = w(t, "activeIndex", 12, 0);
+ function a(c, f) {
+ var d;
+ s(f), (d = i()) == null || d(c, f);
+ }
+ He();
+ var l = _p();
+ let u;
+ return Yt(l, 5, o, Li, (c, f, d) => {
+ var g = wp();
+ ce(g, "tabindex", d), g.__click = () => a(h(f), d), g.__keydown = ($) => {
+ ($.key === "Enter" || $.key === " ") && ($.preventDefault(), a(h(f), d));
+ };
+ var p = X(g);
+ {
+ var x = ($) => {
+ var m = Ie();
+ Ee(() => Rt(m, h(f).label)), L($, m);
+ }, C = ($) => {
+ var m = et(), _ = be(m);
+ lr(_, () => h(f).label ?? dt), L($, m);
+ };
+ ke(p, ($) => {
+ typeof h(f).label == "string" ? $(x) : $(C, !1);
+ });
+ }
+ Z(g), Ee(() => kt(g, 1, `tf-tabs-item ${(d === s() ? "active" : "") ?? ""}`)), L(c, g);
+ }), Z(l), Ee(() => u = on(l, u, {
+ ...r,
+ class: `tf-tabs ${r.class ?? ""}`
+ })), L(e, l), fe({
+ get items() {
+ return o();
+ },
+ set items(c) {
+ o(c), y();
+ },
+ get onChange() {
+ return i();
+ },
+ set onChange(c) {
+ i(c), y();
+ },
+ get activeIndex() {
+ return s();
+ },
+ set activeIndex(c) {
+ s(c), y();
+ }
+ });
+}
+Ai(["click", "keydown"]);
+ae(sd, { items: {}, onChange: {}, activeIndex: {} }, [], [], !0);
+var xp = (e, t, n) => t(h(n)), bp = (e, t, n) => {
+ (e.key === "Enter" || e.key === " ") && (e.preventDefault(), t(h(n)));
+}, Cp = /* @__PURE__ */ ne('<span class="tf-collapse-item-title-icon"><!></span>'), kp = /* @__PURE__ */ ne('<div class="tf-collapse-item-description"><!></div>'), $p = /* @__PURE__ */ ne('<div class="tf-collapse-item-content"><!></div>'), Ep = /* @__PURE__ */ ne('<div class="tf-collapse-item"><div class="tf-collapse-item-title" role="button"><!> <!> <span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></span></div> <!> <!></div>'), Sp = /* @__PURE__ */ ne("<div></div>");
+const Pp = {
+ hash: "svelte-1jfktzw",
+ code: `\r
+ /* 瀹氫箟鏃嬭浆鐨� CSS 绫� */.rotate-90.svelte-1jfktzw {transform:rotate(90deg);transition:transform 0.3s ease;}`
+};
+function ad(e, t) {
+ de(t, !0), Je(e, Pp);
+ let n = w(t, "items", 7), r = w(t, "onChange", 7), o = w(t, "activeKeys", 31, () => Tt([]));
+ function i(a) {
+ var l;
+ o().includes(a.key) ? o(o().filter((u) => u !== a.key)) : (o().push(a.key), o(o())), (l = r()) == null || l(a, o());
+ }
+ var s = Sp();
+ return Yt(s, 21, n, Li, (a, l, u) => {
+ var c = Ep(), f = X(c);
+ ce(f, "tabindex", u), f.__click = [xp, i, l], f.__keydown = [bp, i, l];
+ var d = X(f);
+ {
+ var g = (v) => {
+ var b = Cp(), N = X(b);
+ Fn(N, {
+ get target() {
+ return h(l).icon;
+ }
+ }), Z(b), L(v, b);
+ };
+ ke(d, (v) => {
+ h(l).icon && v(g);
+ });
+ }
+ var p = z(d, 2);
+ Fn(p, {
+ get target() {
+ return h(l).title;
+ }
+ });
+ var x = z(p, 2);
+ Z(f);
+ var C = z(f, 2);
+ {
+ var $ = (v) => {
+ var b = kp(), N = X(b);
+ Fn(N, {
+ get target() {
+ return h(l).description;
+ }
+ }), Z(b), L(v, b);
+ };
+ ke(C, (v) => {
+ h(l).description && v($);
+ });
+ }
+ var m = z(C, 2);
+ {
+ var _ = (v) => {
+ var b = $p(), N = X(b);
+ Fn(N, {
+ get target() {
+ return h(l).content;
+ }
+ }), Z(b), L(v, b);
+ };
+ ke(m, (v) => {
+ o().includes(h(l).key) && v(_);
+ });
+ }
+ Z(c), Ee((v) => kt(x, 1, `tf-collapse-item-title-arrow ${v ?? ""}`, "svelte-1jfktzw"), [
+ () => o().includes(h(l).key) ? "rotate-90" : ""
+ ]), L(a, c);
+ }), Z(s), Ee(() => {
+ ce(s, "style", t.style), kt(s, 1, `tf-collapse ${t.class ?? ""}`, "svelte-1jfktzw");
+ }), L(e, s), fe({
+ get items() {
+ return n();
+ },
+ set items(a) {
+ n(a), y();
+ },
+ get onChange() {
+ return r();
+ },
+ set onChange(a) {
+ r(a), y();
+ },
+ get activeKeys() {
+ return o();
+ },
+ set activeKeys(a = []) {
+ o(a), y();
+ }
+ });
+}
+Ai(["click", "keydown"]);
+ae(ad, { items: {}, onChange: {}, activeKeys: {} }, [], [], !0);
+function Fn(e, t) {
+ de(t, !0);
+ let n = w(t, "target", 7);
+ typeof n() > "u" && n("undefined");
+ var r = et(), o = be(r);
+ {
+ var i = (a) => {
+ var l = et(), u = be(l);
+ lr(u, () => n() ?? dt), L(a, l);
+ }, s = (a) => {
+ var l = et(), u = be(l);
+ mu(u, n), L(a, l);
+ };
+ ke(o, (a) => {
+ typeof n() == "function" ? a(i) : a(s, !1);
+ });
+ }
+ return L(e, r), fe({
+ get target() {
+ return n();
+ },
+ set target(a) {
+ n(a), y();
+ }
+ });
+}
+ae(Fn, { target: {} }, [], [], !0);
+var Np = (e, t, n) => t(h(n)), Mp = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 14L8 10H16L12 14Z"></path></svg>'), Tp = /* @__PURE__ */ ne('<div class="tf-select-content-children"><!></div>'), Hp = /* @__PURE__ */ ne('<button class="tf-select-content-item"><span><!></span> <!></button> <!>', 1), Vp = /* @__PURE__ */ ne('<div class="tf-select-content nopan nodrag"><!></div>'), Dp = /* @__PURE__ */ ne("<!> <!>", 1), Ap = /* @__PURE__ */ ne('<div class="tf-select-input-placeholder"> </div>'), Lp = /* @__PURE__ */ ne('<button><div class="tf-select-input-value"></div> <div class="tf-select-input-arrow"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg></div></button>'), Op = /* @__PURE__ */ ne("<div><!></div>");
+function sn(e, t) {
+ de(t, !0);
+ const n = (_, v = dt) => {
+ var b = et(), N = be(b);
+ Yt(N, 19, v, (E, M) => `${M}_${E.value}`, (E, M) => {
+ var D = Hp(), V = be(D);
+ V.__click = [Np, x, M];
+ var A = X(V), O = X(A);
+ {
+ var R = (P) => {
+ var H = Mp();
+ L(P, H);
+ };
+ ke(O, (P) => {
+ h(M).children && h(M).children.length > 0 && P(R);
+ });
+ }
+ Z(A);
+ var S = z(A, 2);
+ Fn(S, {
+ get target() {
+ return h(M).label;
+ }
+ }), Z(V);
+ var T = z(V, 2);
+ {
+ var k = (P) => {
+ var H = Tp(), I = X(H);
+ n(I, () => h(M).children), Z(H), L(P, H);
+ };
+ ke(T, (P) => {
+ h(M).children && h(M).children.length > 0 && (l() || c().includes(h(M).value)) && P(k);
+ });
+ }
+ L(E, D);
+ }), L(_, b);
+ };
+ let r = w(t, "items", 7), o = w(t, "onExpand", 7), i = w(t, "onSelect", 7), s = w(t, "value", 23, () => []), a = w(t, "defaultValue", 23, () => []), l = w(t, "expandAll", 7, !0), u = w(t, "multiple", 7, !1), c = w(t, "expandValue", 23, () => []), f = w(t, "placeholder", 7), d = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "items",
+ "onExpand",
+ "onSelect",
+ "value",
+ "defaultValue",
+ "expandAll",
+ "multiple",
+ "expandValue",
+ "placeholder"
+ ]), g = /* @__PURE__ */ Me(() => {
+ const _ = [], v = (b) => {
+ for (let N of b)
+ s().length > 0 ? s().includes(N.value) && _.push(N) : a().includes(N.value) && _.push(N), N.children && N.children.length > 0 && v(N.children);
+ };
+ return v(r()), _;
+ }), p;
+ function x(_) {
+ var v, b;
+ if (_.children && _.children.length > 0) {
+ (v = o()) == null || v(_);
+ return;
+ } else
+ p == null || p.hide(), (b = i()) == null || b(_);
+ }
+ var C = Op();
+ let $;
+ var m = X(C);
+ return An(
+ Lo(m, {
+ floating: (v) => {
+ var b = Vp(), N = X(b);
+ n(N, r), Z(b), L(v, b);
+ },
+ children: (v, b) => {
+ var N = Lp();
+ let E;
+ var M = X(N);
+ Yt(
+ M,
+ 23,
+ () => h(g),
+ (D, V) => `${V}_${D.value}`,
+ (D, V, A) => {
+ var O = et(), R = be(O);
+ {
+ var S = (k) => {
+ var P = et(), H = be(P);
+ {
+ var I = (B) => {
+ Fn(B, {
+ get target() {
+ return h(V).label;
+ }
+ });
+ };
+ ke(H, (B) => {
+ h(A) === 0 && B(I);
+ });
+ }
+ L(k, P);
+ }, T = (k) => {
+ var P = Dp(), H = be(P);
+ Fn(H, {
+ get target() {
+ return h(V).label;
+ }
+ });
+ var I = z(H, 2);
+ {
+ var B = (F) => {
+ var K = Ie(",");
+ L(F, K);
+ };
+ ke(I, (F) => {
+ h(A) < h(g).length - 1 && F(B);
+ });
+ }
+ L(k, P);
+ };
+ ke(R, (k) => {
+ u() ? k(T, !1) : k(S);
+ });
+ }
+ L(D, O);
+ },
+ (D) => {
+ var V = Ap(), A = X(V, !0);
+ Z(V), Ee(() => Rt(A, f())), L(D, V);
+ }
+ ), Z(M), Se(2), Z(N), Ee(() => E = on(N, E, {
+ class: "tf-select-input nopan nodrag",
+ ...d
+ })), L(v, N);
+ },
+ $$slots: { floating: !0, default: !0 }
+ }),
+ (v) => p = v,
+ () => p
+ ), Z(C), Ee(() => $ = on(C, $, {
+ ...d,
+ class: `tf-select ${d.class ?? ""}`
+ })), L(e, C), fe({
+ get items() {
+ return r();
+ },
+ set items(_) {
+ r(_), y();
+ },
+ get onExpand() {
+ return o();
+ },
+ set onExpand(_) {
+ o(_), y();
+ },
+ get onSelect() {
+ return i();
+ },
+ set onSelect(_) {
+ i(_), y();
+ },
+ get value() {
+ return s();
+ },
+ set value(_ = []) {
+ s(_), y();
+ },
+ get defaultValue() {
+ return a();
+ },
+ set defaultValue(_ = []) {
+ a(_), y();
+ },
+ get expandAll() {
+ return l();
+ },
+ set expandAll(_ = !0) {
+ l(_), y();
+ },
+ get multiple() {
+ return u();
+ },
+ set multiple(_ = !1) {
+ u(_), y();
+ },
+ get expandValue() {
+ return c();
+ },
+ set expandValue(_ = []) {
+ c(_), y();
+ },
+ get placeholder() {
+ return f();
+ },
+ set placeholder(_) {
+ f(_), y();
+ }
+ });
+}
+Ai(["click"]);
+ae(
+ sn,
+ {
+ items: {},
+ onExpand: {},
+ onSelect: {},
+ value: {},
+ defaultValue: {},
+ expandAll: {},
+ multiple: {},
+ expandValue: {},
+ placeholder: {}
+ },
+ [],
+ [],
+ !0
+);
+const _o = Math.min, Er = Math.max, bi = Math.round, mn = (e) => ({
+ x: e,
+ y: e
+}), Ip = {
+ left: "right",
+ right: "left",
+ bottom: "top",
+ top: "bottom"
+}, zp = {
+ start: "end",
+ end: "start"
+};
+function Ds(e, t, n) {
+ return Er(e, _o(t, n));
+}
+function Vo(e, t) {
+ return typeof e == "function" ? e(t) : e;
+}
+function fr(e) {
+ return e.split("-")[0];
+}
+function Do(e) {
+ return e.split("-")[1];
+}
+function ld(e) {
+ return e === "x" ? "y" : "x";
+}
+function va(e) {
+ return e === "y" ? "height" : "width";
+}
+function Ir(e) {
+ return ["top", "bottom"].includes(fr(e)) ? "y" : "x";
+}
+function pa(e) {
+ return ld(Ir(e));
+}
+function Rp(e, t, n) {
+ n === void 0 && (n = !1);
+ const r = Do(e), o = pa(e), i = va(o);
+ let s = o === "x" ? r === (n ? "end" : "start") ? "right" : "left" : r === "start" ? "bottom" : "top";
+ return t.reference[i] > t.floating[i] && (s = Ci(s)), [s, Ci(s)];
+}
+function Bp(e) {
+ const t = Ci(e);
+ return [As(e), t, As(t)];
+}
+function As(e) {
+ return e.replace(/start|end/g, (t) => zp[t]);
+}
+function Yp(e, t, n) {
+ const r = ["left", "right"], o = ["right", "left"], i = ["top", "bottom"], s = ["bottom", "top"];
+ switch (e) {
+ case "top":
+ case "bottom":
+ return n ? t ? o : r : t ? r : o;
+ case "left":
+ case "right":
+ return t ? i : s;
+ default:
+ return [];
+ }
+}
+function Zp(e, t, n, r) {
+ const o = Do(e);
+ let i = Yp(fr(e), n === "start", r);
+ return o && (i = i.map((s) => s + "-" + o), t && (i = i.concat(i.map(As)))), i;
+}
+function Ci(e) {
+ return e.replace(/left|right|bottom|top/g, (t) => Ip[t]);
+}
+function Xp(e) {
+ return {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0,
+ ...e
+ };
+}
+function ud(e) {
+ return typeof e != "number" ? Xp(e) : {
+ top: e,
+ right: e,
+ bottom: e,
+ left: e
+ };
+}
+function ki(e) {
+ const {
+ x: t,
+ y: n,
+ width: r,
+ height: o
+ } = e;
+ return {
+ width: r,
+ height: o,
+ top: n,
+ left: t,
+ right: t + r,
+ bottom: n + o,
+ x: t,
+ y: n
+ };
+}
+function Tl(e, t, n) {
+ let {
+ reference: r,
+ floating: o
+ } = e;
+ const i = Ir(t), s = pa(t), a = va(s), l = fr(t), u = i === "y", c = r.x + r.width / 2 - o.width / 2, f = r.y + r.height / 2 - o.height / 2, d = r[a] / 2 - o[a] / 2;
+ let g;
+ switch (l) {
+ case "top":
+ g = {
+ x: c,
+ y: r.y - o.height
+ };
+ break;
+ case "bottom":
+ g = {
+ x: c,
+ y: r.y + r.height
+ };
+ break;
+ case "right":
+ g = {
+ x: r.x + r.width,
+ y: f
+ };
+ break;
+ case "left":
+ g = {
+ x: r.x - o.width,
+ y: f
+ };
+ break;
+ default:
+ g = {
+ x: r.x,
+ y: r.y
+ };
+ }
+ switch (Do(t)) {
+ case "start":
+ g[s] -= d * (n && u ? -1 : 1);
+ break;
+ case "end":
+ g[s] += d * (n && u ? -1 : 1);
+ break;
+ }
+ return g;
+}
+const Fp = async (e, t, n) => {
+ const {
+ placement: r = "bottom",
+ strategy: o = "absolute",
+ middleware: i = [],
+ platform: s
+ } = n, a = i.filter(Boolean), l = await (s.isRTL == null ? void 0 : s.isRTL(t));
+ let u = await s.getElementRects({
+ reference: e,
+ floating: t,
+ strategy: o
+ }), {
+ x: c,
+ y: f
+ } = Tl(u, r, l), d = r, g = {}, p = 0;
+ for (let x = 0; x < a.length; x++) {
+ const {
+ name: C,
+ fn: $
+ } = a[x], {
+ x: m,
+ y: _,
+ data: v,
+ reset: b
+ } = await $({
+ x: c,
+ y: f,
+ initialPlacement: r,
+ placement: d,
+ strategy: o,
+ middlewareData: g,
+ rects: u,
+ platform: s,
+ elements: {
+ reference: e,
+ floating: t
+ }
+ });
+ c = m ?? c, f = _ ?? f, g = {
+ ...g,
+ [C]: {
+ ...g[C],
+ ...v
+ }
+ }, b && p <= 50 && (p++, typeof b == "object" && (b.placement && (d = b.placement), b.rects && (u = b.rects === !0 ? await s.getElementRects({
+ reference: e,
+ floating: t,
+ strategy: o
+ }) : b.rects), {
+ x: c,
+ y: f
+ } = Tl(u, d, l)), x = -1);
+ }
+ return {
+ x: c,
+ y: f,
+ placement: d,
+ strategy: o,
+ middlewareData: g
+ };
+};
+async function cd(e, t) {
+ var n;
+ t === void 0 && (t = {});
+ const {
+ x: r,
+ y: o,
+ platform: i,
+ rects: s,
+ elements: a,
+ strategy: l
+ } = e, {
+ boundary: u = "clippingAncestors",
+ rootBoundary: c = "viewport",
+ elementContext: f = "floating",
+ altBoundary: d = !1,
+ padding: g = 0
+ } = Vo(t, e), p = ud(g), C = a[d ? f === "floating" ? "reference" : "floating" : f], $ = ki(await i.getClippingRect({
+ element: (n = await (i.isElement == null ? void 0 : i.isElement(C))) == null || n ? C : C.contextElement || await (i.getDocumentElement == null ? void 0 : i.getDocumentElement(a.floating)),
+ boundary: u,
+ rootBoundary: c,
+ strategy: l
+ })), m = f === "floating" ? {
+ x: r,
+ y: o,
+ width: s.floating.width,
+ height: s.floating.height
+ } : s.reference, _ = await (i.getOffsetParent == null ? void 0 : i.getOffsetParent(a.floating)), v = await (i.isElement == null ? void 0 : i.isElement(_)) ? await (i.getScale == null ? void 0 : i.getScale(_)) || {
+ x: 1,
+ y: 1
+ } : {
+ x: 1,
+ y: 1
+ }, b = ki(i.convertOffsetParentRelativeRectToViewportRelativeRect ? await i.convertOffsetParentRelativeRectToViewportRelativeRect({
+ elements: a,
+ rect: m,
+ offsetParent: _,
+ strategy: l
+ }) : m);
+ return {
+ top: ($.top - b.top + p.top) / v.y,
+ bottom: (b.bottom - $.bottom + p.bottom) / v.y,
+ left: ($.left - b.left + p.left) / v.x,
+ right: (b.right - $.right + p.right) / v.x
+ };
+}
+const Wp = (e) => ({
+ name: "arrow",
+ options: e,
+ async fn(t) {
+ const {
+ x: n,
+ y: r,
+ placement: o,
+ rects: i,
+ platform: s,
+ elements: a,
+ middlewareData: l
+ } = t, {
+ element: u,
+ padding: c = 0
+ } = Vo(e, t) || {};
+ if (u == null)
+ return {};
+ const f = ud(c), d = {
+ x: n,
+ y: r
+ }, g = pa(o), p = va(g), x = await s.getDimensions(u), C = g === "y", $ = C ? "top" : "left", m = C ? "bottom" : "right", _ = C ? "clientHeight" : "clientWidth", v = i.reference[p] + i.reference[g] - d[g] - i.floating[p], b = d[g] - i.reference[g], N = await (s.getOffsetParent == null ? void 0 : s.getOffsetParent(u));
+ let E = N ? N[_] : 0;
+ (!E || !await (s.isElement == null ? void 0 : s.isElement(N))) && (E = a.floating[_] || i.floating[p]);
+ const M = v / 2 - b / 2, D = E / 2 - x[p] / 2 - 1, V = _o(f[$], D), A = _o(f[m], D), O = V, R = E - x[p] - A, S = E / 2 - x[p] / 2 + M, T = Ds(O, S, R), k = !l.arrow && Do(o) != null && S !== T && i.reference[p] / 2 - (S < O ? V : A) - x[p] / 2 < 0, P = k ? S < O ? S - O : S - R : 0;
+ return {
+ [g]: d[g] + P,
+ data: {
+ [g]: T,
+ centerOffset: S - T - P,
+ ...k && {
+ alignmentOffset: P
+ }
+ },
+ reset: k
+ };
+ }
+}), Kp = function(e) {
+ return e === void 0 && (e = {}), {
+ name: "flip",
+ options: e,
+ async fn(t) {
+ var n, r;
+ const {
+ placement: o,
+ middlewareData: i,
+ rects: s,
+ initialPlacement: a,
+ platform: l,
+ elements: u
+ } = t, {
+ mainAxis: c = !0,
+ crossAxis: f = !0,
+ fallbackPlacements: d,
+ fallbackStrategy: g = "bestFit",
+ fallbackAxisSideDirection: p = "none",
+ flipAlignment: x = !0,
+ ...C
+ } = Vo(e, t);
+ if ((n = i.arrow) != null && n.alignmentOffset)
+ return {};
+ const $ = fr(o), m = Ir(a), _ = fr(a) === a, v = await (l.isRTL == null ? void 0 : l.isRTL(u.floating)), b = d || (_ || !x ? [Ci(a)] : Bp(a)), N = p !== "none";
+ !d && N && b.push(...Zp(a, x, p, v));
+ const E = [a, ...b], M = await cd(t, C), D = [];
+ let V = ((r = i.flip) == null ? void 0 : r.overflows) || [];
+ if (c && D.push(M[$]), f) {
+ const S = Rp(o, s, v);
+ D.push(M[S[0]], M[S[1]]);
+ }
+ if (V = [...V, {
+ placement: o,
+ overflows: D
+ }], !D.every((S) => S <= 0)) {
+ var A, O;
+ const S = (((A = i.flip) == null ? void 0 : A.index) || 0) + 1, T = E[S];
+ if (T)
+ return {
+ data: {
+ index: S,
+ overflows: V
+ },
+ reset: {
+ placement: T
+ }
+ };
+ let k = (O = V.filter((P) => P.overflows[0] <= 0).sort((P, H) => P.overflows[1] - H.overflows[1])[0]) == null ? void 0 : O.placement;
+ if (!k)
+ switch (g) {
+ case "bestFit": {
+ var R;
+ const P = (R = V.filter((H) => {
+ if (N) {
+ const I = Ir(H.placement);
+ return I === m || // Create a bias to the `y` side axis due to horizontal
+ // reading directions favoring greater width.
+ I === "y";
+ }
+ return !0;
+ }).map((H) => [H.placement, H.overflows.filter((I) => I > 0).reduce((I, B) => I + B, 0)]).sort((H, I) => H[1] - I[1])[0]) == null ? void 0 : R[0];
+ P && (k = P);
+ break;
+ }
+ case "initialPlacement":
+ k = a;
+ break;
+ }
+ if (o !== k)
+ return {
+ reset: {
+ placement: k
+ }
+ };
+ }
+ return {};
+ }
+ };
+};
+async function qp(e, t) {
+ const {
+ placement: n,
+ platform: r,
+ elements: o
+ } = e, i = await (r.isRTL == null ? void 0 : r.isRTL(o.floating)), s = fr(n), a = Do(n), l = Ir(n) === "y", u = ["left", "top"].includes(s) ? -1 : 1, c = i && l ? -1 : 1, f = Vo(t, e);
+ let {
+ mainAxis: d,
+ crossAxis: g,
+ alignmentAxis: p
+ } = typeof f == "number" ? {
+ mainAxis: f,
+ crossAxis: 0,
+ alignmentAxis: null
+ } : {
+ mainAxis: f.mainAxis || 0,
+ crossAxis: f.crossAxis || 0,
+ alignmentAxis: f.alignmentAxis
+ };
+ return a && typeof p == "number" && (g = a === "end" ? p * -1 : p), l ? {
+ x: g * c,
+ y: d * u
+ } : {
+ x: d * u,
+ y: g * c
+ };
+}
+const Gp = function(e) {
+ return e === void 0 && (e = 0), {
+ name: "offset",
+ options: e,
+ async fn(t) {
+ var n, r;
+ const {
+ x: o,
+ y: i,
+ placement: s,
+ middlewareData: a
+ } = t, l = await qp(t, e);
+ return s === ((n = a.offset) == null ? void 0 : n.placement) && (r = a.arrow) != null && r.alignmentOffset ? {} : {
+ x: o + l.x,
+ y: i + l.y,
+ data: {
+ ...l,
+ placement: s
+ }
+ };
+ }
+ };
+}, Up = function(e) {
+ return e === void 0 && (e = {}), {
+ name: "shift",
+ options: e,
+ async fn(t) {
+ const {
+ x: n,
+ y: r,
+ placement: o
+ } = t, {
+ mainAxis: i = !0,
+ crossAxis: s = !1,
+ limiter: a = {
+ fn: (C) => {
+ let {
+ x: $,
+ y: m
+ } = C;
+ return {
+ x: $,
+ y: m
+ };
+ }
+ },
+ ...l
+ } = Vo(e, t), u = {
+ x: n,
+ y: r
+ }, c = await cd(t, l), f = Ir(fr(o)), d = ld(f);
+ let g = u[d], p = u[f];
+ if (i) {
+ const C = d === "y" ? "top" : "left", $ = d === "y" ? "bottom" : "right", m = g + c[C], _ = g - c[$];
+ g = Ds(m, g, _);
+ }
+ if (s) {
+ const C = f === "y" ? "top" : "left", $ = f === "y" ? "bottom" : "right", m = p + c[C], _ = p - c[$];
+ p = Ds(m, p, _);
+ }
+ const x = a.fn({
+ ...t,
+ [d]: g,
+ [f]: p
+ });
+ return {
+ ...x,
+ data: {
+ x: x.x - n,
+ y: x.y - r,
+ enabled: {
+ [d]: i,
+ [f]: s
+ }
+ }
+ };
+ }
+ };
+};
+function Ki() {
+ return typeof window < "u";
+}
+function Wr(e) {
+ return dd(e) ? (e.nodeName || "").toLowerCase() : "#document";
+}
+function Bt(e) {
+ var t;
+ return (e == null || (t = e.ownerDocument) == null ? void 0 : t.defaultView) || window;
+}
+function zn(e) {
+ var t;
+ return (t = (dd(e) ? e.ownerDocument : e.document) || window.document) == null ? void 0 : t.documentElement;
+}
+function dd(e) {
+ return Ki() ? e instanceof Node || e instanceof Bt(e).Node : !1;
+}
+function an(e) {
+ return Ki() ? e instanceof Element || e instanceof Bt(e).Element : !1;
+}
+function _n(e) {
+ return Ki() ? e instanceof HTMLElement || e instanceof Bt(e).HTMLElement : !1;
+}
+function Hl(e) {
+ return !Ki() || typeof ShadowRoot > "u" ? !1 : e instanceof ShadowRoot || e instanceof Bt(e).ShadowRoot;
+}
+function Ao(e) {
+ const {
+ overflow: t,
+ overflowX: n,
+ overflowY: r,
+ display: o
+ } = ln(e);
+ return /auto|scroll|overlay|hidden|clip/.test(t + r + n) && !["inline", "contents"].includes(o);
+}
+function jp(e) {
+ return ["table", "td", "th"].includes(Wr(e));
+}
+function qi(e) {
+ return [":popover-open", ":modal"].some((t) => {
+ try {
+ return e.matches(t);
+ } catch {
+ return !1;
+ }
+ });
+}
+function ma(e) {
+ const t = ya(), n = an(e) ? ln(e) : e;
+ return ["transform", "translate", "scale", "rotate", "perspective"].some((r) => n[r] ? n[r] !== "none" : !1) || (n.containerType ? n.containerType !== "normal" : !1) || !t && (n.backdropFilter ? n.backdropFilter !== "none" : !1) || !t && (n.filter ? n.filter !== "none" : !1) || ["transform", "translate", "scale", "rotate", "perspective", "filter"].some((r) => (n.willChange || "").includes(r)) || ["paint", "layout", "strict", "content"].some((r) => (n.contain || "").includes(r));
+}
+function Jp(e) {
+ let t = er(e);
+ for (; _n(t) && !zr(t); ) {
+ if (ma(t))
+ return t;
+ if (qi(t))
+ return null;
+ t = er(t);
+ }
+ return null;
+}
+function ya() {
+ return typeof CSS > "u" || !CSS.supports ? !1 : CSS.supports("-webkit-backdrop-filter", "none");
+}
+function zr(e) {
+ return ["html", "body", "#document"].includes(Wr(e));
+}
+function ln(e) {
+ return Bt(e).getComputedStyle(e);
+}
+function Gi(e) {
+ return an(e) ? {
+ scrollLeft: e.scrollLeft,
+ scrollTop: e.scrollTop
+ } : {
+ scrollLeft: e.scrollX,
+ scrollTop: e.scrollY
+ };
+}
+function er(e) {
+ if (Wr(e) === "html")
+ return e;
+ const t = (
+ // Step into the shadow DOM of the parent of a slotted node.
+ e.assignedSlot || // DOM Element detected.
+ e.parentNode || // ShadowRoot detected.
+ Hl(e) && e.host || // Fallback.
+ zn(e)
+ );
+ return Hl(t) ? t.host : t;
+}
+function fd(e) {
+ const t = er(e);
+ return zr(t) ? e.ownerDocument ? e.ownerDocument.body : e.body : _n(t) && Ao(t) ? t : fd(t);
+}
+function gd(e, t, n) {
+ var r;
+ t === void 0 && (t = []);
+ const o = fd(e), i = o === ((r = e.ownerDocument) == null ? void 0 : r.body), s = Bt(o);
+ return i ? (Ls(s), t.concat(s, s.visualViewport || [], Ao(o) ? o : [], [])) : t.concat(o, gd(o, []));
+}
+function Ls(e) {
+ return e.parent && Object.getPrototypeOf(e.parent) ? e.frameElement : null;
+}
+function hd(e) {
+ const t = ln(e);
+ let n = parseFloat(t.width) || 0, r = parseFloat(t.height) || 0;
+ const o = _n(e), i = o ? e.offsetWidth : n, s = o ? e.offsetHeight : r, a = bi(n) !== i || bi(r) !== s;
+ return a && (n = i, r = s), {
+ width: n,
+ height: r,
+ $: a
+ };
+}
+function vd(e) {
+ return an(e) ? e : e.contextElement;
+}
+function Sr(e) {
+ const t = vd(e);
+ if (!_n(t))
+ return mn(1);
+ const n = t.getBoundingClientRect(), {
+ width: r,
+ height: o,
+ $: i
+ } = hd(t);
+ let s = (i ? bi(n.width) : n.width) / r, a = (i ? bi(n.height) : n.height) / o;
+ return (!s || !Number.isFinite(s)) && (s = 1), (!a || !Number.isFinite(a)) && (a = 1), {
+ x: s,
+ y: a
+ };
+}
+const Qp = /* @__PURE__ */ mn(0);
+function pd(e) {
+ const t = Bt(e);
+ return !ya() || !t.visualViewport ? Qp : {
+ x: t.visualViewport.offsetLeft,
+ y: t.visualViewport.offsetTop
+ };
+}
+function em(e, t, n) {
+ return t === void 0 && (t = !1), !n || t && n !== Bt(e) ? !1 : t;
+}
+function xo(e, t, n, r) {
+ t === void 0 && (t = !1), n === void 0 && (n = !1);
+ const o = e.getBoundingClientRect(), i = vd(e);
+ let s = mn(1);
+ t && (r ? an(r) && (s = Sr(r)) : s = Sr(e));
+ const a = em(i, n, r) ? pd(i) : mn(0);
+ let l = (o.left + a.x) / s.x, u = (o.top + a.y) / s.y, c = o.width / s.x, f = o.height / s.y;
+ if (i) {
+ const d = Bt(i), g = r && an(r) ? Bt(r) : r;
+ let p = d, x = Ls(p);
+ for (; x && r && g !== p; ) {
+ const C = Sr(x), $ = x.getBoundingClientRect(), m = ln(x), _ = $.left + (x.clientLeft + parseFloat(m.paddingLeft)) * C.x, v = $.top + (x.clientTop + parseFloat(m.paddingTop)) * C.y;
+ l *= C.x, u *= C.y, c *= C.x, f *= C.y, l += _, u += v, p = Bt(x), x = Ls(p);
+ }
+ }
+ return ki({
+ width: c,
+ height: f,
+ x: l,
+ y: u
+ });
+}
+function wa(e, t) {
+ const n = Gi(e).scrollLeft;
+ return t ? t.left + n : xo(zn(e)).left + n;
+}
+function md(e, t, n) {
+ n === void 0 && (n = !1);
+ const r = e.getBoundingClientRect(), o = r.left + t.scrollLeft - (n ? 0 : (
+ // RTL <body> scrollbar.
+ wa(e, r)
+ )), i = r.top + t.scrollTop;
+ return {
+ x: o,
+ y: i
+ };
+}
+function tm(e) {
+ let {
+ elements: t,
+ rect: n,
+ offsetParent: r,
+ strategy: o
+ } = e;
+ const i = o === "fixed", s = zn(r), a = t ? qi(t.floating) : !1;
+ if (r === s || a && i)
+ return n;
+ let l = {
+ scrollLeft: 0,
+ scrollTop: 0
+ }, u = mn(1);
+ const c = mn(0), f = _n(r);
+ if ((f || !f && !i) && ((Wr(r) !== "body" || Ao(s)) && (l = Gi(r)), _n(r))) {
+ const g = xo(r);
+ u = Sr(r), c.x = g.x + r.clientLeft, c.y = g.y + r.clientTop;
+ }
+ const d = s && !f && !i ? md(s, l, !0) : mn(0);
+ return {
+ width: n.width * u.x,
+ height: n.height * u.y,
+ x: n.x * u.x - l.scrollLeft * u.x + c.x + d.x,
+ y: n.y * u.y - l.scrollTop * u.y + c.y + d.y
+ };
+}
+function nm(e) {
+ return Array.from(e.getClientRects());
+}
+function rm(e) {
+ const t = zn(e), n = Gi(e), r = e.ownerDocument.body, o = Er(t.scrollWidth, t.clientWidth, r.scrollWidth, r.clientWidth), i = Er(t.scrollHeight, t.clientHeight, r.scrollHeight, r.clientHeight);
+ let s = -n.scrollLeft + wa(e);
+ const a = -n.scrollTop;
+ return ln(r).direction === "rtl" && (s += Er(t.clientWidth, r.clientWidth) - o), {
+ width: o,
+ height: i,
+ x: s,
+ y: a
+ };
+}
+function om(e, t) {
+ const n = Bt(e), r = zn(e), o = n.visualViewport;
+ let i = r.clientWidth, s = r.clientHeight, a = 0, l = 0;
+ if (o) {
+ i = o.width, s = o.height;
+ const u = ya();
+ (!u || u && t === "fixed") && (a = o.offsetLeft, l = o.offsetTop);
+ }
+ return {
+ width: i,
+ height: s,
+ x: a,
+ y: l
+ };
+}
+function im(e, t) {
+ const n = xo(e, !0, t === "fixed"), r = n.top + e.clientTop, o = n.left + e.clientLeft, i = _n(e) ? Sr(e) : mn(1), s = e.clientWidth * i.x, a = e.clientHeight * i.y, l = o * i.x, u = r * i.y;
+ return {
+ width: s,
+ height: a,
+ x: l,
+ y: u
+ };
+}
+function Vl(e, t, n) {
+ let r;
+ if (t === "viewport")
+ r = om(e, n);
+ else if (t === "document")
+ r = rm(zn(e));
+ else if (an(t))
+ r = im(t, n);
+ else {
+ const o = pd(e);
+ r = {
+ x: t.x - o.x,
+ y: t.y - o.y,
+ width: t.width,
+ height: t.height
+ };
+ }
+ return ki(r);
+}
+function yd(e, t) {
+ const n = er(e);
+ return n === t || !an(n) || zr(n) ? !1 : ln(n).position === "fixed" || yd(n, t);
+}
+function sm(e, t) {
+ const n = t.get(e);
+ if (n)
+ return n;
+ let r = gd(e, []).filter((a) => an(a) && Wr(a) !== "body"), o = null;
+ const i = ln(e).position === "fixed";
+ let s = i ? er(e) : e;
+ for (; an(s) && !zr(s); ) {
+ const a = ln(s), l = ma(s);
+ !l && a.position === "fixed" && (o = null), (i ? !l && !o : !l && a.position === "static" && !!o && ["absolute", "fixed"].includes(o.position) || Ao(s) && !l && yd(e, s)) ? r = r.filter((c) => c !== s) : o = a, s = er(s);
+ }
+ return t.set(e, r), r;
+}
+function am(e) {
+ let {
+ element: t,
+ boundary: n,
+ rootBoundary: r,
+ strategy: o
+ } = e;
+ const s = [...n === "clippingAncestors" ? qi(t) ? [] : sm(t, this._c) : [].concat(n), r], a = s[0], l = s.reduce((u, c) => {
+ const f = Vl(t, c, o);
+ return u.top = Er(f.top, u.top), u.right = _o(f.right, u.right), u.bottom = _o(f.bottom, u.bottom), u.left = Er(f.left, u.left), u;
+ }, Vl(t, a, o));
+ return {
+ width: l.right - l.left,
+ height: l.bottom - l.top,
+ x: l.left,
+ y: l.top
+ };
+}
+function lm(e) {
+ const {
+ width: t,
+ height: n
+ } = hd(e);
+ return {
+ width: t,
+ height: n
+ };
+}
+function um(e, t, n) {
+ const r = _n(t), o = zn(t), i = n === "fixed", s = xo(e, !0, i, t);
+ let a = {
+ scrollLeft: 0,
+ scrollTop: 0
+ };
+ const l = mn(0);
+ if (r || !r && !i)
+ if ((Wr(t) !== "body" || Ao(o)) && (a = Gi(t)), r) {
+ const d = xo(t, !0, i, t);
+ l.x = d.x + t.clientLeft, l.y = d.y + t.clientTop;
+ } else o && (l.x = wa(o));
+ const u = o && !r && !i ? md(o, a) : mn(0), c = s.left + a.scrollLeft - l.x - u.x, f = s.top + a.scrollTop - l.y - u.y;
+ return {
+ x: c,
+ y: f,
+ width: s.width,
+ height: s.height
+ };
+}
+function fs(e) {
+ return ln(e).position === "static";
+}
+function Dl(e, t) {
+ if (!_n(e) || ln(e).position === "fixed")
+ return null;
+ if (t)
+ return t(e);
+ let n = e.offsetParent;
+ return zn(e) === n && (n = n.ownerDocument.body), n;
+}
+function wd(e, t) {
+ const n = Bt(e);
+ if (qi(e))
+ return n;
+ if (!_n(e)) {
+ let o = er(e);
+ for (; o && !zr(o); ) {
+ if (an(o) && !fs(o))
+ return o;
+ o = er(o);
+ }
+ return n;
+ }
+ let r = Dl(e, t);
+ for (; r && jp(r) && fs(r); )
+ r = Dl(r, t);
+ return r && zr(r) && fs(r) && !ma(r) ? n : r || Jp(e) || n;
+}
+const cm = async function(e) {
+ const t = this.getOffsetParent || wd, n = this.getDimensions, r = await n(e.floating);
+ return {
+ reference: um(e.reference, await t(e.floating), e.strategy),
+ floating: {
+ x: 0,
+ y: 0,
+ width: r.width,
+ height: r.height
+ }
+ };
+};
+function dm(e) {
+ return ln(e).direction === "rtl";
+}
+const fm = {
+ convertOffsetParentRelativeRectToViewportRelativeRect: tm,
+ getDocumentElement: zn,
+ getClippingRect: am,
+ getOffsetParent: wd,
+ getElementRects: cm,
+ getClientRects: nm,
+ getDimensions: lm,
+ getScale: Sr,
+ isElement: an,
+ isRTL: dm
+}, gm = Gp, hm = Up, vm = Kp, pm = Wp, mm = (e, t, n) => {
+ const r = /* @__PURE__ */ new Map(), o = {
+ platform: fm,
+ ...n
+ }, i = {
+ ...o.platform,
+ _c: r
+ };
+ return Fp(e, t, {
+ ...o,
+ platform: i
+ });
+}, ym = ({
+ trigger: e,
+ triggerEvent: t,
+ floatContent: n,
+ placement: r = "bottom",
+ offsetOptions: o,
+ flipOptions: i,
+ shiftOptions: s,
+ interactive: a,
+ showArrow: l
+}) => {
+ if (typeof e == "string") {
+ const $ = document.querySelector(e);
+ if ($)
+ e = $;
+ else
+ throw new Error("element not found by document.querySelector('" + e + "')");
+ }
+ let u;
+ if (typeof n == "string") {
+ const $ = document.querySelector(n);
+ if ($)
+ u = $;
+ else
+ throw new Error("element not found by document.querySelector('" + n + "')");
+ } else
+ u = n;
+ let c;
+ l && (c = document.createElement("div"), c.style.position = "absolute", c.style.backgroundColor = "#222", c.style.width = "8px", c.style.height = "8px", c.style.transform = "rotate(45deg)", c.style.display = "none", u.firstElementChild.before(c));
+ function f() {
+ mm(e, u, {
+ placement: r,
+ middleware: [
+ gm(o),
+ // 鎵嬪姩鍋忕Щ閰嶇疆
+ vm(i),
+ //鑷姩缈昏浆
+ hm(s),
+ //鑷姩鍋忕Щ锛堜娇寰楁诞鍔ㄥ厓绱犺兘澶熻繘鍏ヨ閲庯級
+ ...l ? [pm({ element: c })] : []
+ ]
+ }).then(({ x: $, y: m, placement: _, middlewareData: v }) => {
+ if (Object.assign(u.style, {
+ left: `${$}px`,
+ top: `${m}px`
+ }), l) {
+ const { x: b, y: N } = v.arrow, E = {
+ top: "bottom",
+ right: "left",
+ bottom: "top",
+ left: "right"
+ }[_.split("-")[0]];
+ Object.assign(c.style, {
+ zIndex: -1,
+ left: b != null ? `${b}px` : "",
+ top: N != null ? `${N}px` : "",
+ right: "",
+ bottom: "",
+ [E]: "2px"
+ });
+ }
+ });
+ }
+ let d = !1;
+ function g() {
+ u.style.display = "block", u.style.visibility = "block", u.style.position = "absolute", l && (c.style.display = "block"), d = !0, f();
+ }
+ function p() {
+ u.style.display = "none", l && (c.style.display = "none"), d = !1;
+ }
+ function x($) {
+ $.stopPropagation(), d ? p() : g();
+ }
+ function C($) {
+ u.contains($.target) || p();
+ }
+ return (!t || t.length == 0) && (t = ["click"]), t.forEach(($) => {
+ e.addEventListener($, x);
+ }), document.addEventListener("click", C), {
+ destroy() {
+ t.forEach(($) => {
+ e.removeEventListener($, x);
+ }), document.removeEventListener("click", C);
+ },
+ hide() {
+ p();
+ },
+ isVisible() {
+ return d;
+ }
+ };
+};
+var wm = /* @__PURE__ */ ne('<div style="position: relative"><div><!></div> <div style="display: none; width: 100%;z-index: 9999"><!></div></div>');
+function Lo(e, t) {
+ de(t, !0);
+ const n = w(t, "children", 7), r = w(t, "floating", 7), o = w(t, "placement", 7, "bottom");
+ let i, s, a;
+ un(() => (a = ym({
+ trigger: i,
+ floatContent: s,
+ interactive: !0,
+ placement: o()
+ }), () => {
+ a.destroy();
+ }));
+ function l() {
+ a.hide();
+ }
+ var u = wm(), c = X(u), f = X(c);
+ lr(f, n), Z(c), An(c, (p) => i = p, () => i);
+ var d = z(c, 2), g = X(d);
+ return lr(g, r), Z(d), An(d, (p) => s = p, () => s), Z(u), L(e, u), fe({
+ hide: l,
+ get children() {
+ return n();
+ },
+ set children(p) {
+ n(p), y();
+ },
+ get floating() {
+ return r();
+ },
+ set floating(p) {
+ r(p), y();
+ },
+ get placement() {
+ return o();
+ },
+ set placement(p = "bottom") {
+ o(p), y();
+ }
+ });
+}
+ae(Lo, { children: {}, floating: {}, placement: {} }, [], ["hide"], !0);
+function Ge(e, t) {
+ de(t, !0);
+ const n = w(t, "children", 7), r = w(t, "level", 7, 1), o = w(t, "mt", 7), i = w(t, "mb", 7);
+ var s = et(), a = be(s);
+ return m1(a, () => `h${r()}`, !1, (l, u) => {
+ let c;
+ Ee(() => c = on(
+ l,
+ c,
+ {
+ class: "tf-heading",
+ style: `margin-top:${o() || "0"};margin-bottom:${i() || "0"}`
+ },
+ void 0,
+ l.namespaceURI === Rl,
+ l.nodeName.includes("-")
+ ));
+ var f = et(), d = be(f);
+ lr(d, () => n() ?? dt), L(u, f);
+ }), L(e, s), fe({
+ get children() {
+ return n();
+ },
+ set children(l) {
+ n(l), y();
+ },
+ get level() {
+ return r();
+ },
+ set level(l = 1) {
+ r(l), y();
+ },
+ get mt() {
+ return o();
+ },
+ set mt(l) {
+ o(l), y();
+ },
+ get mb() {
+ return i();
+ },
+ set mb(l) {
+ i(l), y();
+ }
+ });
+}
+ae(Ge, { children: {}, level: {}, mt: {}, mb: {} }, [], [], !0);
+var _m = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="svelte-1rvn4a8"><path d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" class="svelte-1rvn4a8"></path></svg>');
+const xm = {
+ hash: "svelte-1rvn4a8",
+ code: ".input-btn-more {border:1px solid transparent;padding:3px;&:hover {background:#eee;border:1px solid transparent;}}"
+};
+function Ui(e, t) {
+ de(t, !0), Je(e, xm);
+ const n = /* @__PURE__ */ yt(t, ["$$slots", "$$events", "$$legacy", "$$host"]);
+ Ke(e, ut(() => n, {
+ get class() {
+ return `input-btn-more ${t.class ?? ""}`;
+ },
+ children: (r, o) => {
+ var i = _m();
+ L(r, i);
+ },
+ $$slots: { default: !0 }
+ })), fe();
+}
+ae(Ui, {}, [], [], !0);
+const bm = () => {
+ const e = Ue();
+ return {
+ deleteNode: (n) => {
+ e.nodes.update((r) => r.filter((o) => o.id !== n)), e.edges.update(
+ (r) => r.filter((o) => o.source !== n && o.target !== n)
+ );
+ }
+ };
+}, Rr = (e = 16) => {
+ const t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", n = new Uint8Array(e);
+ return crypto.getRandomValues(n), Array.from(n, (r) => t[r % t.length]).join("");
+}, Cm = () => {
+ const { nodes: e, nodeLookup: t } = Ue();
+ return {
+ copyNode: (r) => {
+ var s;
+ const i = (s = q(t).get(r)) == null ? void 0 : s.internals.userNode;
+ if (i) {
+ const a = Rr(), l = {
+ ...i,
+ id: a,
+ position: {
+ x: i.position.x + 50,
+ y: i.position.y + 50
+ }
+ };
+ e.update((u) => [...u, l]), e.update(
+ (u) => u.map(
+ (c) => c.id === a ? { ...c, selected: !0 } : { ...c, selected: !1 }
+ )
+ );
+ }
+ }
+ };
+};
+var km = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 18.3915V5.60846L18.2264 12L8 18.3915ZM6 3.80421V20.1957C6 20.9812 6.86395 21.46 7.53 21.0437L20.6432 12.848C21.2699 12.4563 21.2699 11.5436 20.6432 11.152L7.53 2.95621C6.86395 2.53993 6 3.01878 6 3.80421Z"></path></svg>'), $m = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z"></path></svg>'), Em = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"></path></svg>'), Sm = /* @__PURE__ */ ne('<div class="tf-node-toolbar svelte-44dmwv"><!> <!> <!></div>'), Pm = /* @__PURE__ */ ne('<!> <div class="tf-node-wrapper"><div class="tf-node-wrapper-title">TinyFlow.ai</div> <div class="tf-node-wrapper-body"><!></div></div> <!> <!> <!>', 1);
+const Nm = {
+ hash: "svelte-44dmwv",
+ code: ".tf-node-toolbar.svelte-44dmwv {display:flex;gap:5px;padding:5px;border-radius:5px;background:#fff;border:1px solid #eee;box-shadow:0 0 5px rgba(0, 0, 0, 0.1);}.tf-node-toolbar-item {border:1px solid transparent;}"
+};
+function dn(e, t) {
+ de(t, !0), Je(e, Nm);
+ const n = w(t, "data", 7), r = w(t, "id", 7, ""), o = w(t, "icon", 7), i = w(t, "handle", 7), s = w(t, "children", 7), a = w(t, "allowExecute", 7, !0), l = w(t, "allowCopy", 7, !0), u = w(t, "allowDelete", 7, !0), c = w(t, "showSourceHandle", 7, !0), f = w(t, "showTargetHandle", 7, !0);
+ let d = n().expand ? ["key"] : [];
+ const { updateNodeData: g } = Dt(), p = [
+ {
+ key: "key",
+ icon: o(),
+ title: n().title,
+ description: n().description,
+ content: s()
+ }
+ ], { deleteNode: x } = bm(), { copyNode: C } = Cm();
+ var $ = Pm(), m = be($);
+ {
+ var _ = (O) => {
+ od(O, {
+ get position() {
+ return $e.Top;
+ },
+ align: "end",
+ children: (R, S) => {
+ var T = Sm(), k = X(T);
+ {
+ var P = (K) => {
+ Ke(K, {
+ class: "tf-node-toolbar-item",
+ children: (ie, ee) => {
+ var W = km();
+ L(ie, W);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(k, (K) => {
+ a() && K(P);
+ });
+ }
+ var H = z(k, 2);
+ {
+ var I = (K) => {
+ Ke(K, {
+ class: "tf-node-toolbar-item",
+ onclick: () => {
+ C(r());
+ },
+ children: (ie, ee) => {
+ var W = $m();
+ L(ie, W);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(H, (K) => {
+ l() && K(I);
+ });
+ }
+ var B = z(H, 2);
+ {
+ var F = (K) => {
+ Ke(K, {
+ class: "tf-node-toolbar-item",
+ onclick: () => {
+ x(r());
+ },
+ children: (ie, ee) => {
+ var W = Em();
+ L(ie, W);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(B, (K) => {
+ u() && K(F);
+ });
+ }
+ Z(T), L(R, T);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(m, (O) => {
+ (a() || l() || u()) && O(_);
+ });
+ }
+ var v = z(m, 2), b = z(X(v), 2), N = X(b);
+ ad(N, {
+ items: p,
+ activeKeys: d,
+ onChange: (O, R) => {
+ g(r(), { expand: R == null ? void 0 : R.includes("key") });
+ }
+ }), Z(b), Z(v);
+ var E = z(v, 2);
+ {
+ var M = (O) => {
+ Qn(O, {
+ type: "target",
+ get position() {
+ return $e.Left;
+ },
+ style: " left: -12px;top: 20px"
+ });
+ };
+ ke(E, (O) => {
+ f() && O(M);
+ });
+ }
+ var D = z(E, 2);
+ {
+ var V = (O) => {
+ Qn(O, {
+ type: "source",
+ get position() {
+ return $e.Right;
+ },
+ style: "right: -12px;top: 20px"
+ });
+ };
+ ke(D, (O) => {
+ c() && O(V);
+ });
+ }
+ var A = z(D, 2);
+ return lr(A, () => i() ?? dt), L(e, $), fe({
+ get data() {
+ return n();
+ },
+ set data(O) {
+ n(O), y();
+ },
+ get id() {
+ return r();
+ },
+ set id(O = "") {
+ r(O), y();
+ },
+ get icon() {
+ return o();
+ },
+ set icon(O) {
+ o(O), y();
+ },
+ get handle() {
+ return i();
+ },
+ set handle(O) {
+ i(O), y();
+ },
+ get children() {
+ return s();
+ },
+ set children(O) {
+ s(O), y();
+ },
+ get allowExecute() {
+ return a();
+ },
+ set allowExecute(O = !0) {
+ a(O), y();
+ },
+ get allowCopy() {
+ return l();
+ },
+ set allowCopy(O = !0) {
+ l(O), y();
+ },
+ get allowDelete() {
+ return u();
+ },
+ set allowDelete(O = !0) {
+ u(O), y();
+ },
+ get showSourceHandle() {
+ return c();
+ },
+ set showSourceHandle(O = !0) {
+ c(O), y();
+ },
+ get showTargetHandle() {
+ return f();
+ },
+ set showTargetHandle(O = !0) {
+ f(O), y();
+ }
+ });
+}
+ae(
+ dn,
+ {
+ data: {},
+ id: {},
+ icon: {},
+ handle: {},
+ children: {},
+ allowExecute: {},
+ allowCopy: {},
+ allowDelete: {},
+ showSourceHandle: {},
+ showTargetHandle: {}
+ },
+ [],
+ [],
+ !0
+);
+function ht() {
+ return ar("svelteflow__node_id");
+}
+const _d = [
+ {
+ value: "String",
+ label: "String"
+ },
+ {
+ value: "Number",
+ label: "Number"
+ },
+ {
+ value: "Boolean",
+ label: "Boolean"
+ },
+ {
+ value: "File",
+ label: "File"
+ },
+ {
+ value: "Object",
+ label: "Object"
+ },
+ {
+ value: "Array",
+ label: "Array"
+ }
+], Mm = [
+ {
+ value: "ref",
+ label: "寮曠敤"
+ },
+ {
+ value: "input",
+ label: "鍥哄畾鍊�"
+ }
+];
+var Tm = /* @__PURE__ */ ne('<div class="input-more-setting svelte-laou7w"><div class="input-more-item svelte-laou7w">鍙傛暟绫诲瀷锛� <!></div> <div class="input-more-item svelte-laou7w">榛樿鍊硷細 <!></div> <div class="input-more-item svelte-laou7w">鍙傛暟鎻忚堪锛� <!></div> <div class="input-more-item svelte-laou7w"><!></div></div>'), Hm = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path></svg>'), Vm = /* @__PURE__ */ ne('<div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div>', 1);
+const Dm = {
+ hash: "svelte-laou7w",
+ code: ".input-item.svelte-laou7w {display:flex;align-items:center;}.input-more-setting.svelte-laou7w {display:flex;flex-direction:column;gap:10px;padding:10px;background:#fff;border:1px solid #ddd;border-radius:5px;width:200px;box-shadow:0 0 10px 2px rgba(0, 0, 0, 0.1);}.input-more-setting.svelte-laou7w .input-more-item:where(.svelte-laou7w) {display:flex;flex-direction:column;gap:3px;font-size:12px;color:#666;}"
+};
+function xd(e, t) {
+ de(t, !0), Je(e, Dm);
+ const [n, r] = tt(), o = () => Q(h(l), "$node", n), i = w(t, "parameter", 7), s = w(t, "index", 7);
+ let a = ht(), l = /* @__PURE__ */ Me(() => pr(a)), u = /* @__PURE__ */ Me(() => {
+ var M, D;
+ return {
+ ...i(),
+ ...(D = (M = o()) == null ? void 0 : M.data) == null ? void 0 : D.parameters[s()]
+ };
+ });
+ const { updateNodeData: c } = Dt(), f = (M) => {
+ const D = M.target.value;
+ c(a, (V) => {
+ let A = V.data.parameters;
+ return A[s()].name = D, { parameters: A };
+ });
+ }, d = (M) => {
+ const D = M.target.checked;
+ c(a, (V) => {
+ let A = V.data.parameters;
+ return A[s()].required = D, { parameters: A };
+ });
+ }, g = (M) => {
+ const D = M.value;
+ D && c(a, (V) => {
+ let A = V.data.parameters;
+ return A[s()].dataType = D, { parameters: A };
+ });
+ };
+ let p;
+ const x = () => {
+ c(a, (M) => {
+ let D = M.data.parameters;
+ return D.splice(s(), 1), { parameters: [...D] };
+ }), p == null || p.hide();
+ };
+ var C = Vm(), $ = be(C), m = X($);
+ xt(m, {
+ style: "width: 100%;",
+ get value() {
+ return h(u).name;
+ },
+ placeholder: "璇疯緭鍏ュ弬鏁板悕绉�",
+ oninput: f
+ }), Z($);
+ var _ = z($, 2), v = X(_);
+ id(v, {
+ get checked() {
+ return h(u).required;
+ },
+ onchange: d
+ }), Z(_);
+ var b = z(_, 2), N = X(b);
+ An(
+ Lo(N, {
+ placement: "bottom",
+ floating: (D) => {
+ var V = Tm(), A = X(V), O = z(X(A));
+ const R = /* @__PURE__ */ Me(() => h(u).dataType ? [h(u).dataType] : ["String"]);
+ sn(O, {
+ items: _d,
+ style: "width: 100%",
+ onSelect: g,
+ get value() {
+ return h(R);
+ }
+ }), Z(A);
+ var S = z(A, 2), T = z(X(S));
+ $t(T, { rows: 1, style: "width: 100%;" }), Z(S);
+ var k = z(S, 2), P = z(X(k));
+ $t(P, { rows: 3, style: "width: 100%;" }), Z(k);
+ var H = z(k, 2), I = X(H);
+ Ke(I, {
+ onclick: x,
+ children: (B, F) => {
+ Se();
+ var K = Ie("鍒犻櫎");
+ L(B, K);
+ },
+ $$slots: { default: !0 }
+ }), Z(H), Z(V), L(D, V);
+ },
+ children: (D, V) => {
+ Ke(D, {
+ class: "input-btn-more",
+ children: (A, O) => {
+ var R = Hm();
+ L(A, R);
+ },
+ $$slots: { default: !0 }
+ });
+ },
+ $$slots: { floating: !0, default: !0 }
+ }),
+ (D) => p = D,
+ () => p
+ ), Z(b), L(e, C);
+ var E = fe({
+ get parameter() {
+ return i();
+ },
+ set parameter(M) {
+ i(M), y();
+ },
+ get index() {
+ return s();
+ },
+ set index(M) {
+ s(M), y();
+ }
+ });
+ return r(), E;
+}
+ae(xd, { parameter: {}, index: {} }, [], [], !0);
+var Am = /* @__PURE__ */ ne('<div class="input-header svelte-3n0wca">鍙傛暟鍚嶇О</div> <div class="input-header svelte-3n0wca">蹇呭~</div> <div class="input-header svelte-3n0wca"></div>', 1), Lm = /* @__PURE__ */ ne('<div class="none-params svelte-3n0wca">鏃犺緭鍏ュ弬鏁�</div>'), Om = /* @__PURE__ */ ne('<div class="input-container svelte-3n0wca"><!> <!></div>');
+const Im = {
+ hash: "svelte-3n0wca",
+ code: `.input-container.svelte-3n0wca {display:grid;grid-template-columns:80% 10% 10%;row-gap:5px;column-gap:3px;}.input-container.svelte-3n0wca .none-params:where(.svelte-3n0wca) {font-size:12px;background:#f8f8f8;height:40px;display:flex;justify-content:center;align-items:center;border-radius:5px;width:calc(100% - 5px);grid-column:1 / -1;
+ /* 浠庣涓�鍒楀紑濮嬪埌鏈�鍚庝竴鍒楃粨鏉� */}.input-container.svelte-3n0wca .input-header:where(.svelte-3n0wca) {font-size:12px;color:#666;}`
+};
+function bd(e, t) {
+ de(t, !0), Je(e, Im);
+ const [n, r] = tt(), o = () => Q(h(s), "$node", n);
+ let i = ht(), s = /* @__PURE__ */ Me(() => pr(i)), a = /* @__PURE__ */ Me(() => {
+ var d, g;
+ return [...((g = (d = o()) == null ? void 0 : d.data) == null ? void 0 : g.parameters) || []];
+ });
+ var l = Om(), u = X(l);
+ {
+ var c = (d) => {
+ var g = Am();
+ Se(4), L(d, g);
+ };
+ ke(u, (d) => {
+ h(a).length !== 0 && d(c);
+ });
+ }
+ var f = z(u, 2);
+ Yt(
+ f,
+ 19,
+ () => h(a),
+ (d) => d.id,
+ (d, g, p) => {
+ xd(d, {
+ get parameter() {
+ return h(g);
+ },
+ get index() {
+ return h(p);
+ }
+ });
+ },
+ (d) => {
+ var g = Lm();
+ L(d, g);
+ }
+ ), Z(l), L(e, l), fe(), r();
+}
+ae(bd, {}, [], [], !0);
+const Cd = (e) => {
+ !e || e.length == 0 || e.forEach((t) => {
+ t.id || (t.id = Rr()), Cd(t.children);
+ });
+}, kn = () => {
+ const { updateNodeData: e } = Dt();
+ return {
+ addParameter: (t, n = "parameters", r) => {
+ Cd(r == null ? void 0 : r.children);
+ const o = {
+ ...r,
+ id: Rr()
+ };
+ e(t, (i) => {
+ let s = i.data[n];
+ return s ? s.push(o) : s = [o], {
+ [n]: [...s]
+ };
+ });
+ }
+ };
+};
+var zm = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>'), Rm = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Bm = /* @__PURE__ */ ne('<div class="heading svelte-r5g35l"><!> <!></div> <!>', 1);
+const Ym = {
+ hash: "svelte-r5g35l",
+ code: ".heading.svelte-r5g35l {display:flex;margin-bottom:10px;}.input-btn-more {border:1px solid transparent;padding:3px;}.input-btn-more:hover {background:#eee;border:1px solid transparent;}"
+};
+function kd(e, t) {
+ de(t, !0), Je(e, Ym);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn();
+ return dn(e, ut(() => r, {
+ get data() {
+ return n();
+ },
+ allowExecute: !1,
+ showTargetHandle: !1,
+ icon: (a) => {
+ var l = zm();
+ L(a, l);
+ },
+ children: (a, l) => {
+ var u = Bm(), c = be(u), f = X(c);
+ Ge(f, {
+ level: 3,
+ children: (p, x) => {
+ Se();
+ var C = Ie("杈撳叆鍙傛暟");
+ L(p, C);
+ },
+ $$slots: { default: !0 }
+ });
+ var d = z(f, 2);
+ Ke(d, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (p, x) => {
+ var C = Rm();
+ L(p, C);
+ },
+ $$slots: { default: !0 }
+ }), Z(c);
+ var g = z(c, 2);
+ bd(g, {}), L(a, u);
+ },
+ $$slots: { icon: !0, default: !0 }
+ })), fe({
+ get data() {
+ return n();
+ },
+ set data(s) {
+ n(s), y();
+ }
+ });
+}
+ae(kd, { data: {} }, [], [], !0);
+const $d = (e, t, n) => {
+ for (let r of n)
+ r.target === t && r.source && (e.push(r.source), $d(e, r.source, n));
+}, Al = (e, t) => {
+ if (e.type === "startNode") {
+ const n = e.data.parameters, r = [];
+ if (n)
+ for (const o of n)
+ r.push({
+ label: o.name + (t ? ` (Array<${o.dataType || "String"}>)` : ` (${o.dataType || "String"})`),
+ value: e.id + "." + o.name
+ });
+ return {
+ label: e.data.title,
+ value: e.id,
+ children: r
+ };
+ } else {
+ if (e.type === "loopNode" && t)
+ return {
+ label: e.data.title,
+ value: e.id,
+ children: [
+ {
+ label: "loopItem",
+ value: e.id + ".loop"
+ },
+ {
+ label: "index (Number)",
+ value: e.id + ".index"
+ }
+ ]
+ };
+ {
+ const n = e.data.outputDefs;
+ if (n) {
+ const r = (o, i) => !o || o.length === 0 ? [] : o.map((s) => ({
+ label: s.name + (t ? ` (Array<${s.dataType || "String"}>)` : ` (${s.dataType || "String"})`),
+ // label: param.name ,
+ value: i + "." + s.name,
+ children: r(s.children, i + "." + s.name)
+ }));
+ return {
+ label: e.data.title,
+ value: e.id,
+ children: r(n, e.id)
+ };
+ }
+ }
+ }
+}, Zm = (e = !1) => {
+ const t = ht(), n = pr(t), { nodes: r, edges: o } = Ue();
+ return Kn([n, r, o], ([i, s, a]) => {
+ const l = [];
+ if (e) {
+ for (let u of s)
+ if (u.parentId === i.id) {
+ const c = Al(u, u.parentId === i.id);
+ c && l.push(c);
+ }
+ } else {
+ const u = [];
+ $d(u, t, a);
+ for (let c of s)
+ if (u.includes(c.id)) {
+ const f = Al(c, c.parentId === i.id);
+ f && l.push(f);
+ }
+ }
+ return l;
+ });
+};
+var Xm = /* @__PURE__ */ ne('<div class="input-more-setting svelte-laou7w"><div class="input-more-item svelte-laou7w">鏁版嵁鏉ユ簮锛� <!></div> <div class="input-more-item svelte-laou7w">榛樿鍊硷細 <!></div> <div class="input-more-item svelte-laou7w">鍙傛暟鎻忚堪锛� <!></div> <div class="input-more-item svelte-laou7w"><!></div></div>'), Fm = /* @__PURE__ */ ne('<div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div>', 1);
+const Wm = {
+ hash: "svelte-laou7w",
+ code: ".input-item.svelte-laou7w {display:flex;align-items:center;}.input-more-setting.svelte-laou7w {display:flex;flex-direction:column;gap:10px;padding:10px;background:#fff;border:1px solid #ddd;border-radius:5px;width:200px;box-shadow:0 0 10px 2px rgba(0, 0, 0, 0.1);}.input-more-setting.svelte-laou7w .input-more-item:where(.svelte-laou7w) {display:flex;flex-direction:column;gap:3px;font-size:12px;color:#666;}"
+};
+function Ed(e, t) {
+ de(t, !0), Je(e, Wm);
+ const [n, r] = tt(), o = () => Q(h(c), "$node", n), i = () => Q(v, "$selectItems", n), s = w(t, "parameter", 7), a = w(t, "index", 7), l = w(t, "dataKeyName", 7);
+ let u = ht(), c = /* @__PURE__ */ Me(() => pr(u)), f = /* @__PURE__ */ Me(() => {
+ var T;
+ return {
+ ...s(),
+ ...(T = o()) == null ? void 0 : T.data[l()][a()]
+ };
+ });
+ const { updateNodeData: d } = Dt(), g = (T, k) => {
+ d(u, (P) => {
+ let H = P.data[l()];
+ return H[a()] = { ...H[a()], [T]: k }, { [l()]: H };
+ });
+ }, p = (T) => {
+ const k = T.target.value;
+ g("name", k);
+ }, x = (T) => {
+ const k = T.target.value;
+ g("value", k);
+ }, C = (T) => {
+ const k = T.value;
+ g("ref", k);
+ }, $ = (T) => {
+ const k = T.value;
+ g("refType", k);
+ };
+ let m;
+ const _ = () => {
+ d(u, (T) => {
+ let k = T.data[l()];
+ return k.splice(a(), 1), { [l()]: [...k] };
+ }), m == null || m.hide();
+ }, v = Zm();
+ var b = Fm(), N = be(b), E = X(N);
+ xt(E, {
+ style: "width: 100%;",
+ get value() {
+ return h(f).name;
+ },
+ placeholder: "璇疯緭鍏ュ弬鏁板悕绉�",
+ oninput: p
+ }), Z(N);
+ var M = z(N, 2), D = X(M);
+ {
+ var V = (T) => {
+ xt(T, {
+ get value() {
+ return h(f).value;
+ },
+ placeholder: "璇疯緭鍏ュ弬鏁板��",
+ oninput: x
+ });
+ }, A = (T) => {
+ const k = /* @__PURE__ */ Me(() => [h(f).ref]);
+ sn(T, {
+ get items() {
+ return i();
+ },
+ style: "width: 100%",
+ defaultValue: ["ref"],
+ get value() {
+ return h(k);
+ },
+ expandAll: !0,
+ onSelect: C
+ });
+ };
+ ke(D, (T) => {
+ h(f).refType === "input" ? T(V) : T(A, !1);
+ });
+ }
+ Z(M);
+ var O = z(M, 2), R = X(O);
+ An(
+ Lo(R, {
+ placement: "bottom",
+ floating: (k) => {
+ var P = Xm(), H = X(P), I = z(X(H));
+ const B = /* @__PURE__ */ Me(() => h(f).refType ? [h(f).refType] : []);
+ sn(I, {
+ items: Mm,
+ style: "width: 100%",
+ defaultValue: ["ref"],
+ get value() {
+ return h(B);
+ },
+ onSelect: $
+ }), Z(H);
+ var F = z(H, 2), K = z(X(F));
+ $t(K, {
+ rows: 1,
+ style: "width: 100%;",
+ onchange: (me) => {
+ const Ce = me.target.value;
+ g("defaultValue", Ce);
+ }
+ }), Z(F);
+ var ie = z(F, 2), ee = z(X(ie));
+ $t(ee, {
+ rows: 3,
+ style: "width: 100%;",
+ onchange: (me) => {
+ const Ce = me.target.value;
+ g("description", Ce);
+ }
+ }), Z(ie);
+ var W = z(ie, 2), ue = X(W);
+ Ke(ue, {
+ onclick: _,
+ children: (me, Ce) => {
+ Se();
+ var ge = Ie("鍒犻櫎");
+ L(me, ge);
+ },
+ $$slots: { default: !0 }
+ }), Z(W), Z(P), L(k, P);
+ },
+ children: (k, P) => {
+ Ui(k, {});
+ },
+ $$slots: { floating: !0, default: !0 }
+ }),
+ (k) => m = k,
+ () => m
+ ), Z(O), L(e, b);
+ var S = fe({
+ get parameter() {
+ return s();
+ },
+ set parameter(T) {
+ s(T), y();
+ },
+ get index() {
+ return a();
+ },
+ set index(T) {
+ a(T), y();
+ },
+ get dataKeyName() {
+ return l();
+ },
+ set dataKeyName(T) {
+ l(T), y();
+ }
+ });
+ return r(), S;
+}
+ae(Ed, { parameter: {}, index: {}, dataKeyName: {} }, [], [], !0);
+var Km = /* @__PURE__ */ ne('<div class="input-header svelte-1sm1mgi">鍙傛暟鍚嶇О</div> <div class="input-header svelte-1sm1mgi">鍙傛暟鍊�</div> <div class="input-header svelte-1sm1mgi"></div>', 1), qm = /* @__PURE__ */ ne('<div class="none-params svelte-1sm1mgi"> </div>'), Gm = /* @__PURE__ */ ne('<div class="input-container svelte-1sm1mgi"><!> <!></div>');
+const Um = {
+ hash: "svelte-1sm1mgi",
+ code: `.input-container.svelte-1sm1mgi {display:grid;grid-template-columns:40% 50% 10%;row-gap:5px;column-gap:3px;}.input-container.svelte-1sm1mgi .none-params:where(.svelte-1sm1mgi) {font-size:12px;background:#f8f8f8;height:40px;display:flex;justify-content:center;align-items:center;border-radius:5px;width:calc(100% - 5px);grid-column:1 / -1;
+ /* 浠庣涓�鍒楀紑濮嬪埌鏈�鍚庝竴鍒楃粨鏉� */}.input-container.svelte-1sm1mgi .input-header:where(.svelte-1sm1mgi) {font-size:12px;color:#666;}`
+};
+function zt(e, t) {
+ de(t, !0), Je(e, Um);
+ const [n, r] = tt(), o = () => Q(h(l), "$node", n), i = w(t, "noneParameterText", 7, "鏃犺緭鍏ュ弬鏁�"), s = w(t, "dataKeyName", 7, "parameters");
+ let a = ht(), l = /* @__PURE__ */ Me(() => pr(a)), u = /* @__PURE__ */ Me(() => {
+ var x;
+ return [...((x = o()) == null ? void 0 : x.data[s()]) || []];
+ });
+ var c = Gm(), f = X(c);
+ {
+ var d = (x) => {
+ var C = Km();
+ Se(4), L(x, C);
+ };
+ ke(f, (x) => {
+ h(u).length !== 0 && x(d);
+ });
+ }
+ var g = z(f, 2);
+ Yt(
+ g,
+ 19,
+ () => h(u),
+ (x) => x.id,
+ (x, C, $) => {
+ Ed(x, {
+ get parameter() {
+ return h(C);
+ },
+ get index() {
+ return h($);
+ },
+ get dataKeyName() {
+ return s();
+ }
+ });
+ },
+ (x) => {
+ var C = qm(), $ = X(C, !0);
+ Z(C), Ee(() => Rt($, i())), L(x, C);
+ }
+ ), Z(c), L(e, c);
+ var p = fe({
+ get noneParameterText() {
+ return i();
+ },
+ set noneParameterText(x = "鏃犺緭鍏ュ弬鏁�") {
+ i(x), y();
+ },
+ get dataKeyName() {
+ return s();
+ },
+ set dataKeyName(x = "parameters") {
+ s(x), y();
+ }
+ });
+ return r(), p;
+}
+ae(zt, { noneParameterText: {}, dataKeyName: {} }, [], [], !0);
+var jm = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>'), Jm = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Qm = /* @__PURE__ */ ne('<div class="heading svelte-11h445j"><!> <!></div> <!>', 1);
+const ey = {
+ hash: "svelte-11h445j",
+ code: ".heading.svelte-11h445j {display:flex;margin-bottom:10px;}"
+};
+function Sd(e, t) {
+ de(t, !0), Je(e, ey);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn();
+ return dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ allowExecute: !1,
+ showSourceHandle: !1,
+ icon: (a) => {
+ var l = jm();
+ L(a, l);
+ },
+ children: (a, l) => {
+ var u = Qm(), c = be(u), f = X(c);
+ Ge(f, {
+ level: 3,
+ children: (p, x) => {
+ Se();
+ var C = Ie("杈撳嚭鍙傛暟");
+ L(p, C);
+ },
+ $$slots: { default: !0 }
+ });
+ var d = z(f, 2);
+ Ke(d, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o, "outputDefs");
+ },
+ children: (p, x) => {
+ var C = Jm();
+ L(p, C);
+ },
+ $$slots: { default: !0 }
+ }), Z(c);
+ var g = z(c, 2);
+ zt(g, {
+ noneParameterText: "鏃犺緭鍑哄弬鏁�",
+ dataKeyName: "outputDefs"
+ }), L(a, u);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(s) {
+ n(s), y();
+ }
+ });
+}
+ae(Sd, { data: {} }, [], [], !0);
+const Oo = () => ar("tinyflow_options");
+var ty = /* @__PURE__ */ _e('<svg style="transform: scaleY(-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 8V16C13 17.6569 11.6569 19 10 19H7.82929C7.41746 20.1652 6.30622 21 5 21C3.34315 21 2 19.6569 2 18C2 16.3431 3.34315 15 5 15C6.30622 15 7.41746 15.8348 7.82929 17H10C10.5523 17 11 16.5523 11 16V8C11 6.34315 12.3431 5 14 5H17V2L22 6L17 10V7H14C13.4477 7 13 7.44772 13 8ZM5 19C5.55228 19 6 18.5523 6 18C6 17.4477 5.55228 17 5 17C4.44772 17 4 17.4477 4 18C4 18.5523 4.44772 19 5 19Z"></path></svg>'), ny = /* @__PURE__ */ ne('<div class="input-more-item svelte-1cfeest"><!></div>'), ry = /* @__PURE__ */ ne('<div class="input-more-setting svelte-1cfeest"><div class="input-more-item svelte-1cfeest">榛樿鍊硷細 <!></div> <div class="input-more-item svelte-1cfeest">鍙傛暟鎻忚堪锛� <!></div> <!></div>'), oy = /* @__PURE__ */ ne('<div class="input-item svelte-1cfeest"><!> <!></div> <div class="input-item svelte-1cfeest"><!> <!></div> <div class="input-item svelte-1cfeest"><!></div>', 1);
+const iy = {
+ hash: "svelte-1cfeest",
+ code: ".input-item.svelte-1cfeest {display:flex;align-items:center;gap:2px;}.input-more-setting.svelte-1cfeest {display:flex;flex-direction:column;gap:10px;padding:10px;background:#fff;border:1px solid #ddd;border-radius:5px;width:200px;box-shadow:0 0 10px 2px rgba(0, 0, 0, 0.1);}.input-more-setting.svelte-1cfeest .input-more-item:where(.svelte-1cfeest) {display:flex;flex-direction:column;gap:3px;font-size:12px;color:#666;}"
+};
+function Pd(e, t) {
+ de(t, !0), Je(e, iy);
+ const [n, r] = tt(), o = () => Q(h(u), "$node", n), i = w(t, "parameter", 7), s = w(t, "position", 7), a = w(t, "dataKeyName", 7);
+ let l = ht(), u = /* @__PURE__ */ Me(() => pr(l)), c = /* @__PURE__ */ Me(() => {
+ var I;
+ let P = (I = o()) == null ? void 0 : I.data[a()], H;
+ if (P && s().length > 0) {
+ let B = P;
+ for (let F = 0; F < s().length; F++) {
+ const K = s()[F];
+ F == s().length - 1 ? H = B[K] : B = B[K].children;
+ }
+ }
+ return { ...i(), ...H };
+ });
+ const { updateNodeData: f } = Dt(), d = (P, H) => {
+ f(l, (I) => {
+ const B = I.data[a()];
+ if (B && s().length > 0) {
+ let F = B;
+ for (let K = 0; K < s().length; K++) {
+ const ie = s()[K];
+ K == s().length - 1 ? F[ie] = { ...F[ie], [P]: H } : F = B[ie].children;
+ }
+ }
+ return { [a()]: B };
+ });
+ }, g = (P) => {
+ const H = P.target.value;
+ d("name", H);
+ }, p = (P) => {
+ const H = P.value;
+ d("dataType", H);
+ };
+ let x;
+ const C = () => {
+ f(l, (P) => {
+ let H = P.data[a()];
+ if (H && s().length > 0) {
+ let I = H;
+ for (let B = 0; B < s().length; B++) {
+ const F = s()[B];
+ B == s().length - 1 ? I.splice(F, 1) : I = I[F].children;
+ }
+ }
+ return { [a()]: [...H] };
+ }), x == null || x.hide();
+ }, $ = () => {
+ f(l, (P) => {
+ let H = P.data[a()];
+ if (H && s().length > 0) {
+ let I = H;
+ for (let B = 0; B < s().length; B++) {
+ const F = s()[B];
+ B == s().length - 1 ? I[F].children ? I[F].children.push({
+ id: Rr(),
+ name: "newParam",
+ dataType: "String"
+ }) : I[F].children = [
+ {
+ id: Rr(),
+ name: "newParam",
+ dataType: "String"
+ }
+ ] : I = I[F].children;
+ }
+ }
+ return { [a()]: [...H] };
+ });
+ };
+ var m = oy(), _ = be(m), v = X(_);
+ {
+ var b = (P) => {
+ var H = et(), I = be(H);
+ Yt(I, 17, s, Li, (B, F) => {
+ Se();
+ var K = Ie("聽");
+ L(B, K);
+ }), L(P, H);
+ };
+ ke(v, (P) => {
+ s().length > 1 && P(b);
+ });
+ }
+ var N = z(v, 2);
+ const E = /* @__PURE__ */ Me(() => h(c).nameDisabled === !0);
+ xt(N, {
+ style: "width: 100%;",
+ get value() {
+ return h(c).name;
+ },
+ placeholder: "璇疯緭鍏ュ弬鏁板悕绉�",
+ oninput: g,
+ get disabled() {
+ return h(E);
+ }
+ }), Z(_);
+ var M = z(_, 2), D = X(M);
+ const V = /* @__PURE__ */ Me(() => h(c).dataType ? [h(c).dataType] : []), A = /* @__PURE__ */ Me(() => h(c).dataTypeDisabled === !0);
+ sn(D, {
+ items: _d,
+ style: "width: 100%",
+ defaultValue: ["String"],
+ get value() {
+ return h(V);
+ },
+ get disabled() {
+ return h(A);
+ },
+ onSelect: p
+ });
+ var O = z(D, 2);
+ {
+ var R = (P) => {
+ Ke(P, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: $,
+ children: (H, I) => {
+ var B = ty();
+ L(H, B);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(O, (P) => {
+ (h(c).dataType === "Object" || h(c).dataType === "Array") && h(c).addChildDisabled !== !0 && P(R);
+ });
+ }
+ Z(M);
+ var S = z(M, 2), T = X(S);
+ An(
+ Lo(T, {
+ placement: "bottom",
+ floating: (H) => {
+ var I = ry(), B = X(I), F = z(X(B));
+ $t(F, {
+ rows: 1,
+ style: "width: 100%;",
+ onchange: (ue) => {
+ const me = ue.target.value;
+ d("defaultValue", me);
+ }
+ }), Z(B);
+ var K = z(B, 2), ie = z(X(K));
+ $t(ie, {
+ rows: 3,
+ style: "width: 100%;",
+ onchange: (ue) => {
+ const me = ue.target.value;
+ d("description", me);
+ }
+ }), Z(K);
+ var ee = z(K, 2);
+ {
+ var W = (ue) => {
+ var me = ny(), Ce = X(me);
+ Ke(Ce, {
+ onclick: C,
+ children: (ge, ze) => {
+ Se();
+ var G = Ie("鍒犻櫎");
+ L(ge, G);
+ },
+ $$slots: { default: !0 }
+ }), Z(me), L(ue, me);
+ };
+ ke(ee, (ue) => {
+ h(c).deleteDisabled !== !0 && ue(W);
+ });
+ }
+ Z(I), L(H, I);
+ },
+ children: (H, I) => {
+ Ui(H, {});
+ },
+ $$slots: { floating: !0, default: !0 }
+ }),
+ (H) => x = H,
+ () => x
+ ), Z(S), L(e, m);
+ var k = fe({
+ get parameter() {
+ return i();
+ },
+ set parameter(P) {
+ i(P), y();
+ },
+ get position() {
+ return s();
+ },
+ set position(P) {
+ s(P), y();
+ },
+ get dataKeyName() {
+ return a();
+ },
+ set dataKeyName(P) {
+ a(P), y();
+ }
+ });
+ return r(), k;
+}
+ae(Pd, { parameter: {}, position: {}, dataKeyName: {} }, [], [], !0);
+var sy = /* @__PURE__ */ ne("<!> <!>", 1), ay = /* @__PURE__ */ ne('<div class="none-params svelte-1sm1mgi"> </div>'), ly = /* @__PURE__ */ ne('<div class="input-header svelte-1sm1mgi">鍙傛暟鍚嶇О</div> <div class="input-header svelte-1sm1mgi">鍙傛暟绫诲瀷</div> <div class="input-header svelte-1sm1mgi"></div>', 1), uy = /* @__PURE__ */ ne('<div class="input-container svelte-1sm1mgi"><!> <!></div>');
+const cy = {
+ hash: "svelte-1sm1mgi",
+ code: `.input-container.svelte-1sm1mgi {display:grid;grid-template-columns:40% 50% 10%;row-gap:5px;column-gap:3px;}.input-container.svelte-1sm1mgi .none-params:where(.svelte-1sm1mgi) {font-size:12px;background:#f8f8f8;height:40px;display:flex;justify-content:center;align-items:center;border-radius:5px;width:calc(100% - 5px);grid-column:1 / -1;
+ /* 浠庣涓�鍒楀紑濮嬪埌鏈�鍚庝竴鍒楃粨鏉� */}.input-container.svelte-1sm1mgi .input-header:where(.svelte-1sm1mgi) {font-size:12px;color:#666;}`
+};
+function Rn(e, t) {
+ de(t, !0), Je(e, cy);
+ const [n, r] = tt(), o = () => Q(h(u), "$node", n), i = (C, $ = dt, m = dt) => {
+ var _ = et(), v = be(_);
+ Yt(
+ v,
+ 19,
+ $,
+ (b) => `${b.id}_${b.children ? b.children.length : 0}`,
+ (b, N, E) => {
+ var M = sy(), D = be(M);
+ const V = /* @__PURE__ */ Me(() => [...m(), h(E)]);
+ Pd(D, {
+ get parameter() {
+ return h(N);
+ },
+ get position() {
+ return h(V);
+ },
+ get dataKeyName() {
+ return a();
+ }
+ });
+ var A = z(D, 2);
+ {
+ var O = (R) => {
+ var S = /* @__PURE__ */ pe(() => [...m(), h(E)]);
+ i(R, () => h(N).children, () => h(S));
+ };
+ ke(A, (R) => {
+ h(N).children && R(O);
+ });
+ }
+ L(b, M);
+ },
+ (b) => {
+ var N = et(), E = be(N);
+ {
+ var M = (D) => {
+ var V = ay(), A = X(V, !0);
+ Z(V), Ee(() => Rt(A, s())), L(D, V);
+ };
+ ke(E, (D) => {
+ m().length === 0 && D(M);
+ });
+ }
+ L(b, N);
+ }
+ ), L(C, _);
+ }, s = w(t, "noneParameterText", 7, "鏃犺緭鍑哄弬鏁�"), a = w(t, "dataKeyName", 7, "outputDefs");
+ let l = ht(), u = /* @__PURE__ */ Me(() => pr(l)), c = /* @__PURE__ */ Me(() => {
+ var C;
+ return [...((C = o()) == null ? void 0 : C.data[a()]) || []];
+ });
+ var f = uy(), d = X(f);
+ {
+ var g = (C) => {
+ var $ = ly();
+ Se(4), L(C, $);
+ };
+ ke(d, (C) => {
+ h(c).length !== 0 && C(g);
+ });
+ }
+ var p = z(d, 2);
+ i(p, () => h(c) || [], () => []), Z(f), L(e, f);
+ var x = fe({
+ get noneParameterText() {
+ return s();
+ },
+ set noneParameterText(C = "鏃犺緭鍑哄弬鏁�") {
+ s(C), y();
+ },
+ get dataKeyName() {
+ return a();
+ },
+ set dataKeyName(C = "outputDefs") {
+ a(C), y();
+ }
+ });
+ return r(), x;
+}
+ae(Rn, { noneParameterText: {}, dataKeyName: {} }, [], [], !0);
+var dy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>'), fy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), gy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), hy = /* @__PURE__ */ ne('<div class="heading svelte-wn2kra"><!> <!></div> <!> <!> <div class="setting-title svelte-wn2kra">妯″瀷</div> <div class="setting-item svelte-wn2kra"><!> <!></div> <div class="setting-title svelte-wn2kra">閲囨牱鍙傛暟</div> <div class="setting-item svelte-wn2kra"><div class="slider-container svelte-wn2kra"><label class="svelte-wn2kra"> </label> <input type="range" min="0" max="1" step="0.1" class="svelte-wn2kra"></div></div> <div class="setting-item svelte-wn2kra"><div class="slider-container svelte-wn2kra"><label class="svelte-wn2kra"> </label> <input type="range" min="0" max="1" step="0.1" class="svelte-wn2kra"></div></div> <div class="setting-item svelte-wn2kra"><div class="slider-container svelte-wn2kra"><label class="svelte-wn2kra"> </label> <input type="range" min="0" max="100" step="1" class="svelte-wn2kra"></div></div> <div class="setting-title svelte-wn2kra">绯荤粺鎻愮ず璇�</div> <div class="setting-item svelte-wn2kra"><!></div> <div class="setting-title svelte-wn2kra">鐢ㄦ埛鎻愮ず璇�</div> <div class="setting-item svelte-wn2kra"><!></div> <div class="heading svelte-wn2kra"><!> <!></div> <!>', 1);
+const vy = {
+ hash: "svelte-wn2kra",
+ code: `.heading.svelte-wn2kra {display:flex;margin-bottom:10px;}.setting-title.svelte-wn2kra {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-wn2kra {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}\r
+ /* 鏂板鏍峰紡 */.slider-container.svelte-wn2kra {width:100%;display:flex;flex-direction:column;gap:4px;}.slider-container.svelte-wn2kra label:where(.svelte-wn2kra) {font-size:12px;color:#666;display:flex;justify-content:space-between;align-items:center;}input[type="range"].svelte-wn2kra {width:100%;height:4px;background:#ddd;border-radius:2px;outline:none;-webkit-appearance:none;}input[type="range"].svelte-wn2kra::-webkit-slider-thumb {-webkit-appearance:none;width:14px;height:14px;background:#007bff;border-radius:50%;cursor:pointer;}`
+};
+function Nd(e, t) {
+ de(t, !0), Je(e, vy);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), s = Oo();
+ let a = Un(Tt([]));
+ un(async () => {
+ var c, f;
+ const u = await ((f = (c = s.provider) == null ? void 0 : c.llm) == null ? void 0 : f.call(c));
+ h(a).push(...u || []);
+ });
+ const { updateNodeData: l } = Dt();
+ return dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (c) => {
+ var f = dy();
+ L(c, f);
+ },
+ children: (c, f) => {
+ var d = hy(), g = be(d), p = X(g);
+ Ge(p, {
+ level: 3,
+ children: (G, se) => {
+ Se();
+ var Te = Ie("杈撳叆鍙傛暟");
+ L(G, Te);
+ },
+ $$slots: { default: !0 }
+ });
+ var x = z(p, 2);
+ Ke(x, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (G, se) => {
+ var Te = fy();
+ L(G, Te);
+ },
+ $$slots: { default: !0 }
+ }), Z(g);
+ var C = z(g, 2);
+ zt(C, {});
+ var $ = z(C, 2);
+ Ge($, {
+ level: 3,
+ mt: "10px",
+ children: (G, se) => {
+ Se();
+ var Te = Ie("妯″瀷璁剧疆");
+ L(G, Te);
+ },
+ $$slots: { default: !0 }
+ });
+ var m = z($, 4), _ = X(m);
+ const v = /* @__PURE__ */ Me(() => n().llmId ? [n().llmId] : []);
+ sn(_, {
+ get items() {
+ return h(a);
+ },
+ style: "width: 100%",
+ placeholder: "璇烽�夋嫨妯″瀷",
+ onSelect: (G) => {
+ const se = G.value;
+ l(o, () => ({ llmId: se }));
+ },
+ get value() {
+ return h(v);
+ }
+ });
+ var b = z(_, 2);
+ Ui(b, {}), Z(m);
+ var N = z(m, 4), E = X(N), M = X(E), D = X(M);
+ Z(M);
+ var V = z(M, 2);
+ io(V), Z(E), Z(N);
+ var A = z(N, 2), O = X(A), R = X(O), S = X(R);
+ Z(R);
+ var T = z(R, 2);
+ io(T), Z(O), Z(A);
+ var k = z(A, 2), P = X(k), H = X(P), I = X(H);
+ Z(H);
+ var B = z(H, 2);
+ io(B), Z(P), Z(k);
+ var F = z(k, 4), K = X(F);
+ const ie = /* @__PURE__ */ Me(() => n().systemPrompt || "");
+ $t(K, {
+ rows: 5,
+ placeholder: "璇疯緭鍏ョ郴缁熸彁绀鸿瘝",
+ style: "width: 100%",
+ get value() {
+ return h(ie);
+ },
+ oninput: (G) => {
+ l(o, { systemPrompt: G.target.value });
+ }
+ }), Z(F);
+ var ee = z(F, 4), W = X(ee);
+ const ue = /* @__PURE__ */ Me(() => n().userPrompt || "");
+ $t(W, {
+ rows: 5,
+ placeholder: "璇疯緭鍏ョ敤鎴锋彁绀鸿瘝",
+ style: "width: 100%",
+ get value() {
+ return h(ue);
+ },
+ oninput: (G) => {
+ l(o, { userPrompt: G.target.value });
+ }
+ }), Z(ee);
+ var me = z(ee, 2), Ce = X(me);
+ Ge(Ce, {
+ level: 3,
+ mt: "10px",
+ children: (G, se) => {
+ Se();
+ var Te = Ie("杈撳嚭鍙傛暟");
+ L(G, Te);
+ },
+ $$slots: { default: !0 }
+ });
+ var ge = z(Ce, 2);
+ Ke(ge, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o, "outputDefs");
+ },
+ children: (G, se) => {
+ var Te = gy();
+ L(G, Te);
+ },
+ $$slots: { default: !0 }
+ }), Z(me);
+ var ze = z(me, 2);
+ Rn(ze, {}), Ee(() => {
+ Rt(D, `Temperature: ${n().temperature ?? 0.5}`), Qi(V, n().temperature ?? 0.5), Rt(S, `Top P: ${n().topP ?? 0.9}`), Qi(T, n().topP ?? 0.9), Rt(I, `Top K: ${n().topK ?? 50}`), Qi(B, n().topK ?? 50);
+ }), Ye("mousedown", V, es(function(G) {
+ Ve.call(this, t, G);
+ })), Ye("input", V, (G) => l(o, { temperature: parseFloat(G.target.value) })), Ye("mousedown", T, es(function(G) {
+ Ve.call(this, t, G);
+ })), Ye("input", T, (G) => l(o, { topP: parseFloat(G.target.value) })), Ye("mousedown", B, es(function(G) {
+ Ve.call(this, t, G);
+ })), Ye("input", B, (G) => l(o, { topK: parseInt(G.target.value) })), L(c, d);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(u) {
+ n(u), y();
+ }
+ });
+}
+ae(Nd, { data: {} }, [], [], !0);
+var py = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>'), my = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), yy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), wy = /* @__PURE__ */ ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">鎵ц寮曟搸</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鎵ц浠g爜</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!> <!></div> <!>', 1);
+const _y = {
+ hash: "svelte-15t2v24",
+ code: ".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"
+};
+function Md(e, t) {
+ de(t, !0), Je(e, _y);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), { updateNodeData: s } = Dt(), a = [
+ { label: "QLExpress", value: "qlexpress" },
+ { label: "Groovy", value: "groovy" },
+ { label: "JavaScript", value: "js" }
+ ];
+ return dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (u) => {
+ var c = py();
+ L(u, c);
+ },
+ children: (u, c) => {
+ var f = wy(), d = be(f), g = X(d);
+ Ge(g, {
+ level: 3,
+ children: (A, O) => {
+ Se();
+ var R = Ie("杈撳叆鍙傛暟");
+ L(A, R);
+ },
+ $$slots: { default: !0 }
+ });
+ var p = z(g, 2);
+ Ke(p, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (A, O) => {
+ var R = my();
+ L(A, R);
+ },
+ $$slots: { default: !0 }
+ }), Z(d);
+ var x = z(d, 2);
+ zt(x, {});
+ var C = z(x, 2);
+ Ge(C, {
+ level: 3,
+ mt: "10px",
+ children: (A, O) => {
+ Se();
+ var R = Ie("浠g爜");
+ L(A, R);
+ },
+ $$slots: { default: !0 }
+ });
+ var $ = z(C, 4), m = X($);
+ const _ = /* @__PURE__ */ Me(() => n().engine ? [n().engine] : ["qlexpress"]);
+ sn(m, {
+ items: a,
+ style: "width: 100%",
+ placeholder: "璇烽�夋嫨鎵ц寮曟搸",
+ onSelect: (A) => {
+ const O = A.value;
+ s(o, () => ({ engine: O }));
+ },
+ get value() {
+ return h(_);
+ }
+ }), Z($);
+ var v = z($, 4), b = X(v);
+ const N = /* @__PURE__ */ Me(() => n().code || "");
+ $t(b, {
+ rows: 10,
+ placeholder: "璇疯緭鍏ユ墽琛屼唬鐮侊紝娉細杈撳嚭鍐呭闇�娣诲姞鍒癬result涓紝濡傦細_result.put(key, value)",
+ style: "width: 100%",
+ onchange: (A) => {
+ s(o, () => ({ code: A.target.value }));
+ },
+ get value() {
+ return h(N);
+ }
+ }), Z(v);
+ var E = z(v, 2), M = X(E);
+ Ge(M, {
+ level: 3,
+ mt: "10px",
+ children: (A, O) => {
+ Se();
+ var R = Ie("杈撳嚭鍙傛暟");
+ L(A, R);
+ },
+ $$slots: { default: !0 }
+ });
+ var D = z(M, 2);
+ Ke(D, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o, "outputDefs");
+ },
+ children: (A, O) => {
+ var R = yy();
+ L(A, R);
+ },
+ $$slots: { default: !0 }
+ }), Z(E);
+ var V = z(E, 2);
+ Rn(V, {}), L(u, f);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(l) {
+ n(l), y();
+ }
+ });
+}
+ae(Md, { data: {} }, [], [], !0);
+var xy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>'), by = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Cy = /* @__PURE__ */ ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">鎵ц浠g爜</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!></div> <!>', 1);
+const ky = {
+ hash: "svelte-15t2v24",
+ code: ".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"
+};
+function Td(e, t) {
+ de(t, !0), Je(e, ky);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), { updateNodeData: s } = Dt();
+ return Nr(() => {
+ (!n().outputDefs || n().outputDefs.length === 0) && i(o, "outputDefs", {
+ name: "output",
+ dataType: "String",
+ dataTypeDisabled: !0,
+ deleteDisabled: !0
+ });
+ }), dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (l) => {
+ var u = xy();
+ L(l, u);
+ },
+ children: (l, u) => {
+ var c = Cy(), f = be(c), d = X(f);
+ Ge(d, {
+ level: 3,
+ children: (N, E) => {
+ Se();
+ var M = Ie("杈撳叆鍙傛暟");
+ L(N, M);
+ },
+ $$slots: { default: !0 }
+ });
+ var g = z(d, 2);
+ Ke(g, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (N, E) => {
+ var M = by();
+ L(N, M);
+ },
+ $$slots: { default: !0 }
+ }), Z(f);
+ var p = z(f, 2);
+ zt(p, {});
+ var x = z(p, 2);
+ Ge(x, {
+ level: 3,
+ mt: "10px",
+ children: (N, E) => {
+ Se();
+ var M = Ie("浠g爜");
+ L(N, M);
+ },
+ $$slots: { default: !0 }
+ });
+ var C = z(x, 4), $ = X(C);
+ const m = /* @__PURE__ */ Me(() => n().template || "");
+ $t($, {
+ rows: 10,
+ placeholder: "璇疯緭鍏ユ墽琛屼唬鐮�",
+ style: "width: 100%",
+ onchange: (N) => {
+ s(o, () => ({ template: N.target.value }));
+ },
+ get value() {
+ return h(m);
+ }
+ }), Z(C);
+ var _ = z(C, 2), v = X(_);
+ Ge(v, {
+ level: 3,
+ mt: "10px",
+ children: (N, E) => {
+ Se();
+ var M = Ie("杈撳嚭鍙傛暟");
+ L(N, M);
+ },
+ $$slots: { default: !0 }
+ }), Z(_);
+ var b = z(_, 2);
+ Rn(b, {}), L(l, c);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(a) {
+ n(a), y();
+ }
+ });
+}
+ae(Td, { data: {} }, [], [], !0);
+var $y = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>'), Ey = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Sy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Py = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Ny = /* @__PURE__ */ ne('<div class="heading svelte-1vtcqdz" style="padding-top: 10px"><!> <!></div> <!>', 1), My = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Ty = /* @__PURE__ */ ne('<div class="heading svelte-1vtcqdz" style="padding-top: 10px"><!> <!></div> <!>', 1), Hy = /* @__PURE__ */ ne('<div style="width: 100%"><!></div>'), Vy = /* @__PURE__ */ ne('<div style="width: 100%"><!></div>'), Dy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Ay = /* @__PURE__ */ ne('<div style="display: flex;gap: 2px;width: 100%;padding: 10px 0"><div><!></div> <div style="width: 100%"><!></div></div> <div class="heading svelte-1vtcqdz"><!> <!></div> <!> <div class="heading svelte-1vtcqdz" style="padding-top: 10px"><!> <!></div> <!> <!> <div class="radio-group svelte-1vtcqdz"><label class="svelte-1vtcqdz"><!>none</label> <label class="svelte-1vtcqdz"><!>form-data</label> <label class="svelte-1vtcqdz"><!>x-www-form-urlencoded</label> <label class="svelte-1vtcqdz"><!>json</label> <label class="svelte-1vtcqdz"><!>raw</label></div> <!> <!> <!> <!> <div class="heading svelte-1vtcqdz"><!> <!></div> <!>', 1);
+const Ly = {
+ hash: "svelte-1vtcqdz",
+ code: ".heading.svelte-1vtcqdz {display:flex;margin-bottom:10px;}.radio-group.svelte-1vtcqdz {display:flex;margin:10px 0;}.radio-group.svelte-1vtcqdz label:where(.svelte-1vtcqdz) {display:flex;font-size:14px;}"
+};
+function Hd(e, t) {
+ de(t, !0), Je(e, Ly);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = [
+ { value: "get", label: "GET" },
+ { value: "post", label: "POST" },
+ { value: "put", label: "PUT" },
+ { value: "delete", label: "DELETE" },
+ { value: "head", label: "HEAD" },
+ { value: "patch", label: "PATCH" }
+ ], i = ht(), { addParameter: s } = kn(), { updateNodeData: a } = Dt();
+ return dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (u) => {
+ var c = $y();
+ L(u, c);
+ },
+ children: (u, c) => {
+ var f = Ay(), d = be(f), g = X(d), p = X(g);
+ const x = /* @__PURE__ */ Me(() => n().method ? [n().method] : ["get"]);
+ sn(p, {
+ items: o,
+ style: "width: 100%",
+ placeholder: "璇烽�夋嫨璇锋眰鏂瑰紡",
+ onSelect: (oe) => {
+ const ve = oe.value;
+ a(i, () => ({ method: ve }));
+ },
+ get value() {
+ return h(x);
+ }
+ }), Z(g);
+ var C = z(g, 2), $ = X(C);
+ const m = /* @__PURE__ */ Me(() => n().url || "");
+ xt($, {
+ placeholder: "璇疯緭鍏rl",
+ style: "width: 100%",
+ onchange: (oe) => {
+ a(i, () => ({ url: oe.target.value }));
+ },
+ get value() {
+ return h(m);
+ }
+ }), Z(C), Z(d);
+ var _ = z(d, 2), v = X(_);
+ Ge(v, {
+ level: 3,
+ children: (oe, ve) => {
+ Se();
+ var xe = Ie("Http 澶翠俊鎭�");
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ });
+ var b = z(v, 2);
+ Ke(b, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ s(i, "headers");
+ },
+ children: (oe, ve) => {
+ var xe = Ey();
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ }), Z(_);
+ var N = z(_, 2);
+ zt(N, { dataKeyName: "headers" });
+ var E = z(N, 2), M = X(E);
+ Ge(M, {
+ level: 3,
+ children: (oe, ve) => {
+ Se();
+ var xe = Ie("鍙傛暟");
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ });
+ var D = z(M, 2);
+ Ke(D, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ s(i, "urlParameters");
+ },
+ children: (oe, ve) => {
+ var xe = Sy();
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ }), Z(E);
+ var V = z(E, 2);
+ zt(V, { dataKeyName: "urlParameters" });
+ var A = z(V, 2);
+ Ge(A, {
+ level: 3,
+ mt: "10px",
+ children: (oe, ve) => {
+ Se();
+ var xe = Ie("Body");
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ });
+ var O = z(A, 2), R = X(O), S = X(R);
+ const T = /* @__PURE__ */ Me(() => !n().bodyType);
+ xt(S, {
+ type: "radio",
+ name: "bodyType",
+ value: "",
+ get checked() {
+ return h(T);
+ },
+ onchange: (oe) => {
+ var ve;
+ (ve = oe.target) != null && ve.checked && a(i, { bodyType: "" });
+ }
+ }), Se(), Z(R);
+ var k = z(R, 2), P = X(k);
+ const H = /* @__PURE__ */ Me(() => n().bodyType === "form-data");
+ xt(P, {
+ type: "radio",
+ name: "bodyType",
+ value: "form-data",
+ get checked() {
+ return h(H);
+ },
+ onchange: (oe) => {
+ var ve;
+ (ve = oe.target) != null && ve.checked && a(i, { bodyType: "form-data" });
+ }
+ }), Se(), Z(k);
+ var I = z(k, 2), B = X(I);
+ const F = /* @__PURE__ */ Me(() => n().bodyType === "x-www-form-urlencoded");
+ xt(B, {
+ type: "radio",
+ name: "bodyType",
+ value: "x-www-form-urlencoded",
+ get checked() {
+ return h(F);
+ },
+ onchange: (oe) => {
+ var ve;
+ (ve = oe.target) != null && ve.checked && a(i, { bodyType: "x-www-form-urlencoded" });
+ }
+ }), Se(), Z(I);
+ var K = z(I, 2), ie = X(K);
+ const ee = /* @__PURE__ */ Me(() => n().bodyType === "json");
+ xt(ie, {
+ type: "radio",
+ name: "bodyType",
+ value: "json",
+ get checked() {
+ return h(ee);
+ },
+ onchange: (oe) => {
+ var ve;
+ (ve = oe.target) != null && ve.checked && a(i, { bodyType: "json" });
+ }
+ }), Se(), Z(K);
+ var W = z(K, 2), ue = X(W);
+ const me = /* @__PURE__ */ Me(() => n().bodyType === "raw");
+ xt(ue, {
+ type: "radio",
+ name: "bodyType",
+ value: "raw",
+ get checked() {
+ return h(me);
+ },
+ onchange: (oe) => {
+ var ve;
+ (ve = oe.target) != null && ve.checked && a(i, { bodyType: "raw" });
+ }
+ }), Se(), Z(W), Z(O);
+ var Ce = z(O, 2);
+ {
+ var ge = (oe) => {
+ var ve = Ny(), xe = be(ve), Oe = X(xe);
+ Ge(Oe, {
+ level: 3,
+ children: (J, Re) => {
+ Se();
+ var le = Ie("鍙傛暟");
+ L(J, le);
+ },
+ $$slots: { default: !0 }
+ });
+ var ct = z(Oe, 2);
+ Ke(ct, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ s(i, "fromData");
+ },
+ children: (J, Re) => {
+ var le = Py();
+ L(J, le);
+ },
+ $$slots: { default: !0 }
+ }), Z(xe);
+ var lt = z(xe, 2);
+ zt(lt, { dataKeyName: "fromData" }), L(oe, ve);
+ };
+ ke(Ce, (oe) => {
+ n().bodyType === "form-data" && oe(ge);
+ });
+ }
+ var ze = z(Ce, 2);
+ {
+ var G = (oe) => {
+ var ve = Ty(), xe = be(ve), Oe = X(xe);
+ Ge(Oe, {
+ level: 3,
+ children: (J, Re) => {
+ Se();
+ var le = Ie("鍙傛暟");
+ L(J, le);
+ },
+ $$slots: { default: !0 }
+ });
+ var ct = z(Oe, 2);
+ Ke(ct, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ s(i, "fromUrlencoded");
+ },
+ children: (J, Re) => {
+ var le = My();
+ L(J, le);
+ },
+ $$slots: { default: !0 }
+ }), Z(xe);
+ var lt = z(xe, 2);
+ zt(lt, { dataKeyName: "fromUrlencoded" }), L(oe, ve);
+ };
+ ke(ze, (oe) => {
+ n().bodyType === "x-www-form-urlencoded" && oe(G);
+ });
+ }
+ var se = z(ze, 2);
+ {
+ var Te = (oe) => {
+ var ve = Hy(), xe = X(ve);
+ $t(xe, {
+ rows: "5",
+ style: "width: 100%",
+ placeholder: "璇疯緭鍏� json 淇℃伅",
+ get value() {
+ return n().bodyJson;
+ },
+ oninput: (Oe) => {
+ a(i, { bodyJson: Oe.target.value });
+ }
+ }), Z(ve), L(oe, ve);
+ };
+ ke(se, (oe) => {
+ n().bodyType === "json" && oe(Te);
+ });
+ }
+ var Ae = z(se, 2);
+ {
+ var Xe = (oe) => {
+ var ve = Vy(), xe = X(ve);
+ $t(xe, {
+ rows: "5",
+ style: "width: 100%",
+ placeholder: "璇疯緭鍏ヨ姹備俊鎭�",
+ get value() {
+ return n().bodyRaw;
+ },
+ oninput: (Oe) => {
+ a(i, { bodyRaw: Oe.target.value });
+ }
+ }), Z(ve), L(oe, ve);
+ };
+ ke(Ae, (oe) => {
+ n().bodyType === "raw" && oe(Xe);
+ });
+ }
+ var te = z(Ae, 2), Fe = X(te);
+ Ge(Fe, {
+ level: 3,
+ mt: "10px",
+ children: (oe, ve) => {
+ Se();
+ var xe = Ie("杈撳嚭鍙傛暟");
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ });
+ var Le = z(Fe, 2);
+ Ke(Le, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ s(i, "outputDefs");
+ },
+ children: (oe, ve) => {
+ var xe = Dy();
+ L(oe, xe);
+ },
+ $$slots: { default: !0 }
+ }), Z(te);
+ var Qe = z(te, 2);
+ Rn(Qe, {}), L(u, f);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(l) {
+ n(l), y();
+ }
+ });
+}
+ae(Hd, { data: {} }, [], [], !0);
+var Oy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>'), Iy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), zy = /* @__PURE__ */ ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">鐭ヨ瘑搴�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鑾峰彇鏁版嵁閲�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!></div> <!>', 1);
+const Ry = {
+ hash: "svelte-15t2v24",
+ code: ".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"
+};
+function Vd(e, t) {
+ de(t, !0), Je(e, Ry);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), s = Oo();
+ let a = Un(Tt([]));
+ un(async () => {
+ var c, f;
+ const u = await ((f = (c = s.provider) == null ? void 0 : c.knowledge) == null ? void 0 : f.call(c));
+ h(a).push(...u || []);
+ });
+ const { updateNodeData: l } = Dt();
+ return Nr(() => {
+ (!n().outputDefs || n().outputDefs.length === 0) && i(o, "outputDefs", {
+ name: "documents",
+ dataType: "Array",
+ nameDisabled: !0,
+ dataTypeDisabled: !0,
+ addChildDisabled: !0,
+ children: [
+ {
+ name: "title",
+ dataType: "String",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ },
+ {
+ name: "content",
+ dataType: "String",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ },
+ {
+ name: "documentId",
+ dataType: "Number",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ },
+ {
+ name: "knowledgeId",
+ dataType: "Number",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ }
+ ]
+ });
+ }), dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (c) => {
+ var f = Oy();
+ L(c, f);
+ },
+ children: (c, f) => {
+ var d = zy(), g = be(d), p = X(g);
+ Ge(p, {
+ level: 3,
+ children: (V, A) => {
+ Se();
+ var O = Ie("杈撳叆鍙傛暟");
+ L(V, O);
+ },
+ $$slots: { default: !0 }
+ });
+ var x = z(p, 2);
+ Ke(x, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (V, A) => {
+ var O = Iy();
+ L(V, O);
+ },
+ $$slots: { default: !0 }
+ }), Z(g);
+ var C = z(g, 2);
+ zt(C, {});
+ var $ = z(C, 2);
+ Ge($, {
+ level: 3,
+ mt: "10px",
+ children: (V, A) => {
+ Se();
+ var O = Ie("鐭ヨ瘑搴撹缃�");
+ L(V, O);
+ },
+ $$slots: { default: !0 }
+ });
+ var m = z($, 4), _ = X(m);
+ const v = /* @__PURE__ */ Me(() => n().knowledgeId ? [n().knowledgeId] : []);
+ sn(_, {
+ get items() {
+ return h(a);
+ },
+ style: "width: 100%",
+ placeholder: "璇烽�夋嫨鐭ヨ瘑搴�",
+ onSelect: (V) => {
+ const A = V.value;
+ l(o, () => ({ knowledgeId: A }));
+ },
+ get value() {
+ return h(v);
+ }
+ }), Z(m);
+ var b = z(m, 4), N = X(b);
+ xt(N, { placeholder: "鎼滅储鐨勬暟鎹潯鏁�", style: "width: 100%" }), Z(b);
+ var E = z(b, 2), M = X(E);
+ Ge(M, {
+ level: 3,
+ mt: "10px",
+ children: (V, A) => {
+ Se();
+ var O = Ie("杈撳嚭鍙傛暟");
+ L(V, O);
+ },
+ $$slots: { default: !0 }
+ }), Z(E);
+ var D = z(E, 2);
+ Rn(D, {}), L(c, d);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(u) {
+ n(u), y();
+ }
+ });
+}
+ae(Vd, { data: {} }, [], [], !0);
+var By = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>'), Yy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Zy = /* @__PURE__ */ ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">API 鏈嶅姟鍟�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">API Key</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鍏抽敭瀛�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鏁版嵁閲�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鍏朵粬鍙傛暟</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!></div> <!>', 1);
+const Xy = {
+ hash: "svelte-15t2v24",
+ code: ".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"
+};
+function Dd(e, t) {
+ de(t, !0), Je(e, Xy);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), s = Oo();
+ let a = Un(Tt([]));
+ un(async () => {
+ var c;
+ const u = await ((c = s.provider) == null ? void 0 : c.knowledge());
+ h(a).push(...u || []);
+ });
+ const { updateNodeData: l } = Dt();
+ return Nr(() => {
+ (!n().outputDefs || n().outputDefs.length === 0) && i(o, "outputDefs", {
+ name: "documents",
+ dataType: "Array",
+ nameDisabled: !0,
+ dataTypeDisabled: !0,
+ addChildDisabled: !0,
+ children: [
+ {
+ name: "title",
+ dataType: "String",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ },
+ {
+ name: "content",
+ dataType: "String",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ },
+ {
+ name: "documentId",
+ dataType: "Number",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ },
+ {
+ name: "knowledgeId",
+ dataType: "Number",
+ nameDisabled: !0,
+ dataTypeDisabled: !0
+ }
+ ]
+ });
+ }), dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (c) => {
+ var f = By();
+ L(c, f);
+ },
+ children: (c, f) => {
+ var d = Zy(), g = be(d), p = X(g);
+ Ge(p, {
+ level: 3,
+ children: (k, P) => {
+ Se();
+ var H = Ie("杈撳叆鍙傛暟");
+ L(k, H);
+ },
+ $$slots: { default: !0 }
+ });
+ var x = z(p, 2);
+ Ke(x, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (k, P) => {
+ var H = Yy();
+ L(k, H);
+ },
+ $$slots: { default: !0 }
+ }), Z(g);
+ var C = z(g, 2);
+ zt(C, {});
+ var $ = z(C, 2);
+ Ge($, {
+ level: 3,
+ mt: "10px",
+ children: (k, P) => {
+ Se();
+ var H = Ie("鎼滅储寮曟搸璁剧疆");
+ L(k, H);
+ },
+ $$slots: { default: !0 }
+ });
+ var m = z($, 4), _ = X(m);
+ const v = /* @__PURE__ */ Me(() => n().knowledgeId ? [n().knowledgeId] : []);
+ sn(_, {
+ get items() {
+ return h(a);
+ },
+ style: "width: 100%",
+ placeholder: "璇烽�夋嫨 API 鏈嶅姟鍟�",
+ onSelect: (k) => {
+ const P = k.value;
+ l(o, () => ({ knowledgeId: P }));
+ },
+ get value() {
+ return h(v);
+ }
+ }), Z(m);
+ var b = z(m, 4), N = X(b);
+ xt(N, {
+ placeholder: "璇疯緭鍏� API Key",
+ style: "width: 100%"
+ }), Z(b);
+ var E = z(b, 4), M = X(E);
+ xt(M, { placeholder: "璇疯緭鍏ュ叧閿瓧", style: "width: 100%" }), Z(E);
+ var D = z(E, 4), V = X(D);
+ xt(V, { placeholder: "鎼滅储鐨勬暟鎹潯鏁�", style: "width: 100%" }), Z(D);
+ var A = z(D, 4), O = X(A);
+ $t(O, {
+ rows: 3,
+ placeholder: "璇疯緭鍏ュ叾浠栧弬鏁帮紙Property 鏍煎紡锛�",
+ style: "width: 100%"
+ }), Z(A);
+ var R = z(A, 2), S = X(R);
+ Ge(S, {
+ level: 3,
+ mt: "10px",
+ children: (k, P) => {
+ Se();
+ var H = Ie("杈撳嚭鍙傛暟");
+ L(k, H);
+ },
+ $$slots: { default: !0 }
+ }), Z(R);
+ var T = z(R, 2);
+ Rn(T, {}), L(c, d);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(u) {
+ n(u), y();
+ }
+ });
+}
+ae(Dd, { data: {} }, [], [], !0);
+var Fy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>'), Wy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Ky = /* @__PURE__ */ ne('<div class="heading svelte-md8tgj"><!> <!></div> <!> <div class="heading svelte-md8tgj"><!></div> <!>', 1);
+const qy = {
+ hash: "svelte-md8tgj",
+ code: ".heading.svelte-md8tgj {display:flex;margin-bottom:10px;}.loop_handle_wrapper ::after {content:'寰幆浣�';width:100px;height:20px;background:#000;color:#fff;display:flex;justify-content:center;align-items:center;}"
+};
+function Ad(e, t) {
+ de(t, !0), Je(e, qy);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), s = Oo();
+ let a = Un(Tt([]));
+ return un(async () => {
+ var u;
+ const l = await ((u = s.provider) == null ? void 0 : u.knowledge());
+ h(a).push(...l || []);
+ }), dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (c) => {
+ var f = Fy();
+ L(c, f);
+ },
+ handle: (c) => {
+ Qn(c, {
+ type: "source",
+ get position() {
+ return $e.Bottom;
+ },
+ id: "loop_handle",
+ style: "bottom: -12px;width: 100px",
+ class: "loop_handle_wrapper"
+ });
+ },
+ children: (c, f) => {
+ var d = Ky(), g = be(d), p = X(g);
+ Ge(p, {
+ level: 3,
+ children: (v, b) => {
+ Se();
+ var N = Ie("寰幆鍙橀噺");
+ L(v, N);
+ },
+ $$slots: { default: !0 }
+ });
+ var x = z(p, 2);
+ Ke(x, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (v, b) => {
+ var N = Wy();
+ L(v, N);
+ },
+ $$slots: { default: !0 }
+ }), Z(g);
+ var C = z(g, 2);
+ zt(C, {});
+ var $ = z(C, 2), m = X($);
+ Ge(m, {
+ level: 3,
+ mt: "10px",
+ children: (v, b) => {
+ Se();
+ var N = Ie("杈撳嚭鍙傛暟");
+ L(v, N);
+ },
+ $$slots: { default: !0 }
+ }), Z($);
+ var _ = z($, 2);
+ Rn(_, {}), L(c, d);
+ },
+ $$slots: { icon: !0, handle: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(l) {
+ n(l), y();
+ }
+ });
+}
+ae(Ad, { data: {} }, [], [], !0);
+var Gy = /* @__PURE__ */ _e('<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor" p-id="2577" width="200" height="200"><path d="M312.096 408.576l67.84 67.84 45.312-45.216a32 32 0 0 1 45.248 45.248l-45.28 45.248 90.496 90.496 45.28-45.216a32 32 0 0 1 45.248 45.248l-45.248 45.248 67.904 67.872-90.528 90.528a224.064 224.064 0 0 1-292.544 21.024L176.32 906.368a32 32 0 0 1-45.248-45.248l69.504-69.472a224.064 224.064 0 0 1 21.024-292.576l90.496-90.496z m0 90.496L266.848 544.32a160 160 0 0 0-4.8 221.28l4.8 4.992a160 160 0 0 0 221.248 4.8l5.024-4.8 45.248-45.248-226.272-226.24z m610.272-384a32 32 0 0 1 0 45.248l-69.44 69.504a224.064 224.064 0 0 1-21.056 292.544l-90.528 90.528-316.8-316.8 90.56-90.496a224.064 224.064 0 0 1 292.544-21.024l69.44-69.504a32 32 0 0 1 45.28 0zM565.344 246.08l-5.024 4.8-45.248 45.248 226.272 226.272 45.248-45.248a160 160 0 0 0 4.8-221.28l-4.8-4.992a160 160 0 0 0-221.248-4.8z" p-id="2578"></path></svg>'), Uy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), jy = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'), Jy = /* @__PURE__ */ ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">閫夋嫨鍐呴儴鎺ュ彛</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!> <!></div> <!>', 1);
+const Qy = {
+ hash: "svelte-15t2v24",
+ code: ".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"
+};
+function Ld(e, t) {
+ de(t, !0), Je(e, Qy);
+ const n = w(t, "data", 7), r = /* @__PURE__ */ yt(t, [
+ "$$slots",
+ "$$events",
+ "$$legacy",
+ "$$host",
+ "data"
+ ]), o = ht(), { addParameter: i } = kn(), { updateNodeData: s } = Dt(), a = Oo();
+ let l = Un(Tt([]));
+ return un(async () => {
+ var c, f;
+ const u = await ((f = (c = a.provider) == null ? void 0 : c.internal) == null ? void 0 : f.call(c));
+ h(l).push(...u || []);
+ }), dn(e, ut(
+ {
+ get data() {
+ return n();
+ }
+ },
+ () => r,
+ {
+ icon: (c) => {
+ var f = Gy();
+ L(c, f);
+ },
+ children: (c, f) => {
+ var d = Jy(), g = be(d), p = X(g);
+ Ge(p, {
+ level: 3,
+ children: (D, V) => {
+ Se();
+ var A = Ie("杈撳叆鍙傛暟");
+ L(D, A);
+ },
+ $$slots: { default: !0 }
+ });
+ var x = z(p, 2);
+ Ke(x, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o);
+ },
+ children: (D, V) => {
+ var A = Uy();
+ L(D, A);
+ },
+ $$slots: { default: !0 }
+ }), Z(g);
+ var C = z(g, 2);
+ zt(C, {});
+ var $ = z(C, 2);
+ Ge($, {
+ level: 3,
+ mt: "10px",
+ children: (D, V) => {
+ Se();
+ var A = Ie("鎺ュ彛");
+ L(D, A);
+ },
+ $$slots: { default: !0 }
+ });
+ var m = z($, 4), _ = X(m);
+ const v = /* @__PURE__ */ Me(() => n().method ? [n().method] : [""]);
+ sn(_, {
+ get items() {
+ return h(l);
+ },
+ style: "width: 100%",
+ placeholder: "璇烽�夋嫨鍐呴儴鎺ュ彛",
+ onSelect: (D) => {
+ const V = D.value;
+ s(o, () => ({ method: V }));
+ },
+ get value() {
+ return h(v);
+ }
+ }), Z(m);
+ var b = z(m, 2), N = X(b);
+ Ge(N, {
+ level: 3,
+ mt: "10px",
+ children: (D, V) => {
+ Se();
+ var A = Ie("杈撳嚭鍙傛暟");
+ L(D, A);
+ },
+ $$slots: { default: !0 }
+ });
+ var E = z(N, 2);
+ Ke(E, {
+ class: "input-btn-more",
+ style: "margin-left: auto",
+ onclick: () => {
+ i(o, "outputDefs");
+ },
+ children: (D, V) => {
+ var A = jy();
+ L(D, A);
+ },
+ $$slots: { default: !0 }
+ }), Z(b);
+ var M = z(b, 2);
+ Rn(M, {}), L(c, d);
+ },
+ $$slots: { icon: !0, default: !0 }
+ }
+ )), fe({
+ get data() {
+ return n();
+ },
+ set data(u) {
+ n(u), y();
+ }
+ });
+}
+ae(Ld, { data: {} }, [], [], !0);
+const ew = {
+ startNode: kd,
+ codeNode: Md,
+ llmNode: Nd,
+ templateNode: Td,
+ httpNode: Hd,
+ knowledgeNode: Vd,
+ searchEngineNode: Dd,
+ loopNode: Ad,
+ internalNode: Ld,
+ endNode: Sd
+};
+var tw = /* @__PURE__ */ ne("<!> ", 1);
+function Od(e, t) {
+ de(t, !0);
+ const n = w(t, "icon", 7), r = w(t, "title", 7), o = w(t, "type", 7), i = w(t, "description", 7), s = w(t, "extra", 7);
+ return Ke(e, {
+ draggable: !0,
+ ondragstart: (l) => {
+ if (!l.dataTransfer)
+ return null;
+ const u = {
+ type: o(),
+ data: {
+ title: r(),
+ description: i(),
+ systemPrompt: "",
+ userPrompt: "",
+ ...s()
+ }
+ };
+ l.dataTransfer.setData("application/tinyflow", JSON.stringify(u)), l.dataTransfer.effectAllowed = "move";
+ },
+ children: (l, u) => {
+ var c = tw(), f = be(c);
+ mu(f, n);
+ var d = z(f);
+ Ee(() => Rt(d, ` ${r() ?? ""}`)), L(l, c);
+ },
+ $$slots: { default: !0 }
+ }), fe({
+ get icon() {
+ return n();
+ },
+ set icon(l) {
+ n(l), y();
+ },
+ get title() {
+ return r();
+ },
+ set title(l) {
+ r(l), y();
+ },
+ get type() {
+ return o();
+ },
+ set type(l) {
+ o(l), y();
+ },
+ get description() {
+ return i();
+ },
+ set description(l) {
+ i(l), y();
+ },
+ get extra() {
+ return s();
+ },
+ set extra(l) {
+ s(l), y();
+ }
+ });
+}
+ae(
+ Od,
+ {
+ icon: {},
+ title: {},
+ type: {},
+ description: {},
+ extra: {}
+ },
+ [],
+ [],
+ !0
+);
+var nw = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4.83582 12L11.0429 18.2071L12.4571 16.7929L7.66424 12L12.4571 7.20712L11.0429 5.79291L4.83582 12ZM10.4857 12L16.6928 18.2071L18.107 16.7929L13.3141 12L18.107 7.20712L16.6928 5.79291L10.4857 12Z"></path></svg>'), rw = /* @__PURE__ */ _e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.1642 12L12.9571 5.79291L11.5429 7.20712L16.3358 12L11.5429 16.7929L12.9571 18.2071L19.1642 12ZM13.5143 12L7.30722 5.79291L5.89301 7.20712L10.6859 12L5.89301 16.7929L7.30722 18.2071L13.5143 12Z"></path></svg>'), ow = /* @__PURE__ */ ne('<div><div class="tf-toolbar-container "><div class="tf-toolbar-container-header"><!></div> <div class="tf-toolbar-container-body"><div class="tf-toolbar-container-base"></div> <div class="tf-toolbar-container-tools"><!></div></div></div> <!></div>');
+function Id(e) {
+ let t = Un("base"), n = Un("show");
+ const r = [
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',
+ title: "寮�濮嬭妭鐐�",
+ type: "startNode",
+ description: "寮�濮嬪畾涔夎緭鍏ュ弬鏁�"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',
+ title: "寰幆",
+ type: "loopNode",
+ description: "鐢ㄤ簬寰幆鎵ц浠诲姟"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',
+ title: "澶фā鍨�",
+ type: "llmNode",
+ description: "浣跨敤澶фā鍨嬪鐞嗛棶棰�"
+ },
+ {
+ // icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4.7134 7.12811L4.46682 7.69379C4.28637 8.10792 3.71357 8.10792 3.53312 7.69379L3.28656 7.12811C2.84706 6.11947 2.05545 5.31641 1.06767 4.87708L0.308047 4.53922C-0.102682 4.35653 -0.102682 3.75881 0.308047 3.57612L1.0252 3.25714C2.03838 2.80651 2.84417 1.97373 3.27612 0.930828L3.52932 0.319534C3.70578 -0.106511 4.29417 -0.106511 4.47063 0.319534L4.72382 0.930828C5.15577 1.97373 5.96158 2.80651 6.9748 3.25714L7.69188 3.57612C8.10271 3.75881 8.10271 4.35653 7.69188 4.53922L6.93228 4.87708C5.94451 5.31641 5.15288 6.11947 4.7134 7.12811ZM6.33421 15.8154C6.51032 15.233 6.7072 14.6562 6.93912 14.0327C8.99484 8.50636 12.4197 5.08172 18.0129 4.21479C17.5 5.35838 17.0151 6.15301 16.5858 6.58237C16.2521 6.91603 15.9185 7.24993 15.5848 7.58407L14.1721 8.99878L15.6279 10.4535C14.4976 12.5384 12.2652 14.1979 9.75193 14.512C8.43544 14.6766 7.29345 15.1188 6.33421 15.8154ZM18 9.99658L17 8.99728C17.3331 8.66372 17.6662 8.33039 18.0027 7.99391C19.0018 6.99303 20.0009 4.99392 21 1.99658C6.31105 1.99658 4.08854 15.422 3.06361 21.6132C3.0419 21.7443 3.02074 21.8722 3 21.9966H4.99824C5.66421 18.6635 7.33146 16.8301 10 16.4966C14 15.9966 17 12.9966 18 9.99658Z"></path></svg>',
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',
+ title: "鐭ヨ瘑搴�",
+ type: "knowledgeNode",
+ description: "閫氳繃鐭ヨ瘑搴撹幏鍙栧唴瀹�"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',
+ title: "鎼滅储寮曟搸",
+ type: "searchEngineNode",
+ description: "閫氳繃鎼滅储寮曟搸鎼滅储鍐呭"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',
+ title: "Http 璇锋眰",
+ type: "httpNode",
+ description: "閫氳繃 HTTP 璇锋眰鑾峰彇鏁版嵁"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',
+ title: "鍔ㄦ�佷唬鐮�",
+ type: "codeNode",
+ description: "鍔ㄦ�佹墽琛屼唬鐮�"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',
+ title: "鍐呭妯℃澘",
+ type: "templateNode",
+ description: "閫氳繃妯℃澘寮曟搸鐢熸垚鍐呭"
+ },
+ {
+ icon: '<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor" p-id="2577" width="200" height="200"><path d="M312.096 408.576l67.84 67.84 45.312-45.216a32 32 0 0 1 45.248 45.248l-45.28 45.248 90.496 90.496 45.28-45.216a32 32 0 0 1 45.248 45.248l-45.248 45.248 67.904 67.872-90.528 90.528a224.064 224.064 0 0 1-292.544 21.024L176.32 906.368a32 32 0 0 1-45.248-45.248l69.504-69.472a224.064 224.064 0 0 1 21.024-292.576l90.496-90.496z m0 90.496L266.848 544.32a160 160 0 0 0-4.8 221.28l4.8 4.992a160 160 0 0 0 221.248 4.8l5.024-4.8 45.248-45.248-226.272-226.24z m610.272-384a32 32 0 0 1 0 45.248l-69.44 69.504a224.064 224.064 0 0 1-21.056 292.544l-90.528 90.528-316.8-316.8 90.56-90.496a224.064 224.064 0 0 1 292.544-21.024l69.44-69.504a32 32 0 0 1 45.28 0zM565.344 246.08l-5.024 4.8-45.248 45.248 226.272 226.272 45.248-45.248a160 160 0 0 0 4.8-221.28l-4.8-4.992a160 160 0 0 0-221.248-4.8z" p-id="2578"></path></svg>',
+ title: "鍐呴儴鎺ュ彛",
+ type: "internalNode",
+ description: "鎵ц鍐呴儴鎻愪緵鎺ュ彛"
+ },
+ {
+ icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',
+ title: "缁撴潫鑺傜偣",
+ type: "endNode",
+ description: "缁撴潫瀹氫箟杈撳嚭鍙傛暟"
+ }
+ ], o = [
+ {
+ label: "鍩虹鑺傜偣",
+ value: "base"
+ },
+ {
+ label: "涓氬姟宸ュ叿",
+ value: "tools"
+ }
+ ];
+ var i = ow(), s = X(i), a = X(s), l = X(a);
+ sd(l, {
+ style: "width: 100%",
+ items: o,
+ onChange: (p) => {
+ U(t, Tt(p.value.toString()));
+ }
+ }), Z(a);
+ var u = z(a, 2), c = X(u);
+ Yt(c, 21, () => r, Li, (p, x) => {
+ Od(p, ut(() => h(x)));
+ }), Z(c);
+ var f = z(c, 2), d = X(f);
+ Ke(d, {
+ children: (p, x) => {
+ Se();
+ var C = Ie("娴嬭瘯涓氬姟鎸夐挳");
+ L(p, C);
+ },
+ $$slots: { default: !0 }
+ }), Z(f), Z(u), Z(s);
+ var g = z(s, 2);
+ Ke(g, {
+ onclick: () => {
+ U(n, Tt(h(n) ? "" : "show"));
+ },
+ children: (p, x) => {
+ var C = et(), $ = be(C);
+ {
+ var m = (v) => {
+ var b = nw();
+ L(v, b);
+ }, _ = (v) => {
+ var b = rw();
+ L(v, b);
+ };
+ ke($, (v) => {
+ h(n) === "show" ? v(m) : v(_, !1);
+ });
+ }
+ L(p, C);
+ },
+ $$slots: { default: !0 }
+ }), Z(i), Ee(() => {
+ kt(i, 1, `tf-toolbar ${h(n) ?? ""}`), ce(c, "style", `display: ${(h(t) === "base" ? "flex" : "none") ?? ""}`), ce(f, "style", `display: ${(h(t) !== "base" ? "flex" : "none") ?? ""}`);
+ }), L(e, i);
+}
+ae(Id, {}, [], [], !0);
+const iw = () => {
+ const { nodeLookup: e } = Ue();
+ return {
+ getNode: (n) => {
+ var o;
+ return (o = q(e).get(n)) == null ? void 0 : o.internals.userNode;
+ }
+ };
+}, sw = () => {
+ const { nodes: e } = Ue();
+ return {
+ ensureParentInNodesBefore: (n, r) => {
+ e.update((o) => {
+ let i = -1;
+ for (let l = 0; l < o.length; l++)
+ if (o[l].id === n) {
+ i = l;
+ break;
+ }
+ if (i <= 0)
+ return o;
+ let s = -1;
+ for (let l = 0; l < i; l++)
+ if (o[l].parentId === n || o[l].id === r) {
+ s = l;
+ break;
+ }
+ if (s == -1)
+ return o;
+ const a = o[i];
+ for (let l = i; l > s; l--)
+ o[l] = o[l - 1];
+ return o[s] = a, o;
+ });
+ }
+ };
+}, aw = () => {
+ const { edges: e } = Ue();
+ return {
+ getEdgesByTarget: (n) => q(e).filter((o) => o.target === n)
+ };
+};
+var lw = /* @__PURE__ */ ne('<div class="panel-content svelte-1oe15vw"><div>杈瑰睘鎬ц缃�</div> <div class="setting-title svelte-1oe15vw">杈规潯浠惰缃�</div> <div class="setting-item"><!></div></div>'), uw = /* @__PURE__ */ ne("<!> <!> <!> <!>", 1), cw = /* @__PURE__ */ ne('<div style="position: relative; height: 100%; width: 100%"><!> <!></div>');
+const dw = {
+ hash: "svelte-1oe15vw",
+ code: ".panel-content.svelte-1oe15vw {padding:10px;background-color:#fff;border-radius:5px;box-shadow:0 2px 4px rgba(0, 0, 0, 0.1);width:200px;border:1px solid #efefef;}.setting-title.svelte-1oe15vw {margin:10px 0;font-size:12px;color:#999;}"
+};
+function zd(e, t) {
+ de(t, !0), Je(e, dw);
+ const n = w(t, "onInit", 7), r = Dt();
+ n()(r);
+ let o = Un(!1);
+ const i = (_) => {
+ _.preventDefault(), _.dataTransfer && (_.dataTransfer.dropEffect = "move");
+ }, s = (_) => {
+ var M;
+ _.preventDefault();
+ const v = r.screenToFlowPosition({
+ x: _.clientX - 250,
+ y: _.clientY - 100
+ }), b = (M = _.dataTransfer) == null ? void 0 : M.getData("application/tinyflow"), N = b ? JSON.parse(b) : {}, E = {
+ id: `node_${Rr()}`,
+ position: v,
+ data: {},
+ ...N
+ };
+ ei.addNode(E), ei.selectNodeOnly(E.id);
+ }, { getNode: a } = iw(), l = (_) => {
+ const v = a(_.source), b = a(_.target);
+ if (_.sourceHandle === "loop_handle" || v.parentId) {
+ const N = r.getEdges();
+ for (let E of N)
+ if (E.target === _.target) {
+ const M = a(E.source);
+ if (_.sourceHandle === "loop_handle" && M.parentId !== v.id || v.parentId && M.parentId !== v.parentId)
+ return !1;
+ }
+ }
+ return !(!v.parentId && b.parentId && b.parentId !== v.id);
+ }, { ensureParentInNodesBefore: u } = sw(), c = (_, v) => {
+ if (!v.isValid)
+ return;
+ const b = v.toNode;
+ if (b.parentId)
+ return;
+ const N = v.fromNode, E = v.fromHandle, M = { position: { ...b.position } };
+ if (E.id === "loop_handle" ? M.parentId = N.id : N.parentId && (M.parentId = N.parentId), M.parentId) {
+ const D = a(M.parentId);
+ M.position = {
+ x: b.position.x - D.position.x,
+ y: b.position.y - D.position.y
+ }, u(M.parentId, b.id), r.updateNode(b.id, M);
+ }
+ }, { getEdgesByTarget: f } = aw(), d = (_) => {
+ _.edges.forEach((b) => {
+ const N = a(b.target);
+ if (N.parentId) {
+ const E = f(b.target), M = a(N.parentId);
+ if (E.length === 0)
+ r.updateNode(N.id, {
+ parentId: void 0,
+ position: {
+ x: N.position.x + M.position.x,
+ y: N.position.y + M.position.y
+ }
+ });
+ else {
+ let D = !1;
+ for (let V = 0; V < E.length; V++) {
+ const A = E[V], O = a(A.source);
+ if (O.parentId || O.type === "loopNode") {
+ D = !0;
+ break;
+ }
+ }
+ D || r.updateNode(N.id, {
+ parentId: void 0,
+ position: {
+ x: N.position.x + M.position.x,
+ y: N.position.y + M.position.y
+ }
+ });
+ }
+ }
+ });
+ }, g = (_, v) => {
+ console.log("onconnectstart: ", _, v);
+ }, p = (_) => {
+ console.log("onconnect: ", _);
+ };
+ var x = cw(), C = X(x);
+ Id(C);
+ var $ = z(C, 2);
+ const m = /* @__PURE__ */ Me(() => ({
+ // animated: true,
+ // label: 'edge label',
+ markerEnd: {
+ type: mo.ArrowClosed,
+ // color: 'red',
+ width: 20,
+ height: 20
+ }
+ }));
+ return Fc($, ut({ nodeTypes: ew }, ei, {
+ class: "tinyflow-logo",
+ isValidConnection: l,
+ onconnectend: c,
+ onconnectstart: g,
+ onconnect: p,
+ connectionRadius: 50,
+ ondelete: d,
+ onclick: (_) => {
+ const v = _.target;
+ v.classList.contains("svelte-flow__edge-interaction") || v.classList.contains("panel-content") || v.closest(".panel-content") || U(o, !1);
+ },
+ get defaultEdgeOptions() {
+ return h(m);
+ },
+ $$events: {
+ drop: s,
+ dragover: i,
+ edgeclick: () => {
+ U(o, !0);
+ }
+ },
+ children: (_, v) => {
+ var b = uw(), N = be(b);
+ td(N, {});
+ var E = z(N, 2);
+ Jc(E, {});
+ var M = z(E, 2);
+ rd(M, {});
+ var D = z(M, 2);
+ {
+ var V = (A) => {
+ Ho(A, {
+ children: (O, R) => {
+ var S = lw(), T = z(X(S), 4), k = X(T);
+ $t(k, {
+ rows: 3,
+ placeholder: "璇疯緭鍏ヨ竟鏉′欢",
+ style: "width: 100%",
+ oninput: (P) => {
+ }
+ }), Z(T), Z(S), L(O, S);
+ },
+ $$slots: { default: !0 }
+ });
+ };
+ ke(D, (A) => {
+ h(o) && A(V);
+ });
+ }
+ L(_, b);
+ },
+ $$slots: { default: !0 }
+ })), Z(x), L(e, x), fe({
+ get onInit() {
+ return n();
+ },
+ set onInit(_) {
+ n(_), y();
+ }
+ });
+}
+ae(zd, { onInit: {} }, [], [], !0);
+function fw(e, t) {
+ de(t, !0);
+ const n = w(t, "options", 7), r = w(t, "onInit", 7), { data: o } = n();
+ return ei.init((o == null ? void 0 : o.nodes) || [], (o == null ? void 0 : o.edges) || []), Tr("tinyflow_options", n()), Wc(e, {
+ fitView: !0,
+ children: (i, s) => {
+ zd(i, {
+ get onInit() {
+ return r();
+ }
+ });
+ },
+ $$slots: { default: !0 }
+ }), fe({
+ get options() {
+ return n();
+ },
+ set options(i) {
+ n(i), y();
+ },
+ get onInit() {
+ return r();
+ },
+ set onInit(i) {
+ r(i), y();
+ }
+ });
+}
+customElements.define("tinyflow-component", ae(fw, { options: {}, onInit: {} }, [], [], !1));
+export {
+ yw as Tinyflow
+};
+//# sourceMappingURL=index.js.map
diff --git a/src/components/Tinyflow/ui/index.umd.js b/src/components/Tinyflow/ui/index.umd.js
new file mode 100644
index 0000000..baf0ad5
--- /dev/null
+++ b/src/components/Tinyflow/ui/index.umd.js
@@ -0,0 +1,9 @@
+(function(We,Je){typeof exports=="object"&&typeof module<"u"?Je(exports):typeof define=="function"&&define.amd?define(["exports"],Je):(We=typeof globalThis<"u"?globalThis:We||self,Je(We.Tinyflow={}))})(this,function(We){"use strict";var fw=Object.defineProperty;var Rd=We=>{throw TypeError(We)};var gw=(We,Je,ot)=>Je in We?fw(We,Je,{enumerable:!0,configurable:!0,writable:!0,value:ot}):We[Je]=ot;var Nt=(We,Je,ot)=>gw(We,typeof Je!="symbol"?Je+"":Je,ot),ba=(We,Je,ot)=>Je.has(We)||Rd("Cannot "+ot);var ut=(We,Je,ot)=>(ba(We,Je,"read from private field"),ot?ot.call(We):Je.get(We)),wr=(We,Je,ot)=>Je.has(We)?Rd("Cannot add the same private member more than once"):Je instanceof WeakSet?Je.add(We):Je.set(We,ot),zo=(We,Je,ot,Ro)=>(ba(We,Je,"write to private field"),Ro?Ro.call(We,ot):Je.set(We,ot),ot),Bd=(We,Je,ot)=>(ba(We,Je,"access private method"),ot);var Md,or,qr,Lo,Gi,Yd,Zn,jt;const Je="5";typeof window<"u"&&((Md=window.__svelte??(window.__svelte={})).v??(Md.v=new Set)).add(Je);let ot=!1,Ro=!1;function Zd(){ot=!0}Zd();const es=1,ts=2,Ca=4,Xd=8,Fd=16,Wd=1,Kd=2,ka=4,qd=8,Gd=16,$a=1,Ud=2,ns="[",rs="[!",os="]",_r={},Tt=Symbol(),Ea="http://www.w3.org/2000/svg",Sa=!1,Qt=2,Pa=4,Bo=8,is=16,Pn=32,xr=64,Yo=128,Kt=256,Zo=512,yt=1024,Nn=2048,ir=4096,Tn=8192,Xo=16384,jd=32768,br=65536,Jd=1<<17,Qd=1<<19,Na=1<<20,Xn=Symbol("$state"),ss=Symbol("legacy props"),ef=Symbol("");var Gr=Array.isArray,tf=Array.prototype.indexOf,as=Array.from,Fo=Object.keys,Ur=Object.defineProperty,Mn=Object.getOwnPropertyDescriptor,Ta=Object.getOwnPropertyDescriptors,nf=Object.prototype,rf=Array.prototype,ls=Object.getPrototypeOf;function jr(e){return typeof e=="function"}const gt=()=>{};function of(e){return e()}function Jr(e){for(var t=0;t<e.length;t++)e[t]()}const sf=typeof requestIdleCallback>"u"?e=>setTimeout(e,1):requestIdleCallback;let Qr=[],eo=[];function Ma(){var e=Qr;Qr=[],Jr(e)}function Ha(){var e=eo;eo=[],Jr(e)}function to(e){Qr.length===0&&queueMicrotask(Ma),Qr.push(e)}function af(e){eo.length===0&&sf(Ha),eo.push(e)}function Va(){Qr.length>0&&Ma(),eo.length>0&&Ha()}function Da(e){return e===this.v}function us(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function cs(e){return!us(e,this.v)}function lf(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function uf(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function cf(e){throw new Error("https://svelte.dev/e/effect_orphan")}function df(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function ff(){throw new Error("https://svelte.dev/e/hydration_failed")}function gf(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function hf(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function vf(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function pf(){throw new Error("https://svelte.dev/e/state_unsafe_local_read")}function mf(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Mt(e,t){var n={f:0,v:e,reactions:null,equals:Da,rv:0,wv:0};return n}function Fn(e){return Aa(Mt(e))}function no(e,t=!1){var r;const n=Mt(e);return t||(n.equals=cs),ot&&Ye!==null&&Ye.l!==null&&((r=Ye.l).s??(r.s=[])).push(n),n}function re(e,t=!1){return Aa(no(e,t))}function Aa(e){return Qe!==null&&!en&&Qe.f&Qt&&(mn===null?_f([e]):mn.push(e)),e}function U(e,t){return Qe!==null&&!en&&ni()&&Qe.f&(Qt|is)&&(mn===null||!mn.includes(e))&&mf(),ds(e,t)}function ds(e,t){return e.equals(t)||(e.v,e.v=t,e.wv=Wa(),Oa(e,Nn),ni()&&Ke!==null&&Ke.f&yt&&!(Ke.f&(Pn|xr))&&(Vn===null?xf([e]):Vn.push(e))),t}function La(e,t=1){var n=h(e),r=t===1?n++:n--;return U(e,n),r}function Oa(e,t){var n=e.reactions;if(n!==null)for(var r=ni(),o=n.length,i=0;i<o;i++){var s=n[i],a=s.f;a&Nn||!r&&s===Ke||(tn(s,t),a&(yt|Kt)&&(a&Qt?Oa(s,ir):ei(s)))}}function Ne(e){var t=Qt|Nn,n=Qe!==null&&Qe.f&Qt?Qe:null;return Ke===null||n!==null&&n.f&Kt?t|=Kt:Ke.f|=Na,{ctx:Ye,deps:null,effects:null,equals:Da,f:t,fn:e,reactions:null,rv:0,v:null,wv:0,parent:n??Ke}}function ve(e){const t=Ne(e);return t.equals=cs,t}function Ia(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n<t.length;n+=1)qt(t[n])}}function yf(e){for(var t=e.parent;t!==null;){if(!(t.f&Qt))return t;t=t.parent}return null}function wf(e){var t,n=Ke;Kn(yf(e));try{Ia(e),t=qa(e)}finally{Kn(n)}return t}function za(e){var t=wf(e),n=(qn||e.f&Kt)&&e.deps!==null?ir:yt;tn(e,n),e.equals(t)||(e.v=t,e.wv=Wa())}function Wo(e){console.warn("https://svelte.dev/e/hydration_mismatch")}let Se=!1;function It(e){Se=e}let Ve;function bt(e){if(e===null)throw Wo(),_r;return Ve=e}function vn(){return bt(pn(Ve))}function Z(e){if(Se){if(pn(Ve)!==null)throw Wo(),_r;Ve=e}}function Pe(e=1){if(Se){for(var t=e,n=Ve;t--;)n=pn(n);Ve=n}}function fs(){for(var e=0,t=Ve;;){if(t.nodeType===8){var n=t.data;if(n===os){if(e===0)return t;e-=1}else(n===ns||n===rs)&&(e+=1)}var r=pn(t);t.remove(),t=r}}function Ht(e,t=null,n){if(typeof e!="object"||e===null||Xn in e)return e;const r=ls(e);if(r!==nf&&r!==rf)return e;var o=new Map,i=Gr(e),s=Mt(0);i&&o.set("length",Mt(e.length));var a;return new Proxy(e,{defineProperty(l,u,c){(!("value"in c)||c.configurable===!1||c.enumerable===!1||c.writable===!1)&&hf();var f=o.get(u);return f===void 0?(f=Mt(c.value),o.set(u,f)):U(f,Ht(c.value,a)),!0},deleteProperty(l,u){var c=o.get(u);if(c===void 0)u in l&&o.set(u,Mt(Tt));else{if(i&&typeof u=="string"){var f=o.get("length"),d=Number(u);Number.isInteger(d)&&d<f.v&&U(f,d)}U(c,Tt),Ra(s)}return!0},get(l,u,c){var p;if(u===Xn)return e;var f=o.get(u),d=u in l;if(f===void 0&&(!d||(p=Mn(l,u))!=null&&p.writable)&&(f=Mt(Ht(d?l[u]:Tt,a)),o.set(u,f)),f!==void 0){var g=h(f);return g===Tt?void 0:g}return Reflect.get(l,u,c)},getOwnPropertyDescriptor(l,u){var c=Reflect.getOwnPropertyDescriptor(l,u);if(c&&"value"in c){var f=o.get(u);f&&(c.value=h(f))}else if(c===void 0){var d=o.get(u),g=d==null?void 0:d.v;if(d!==void 0&&g!==Tt)return{enumerable:!0,configurable:!0,value:g,writable:!0}}return c},has(l,u){var g;if(u===Xn)return!0;var c=o.get(u),f=c!==void 0&&c.v!==Tt||Reflect.has(l,u);if(c!==void 0||Ke!==null&&(!f||(g=Mn(l,u))!=null&&g.writable)){c===void 0&&(c=Mt(f?Ht(l[u],a):Tt),o.set(u,c));var d=h(c);if(d===Tt)return!1}return f},set(l,u,c,f){var _;var d=o.get(u),g=u in l;if(i&&u==="length")for(var p=c;p<d.v;p+=1){var x=o.get(p+"");x!==void 0?U(x,Tt):p in l&&(x=Mt(Tt),o.set(p+"",x))}d===void 0?(!g||(_=Mn(l,u))!=null&&_.writable)&&(d=Mt(void 0),U(d,Ht(c,a)),o.set(u,d)):(g=d.v!==Tt,U(d,Ht(c,a)));var C=Reflect.getOwnPropertyDescriptor(l,u);if(C!=null&&C.set&&C.set.call(f,c),!g){if(i&&typeof u=="string"){var $=o.get("length"),m=Number(u);Number.isInteger(m)&&m>=$.v&&U($,m+1)}Ra(s)}return!0},ownKeys(l){h(s);var u=Reflect.ownKeys(l).filter(d=>{var g=o.get(d);return g===void 0||g.v!==Tt});for(var[c,f]of o)f.v!==Tt&&!(c in l)&&u.push(c);return u},setPrototypeOf(){vf()}})}function Ra(e,t=1){U(e,e.v+t)}var Vt,Ba,Ya,Za;function gs(){if(Vt===void 0){Vt=window,Ba=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype;Ya=Mn(t,"firstChild").get,Za=Mn(t,"nextSibling").get,e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__styles=null,e.__e=void 0,Text.prototype.__t=void 0}}function Hn(e=""){return document.createTextNode(e)}function Ct(e){return Ya.call(e)}function pn(e){return Za.call(e)}function X(e,t){if(!Se)return Ct(e);var n=Ct(Ve);if(n===null)n=Ve.appendChild(Hn());else if(t&&n.nodeType!==3){var r=Hn();return n==null||n.before(r),bt(r),r}return bt(n),n}function xe(e,t){if(!Se){var n=Ct(e);return n instanceof Comment&&n.data===""?pn(n):n}return Ve}function z(e,t=1,n=!1){let r=Se?Ve:e;for(var o;t--;)o=r,r=pn(r);if(!Se)return r;var i=r==null?void 0:r.nodeType;if(n&&i!==3){var s=Hn();return r===null?o==null||o.after(s):r.before(s),bt(s),s}return bt(r),r}function hs(e){e.textContent=""}let Ko=!1,qo=!1,Go=null,sr=!1,vs=!1;function Xa(e){vs=e}let ro=[],hw=[],Qe=null,en=!1;function Wn(e){Qe=e}let Ke=null;function Kn(e){Ke=e}let mn=null;function _f(e){mn=e}let kt=null,zt=0,Vn=null;function xf(e){Vn=e}let Fa=1,Uo=0,qn=!1;function Wa(){return++Fa}function Cr(e){var f;var t=e.f;if(t&Nn)return!0;if(t&ir){var n=e.deps,r=(t&Kt)!==0;if(n!==null){var o,i,s=(t&Zo)!==0,a=r&&Ke!==null&&!qn,l=n.length;if(s||a){var u=e,c=u.parent;for(o=0;o<l;o++)i=n[o],(s||!((f=i==null?void 0:i.reactions)!=null&&f.includes(u)))&&(i.reactions??(i.reactions=[])).push(u);s&&(u.f^=Zo),a&&c!==null&&!(c.f&Kt)&&(u.f^=Kt)}for(o=0;o<l;o++)if(i=n[o],Cr(i)&&za(i),i.wv>e.wv)return!0}(!r||Ke!==null&&!qn)&&tn(e,yt)}return!1}function bf(e,t){for(var n=t;n!==null;){if(n.f&Yo)try{n.fn(e);return}catch{n.f^=Yo}n=n.parent}throw Ko=!1,e}function Cf(e){return(e.f&Xo)===0&&(e.parent===null||(e.parent.f&Yo)===0)}function jo(e,t,n,r){if(Ko){if(n===null&&(Ko=!1),Cf(t))throw e;return}n!==null&&(Ko=!0);{bf(e,t);return}}function Ka(e,t,n=!0){var r=e.reactions;if(r!==null)for(var o=0;o<r.length;o++){var i=r[o];i.f&Qt?Ka(i,t,!1):t===i&&(n?tn(i,Nn):i.f&yt&&tn(i,ir),ei(i))}}function qa(e){var g;var t=kt,n=zt,r=Vn,o=Qe,i=qn,s=mn,a=Ye,l=en,u=e.f;kt=null,zt=0,Vn=null,qn=(u&Kt)!==0&&(en||!sr||Qe===null),Qe=u&(Pn|xr)?null:e,mn=null,rl(e.ctx),en=!1,Uo++;try{var c=(0,e.fn)(),f=e.deps;if(kt!==null){var d;if(Jo(e,zt),f!==null&&zt>0)for(f.length=zt+kt.length,d=0;d<kt.length;d++)f[zt+d]=kt[d];else e.deps=f=kt;if(!qn)for(d=zt;d<f.length;d++)((g=f[d]).reactions??(g.reactions=[])).push(e)}else f!==null&&zt<f.length&&(Jo(e,zt),f.length=zt);if(ni()&&Vn!==null&&!en&&f!==null&&!(e.f&(Qt|ir|Nn)))for(d=0;d<Vn.length;d++)Ka(Vn[d],e);return o!==null&&Uo++,c}finally{kt=t,zt=n,Vn=r,Qe=o,qn=i,mn=s,rl(a),en=l}}function kf(e,t){let n=t.reactions;if(n!==null){var r=tf.call(n,e);if(r!==-1){var o=n.length-1;o===0?n=t.reactions=null:(n[r]=n[o],n.pop())}}n===null&&t.f&Qt&&(kt===null||!kt.includes(t))&&(tn(t,ir),t.f&(Kt|Zo)||(t.f^=Zo),Ia(t),Jo(t,0))}function Jo(e,t){var n=e.deps;if(n!==null)for(var r=t;r<n.length;r++)kf(e,n[r])}function Qo(e){var t=e.f;if(!(t&Xo)){tn(e,yt);var n=Ke,r=Ye,o=sr;Ke=e,sr=!0;try{t&is?Vf(e):Qa(e),Ja(e);var i=qa(e);e.teardown=typeof i=="function"?i:null,e.wv=Fa;var s=e.deps,a;Sa&&Ro&&e.f&Nn}catch(l){jo(l,e,n,r||e.ctx)}finally{sr=o,Ke=n}}}function $f(){try{df()}catch(e){if(Go!==null)jo(e,Go,null);else throw e}}function Ga(){var e=sr;try{var t=0;for(sr=!0;ro.length>0;){t++>1e3&&$f();var n=ro,r=n.length;ro=[];for(var o=0;o<r;o++){var i=n[o];i.f&yt||(i.f^=yt);var s=Sf(i);Ef(s)}}}finally{qo=!1,sr=e,Go=null}}function Ef(e){var t=e.length;if(t!==0)for(var n=0;n<t;n++){var r=e[n];if(!(r.f&(Xo|Tn)))try{Cr(r)&&(Qo(r),r.deps===null&&r.first===null&&r.nodes_start===null&&(r.teardown===null?el(r):r.fn=null))}catch(o){jo(o,r,null,r.ctx)}}}function ei(e){qo||(qo=!0,queueMicrotask(Ga));for(var t=Go=e;t.parent!==null;){t=t.parent;var n=t.f;if(n&(xr|Pn)){if(!(n&yt))return;t.f^=yt}}ro.push(t)}function Sf(e){for(var t=[],n=e.first;n!==null;){var r=n.f,o=(r&Pn)!==0,i=o&&(r&yt)!==0;if(!i&&!(r&Tn)){if(r&Pa)t.push(n);else if(o)n.f^=yt;else{var s=Qe;try{Qe=n,Cr(n)&&Qo(n)}catch(u){jo(u,n,null,n.ctx)}finally{Qe=s}}var a=n.first;if(a!==null){n=a;continue}}var l=n.parent;for(n=n.next;n===null&&l!==null;)n=l.next,l=l.parent}return t}function y(e){var t;for(Va();ro.length>0;)qo=!0,Ga(),Va();return t}function h(e){var t=e.f,n=(t&Qt)!==0;if(Qe!==null&&!en){mn!==null&&mn.includes(e)&&pf();var r=Qe.deps;e.rv<Uo&&(e.rv=Uo,kt===null&&r!==null&&r[zt]===e?zt++:kt===null?kt=[e]:(!qn||!kt.includes(e))&&kt.push(e))}else if(n&&e.deps===null&&e.effects===null){var o=e,i=o.parent;i!==null&&!(i.f&Kt)&&(o.f^=Kt)}return n&&(o=e,Cr(o)&&za(o)),e.v}function yn(e){var t=en;try{return en=!0,e()}finally{en=t}}const Pf=-7169;function tn(e,t){e.f=e.f&Pf|t}function j(e){if(!(typeof e!="object"||!e||e instanceof EventTarget)){if(Xn in e)ps(e);else if(!Array.isArray(e))for(let t in e){const n=e[t];typeof n=="object"&&n&&Xn in n&&ps(n)}}}function ps(e,t=new Set){if(typeof e=="object"&&e!==null&&!(e instanceof EventTarget)&&!t.has(e)){t.add(e),e instanceof Date&&e.getTime();for(let r in e)try{ps(e[r],t)}catch{}const n=ls(e);if(n!==Object.prototype&&n!==Array.prototype&&n!==Map.prototype&&n!==Set.prototype&&n!==Date.prototype){const r=Ta(n);for(let o in r){const i=r[o].get;if(i)try{i.call(e)}catch{}}}}}function Ua(e){Ke===null&&Qe===null&&cf(),Qe!==null&&Qe.f&Kt&&Ke===null&&uf(),vs&&lf()}function Nf(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function ar(e,t,n,r=!0){var o=(e&xr)!==0,i=Ke,s={ctx:Ye,deps:null,nodes_start:null,nodes_end:null,f:e|Nn,first:null,fn:t,last:null,next:null,parent:o?null:i,prev:null,teardown:null,transitions:null,wv:0};if(n)try{Qo(s),s.f|=jd}catch(u){throw qt(s),u}else t!==null&&ei(s);var a=n&&s.deps===null&&s.first===null&&s.nodes_start===null&&s.teardown===null&&(s.f&(Na|Yo))===0;if(!a&&!o&&r&&(i!==null&&Nf(s,i),Qe!==null&&Qe.f&Qt)){var l=Qe;(l.effects??(l.effects=[])).push(s)}return s}function ja(e){const t=ar(Bo,null,!1);return tn(t,yt),t.teardown=e,t}function kr(e){Ua();var t=Ke!==null&&(Ke.f&Pn)!==0&&Ye!==null&&!Ye.m;if(t){var n=Ye;(n.e??(n.e=[])).push({fn:e,effect:Ke,reaction:Qe})}else{var r=Rt(e);return r}}function Tf(e){return Ua(),$r(e)}function Mf(e){const t=ar(xr,e,!0);return()=>{qt(t)}}function Hf(e){const t=ar(xr,e,!0);return(n={})=>new Promise(r=>{n.outro?Er(t,()=>{qt(t),r(void 0)}):(qt(t),r(void 0))})}function Rt(e){return ar(Pa,e,!1)}function ge(e,t){var n=Ye,r={effect:null,ran:!1};n.l.r1.push(r),r.effect=$r(()=>{e(),!r.ran&&(r.ran=!0,U(n.l.r2,!0),yn(t))})}function vt(){var e=Ye;$r(()=>{if(h(e.l.r2)){for(var t of e.l.r1){var n=t.effect;n.f&yt&&tn(n,ir),Cr(n)&&Qo(n),t.ran=!1}e.l.r2.v=!1}})}function $r(e){return ar(Bo,e,!0)}function Ee(e,t=[],n=Ne){const r=t.map(n);return lr(()=>e(...r.map(h)))}function lr(e,t=0){return ar(Bo|is|t,e,!0)}function Dn(e,t=!0){return ar(Bo|Pn,e,!0,t)}function Ja(e){var t=e.teardown;if(t!==null){const n=vs,r=Qe;Xa(!0),Wn(null);try{t.call(null)}finally{Xa(n),Wn(r)}}}function Qa(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){var r=n.next;qt(n,t),n=r}}function Vf(e){for(var t=e.first;t!==null;){var n=t.next;t.f&Pn||qt(t),t=n}}function qt(e,t=!0){var n=!1;if((t||e.f&Qd)&&e.nodes_start!==null){for(var r=e.nodes_start,o=e.nodes_end;r!==null;){var i=r===o?null:pn(r);r.remove(),r=i}n=!0}Qa(e,t&&!n),Jo(e,0),tn(e,Xo);var s=e.transitions;if(s!==null)for(const l of s)l.stop();Ja(e);var a=e.parent;a!==null&&a.first!==null&&el(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes_start=e.nodes_end=null}function el(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function Er(e,t){var n=[];ms(e,n,!0),tl(n,()=>{qt(e),t&&t()})}function tl(e,t){var n=e.length;if(n>0){var r=()=>--n||t();for(var o of e)o.out(r)}else t()}function ms(e,t,n){if(!(e.f&Tn)){if(e.f^=Tn,e.transitions!==null)for(const s of e.transitions)(s.is_global||n)&&t.push(s);for(var r=e.first;r!==null;){var o=r.next,i=(r.f&br)!==0||(r.f&Pn)!==0;ms(r,t,i?n:!1),r=o}}}function oo(e){nl(e,!0)}function nl(e,t){if(e.f&Tn){e.f^=Tn,e.f&yt||(e.f^=yt),Cr(e)&&(tn(e,Nn),ei(e));for(var n=e.first;n!==null;){var r=n.next,o=(n.f&br)!==0||(n.f&Pn)!==0;nl(n,o?t:!1),n=r}if(e.transitions!==null)for(const i of e.transitions)(i.is_global||t)&&i.in()}}function ti(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}let Ye=null;function rl(e){Ye=e}function ur(e){return ys().get(e)}function Sr(e,t){return ys().set(e,t),t}function Df(e){return ys().has(e)}function ue(e,t=!1,n){Ye={p:Ye,c:null,e:null,m:!1,s:e,x:null,l:null},ot&&!t&&(Ye.l={s:null,u:null,r1:[],r2:Mt(!1)})}function ce(e){const t=Ye;if(t!==null){e!==void 0&&(t.x=e);const s=t.e;if(s!==null){var n=Ke,r=Qe;t.e=null;try{for(var o=0;o<s.length;o++){var i=s[o];Kn(i.effect),Wn(i.reaction),Rt(i.fn)}}finally{Kn(n),Wn(r)}}Ye=t.p,t.m=!0}return e||{}}function ni(){return!ot||Ye!==null&&Ye.l===null}function ys(e){return Ye===null&&ti(),Ye.c??(Ye.c=new Map(Af(Ye)||void 0))}function Af(e){let t=e.p;for(;t!==null;){const n=t.c;if(n!==null)return n;t=t.p}return null}function Lf(e){return e.endsWith("capture")&&e!=="gotpointercapture"&&e!=="lostpointercapture"}const Of=["beforeinput","click","change","dblclick","contextmenu","focusin","focusout","input","keydown","keyup","mousedown","mousemove","mouseout","mouseover","mouseup","pointerdown","pointermove","pointerout","pointerover","pointerup","touchend","touchmove","touchstart"];function If(e){return Of.includes(e)}const zf={formnovalidate:"formNoValidate",ismap:"isMap",nomodule:"noModule",playsinline:"playsInline",readonly:"readOnly",defaultvalue:"defaultValue",defaultchecked:"defaultChecked",srcobject:"srcObject",novalidate:"noValidate",allowfullscreen:"allowFullscreen",disablepictureinpicture:"disablePictureInPicture",disableremoteplayback:"disableRemotePlayback"};function Rf(e){return e=e.toLowerCase(),zf[e]??e}const Bf=["touchstart","touchmove"];function Yf(e){return Bf.includes(e)}const Zf=["textarea","script","style","title"];function Xf(e){return Zf.includes(e)}function Ff(e,t){if(t){const n=document.body;e.autofocus=!0,to(()=>{document.activeElement===n&&e.focus()})}}function Wf(e){Se&&Ct(e)!==null&&hs(e)}let ol=!1;function Kf(){ol||(ol=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const n of e.target.elements)(t=n.__on_r)==null||t.call(n)})},{capture:!0}))}function qf(e){var t=Qe,n=Ke;Wn(null),Kn(null);try{return e()}finally{Wn(t),Kn(n)}}const il=new Set,ws=new Set;function sl(e,t,n,r={}){function o(i){if(r.capture||io.call(t,i),!i.cancelBubble)return qf(()=>n==null?void 0:n.call(this,i))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?to(()=>{t.addEventListener(e,o,r)}):t.addEventListener(e,o,r),o}function Ze(e,t,n,r,o){var i={capture:r,passive:o},s=sl(e,t,n,i);(t===document.body||t===window||t===document)&&ja(()=>{t.removeEventListener(e,s,i)})}function ri(e){for(var t=0;t<e.length;t++)il.add(e[t]);for(var n of ws)n(e)}function io(e){var m;var t=this,n=t.ownerDocument,r=e.type,o=((m=e.composedPath)==null?void 0:m.call(e))||[],i=o[0]||e.target,s=0,a=e.__root;if(a){var l=o.indexOf(a);if(l!==-1&&(t===document||t===window)){e.__root=t;return}var u=o.indexOf(t);if(u===-1)return;l<=u&&(s=l)}if(i=o[s]||e.target,i!==t){Ur(e,"currentTarget",{configurable:!0,get(){return i||n}});var c=Qe,f=Ke;Wn(null),Kn(null);try{for(var d,g=[];i!==null;){var p=i.assignedSlot||i.parentNode||i.host||null;try{var x=i["__"+r];if(x!==void 0&&(!i.disabled||e.target===i))if(Gr(x)){var[C,...$]=x;C.apply(i,[e,...$])}else x.call(i,e)}catch(_){d?g.push(_):d=_}if(e.cancelBubble||p===t||p===null)break;i=p}if(d){for(let _ of g)queueMicrotask(()=>{throw _});throw d}}finally{e.__root=t,delete e.currentTarget,Wn(c),Kn(f)}}}function _s(e){var t=document.createElement("template");return t.innerHTML=e,t.content}function Dt(e,t){var n=Ke;n.nodes_start===null&&(n.nodes_start=e,n.nodes_end=t)}function ne(e,t){var n=(t&$a)!==0,r=(t&Ud)!==0,o,i=!e.startsWith("<!>");return()=>{if(Se)return Dt(Ve,null),Ve;o===void 0&&(o=_s(i?e:"<!>"+e),n||(o=Ct(o)));var s=r||Ba?document.importNode(o,!0):o.cloneNode(!0);if(n){var a=Ct(s),l=s.lastChild;Dt(a,l)}else Dt(s,s);return s}}function _e(e,t,n="svg"){var r=!e.startsWith("<!>"),o=(t&$a)!==0,i=`<${n}>${r?e:"<!>"+e}</${n}>`,s;return()=>{if(Se)return Dt(Ve,null),Ve;if(!s){var a=_s(i),l=Ct(a);if(o)for(s=document.createDocumentFragment();Ct(l);)s.appendChild(Ct(l));else s=Ct(l)}var u=s.cloneNode(!0);if(o){var c=Ct(u),f=u.lastChild;Dt(c,f)}else Dt(u,u);return u}}function Ae(e=""){if(!Se){var t=Hn(e+"");return Dt(t,t),t}var n=Ve;return n.nodeType!==3&&(n.before(n=Hn()),bt(n)),Dt(n,n),n}function tt(){if(Se)return Dt(Ve,null),Ve;var e=document.createDocumentFragment(),t=document.createComment(""),n=Hn();return e.append(t,n),Dt(t,n),e}function L(e,t){if(Se){Ke.nodes_end=Ve,vn();return}e!==null&&e.before(t)}function Bt(e,t){var n=t==null?"":typeof t=="object"?t+"":t;n!==(e.__t??(e.__t=e.nodeValue))&&(e.__t=n,e.nodeValue=n+"")}function al(e,t){return ll(e,t)}function Gf(e,t){gs(),t.intro=t.intro??!1;const n=t.target,r=Se,o=Ve;try{for(var i=Ct(n);i&&(i.nodeType!==8||i.data!==ns);)i=pn(i);if(!i)throw _r;It(!0),bt(i),vn();const s=ll(e,{...t,anchor:i});if(Ve===null||Ve.nodeType!==8||Ve.data!==os)throw Wo(),_r;return It(!1),s}catch(s){if(s===_r)return t.recover===!1&&ff(),gs(),hs(n),It(!1),al(e,t);throw s}finally{It(r),bt(o)}}const Pr=new Map;function ll(e,{target:t,anchor:n,props:r={},events:o,context:i,intro:s=!0}){gs();var a=new Set,l=f=>{for(var d=0;d<f.length;d++){var g=f[d];if(!a.has(g)){a.add(g);var p=Yf(g);t.addEventListener(g,io,{passive:p});var x=Pr.get(g);x===void 0?(document.addEventListener(g,io,{passive:p}),Pr.set(g,1)):Pr.set(g,x+1)}}};l(as(il)),ws.add(l);var u=void 0,c=Hf(()=>{var f=n??t.appendChild(Hn());return Dn(()=>{if(i){ue({});var d=Ye;d.c=i}o&&(r.$$events=o),Se&&Dt(f,null),u=e(f,r)||{},Se&&(Ke.nodes_end=Ve),i&&ce()}),()=>{var p;for(var d of a){t.removeEventListener(d,io);var g=Pr.get(d);--g===0?(document.removeEventListener(d,io),Pr.delete(d)):Pr.set(d,g)}ws.delete(l),f!==n&&((p=f.parentNode)==null||p.removeChild(f))}});return xs.set(u,c),u}let xs=new WeakMap;function Uf(e,t){const n=xs.get(e);return n?(xs.delete(e),n(t)):Promise.resolve()}function ke(e,t,[n,r]=[0,0]){Se&&n===0&&vn();var o=e,i=null,s=null,a=Tt,l=n>0?br:0,u=!1;const c=(d,g=!0)=>{u=!0,f(g,d)},f=(d,g)=>{if(a===(a=d))return;let p=!1;if(Se&&r!==-1){if(n===0){const C=o.data;C===ns?r=0:C===rs?r=1/0:(r=parseInt(C.substring(1)),r!==r&&(r=a?1/0:-1))}const x=r>n;!!a===x&&(o=fs(),bt(o),It(!1),p=!0,r=-1)}a?(i?oo(i):g&&(i=Dn(()=>g(o))),s&&Er(s,()=>{s=null})):(s?oo(s):g&&(s=Dn(()=>g(o,[n+1,r]))),i&&Er(i,()=>{i=null})),p&&It(!0)};lr(()=>{u=!1,t(c),u||f(null,null)},l),Se&&(o=Ve)}function oi(e,t){return t}function jf(e,t,n,r){for(var o=[],i=t.length,s=0;s<i;s++)ms(t[s].e,o,!0);var a=i>0&&o.length===0&&n!==null;if(a){var l=n.parentNode;hs(l),l.append(n),r.clear(),Gn(e,t[0].prev,t[i-1].next)}tl(o,()=>{for(var u=0;u<i;u++){var c=t[u];a||(r.delete(c.k),Gn(e,c.prev,c.next)),qt(c.e,!a)}})}function Yt(e,t,n,r,o,i=null){var s=e,a={flags:t,items:new Map,first:null},l=(t&Ca)!==0;if(l){var u=e;s=Se?bt(Ct(u)):u.appendChild(Hn())}Se&&vn();var c=null,f=!1,d=ve(()=>{var g=n();return Gr(g)?g:g==null?[]:as(g)});lr(()=>{var g=h(d),p=g.length;if(f&&p===0)return;f=p===0;let x=!1;if(Se){var C=s.data===rs;C!==(p===0)&&(s=fs(),bt(s),It(!1),x=!0)}if(Se){for(var $=null,m,_=0;_<p;_++){if(Ve.nodeType===8&&Ve.data===os){s=Ve,x=!0,It(!1);break}var v=g[_],b=r(v,_);m=ul(Ve,a,$,null,v,b,_,o,t,n),a.items.set(b,m),$=m}p>0&&bt(fs())}Se||Jf(g,a,s,o,t,r,n),i!==null&&(p===0?c?oo(c):c=Dn(()=>i(s)):c!==null&&Er(c,()=>{c=null})),x&&It(!0),h(d)}),Se&&(s=Ve)}function Jf(e,t,n,r,o,i,s){var S,M,k,P;var a=(o&Xd)!==0,l=(o&(es|ts))!==0,u=e.length,c=t.items,f=t.first,d=f,g,p=null,x,C=[],$=[],m,_,v,b;if(a)for(b=0;b<u;b+=1)m=e[b],_=i(m,b),v=c.get(_),v!==void 0&&((S=v.a)==null||S.measure(),(x??(x=new Set)).add(v));for(b=0;b<u;b+=1){if(m=e[b],_=i(m,b),v=c.get(_),v===void 0){var N=d?d.e.nodes_start:n;p=ul(N,t,p,p===null?t.first:p.next,m,_,b,r,o,s),c.set(_,p),C=[],$=[],d=p.next;continue}if(l&&Qf(v,m,b,o),v.e.f&Tn&&(oo(v.e),a&&((M=v.a)==null||M.unfix(),(x??(x=new Set)).delete(v))),v!==d){if(g!==void 0&&g.has(v)){if(C.length<$.length){var E=$[0],T;p=E.prev;var D=C[0],V=C[C.length-1];for(T=0;T<C.length;T+=1)cl(C[T],E,n);for(T=0;T<$.length;T+=1)g.delete($[T]);Gn(t,D.prev,V.next),Gn(t,p,D),Gn(t,V,E),d=E,p=V,b-=1,C=[],$=[]}else g.delete(v),cl(v,d,n),Gn(t,v.prev,v.next),Gn(t,v,p===null?t.first:p.next),Gn(t,p,v),p=v;continue}for(C=[],$=[];d!==null&&d.k!==_;)d.e.f&Tn||(g??(g=new Set)).add(d),$.push(d),d=d.next;if(d===null)continue;v=d}C.push(v),p=v,d=v.next}if(d!==null||g!==void 0){for(var A=g===void 0?[]:as(g);d!==null;)d.e.f&Tn||A.push(d),d=d.next;var O=A.length;if(O>0){var R=o&Ca&&u===0?n:null;if(a){for(b=0;b<O;b+=1)(k=A[b].a)==null||k.measure();for(b=0;b<O;b+=1)(P=A[b].a)==null||P.fix()}jf(t,A,R,c)}}a&&to(()=>{var H;if(x!==void 0)for(v of x)(H=v.a)==null||H.apply()}),Ke.first=t.first&&t.first.e,Ke.last=p&&p.e}function Qf(e,t,n,r){r&es&&ds(e.v,t),r&ts?ds(e.i,n):e.i=n}function ul(e,t,n,r,o,i,s,a,l,u){var c=(l&es)!==0,f=(l&Fd)===0,d=c?f?no(o):Mt(o):o,g=l&ts?Mt(s):s,p={i:g,v:d,k:i,a:null,e:null,prev:n,next:r};try{return p.e=Dn(()=>a(e,d,g,u),Se),p.e.prev=n&&n.e,p.e.next=r&&r.e,n===null?t.first=p:(n.next=p,n.e.next=p.e),r!==null&&(r.prev=p,r.e.prev=p.e),p}finally{}}function cl(e,t,n){for(var r=e.next?e.next.e.nodes_start:n,o=t?t.e.nodes_start:n,i=e.e.nodes_start;i!==r;){var s=pn(i);o.before(i),i=s}}function Gn(e,t,n){t===null?e.first=n:(t.next=n,t.e.next=n&&n.e),n!==null&&(n.prev=t,n.e.prev=t&&t.e)}function dl(e,t,n,r,o){var i=e,s="",a;lr(()=>{if(s===(s=t()??"")){Se&&vn();return}a!==void 0&&(qt(a),a=void 0),s!==""&&(a=Dn(()=>{if(Se){Ve.data;for(var l=vn(),u=l;l!==null&&(l.nodeType!==8||l.data!=="");)u=l,l=pn(l);if(l===null)throw Wo(),_r;Dt(Ve,u),i=bt(l);return}var c=s+"",f=_s(c);Dt(Ct(f),f.lastChild),i.before(f)}))})}function wt(e,t,n,r,o){var a;Se&&vn();var i=(a=t.$$slots)==null?void 0:a[n],s=!1;i===!0&&(i=t[n==="default"?"children":n],s=!0),i===void 0||i(e,s?()=>r:r)}function e1(e){const t={};e.children&&(t.default=!0);for(const n in e.$$slots)t[n]=!0;return t}function cr(e,t,...n){var r=e,o=gt,i;lr(()=>{o!==(o=t())&&(i&&(qt(i),i=null),i=Dn(()=>o(r,...n)))},br),Se&&(r=Ve)}function fl(e,t,n){Se&&vn();var r=e,o,i;lr(()=>{o!==(o=t())&&(i&&(Er(i),i=null),o&&(i=Dn(()=>n(r,o))))},br),Se&&(r=Ve)}function t1(e,t,n,r,o,i){let s=Se;Se&&vn();var a,l,u=null;Se&&Ve.nodeType===1&&(u=Ve,vn());var c=Se?Ve:e,f;lr(()=>{const d=t()||null;var g=d==="svg"?Ea:null;d!==a&&(f&&(d===null?Er(f,()=>{f=null,l=null}):d===l?oo(f):qt(f)),d&&d!==l&&(f=Dn(()=>{if(u=Se?u:g?document.createElementNS(g,d):document.createElement(d),Dt(u,u),r){Se&&Xf(d)&&u.append(document.createComment(""));var p=Se?Ct(u):u.appendChild(Hn());Se&&(p===null?It(!1):bt(p)),r(u,p)}Ke.nodes_end=u,c.before(u)})),a=d,a&&(l=a))},br),s&&(It(!0),bt(c))}function et(e,t){to(()=>{var n=e.getRootNode(),r=n.host?n:n.head??n.ownerDocument.head;if(!r.querySelector("#"+t.hash)){const o=document.createElement("style");o.id=t.hash,o.textContent=t.code,r.appendChild(o)}})}function _t(e,t,n){Rt(()=>{var r=yn(()=>t(e,n==null?void 0:n())||{});if(n&&(r!=null&&r.update)){var o=!1,i={};$r(()=>{var s=n();j(s),o&&us(i,s)&&(i=s,r.update(s))}),o=!0}if(r!=null&&r.destroy)return()=>r.destroy()})}function gl(e){var t,n,r="";if(typeof e=="string"||typeof e=="number")r+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(n=gl(e[t]))&&(r&&(r+=" "),r+=n)}else for(n in e)e[n]&&(r&&(r+=" "),r+=n);return r}function n1(){for(var e,t,n=0,r="",o=arguments.length;n<o;n++)(e=arguments[n])&&(t=gl(e))&&(r&&(r+=" "),r+=t);return r}function wn(e){return typeof e=="object"?n1(e):e??""}const hl=[...`
+\r\f聽\v\uFEFF`];function r1(e,t,n){var r=e==null?"":""+e;if(t&&(r=r?r+" "+t:t),n){for(var o in n)if(n[o])r=r?r+" "+o:o;else if(r.length)for(var i=o.length,s=0;(s=r.indexOf(o,s))>=0;){var a=s+i;(s===0||hl.includes(r[s-1]))&&(a===r.length||hl.includes(r[a]))?r=(s===0?"":r.substring(0,s))+r.substring(a+1):s=a}}return r===""?null:r}function $t(e,t,n,r,o,i){var s=e.__className;if(Se||s!==n){var a=r1(n,r,i);(!Se||a!==e.getAttribute("class"))&&(a==null?e.removeAttribute("class"):t?e.className=a:e.setAttribute("class",a)),e.__className=n}else if(i)for(var l in i){var u=!!i[l];(o==null||u!==!!o[l])&&e.classList.toggle(l,u)}return i}const so=Symbol("class");function ao(e){if(Se){var t=!1,n=()=>{if(!t){if(t=!0,e.hasAttribute("value")){var r=e.value;de(e,"value",null),e.value=r}if(e.hasAttribute("checked")){var o=e.checked;de(e,"checked",null),e.checked=o}}};e.__on_r=n,af(n),Kf()}}function bs(e,t){var n=e.__attributes??(e.__attributes={});n.value===(n.value=t??void 0)||e.value===t&&(t!==0||e.nodeName!=="PROGRESS")||(e.value=t??"")}function o1(e,t){t?e.hasAttribute("selected")||e.setAttribute("selected",""):e.removeAttribute("selected")}function de(e,t,n,r){var o=e.__attributes??(e.__attributes={});Se&&(o[t]=e.getAttribute(t),t==="src"||t==="srcset"||t==="href"&&e.nodeName==="LINK")||o[t]!==(o[t]=n)&&(t==="style"&&"__styles"in e&&(e.__styles={}),t==="loading"&&(e[ef]=n),n==null?e.removeAttribute(t):typeof n!="string"&&pl(e).includes(t)?e[t]=n:e.setAttribute(t,n))}function nn(e,t,n,r,o=!1,i=!1,s=!1){let a=Se&&i;a&&It(!1);var l=t||{},u=e.tagName==="OPTION";for(var c in t)c in n||(n[c]=null);n.class?n.class=wn(n.class):(r||n[so])&&(n.class=null);var f=pl(e),d=e.__attributes??(e.__attributes={});for(const _ in n){let v=n[_];if(u&&_==="value"&&v==null){e.value=e.__value="",l[_]=v;continue}if(_==="class"){var g=e.namespaceURI==="http://www.w3.org/1999/xhtml";$t(e,g,v,r,t==null?void 0:t[so],n[so]),l[_]=v,l[so]=n[so];continue}var p=l[_];if(v!==p){l[_]=v;var x=_[0]+_[1];if(x!=="$$"){if(x==="on"){const b={},N="$$"+_;let E=_.slice(2);var C=If(E);if(Lf(E)&&(E=E.slice(0,-7),b.capture=!0),!C&&p){if(v!=null)continue;e.removeEventListener(E,l[N],b),l[N]=null}if(v!=null)if(C)e[`__${E}`]=v,ri([E]);else{let T=function(D){l[_].call(this,D)};l[N]=sl(E,e,T,b)}else C&&(e[`__${E}`]=void 0)}else if(_==="style"&&v!=null)e.style.cssText=v+"";else if(_==="autofocus")Ff(e,!!v);else if(!i&&(_==="__value"||_==="value"&&v!=null))e.value=e.__value=v;else if(_==="selected"&&u)o1(e,v);else{var $=_;o||($=Rf($));var m=$==="defaultValue"||$==="defaultChecked";if(v==null&&!i&&!m)if(d[_]=null,$==="value"||$==="checked"){let b=e;const N=t===void 0;if($==="value"){let E=b.defaultValue;b.removeAttribute($),b.defaultValue=E,b.value=b.__value=N?E:null}else{let E=b.defaultChecked;b.removeAttribute($),b.defaultChecked=E,b.checked=N?E:!1}}else e.removeAttribute(_);else m||f.includes($)&&(i||typeof v!="string")?e[$]=v:typeof v!="function"&&de(e,$,v)}_==="style"&&"__styles"in e&&(e.__styles={})}}}return a&&It(!0),l}var vl=new Map;function pl(e){var t=vl.get(e.nodeName);if(t)return t;vl.set(e.nodeName,t=[]);for(var n,r=e,o=Element.prototype;o!==r;){n=Ta(r);for(var i in n)n[i].set&&t.push(i);r=ls(r)}return t}function at(e,t,n,r){var o=e.__styles??(e.__styles={});o[t]!==n&&(o[t]=n,n==null?e.style.removeProperty(t):e.style.setProperty(t,n,""))}const Ui=class Ui{constructor(t){wr(this,Gi);wr(this,or,new WeakMap);wr(this,qr);wr(this,Lo);zo(this,Lo,t)}observe(t,n){var r=ut(this,or).get(t)||new Set;return r.add(n),ut(this,or).set(t,r),Bd(this,Gi,Yd).call(this).observe(t,ut(this,Lo)),()=>{var o=ut(this,or).get(t);o.delete(n),o.size===0&&(ut(this,or).delete(t),ut(this,qr).unobserve(t))}}};or=new WeakMap,qr=new WeakMap,Lo=new WeakMap,Gi=new WeakSet,Yd=function(){return ut(this,qr)??zo(this,qr,new ResizeObserver(t=>{for(var n of t){Ui.entries.set(n.target,n);for(var r of ut(this,or).get(n.target)||[])r(n)}}))},Nt(Ui,"entries",new WeakMap);let Cs=Ui;var i1=new Cs({box:"border-box"});function ml(e,t,n){var r=i1.observe(e,()=>n(e[t]));Rt(()=>(yn(()=>n(e[t])),r))}function yl(e,t){return e===t||(e==null?void 0:e[Xn])===t}function An(e={},t,n,r){return Rt(()=>{var o,i;return $r(()=>{o=i,i=[],yn(()=>{e!==n(...i)&&(t(e,...i),o&&yl(n(...o),e)&&t(null,...o))})}),()=>{to(()=>{i&&yl(n(...i),e)&&t(null,...i)})}}),e}function ks(e){return function(...t){var n=t[0];return n.stopPropagation(),e==null?void 0:e.apply(this,t)}}function He(e=!1){const t=Ye,n=t.l.u;if(!n)return;let r=()=>j(t.s);if(e){let o=0,i={};const s=Ne(()=>{let a=!1;const l=t.s;for(const u in l)l[u]!==i[u]&&(i[u]=l[u],a=!0);return a&&o++,o});r=()=>h(s)}n.b.length&&Tf(()=>{wl(t,r),Jr(n.b)}),kr(()=>{const o=yn(()=>n.m.map(of));return()=>{for(const i of o)typeof i=="function"&&i()}}),n.a.length&&kr(()=>{wl(t,r),Jr(n.a)})}function wl(e,t){if(e.l.s)for(const n of e.l.s)h(n);t()}function De(e,t){var i;var n=(i=e.$$events)==null?void 0:i[t.type],r=Gr(n)?n.slice():n==null?[]:[n];for(var o of r)o.call(this,t)}function rn(e){Ye===null&&ti(),ot&&Ye.l!==null?a1(Ye).m.push(e):kr(()=>{const t=yn(e);if(typeof t=="function")return t})}function $s(e){Ye===null&&ti(),rn(()=>()=>yn(e))}function s1(e,t,{bubbles:n=!1,cancelable:r=!1}={}){return new CustomEvent(e,{detail:t,bubbles:n,cancelable:r})}function ii(){const e=Ye;return e===null&&ti(),(t,n,r)=>{var i;const o=(i=e.s.$$events)==null?void 0:i[t];if(o){const s=Gr(o)?o.slice():[o],a=s1(t,n,r);for(const l of s)l.call(e.x,a);return!a.defaultPrevented}return!0}}function a1(e){var t=e.l;return t.u??(t.u={a:[],b:[],m:[]})}function Es(e,t,n){if(e==null)return t(void 0),n&&n(void 0),gt;const r=yn(()=>e.subscribe(t,n));return r.unsubscribe?()=>r.unsubscribe():r}const Nr=[];function Gt(e,t){return{subscribe:we(e,t).subscribe}}function we(e,t=gt){let n=null;const r=new Set;function o(a){if(us(e,a)&&(e=a,n)){const l=!Nr.length;for(const u of r)u[1](),Nr.push(u,e);if(l){for(let u=0;u<Nr.length;u+=2)Nr[u][0](Nr[u+1]);Nr.length=0}}}function i(a){o(a(e))}function s(a,l=gt){const u=[a,l];return r.add(u),r.size===1&&(n=t(o,i)||gt),a(e),()=>{r.delete(u),r.size===0&&n&&(n(),n=null)}}return{set:o,update:i,subscribe:s}}function Un(e,t,n){const r=!Array.isArray(e),o=r?[e]:e;if(!o.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const i=t.length<2;return Gt(n,(s,a)=>{let l=!1;const u=[];let c=0,f=gt;const d=()=>{if(c)return;f();const p=t(r?u[0]:u,s,a);i?s(p):f=typeof p=="function"?p:gt},g=o.map((p,x)=>Es(p,C=>{u[x]=C,c&=~(1<<x),l&&d()},()=>{c|=1<<x}));return l=!0,d(),function(){Jr(g),f(),l=!1}})}function q(e){let t;return Es(e,n=>t=n)(),t}let si=!1,Ss=Symbol();function Q(e,t,n){const r=n[t]??(n[t]={store:null,source:no(void 0),unsubscribe:gt});if(r.store!==e&&!(Ss in n))if(r.unsubscribe(),r.store=e??null,e==null)r.source.v=void 0,r.unsubscribe=gt;else{var o=!0;r.unsubscribe=Es(e,i=>{o?r.source.v=i:U(r.source,i)}),o=!1}return e&&Ss in n?q(e):h(r.source)}function l1(e,t,n){let r=n[t];return r&&r.store!==e&&(r.unsubscribe(),r.unsubscribe=gt),e}function ai(e,t){return e.set(t),t}function nt(){const e={};function t(){ja(()=>{for(var n in e)e[n].unsubscribe();Ur(e,Ss,{enumerable:!1,value:!0})})}return[e,t]}function u1(e){var t=si;try{return si=!1,[e(),si]}finally{si=t}}const c1={get(e,t){if(!e.exclude.includes(t))return e.props[t]},set(e,t){return!1},getOwnPropertyDescriptor(e,t){if(!e.exclude.includes(t)&&t in e.props)return{enumerable:!0,configurable:!0,value:e.props[t]}},has(e,t){return e.exclude.includes(t)?!1:t in e.props},ownKeys(e){return Reflect.ownKeys(e.props).filter(t=>!e.exclude.includes(t))}};function xt(e,t,n){return new Proxy({props:e,exclude:t},c1)}const d1={get(e,t){if(!e.exclude.includes(t))return h(e.version),t in e.special?e.special[t]():e.props[t]},set(e,t,n){return t in e.special||(e.special[t]=w({get[t](){return e.props[t]}},t,ka)),e.special[t](n),La(e.version),!0},getOwnPropertyDescriptor(e,t){if(!e.exclude.includes(t)&&t in e.props)return{enumerable:!0,configurable:!0,value:e.props[t]}},deleteProperty(e,t){return e.exclude.includes(t)||(e.exclude.push(t),La(e.version)),!0},has(e,t){return e.exclude.includes(t)?!1:t in e.props},ownKeys(e){return Reflect.ownKeys(e.props).filter(t=>!e.exclude.includes(t))}};function it(e,t){return new Proxy({props:e,exclude:t,special:{},version:Mt(0)},d1)}const f1={get(e,t){let n=e.props.length;for(;n--;){let r=e.props[n];if(jr(r)&&(r=r()),typeof r=="object"&&r!==null&&t in r)return r[t]}},set(e,t,n){let r=e.props.length;for(;r--;){let o=e.props[r];jr(o)&&(o=o());const i=Mn(o,t);if(i&&i.set)return i.set(n),!0}return!1},getOwnPropertyDescriptor(e,t){let n=e.props.length;for(;n--;){let r=e.props[n];if(jr(r)&&(r=r()),typeof r=="object"&&r!==null&&t in r){const o=Mn(r,t);return o&&!o.configurable&&(o.configurable=!0),o}}},has(e,t){if(t===Xn||t===ss)return!1;for(let n of e.props)if(jr(n)&&(n=n()),n!=null&&t in n)return!0;return!1},ownKeys(e){const t=[];for(let n of e.props){jr(n)&&(n=n());for(const r in n)t.includes(r)||t.push(r)}return t}};function ft(...e){return new Proxy({props:e},f1)}function w(e,t,n,r){var N;var o=(n&Wd)!==0,i=!ot||(n&Kd)!==0,s=(n&qd)!==0,a=(n&Gd)!==0,l=!1,u;s?[u,l]=u1(()=>e[t]):u=e[t];var c=Xn in e||ss in e,f=s&&(((N=Mn(e,t))==null?void 0:N.set)??(c&&t in e&&(E=>e[t]=E)))||void 0,d=r,g=!0,p=!1,x=()=>(p=!0,g&&(g=!1,a?d=yn(r):d=r),d);u===void 0&&r!==void 0&&(f&&i&&gf(),u=x(),f&&f(u));var C;if(i)C=()=>{var E=e[t];return E===void 0?x():(g=!0,p=!1,E)};else{var $=(o?Ne:ve)(()=>e[t]);$.f|=Jd,C=()=>{var E=h($);return E!==void 0&&(d=void 0),E===void 0?d:E}}if(!(n&ka))return C;if(f){var m=e.$$legacy;return function(E,T){return arguments.length>0?((!i||!T||m||l)&&f(T?C():E),E):C()}}var _=!1,v=no(u),b=Ne(()=>{var E=C(),T=h(v);return _?(_=!1,T):v.v=E});return o||(b.equals=cs),function(E,T){if(arguments.length>0){const D=T?h(b):i&&s?Ht(E):E;return b.equals(D)||(_=!0,U(v,D),p&&d!==void 0&&(d=D),yn(()=>h(b))),E}return h(b)}}function g1(e){return new h1(e)}class h1{constructor(t){wr(this,Zn);wr(this,jt);var i;var n=new Map,r=(s,a)=>{var l=no(a);return n.set(s,l),l};const o=new Proxy({...t.props||{},$$events:{}},{get(s,a){return h(n.get(a)??r(a,Reflect.get(s,a)))},has(s,a){return a===ss?!0:(h(n.get(a)??r(a,Reflect.get(s,a))),Reflect.has(s,a))},set(s,a,l){return U(n.get(a)??r(a,l),l),Reflect.set(s,a,l)}});zo(this,jt,(t.hydrate?Gf:al)(t.component,{target:t.target,anchor:t.anchor,props:o,context:t.context,intro:t.intro??!1,recover:t.recover})),(!((i=t==null?void 0:t.props)!=null&&i.$$host)||t.sync===!1)&&y(),zo(this,Zn,o.$$events);for(const s of Object.keys(ut(this,jt)))s==="$set"||s==="$destroy"||s==="$on"||Ur(this,s,{get(){return ut(this,jt)[s]},set(a){ut(this,jt)[s]=a},enumerable:!0});ut(this,jt).$set=s=>{Object.assign(o,s)},ut(this,jt).$destroy=()=>{Uf(ut(this,jt))}}$set(t){ut(this,jt).$set(t)}$on(t,n){ut(this,Zn)[t]=ut(this,Zn)[t]||[];const r=(...o)=>n.call(this,...o);return ut(this,Zn)[t].push(r),()=>{ut(this,Zn)[t]=ut(this,Zn)[t].filter(o=>o!==r)}}$destroy(){ut(this,jt).$destroy()}}Zn=new WeakMap,jt=new WeakMap;let _l;typeof HTMLElement=="function"&&(_l=class extends HTMLElement{constructor(t,n,r){super();Nt(this,"$$ctor");Nt(this,"$$s");Nt(this,"$$c");Nt(this,"$$cn",!1);Nt(this,"$$d",{});Nt(this,"$$r",!1);Nt(this,"$$p_d",{});Nt(this,"$$l",{});Nt(this,"$$l_u",new Map);Nt(this,"$$me");this.$$ctor=t,this.$$s=n,r&&this.attachShadow({mode:"open"})}addEventListener(t,n,r){if(this.$$l[t]=this.$$l[t]||[],this.$$l[t].push(n),this.$$c){const o=this.$$c.$on(t,n);this.$$l_u.set(n,o)}super.addEventListener(t,n,r)}removeEventListener(t,n,r){if(super.removeEventListener(t,n,r),this.$$c){const o=this.$$l_u.get(n);o&&(o(),this.$$l_u.delete(n))}}async connectedCallback(){if(this.$$cn=!0,!this.$$c){let t=function(o){return i=>{const s=document.createElement("slot");o!=="default"&&(s.name=o),L(i,s)}};if(await Promise.resolve(),!this.$$cn||this.$$c)return;const n={},r=v1(this);for(const o of this.$$s)o in r&&(o==="default"&&!this.$$d.children?(this.$$d.children=t(o),n.default=!0):n[o]=t(o));for(const o of this.attributes){const i=this.$$g_p(o.name);i in this.$$d||(this.$$d[i]=li(i,o.value,this.$$p_d,"toProp"))}for(const o in this.$$p_d)!(o in this.$$d)&&this[o]!==void 0&&(this.$$d[o]=this[o],delete this[o]);this.$$c=g1({component:this.$$ctor,target:this.shadowRoot||this,props:{...this.$$d,$$slots:n,$$host:this}}),this.$$me=Mf(()=>{$r(()=>{var o;this.$$r=!0;for(const i of Fo(this.$$c)){if(!((o=this.$$p_d[i])!=null&&o.reflect))continue;this.$$d[i]=this.$$c[i];const s=li(i,this.$$d[i],this.$$p_d,"toAttribute");s==null?this.removeAttribute(this.$$p_d[i].attribute||i):this.setAttribute(this.$$p_d[i].attribute||i,s)}this.$$r=!1})});for(const o in this.$$l)for(const i of this.$$l[o]){const s=this.$$c.$on(o,i);this.$$l_u.set(i,s)}this.$$l={}}}attributeChangedCallback(t,n,r){var o;this.$$r||(t=this.$$g_p(t),this.$$d[t]=li(t,r,this.$$p_d,"toProp"),(o=this.$$c)==null||o.$set({[t]:this.$$d[t]}))}disconnectedCallback(){this.$$cn=!1,Promise.resolve().then(()=>{!this.$$cn&&this.$$c&&(this.$$c.$destroy(),this.$$me(),this.$$c=void 0)})}$$g_p(t){return Fo(this.$$p_d).find(n=>this.$$p_d[n].attribute===t||!this.$$p_d[n].attribute&&n.toLowerCase()===t)||t}});function li(e,t,n,r){var i;const o=(i=n[e])==null?void 0:i.type;if(t=o==="Boolean"&&typeof t!="boolean"?t!=null:t,!r||!n[e])return t;if(r==="toAttribute")switch(o){case"Object":case"Array":return t==null?null:JSON.stringify(t);case"Boolean":return t?"":null;case"Number":return t??null;default:return t}else switch(o){case"Object":case"Array":return t&&JSON.parse(t);case"Boolean":return t;case"Number":return t!=null?+t:t;default:return t}}function v1(e){const t={};return e.childNodes.forEach(n=>{t[n.slot||"default"]=!0}),t}function ie(e,t,n,r,o,i){let s=class extends _l{constructor(){super(e,n,o),this.$$p_d=t}static get observedAttributes(){return Fo(t).map(a=>(t[a].attribute||a).toLowerCase())}};return Fo(t).forEach(a=>{Ur(s.prototype,a,{get(){return this.$$c&&a in this.$$c?this.$$c[a]:this.$$d[a]},set(l){var f;l=li(a,l,t),this.$$d[a]=l;var u=this.$$c;if(u){var c=(f=Mn(u,a))==null?void 0:f.get;c?u[a]=l:u.$set({[a]:l})}}})}),r.forEach(a=>{Ur(s.prototype,a,{get(){var l;return(l=this.$$c)==null?void 0:l[a]}})}),e.element=s,s}function Et(e){if(typeof e=="string"||typeof e=="number")return""+e;let t="";if(Array.isArray(e))for(let n=0,r;n<e.length;n++)(r=Et(e[n]))!==""&&(t+=(t&&" ")+r);else for(let n in e)e[n]&&(t+=(t&&" ")+n);return t}var p1={value:()=>{}};function ui(){for(var e=0,t=arguments.length,n={},r;e<t;++e){if(!(r=arguments[e]+"")||r in n||/[\s.]/.test(r))throw new Error("illegal type: "+r);n[r]=[]}return new ci(n)}function ci(e){this._=e}function m1(e,t){return e.trim().split(/^|\s+/).map(function(n){var r="",o=n.indexOf(".");if(o>=0&&(r=n.slice(o+1),n=n.slice(0,o)),n&&!t.hasOwnProperty(n))throw new Error("unknown type: "+n);return{type:n,name:r}})}ci.prototype=ui.prototype={constructor:ci,on:function(e,t){var n=this._,r=m1(e+"",n),o,i=-1,s=r.length;if(arguments.length<2){for(;++i<s;)if((o=(e=r[i]).type)&&(o=y1(n[o],e.name)))return o;return}if(t!=null&&typeof t!="function")throw new Error("invalid callback: "+t);for(;++i<s;)if(o=(e=r[i]).type)n[o]=xl(n[o],e.name,t);else if(t==null)for(o in n)n[o]=xl(n[o],e.name,null);return this},copy:function(){var e={},t=this._;for(var n in t)e[n]=t[n].slice();return new ci(e)},call:function(e,t){if((o=arguments.length-2)>0)for(var n=new Array(o),r=0,o,i;r<o;++r)n[r]=arguments[r+2];if(!this._.hasOwnProperty(e))throw new Error("unknown type: "+e);for(i=this._[e],r=0,o=i.length;r<o;++r)i[r].value.apply(t,n)},apply:function(e,t,n){if(!this._.hasOwnProperty(e))throw new Error("unknown type: "+e);for(var r=this._[e],o=0,i=r.length;o<i;++o)r[o].value.apply(t,n)}};function y1(e,t){for(var n=0,r=e.length,o;n<r;++n)if((o=e[n]).name===t)return o.value}function xl(e,t,n){for(var r=0,o=e.length;r<o;++r)if(e[r].name===t){e[r]=p1,e=e.slice(0,r).concat(e.slice(r+1));break}return n!=null&&e.push({name:t,value:n}),e}var Ps="http://www.w3.org/1999/xhtml";const bl={svg:"http://www.w3.org/2000/svg",xhtml:Ps,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function di(e){var t=e+="",n=t.indexOf(":");return n>=0&&(t=e.slice(0,n))!=="xmlns"&&(e=e.slice(n+1)),bl.hasOwnProperty(t)?{space:bl[t],local:e}:e}function w1(e){return function(){var t=this.ownerDocument,n=this.namespaceURI;return n===Ps&&t.documentElement.namespaceURI===Ps?t.createElement(e):t.createElementNS(n,e)}}function _1(e){return function(){return this.ownerDocument.createElementNS(e.space,e.local)}}function Cl(e){var t=di(e);return(t.local?_1:w1)(t)}function x1(){}function Ns(e){return e==null?x1:function(){return this.querySelector(e)}}function b1(e){typeof e!="function"&&(e=Ns(e));for(var t=this._groups,n=t.length,r=new Array(n),o=0;o<n;++o)for(var i=t[o],s=i.length,a=r[o]=new Array(s),l,u,c=0;c<s;++c)(l=i[c])&&(u=e.call(l,l.__data__,c,i))&&("__data__"in l&&(u.__data__=l.__data__),a[c]=u);return new Zt(r,this._parents)}function C1(e){return e==null?[]:Array.isArray(e)?e:Array.from(e)}function k1(){return[]}function kl(e){return e==null?k1:function(){return this.querySelectorAll(e)}}function $1(e){return function(){return C1(e.apply(this,arguments))}}function E1(e){typeof e=="function"?e=$1(e):e=kl(e);for(var t=this._groups,n=t.length,r=[],o=[],i=0;i<n;++i)for(var s=t[i],a=s.length,l,u=0;u<a;++u)(l=s[u])&&(r.push(e.call(l,l.__data__,u,s)),o.push(l));return new Zt(r,o)}function $l(e){return function(){return this.matches(e)}}function El(e){return function(t){return t.matches(e)}}var S1=Array.prototype.find;function P1(e){return function(){return S1.call(this.children,e)}}function N1(){return this.firstElementChild}function T1(e){return this.select(e==null?N1:P1(typeof e=="function"?e:El(e)))}var M1=Array.prototype.filter;function H1(){return Array.from(this.children)}function V1(e){return function(){return M1.call(this.children,e)}}function D1(e){return this.selectAll(e==null?H1:V1(typeof e=="function"?e:El(e)))}function A1(e){typeof e!="function"&&(e=$l(e));for(var t=this._groups,n=t.length,r=new Array(n),o=0;o<n;++o)for(var i=t[o],s=i.length,a=r[o]=[],l,u=0;u<s;++u)(l=i[u])&&e.call(l,l.__data__,u,i)&&a.push(l);return new Zt(r,this._parents)}function Sl(e){return new Array(e.length)}function L1(){return new Zt(this._enter||this._groups.map(Sl),this._parents)}function fi(e,t){this.ownerDocument=e.ownerDocument,this.namespaceURI=e.namespaceURI,this._next=null,this._parent=e,this.__data__=t}fi.prototype={constructor:fi,appendChild:function(e){return this._parent.insertBefore(e,this._next)},insertBefore:function(e,t){return this._parent.insertBefore(e,t)},querySelector:function(e){return this._parent.querySelector(e)},querySelectorAll:function(e){return this._parent.querySelectorAll(e)}};function O1(e){return function(){return e}}function I1(e,t,n,r,o,i){for(var s=0,a,l=t.length,u=i.length;s<u;++s)(a=t[s])?(a.__data__=i[s],r[s]=a):n[s]=new fi(e,i[s]);for(;s<l;++s)(a=t[s])&&(o[s]=a)}function z1(e,t,n,r,o,i,s){var a,l,u=new Map,c=t.length,f=i.length,d=new Array(c),g;for(a=0;a<c;++a)(l=t[a])&&(d[a]=g=s.call(l,l.__data__,a,t)+"",u.has(g)?o[a]=l:u.set(g,l));for(a=0;a<f;++a)g=s.call(e,i[a],a,i)+"",(l=u.get(g))?(r[a]=l,l.__data__=i[a],u.delete(g)):n[a]=new fi(e,i[a]);for(a=0;a<c;++a)(l=t[a])&&u.get(d[a])===l&&(o[a]=l)}function R1(e){return e.__data__}function B1(e,t){if(!arguments.length)return Array.from(this,R1);var n=t?z1:I1,r=this._parents,o=this._groups;typeof e!="function"&&(e=O1(e));for(var i=o.length,s=new Array(i),a=new Array(i),l=new Array(i),u=0;u<i;++u){var c=r[u],f=o[u],d=f.length,g=Y1(e.call(c,c&&c.__data__,u,r)),p=g.length,x=a[u]=new Array(p),C=s[u]=new Array(p),$=l[u]=new Array(d);n(c,f,x,C,$,g,t);for(var m=0,_=0,v,b;m<p;++m)if(v=x[m]){for(m>=_&&(_=m+1);!(b=C[_])&&++_<p;);v._next=b||null}}return s=new Zt(s,r),s._enter=a,s._exit=l,s}function Y1(e){return typeof e=="object"&&"length"in e?e:Array.from(e)}function Z1(){return new Zt(this._exit||this._groups.map(Sl),this._parents)}function X1(e,t,n){var r=this.enter(),o=this,i=this.exit();return typeof e=="function"?(r=e(r),r&&(r=r.selection())):r=r.append(e+""),t!=null&&(o=t(o),o&&(o=o.selection())),n==null?i.remove():n(i),r&&o?r.merge(o).order():o}function F1(e){for(var t=e.selection?e.selection():e,n=this._groups,r=t._groups,o=n.length,i=r.length,s=Math.min(o,i),a=new Array(o),l=0;l<s;++l)for(var u=n[l],c=r[l],f=u.length,d=a[l]=new Array(f),g,p=0;p<f;++p)(g=u[p]||c[p])&&(d[p]=g);for(;l<o;++l)a[l]=n[l];return new Zt(a,this._parents)}function W1(){for(var e=this._groups,t=-1,n=e.length;++t<n;)for(var r=e[t],o=r.length-1,i=r[o],s;--o>=0;)(s=r[o])&&(i&&s.compareDocumentPosition(i)^4&&i.parentNode.insertBefore(s,i),i=s);return this}function K1(e){e||(e=q1);function t(f,d){return f&&d?e(f.__data__,d.__data__):!f-!d}for(var n=this._groups,r=n.length,o=new Array(r),i=0;i<r;++i){for(var s=n[i],a=s.length,l=o[i]=new Array(a),u,c=0;c<a;++c)(u=s[c])&&(l[c]=u);l.sort(t)}return new Zt(o,this._parents).order()}function q1(e,t){return e<t?-1:e>t?1:e>=t?0:NaN}function G1(){var e=arguments[0];return arguments[0]=this,e.apply(null,arguments),this}function U1(){return Array.from(this)}function j1(){for(var e=this._groups,t=0,n=e.length;t<n;++t)for(var r=e[t],o=0,i=r.length;o<i;++o){var s=r[o];if(s)return s}return null}function J1(){let e=0;for(const t of this)++e;return e}function Q1(){return!this.node()}function eg(e){for(var t=this._groups,n=0,r=t.length;n<r;++n)for(var o=t[n],i=0,s=o.length,a;i<s;++i)(a=o[i])&&e.call(a,a.__data__,i,o);return this}function tg(e){return function(){this.removeAttribute(e)}}function ng(e){return function(){this.removeAttributeNS(e.space,e.local)}}function rg(e,t){return function(){this.setAttribute(e,t)}}function og(e,t){return function(){this.setAttributeNS(e.space,e.local,t)}}function ig(e,t){return function(){var n=t.apply(this,arguments);n==null?this.removeAttribute(e):this.setAttribute(e,n)}}function sg(e,t){return function(){var n=t.apply(this,arguments);n==null?this.removeAttributeNS(e.space,e.local):this.setAttributeNS(e.space,e.local,n)}}function ag(e,t){var n=di(e);if(arguments.length<2){var r=this.node();return n.local?r.getAttributeNS(n.space,n.local):r.getAttribute(n)}return this.each((t==null?n.local?ng:tg:typeof t=="function"?n.local?sg:ig:n.local?og:rg)(n,t))}function Pl(e){return e.ownerDocument&&e.ownerDocument.defaultView||e.document&&e||e.defaultView}function lg(e){return function(){this.style.removeProperty(e)}}function ug(e,t,n){return function(){this.style.setProperty(e,t,n)}}function cg(e,t,n){return function(){var r=t.apply(this,arguments);r==null?this.style.removeProperty(e):this.style.setProperty(e,r,n)}}function dg(e,t,n){return arguments.length>1?this.each((t==null?lg:typeof t=="function"?cg:ug)(e,t,n??"")):Tr(this.node(),e)}function Tr(e,t){return e.style.getPropertyValue(t)||Pl(e).getComputedStyle(e,null).getPropertyValue(t)}function fg(e){return function(){delete this[e]}}function gg(e,t){return function(){this[e]=t}}function hg(e,t){return function(){var n=t.apply(this,arguments);n==null?delete this[e]:this[e]=n}}function vg(e,t){return arguments.length>1?this.each((t==null?fg:typeof t=="function"?hg:gg)(e,t)):this.node()[e]}function Nl(e){return e.trim().split(/^|\s+/)}function Ts(e){return e.classList||new Tl(e)}function Tl(e){this._node=e,this._names=Nl(e.getAttribute("class")||"")}Tl.prototype={add:function(e){var t=this._names.indexOf(e);t<0&&(this._names.push(e),this._node.setAttribute("class",this._names.join(" ")))},remove:function(e){var t=this._names.indexOf(e);t>=0&&(this._names.splice(t,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(e){return this._names.indexOf(e)>=0}};function Ml(e,t){for(var n=Ts(e),r=-1,o=t.length;++r<o;)n.add(t[r])}function Hl(e,t){for(var n=Ts(e),r=-1,o=t.length;++r<o;)n.remove(t[r])}function pg(e){return function(){Ml(this,e)}}function mg(e){return function(){Hl(this,e)}}function yg(e,t){return function(){(t.apply(this,arguments)?Ml:Hl)(this,e)}}function wg(e,t){var n=Nl(e+"");if(arguments.length<2){for(var r=Ts(this.node()),o=-1,i=n.length;++o<i;)if(!r.contains(n[o]))return!1;return!0}return this.each((typeof t=="function"?yg:t?pg:mg)(n,t))}function _g(){this.textContent=""}function xg(e){return function(){this.textContent=e}}function bg(e){return function(){var t=e.apply(this,arguments);this.textContent=t??""}}function Cg(e){return arguments.length?this.each(e==null?_g:(typeof e=="function"?bg:xg)(e)):this.node().textContent}function kg(){this.innerHTML=""}function $g(e){return function(){this.innerHTML=e}}function Eg(e){return function(){var t=e.apply(this,arguments);this.innerHTML=t??""}}function Sg(e){return arguments.length?this.each(e==null?kg:(typeof e=="function"?Eg:$g)(e)):this.node().innerHTML}function Pg(){this.nextSibling&&this.parentNode.appendChild(this)}function Ng(){return this.each(Pg)}function Tg(){this.previousSibling&&this.parentNode.insertBefore(this,this.parentNode.firstChild)}function Mg(){return this.each(Tg)}function Hg(e){var t=typeof e=="function"?e:Cl(e);return this.select(function(){return this.appendChild(t.apply(this,arguments))})}function Vg(){return null}function Dg(e,t){var n=typeof e=="function"?e:Cl(e),r=t==null?Vg:typeof t=="function"?t:Ns(t);return this.select(function(){return this.insertBefore(n.apply(this,arguments),r.apply(this,arguments)||null)})}function Ag(){var e=this.parentNode;e&&e.removeChild(this)}function Lg(){return this.each(Ag)}function Og(){var e=this.cloneNode(!1),t=this.parentNode;return t?t.insertBefore(e,this.nextSibling):e}function Ig(){var e=this.cloneNode(!0),t=this.parentNode;return t?t.insertBefore(e,this.nextSibling):e}function zg(e){return this.select(e?Ig:Og)}function Rg(e){return arguments.length?this.property("__data__",e):this.node().__data__}function Bg(e){return function(t){e.call(this,t,this.__data__)}}function Yg(e){return e.trim().split(/^|\s+/).map(function(t){var n="",r=t.indexOf(".");return r>=0&&(n=t.slice(r+1),t=t.slice(0,r)),{type:t,name:n}})}function Zg(e){return function(){var t=this.__on;if(t){for(var n=0,r=-1,o=t.length,i;n<o;++n)i=t[n],(!e.type||i.type===e.type)&&i.name===e.name?this.removeEventListener(i.type,i.listener,i.options):t[++r]=i;++r?t.length=r:delete this.__on}}}function Xg(e,t,n){return function(){var r=this.__on,o,i=Bg(t);if(r){for(var s=0,a=r.length;s<a;++s)if((o=r[s]).type===e.type&&o.name===e.name){this.removeEventListener(o.type,o.listener,o.options),this.addEventListener(o.type,o.listener=i,o.options=n),o.value=t;return}}this.addEventListener(e.type,i,n),o={type:e.type,name:e.name,value:t,listener:i,options:n},r?r.push(o):this.__on=[o]}}function Fg(e,t,n){var r=Yg(e+""),o,i=r.length,s;if(arguments.length<2){var a=this.node().__on;if(a){for(var l=0,u=a.length,c;l<u;++l)for(o=0,c=a[l];o<i;++o)if((s=r[o]).type===c.type&&s.name===c.name)return c.value}return}for(a=t?Xg:Zg,o=0;o<i;++o)this.each(a(r[o],t,n));return this}function Vl(e,t,n){var r=Pl(e),o=r.CustomEvent;typeof o=="function"?o=new o(t,n):(o=r.document.createEvent("Event"),n?(o.initEvent(t,n.bubbles,n.cancelable),o.detail=n.detail):o.initEvent(t,!1,!1)),e.dispatchEvent(o)}function Wg(e,t){return function(){return Vl(this,e,t)}}function Kg(e,t){return function(){return Vl(this,e,t.apply(this,arguments))}}function qg(e,t){return this.each((typeof t=="function"?Kg:Wg)(e,t))}function*Gg(){for(var e=this._groups,t=0,n=e.length;t<n;++t)for(var r=e[t],o=0,i=r.length,s;o<i;++o)(s=r[o])&&(yield s)}var Dl=[null];function Zt(e,t){this._groups=e,this._parents=t}function lo(){return new Zt([[document.documentElement]],Dl)}function Ug(){return this}Zt.prototype=lo.prototype={constructor:Zt,select:b1,selectAll:E1,selectChild:T1,selectChildren:D1,filter:A1,data:B1,enter:L1,exit:Z1,join:X1,merge:F1,selection:Ug,order:W1,sort:K1,call:G1,nodes:U1,node:j1,size:J1,empty:Q1,each:eg,attr:ag,style:dg,property:vg,classed:wg,text:Cg,html:Sg,raise:Ng,lower:Mg,append:Hg,insert:Dg,remove:Lg,clone:zg,datum:Rg,on:Fg,dispatch:qg,[Symbol.iterator]:Gg};function Ut(e){return typeof e=="string"?new Zt([[document.querySelector(e)]],[document.documentElement]):new Zt([[e]],Dl)}function jg(e){let t;for(;t=e.sourceEvent;)e=t;return e}function on(e,t){if(e=jg(e),t===void 0&&(t=e.currentTarget),t){var n=t.ownerSVGElement||t;if(n.createSVGPoint){var r=n.createSVGPoint();return r.x=e.clientX,r.y=e.clientY,r=r.matrixTransform(t.getScreenCTM().inverse()),[r.x,r.y]}if(t.getBoundingClientRect){var o=t.getBoundingClientRect();return[e.clientX-o.left-t.clientLeft,e.clientY-o.top-t.clientTop]}}return[e.pageX,e.pageY]}const Jg={passive:!1},uo={capture:!0,passive:!1};function Ms(e){e.stopImmediatePropagation()}function Mr(e){e.preventDefault(),e.stopImmediatePropagation()}function Al(e){var t=e.document.documentElement,n=Ut(e).on("dragstart.drag",Mr,uo);"onselectstart"in t?n.on("selectstart.drag",Mr,uo):(t.__noselect=t.style.MozUserSelect,t.style.MozUserSelect="none")}function Ll(e,t){var n=e.document.documentElement,r=Ut(e).on("dragstart.drag",null);t&&(r.on("click.drag",Mr,uo),setTimeout(function(){r.on("click.drag",null)},0)),"onselectstart"in n?r.on("selectstart.drag",null):(n.style.MozUserSelect=n.__noselect,delete n.__noselect)}const gi=e=>()=>e;function Hs(e,{sourceEvent:t,subject:n,target:r,identifier:o,active:i,x:s,y:a,dx:l,dy:u,dispatch:c}){Object.defineProperties(this,{type:{value:e,enumerable:!0,configurable:!0},sourceEvent:{value:t,enumerable:!0,configurable:!0},subject:{value:n,enumerable:!0,configurable:!0},target:{value:r,enumerable:!0,configurable:!0},identifier:{value:o,enumerable:!0,configurable:!0},active:{value:i,enumerable:!0,configurable:!0},x:{value:s,enumerable:!0,configurable:!0},y:{value:a,enumerable:!0,configurable:!0},dx:{value:l,enumerable:!0,configurable:!0},dy:{value:u,enumerable:!0,configurable:!0},_:{value:c}})}Hs.prototype.on=function(){var e=this._.on.apply(this._,arguments);return e===this._?this:e};function Qg(e){return!e.ctrlKey&&!e.button}function eh(){return this.parentNode}function th(e,t){return t??{x:e.x,y:e.y}}function nh(){return navigator.maxTouchPoints||"ontouchstart"in this}function rh(){var e=Qg,t=eh,n=th,r=nh,o={},i=ui("start","drag","end"),s=0,a,l,u,c,f=0;function d(v){v.on("mousedown.drag",g).filter(r).on("touchstart.drag",C).on("touchmove.drag",$,Jg).on("touchend.drag touchcancel.drag",m).style("touch-action","none").style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function g(v,b){if(!(c||!e.call(this,v,b))){var N=_(this,t.call(this,v,b),v,b,"mouse");N&&(Ut(v.view).on("mousemove.drag",p,uo).on("mouseup.drag",x,uo),Al(v.view),Ms(v),u=!1,a=v.clientX,l=v.clientY,N("start",v))}}function p(v){if(Mr(v),!u){var b=v.clientX-a,N=v.clientY-l;u=b*b+N*N>f}o.mouse("drag",v)}function x(v){Ut(v.view).on("mousemove.drag mouseup.drag",null),Ll(v.view,u),Mr(v),o.mouse("end",v)}function C(v,b){if(e.call(this,v,b)){var N=v.changedTouches,E=t.call(this,v,b),T=N.length,D,V;for(D=0;D<T;++D)(V=_(this,E,v,b,N[D].identifier,N[D]))&&(Ms(v),V("start",v,N[D]))}}function $(v){var b=v.changedTouches,N=b.length,E,T;for(E=0;E<N;++E)(T=o[b[E].identifier])&&(Mr(v),T("drag",v,b[E]))}function m(v){var b=v.changedTouches,N=b.length,E,T;for(c&&clearTimeout(c),c=setTimeout(function(){c=null},500),E=0;E<N;++E)(T=o[b[E].identifier])&&(Ms(v),T("end",v,b[E]))}function _(v,b,N,E,T,D){var V=i.copy(),A=on(D||N,b),O,R,S;if((S=n.call(v,new Hs("beforestart",{sourceEvent:N,target:d,identifier:T,active:s,x:A[0],y:A[1],dx:0,dy:0,dispatch:V}),E))!=null)return O=S.x-A[0]||0,R=S.y-A[1]||0,function M(k,P,H){var I=A,B;switch(k){case"start":o[T]=M,B=s++;break;case"end":delete o[T],--s;case"drag":A=on(H||P,b),B=s;break}V.call(k,v,new Hs(k,{sourceEvent:P,subject:S,target:d,identifier:T,active:B,x:A[0]+O,y:A[1]+R,dx:A[0]-I[0],dy:A[1]-I[1],dispatch:V}),E)}}return d.filter=function(v){return arguments.length?(e=typeof v=="function"?v:gi(!!v),d):e},d.container=function(v){return arguments.length?(t=typeof v=="function"?v:gi(v),d):t},d.subject=function(v){return arguments.length?(n=typeof v=="function"?v:gi(v),d):n},d.touchable=function(v){return arguments.length?(r=typeof v=="function"?v:gi(!!v),d):r},d.on=function(){var v=i.on.apply(i,arguments);return v===i?d:v},d.clickDistance=function(v){return arguments.length?(f=(v=+v)*v,d):Math.sqrt(f)},d}function Vs(e,t,n){e.prototype=t.prototype=n,n.constructor=e}function Ol(e,t){var n=Object.create(e.prototype);for(var r in t)n[r]=t[r];return n}function co(){}var fo=.7,hi=1/fo,Hr="\\s*([+-]?\\d+)\\s*",go="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",_n="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",oh=/^#([0-9a-f]{3,8})$/,ih=new RegExp(`^rgb\\(${Hr},${Hr},${Hr}\\)$`),sh=new RegExp(`^rgb\\(${_n},${_n},${_n}\\)$`),ah=new RegExp(`^rgba\\(${Hr},${Hr},${Hr},${go}\\)$`),lh=new RegExp(`^rgba\\(${_n},${_n},${_n},${go}\\)$`),uh=new RegExp(`^hsl\\(${go},${_n},${_n}\\)$`),ch=new RegExp(`^hsla\\(${go},${_n},${_n},${go}\\)$`),Il={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};Vs(co,ho,{copy(e){return Object.assign(new this.constructor,this,e)},displayable(){return this.rgb().displayable()},hex:zl,formatHex:zl,formatHex8:dh,formatHsl:fh,formatRgb:Rl,toString:Rl});function zl(){return this.rgb().formatHex()}function dh(){return this.rgb().formatHex8()}function fh(){return Fl(this).formatHsl()}function Rl(){return this.rgb().formatRgb()}function ho(e){var t,n;return e=(e+"").trim().toLowerCase(),(t=oh.exec(e))?(n=t[1].length,t=parseInt(t[1],16),n===6?Bl(t):n===3?new At(t>>8&15|t>>4&240,t>>4&15|t&240,(t&15)<<4|t&15,1):n===8?vi(t>>24&255,t>>16&255,t>>8&255,(t&255)/255):n===4?vi(t>>12&15|t>>8&240,t>>8&15|t>>4&240,t>>4&15|t&240,((t&15)<<4|t&15)/255):null):(t=ih.exec(e))?new At(t[1],t[2],t[3],1):(t=sh.exec(e))?new At(t[1]*255/100,t[2]*255/100,t[3]*255/100,1):(t=ah.exec(e))?vi(t[1],t[2],t[3],t[4]):(t=lh.exec(e))?vi(t[1]*255/100,t[2]*255/100,t[3]*255/100,t[4]):(t=uh.exec(e))?Xl(t[1],t[2]/100,t[3]/100,1):(t=ch.exec(e))?Xl(t[1],t[2]/100,t[3]/100,t[4]):Il.hasOwnProperty(e)?Bl(Il[e]):e==="transparent"?new At(NaN,NaN,NaN,0):null}function Bl(e){return new At(e>>16&255,e>>8&255,e&255,1)}function vi(e,t,n,r){return r<=0&&(e=t=n=NaN),new At(e,t,n,r)}function gh(e){return e instanceof co||(e=ho(e)),e?(e=e.rgb(),new At(e.r,e.g,e.b,e.opacity)):new At}function Ds(e,t,n,r){return arguments.length===1?gh(e):new At(e,t,n,r??1)}function At(e,t,n,r){this.r=+e,this.g=+t,this.b=+n,this.opacity=+r}Vs(At,Ds,Ol(co,{brighter(e){return e=e==null?hi:Math.pow(hi,e),new At(this.r*e,this.g*e,this.b*e,this.opacity)},darker(e){return e=e==null?fo:Math.pow(fo,e),new At(this.r*e,this.g*e,this.b*e,this.opacity)},rgb(){return this},clamp(){return new At(dr(this.r),dr(this.g),dr(this.b),pi(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Yl,formatHex:Yl,formatHex8:hh,formatRgb:Zl,toString:Zl}));function Yl(){return`#${fr(this.r)}${fr(this.g)}${fr(this.b)}`}function hh(){return`#${fr(this.r)}${fr(this.g)}${fr(this.b)}${fr((isNaN(this.opacity)?1:this.opacity)*255)}`}function Zl(){const e=pi(this.opacity);return`${e===1?"rgb(":"rgba("}${dr(this.r)}, ${dr(this.g)}, ${dr(this.b)}${e===1?")":`, ${e})`}`}function pi(e){return isNaN(e)?1:Math.max(0,Math.min(1,e))}function dr(e){return Math.max(0,Math.min(255,Math.round(e)||0))}function fr(e){return e=dr(e),(e<16?"0":"")+e.toString(16)}function Xl(e,t,n,r){return r<=0?e=t=n=NaN:n<=0||n>=1?e=t=NaN:t<=0&&(e=NaN),new sn(e,t,n,r)}function Fl(e){if(e instanceof sn)return new sn(e.h,e.s,e.l,e.opacity);if(e instanceof co||(e=ho(e)),!e)return new sn;if(e instanceof sn)return e;e=e.rgb();var t=e.r/255,n=e.g/255,r=e.b/255,o=Math.min(t,n,r),i=Math.max(t,n,r),s=NaN,a=i-o,l=(i+o)/2;return a?(t===i?s=(n-r)/a+(n<r)*6:n===i?s=(r-t)/a+2:s=(t-n)/a+4,a/=l<.5?i+o:2-i-o,s*=60):a=l>0&&l<1?0:s,new sn(s,a,l,e.opacity)}function vh(e,t,n,r){return arguments.length===1?Fl(e):new sn(e,t,n,r??1)}function sn(e,t,n,r){this.h=+e,this.s=+t,this.l=+n,this.opacity=+r}Vs(sn,vh,Ol(co,{brighter(e){return e=e==null?hi:Math.pow(hi,e),new sn(this.h,this.s,this.l*e,this.opacity)},darker(e){return e=e==null?fo:Math.pow(fo,e),new sn(this.h,this.s,this.l*e,this.opacity)},rgb(){var e=this.h%360+(this.h<0)*360,t=isNaN(e)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*t,o=2*n-r;return new At(As(e>=240?e-240:e+120,o,r),As(e,o,r),As(e<120?e+240:e-120,o,r),this.opacity)},clamp(){return new sn(Wl(this.h),mi(this.s),mi(this.l),pi(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const e=pi(this.opacity);return`${e===1?"hsl(":"hsla("}${Wl(this.h)}, ${mi(this.s)*100}%, ${mi(this.l)*100}%${e===1?")":`, ${e})`}`}}));function Wl(e){return e=(e||0)%360,e<0?e+360:e}function mi(e){return Math.max(0,Math.min(1,e||0))}function As(e,t,n){return(e<60?t+(n-t)*e/60:e<180?n:e<240?t+(n-t)*(240-e)/60:t)*255}const Kl=e=>()=>e;function ph(e,t){return function(n){return e+n*t}}function mh(e,t,n){return e=Math.pow(e,n),t=Math.pow(t,n)-e,n=1/n,function(r){return Math.pow(e+r*t,n)}}function yh(e){return(e=+e)==1?ql:function(t,n){return n-t?mh(t,n,e):Kl(isNaN(t)?n:t)}}function ql(e,t){var n=t-e;return n?ph(e,n):Kl(isNaN(e)?t:e)}const Gl=function e(t){var n=yh(t);function r(o,i){var s=n((o=Ds(o)).r,(i=Ds(i)).r),a=n(o.g,i.g),l=n(o.b,i.b),u=ql(o.opacity,i.opacity);return function(c){return o.r=s(c),o.g=a(c),o.b=l(c),o.opacity=u(c),o+""}}return r.gamma=e,r}(1);function jn(e,t){return e=+e,t=+t,function(n){return e*(1-n)+t*n}}var Ls=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,Os=new RegExp(Ls.source,"g");function wh(e){return function(){return e}}function _h(e){return function(t){return e(t)+""}}function xh(e,t){var n=Ls.lastIndex=Os.lastIndex=0,r,o,i,s=-1,a=[],l=[];for(e=e+"",t=t+"";(r=Ls.exec(e))&&(o=Os.exec(t));)(i=o.index)>n&&(i=t.slice(n,i),a[s]?a[s]+=i:a[++s]=i),(r=r[0])===(o=o[0])?a[s]?a[s]+=o:a[++s]=o:(a[++s]=null,l.push({i:s,x:jn(r,o)})),n=Os.lastIndex;return n<t.length&&(i=t.slice(n),a[s]?a[s]+=i:a[++s]=i),a.length<2?l[0]?_h(l[0].x):wh(t):(t=l.length,function(u){for(var c=0,f;c<t;++c)a[(f=l[c]).i]=f.x(u);return a.join("")})}var Ul=180/Math.PI,Is={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1};function jl(e,t,n,r,o,i){var s,a,l;return(s=Math.sqrt(e*e+t*t))&&(e/=s,t/=s),(l=e*n+t*r)&&(n-=e*l,r-=t*l),(a=Math.sqrt(n*n+r*r))&&(n/=a,r/=a,l/=a),e*r<t*n&&(e=-e,t=-t,l=-l,s=-s),{translateX:o,translateY:i,rotate:Math.atan2(t,e)*Ul,skewX:Math.atan(l)*Ul,scaleX:s,scaleY:a}}var yi;function bh(e){const t=new(typeof DOMMatrix=="function"?DOMMatrix:WebKitCSSMatrix)(e+"");return t.isIdentity?Is:jl(t.a,t.b,t.c,t.d,t.e,t.f)}function Ch(e){return e==null||(yi||(yi=document.createElementNS("http://www.w3.org/2000/svg","g")),yi.setAttribute("transform",e),!(e=yi.transform.baseVal.consolidate()))?Is:(e=e.matrix,jl(e.a,e.b,e.c,e.d,e.e,e.f))}function Jl(e,t,n,r){function o(u){return u.length?u.pop()+" ":""}function i(u,c,f,d,g,p){if(u!==f||c!==d){var x=g.push("translate(",null,t,null,n);p.push({i:x-4,x:jn(u,f)},{i:x-2,x:jn(c,d)})}else(f||d)&&g.push("translate("+f+t+d+n)}function s(u,c,f,d){u!==c?(u-c>180?c+=360:c-u>180&&(u+=360),d.push({i:f.push(o(f)+"rotate(",null,r)-2,x:jn(u,c)})):c&&f.push(o(f)+"rotate("+c+r)}function a(u,c,f,d){u!==c?d.push({i:f.push(o(f)+"skewX(",null,r)-2,x:jn(u,c)}):c&&f.push(o(f)+"skewX("+c+r)}function l(u,c,f,d,g,p){if(u!==f||c!==d){var x=g.push(o(g)+"scale(",null,",",null,")");p.push({i:x-4,x:jn(u,f)},{i:x-2,x:jn(c,d)})}else(f!==1||d!==1)&&g.push(o(g)+"scale("+f+","+d+")")}return function(u,c){var f=[],d=[];return u=e(u),c=e(c),i(u.translateX,u.translateY,c.translateX,c.translateY,f,d),s(u.rotate,c.rotate,f,d),a(u.skewX,c.skewX,f,d),l(u.scaleX,u.scaleY,c.scaleX,c.scaleY,f,d),u=c=null,function(g){for(var p=-1,x=d.length,C;++p<x;)f[(C=d[p]).i]=C.x(g);return f.join("")}}}var kh=Jl(bh,"px, ","px)","deg)"),$h=Jl(Ch,", ",")",")"),Eh=1e-12;function Ql(e){return((e=Math.exp(e))+1/e)/2}function Sh(e){return((e=Math.exp(e))-1/e)/2}function Ph(e){return((e=Math.exp(2*e))-1)/(e+1)}const Nh=function e(t,n,r){function o(i,s){var a=i[0],l=i[1],u=i[2],c=s[0],f=s[1],d=s[2],g=c-a,p=f-l,x=g*g+p*p,C,$;if(x<Eh)$=Math.log(d/u)/t,C=function(E){return[a+E*g,l+E*p,u*Math.exp(t*E*$)]};else{var m=Math.sqrt(x),_=(d*d-u*u+r*x)/(2*u*n*m),v=(d*d-u*u-r*x)/(2*d*n*m),b=Math.log(Math.sqrt(_*_+1)-_),N=Math.log(Math.sqrt(v*v+1)-v);$=(N-b)/t,C=function(E){var T=E*$,D=Ql(b),V=u/(n*m)*(D*Ph(t*T+b)-Sh(b));return[a+V*g,l+V*p,u*D/Ql(t*T+b)]}}return C.duration=$*1e3*t/Math.SQRT2,C}return o.rho=function(i){var s=Math.max(.001,+i),a=s*s,l=a*a;return e(s,a,l)},o}(Math.SQRT2,2,4);var Vr=0,vo=0,po=0,eu=1e3,wi,mo,_i=0,gr=0,xi=0,yo=typeof performance=="object"&&performance.now?performance:Date,tu=typeof window=="object"&&window.requestAnimationFrame?window.requestAnimationFrame.bind(window):function(e){setTimeout(e,17)};function zs(){return gr||(tu(Th),gr=yo.now()+xi)}function Th(){gr=0}function bi(){this._call=this._time=this._next=null}bi.prototype=nu.prototype={constructor:bi,restart:function(e,t,n){if(typeof e!="function")throw new TypeError("callback is not a function");n=(n==null?zs():+n)+(t==null?0:+t),!this._next&&mo!==this&&(mo?mo._next=this:wi=this,mo=this),this._call=e,this._time=n,Rs()},stop:function(){this._call&&(this._call=null,this._time=1/0,Rs())}};function nu(e,t,n){var r=new bi;return r.restart(e,t,n),r}function Mh(){zs(),++Vr;for(var e=wi,t;e;)(t=gr-e._time)>=0&&e._call.call(void 0,t),e=e._next;--Vr}function ru(){gr=(_i=yo.now())+xi,Vr=vo=0;try{Mh()}finally{Vr=0,Vh(),gr=0}}function Hh(){var e=yo.now(),t=e-_i;t>eu&&(xi-=t,_i=e)}function Vh(){for(var e,t=wi,n,r=1/0;t;)t._call?(r>t._time&&(r=t._time),e=t,t=t._next):(n=t._next,t._next=null,t=e?e._next=n:wi=n);mo=e,Rs(r)}function Rs(e){if(!Vr){vo&&(vo=clearTimeout(vo));var t=e-gr;t>24?(e<1/0&&(vo=setTimeout(ru,e-yo.now()-xi)),po&&(po=clearInterval(po))):(po||(_i=yo.now(),po=setInterval(Hh,eu)),Vr=1,tu(ru))}}function ou(e,t,n){var r=new bi;return t=t==null?0:+t,r.restart(o=>{r.stop(),e(o+t)},t,n),r}var Dh=ui("start","end","cancel","interrupt"),Ah=[],iu=0,su=1,Bs=2,Ci=3,au=4,Ys=5,ki=6;function $i(e,t,n,r,o,i){var s=e.__transition;if(!s)e.__transition={};else if(n in s)return;Lh(e,n,{name:t,index:r,group:o,on:Dh,tween:Ah,time:i.time,delay:i.delay,duration:i.duration,ease:i.ease,timer:null,state:iu})}function Zs(e,t){var n=an(e,t);if(n.state>iu)throw new Error("too late; already scheduled");return n}function xn(e,t){var n=an(e,t);if(n.state>Ci)throw new Error("too late; already running");return n}function an(e,t){var n=e.__transition;if(!n||!(n=n[t]))throw new Error("transition not found");return n}function Lh(e,t,n){var r=e.__transition,o;r[t]=n,n.timer=nu(i,0,n.time);function i(u){n.state=su,n.timer.restart(s,n.delay,n.time),n.delay<=u&&s(u-n.delay)}function s(u){var c,f,d,g;if(n.state!==su)return l();for(c in r)if(g=r[c],g.name===n.name){if(g.state===Ci)return ou(s);g.state===au?(g.state=ki,g.timer.stop(),g.on.call("interrupt",e,e.__data__,g.index,g.group),delete r[c]):+c<t&&(g.state=ki,g.timer.stop(),g.on.call("cancel",e,e.__data__,g.index,g.group),delete r[c])}if(ou(function(){n.state===Ci&&(n.state=au,n.timer.restart(a,n.delay,n.time),a(u))}),n.state=Bs,n.on.call("start",e,e.__data__,n.index,n.group),n.state===Bs){for(n.state=Ci,o=new Array(d=n.tween.length),c=0,f=-1;c<d;++c)(g=n.tween[c].value.call(e,e.__data__,n.index,n.group))&&(o[++f]=g);o.length=f+1}}function a(u){for(var c=u<n.duration?n.ease.call(null,u/n.duration):(n.timer.restart(l),n.state=Ys,1),f=-1,d=o.length;++f<d;)o[f].call(e,c);n.state===Ys&&(n.on.call("end",e,e.__data__,n.index,n.group),l())}function l(){n.state=ki,n.timer.stop(),delete r[t];for(var u in r)return;delete e.__transition}}function Ei(e,t){var n=e.__transition,r,o,i=!0,s;if(n){t=t==null?null:t+"";for(s in n){if((r=n[s]).name!==t){i=!1;continue}o=r.state>Bs&&r.state<Ys,r.state=ki,r.timer.stop(),r.on.call(o?"interrupt":"cancel",e,e.__data__,r.index,r.group),delete n[s]}i&&delete e.__transition}}function Oh(e){return this.each(function(){Ei(this,e)})}function Ih(e,t){var n,r;return function(){var o=xn(this,e),i=o.tween;if(i!==n){r=n=i;for(var s=0,a=r.length;s<a;++s)if(r[s].name===t){r=r.slice(),r.splice(s,1);break}}o.tween=r}}function zh(e,t,n){var r,o;if(typeof n!="function")throw new Error;return function(){var i=xn(this,e),s=i.tween;if(s!==r){o=(r=s).slice();for(var a={name:t,value:n},l=0,u=o.length;l<u;++l)if(o[l].name===t){o[l]=a;break}l===u&&o.push(a)}i.tween=o}}function Rh(e,t){var n=this._id;if(e+="",arguments.length<2){for(var r=an(this.node(),n).tween,o=0,i=r.length,s;o<i;++o)if((s=r[o]).name===e)return s.value;return null}return this.each((t==null?Ih:zh)(n,e,t))}function Xs(e,t,n){var r=e._id;return e.each(function(){var o=xn(this,r);(o.value||(o.value={}))[t]=n.apply(this,arguments)}),function(o){return an(o,r).value[t]}}function lu(e,t){var n;return(typeof t=="number"?jn:t instanceof ho?Gl:(n=ho(t))?(t=n,Gl):xh)(e,t)}function Bh(e){return function(){this.removeAttribute(e)}}function Yh(e){return function(){this.removeAttributeNS(e.space,e.local)}}function Zh(e,t,n){var r,o=n+"",i;return function(){var s=this.getAttribute(e);return s===o?null:s===r?i:i=t(r=s,n)}}function Xh(e,t,n){var r,o=n+"",i;return function(){var s=this.getAttributeNS(e.space,e.local);return s===o?null:s===r?i:i=t(r=s,n)}}function Fh(e,t,n){var r,o,i;return function(){var s,a=n(this),l;return a==null?void this.removeAttribute(e):(s=this.getAttribute(e),l=a+"",s===l?null:s===r&&l===o?i:(o=l,i=t(r=s,a)))}}function Wh(e,t,n){var r,o,i;return function(){var s,a=n(this),l;return a==null?void this.removeAttributeNS(e.space,e.local):(s=this.getAttributeNS(e.space,e.local),l=a+"",s===l?null:s===r&&l===o?i:(o=l,i=t(r=s,a)))}}function Kh(e,t){var n=di(e),r=n==="transform"?$h:lu;return this.attrTween(e,typeof t=="function"?(n.local?Wh:Fh)(n,r,Xs(this,"attr."+e,t)):t==null?(n.local?Yh:Bh)(n):(n.local?Xh:Zh)(n,r,t))}function qh(e,t){return function(n){this.setAttribute(e,t.call(this,n))}}function Gh(e,t){return function(n){this.setAttributeNS(e.space,e.local,t.call(this,n))}}function Uh(e,t){var n,r;function o(){var i=t.apply(this,arguments);return i!==r&&(n=(r=i)&&Gh(e,i)),n}return o._value=t,o}function jh(e,t){var n,r;function o(){var i=t.apply(this,arguments);return i!==r&&(n=(r=i)&&qh(e,i)),n}return o._value=t,o}function Jh(e,t){var n="attr."+e;if(arguments.length<2)return(n=this.tween(n))&&n._value;if(t==null)return this.tween(n,null);if(typeof t!="function")throw new Error;var r=di(e);return this.tween(n,(r.local?Uh:jh)(r,t))}function Qh(e,t){return function(){Zs(this,e).delay=+t.apply(this,arguments)}}function ev(e,t){return t=+t,function(){Zs(this,e).delay=t}}function tv(e){var t=this._id;return arguments.length?this.each((typeof e=="function"?Qh:ev)(t,e)):an(this.node(),t).delay}function nv(e,t){return function(){xn(this,e).duration=+t.apply(this,arguments)}}function rv(e,t){return t=+t,function(){xn(this,e).duration=t}}function ov(e){var t=this._id;return arguments.length?this.each((typeof e=="function"?nv:rv)(t,e)):an(this.node(),t).duration}function iv(e,t){if(typeof t!="function")throw new Error;return function(){xn(this,e).ease=t}}function sv(e){var t=this._id;return arguments.length?this.each(iv(t,e)):an(this.node(),t).ease}function av(e,t){return function(){var n=t.apply(this,arguments);if(typeof n!="function")throw new Error;xn(this,e).ease=n}}function lv(e){if(typeof e!="function")throw new Error;return this.each(av(this._id,e))}function uv(e){typeof e!="function"&&(e=$l(e));for(var t=this._groups,n=t.length,r=new Array(n),o=0;o<n;++o)for(var i=t[o],s=i.length,a=r[o]=[],l,u=0;u<s;++u)(l=i[u])&&e.call(l,l.__data__,u,i)&&a.push(l);return new Ln(r,this._parents,this._name,this._id)}function cv(e){if(e._id!==this._id)throw new Error;for(var t=this._groups,n=e._groups,r=t.length,o=n.length,i=Math.min(r,o),s=new Array(r),a=0;a<i;++a)for(var l=t[a],u=n[a],c=l.length,f=s[a]=new Array(c),d,g=0;g<c;++g)(d=l[g]||u[g])&&(f[g]=d);for(;a<r;++a)s[a]=t[a];return new Ln(s,this._parents,this._name,this._id)}function dv(e){return(e+"").trim().split(/^|\s+/).every(function(t){var n=t.indexOf(".");return n>=0&&(t=t.slice(0,n)),!t||t==="start"})}function fv(e,t,n){var r,o,i=dv(t)?Zs:xn;return function(){var s=i(this,e),a=s.on;a!==r&&(o=(r=a).copy()).on(t,n),s.on=o}}function gv(e,t){var n=this._id;return arguments.length<2?an(this.node(),n).on.on(e):this.each(fv(n,e,t))}function hv(e){return function(){var t=this.parentNode;for(var n in this.__transition)if(+n!==e)return;t&&t.removeChild(this)}}function vv(){return this.on("end.remove",hv(this._id))}function pv(e){var t=this._name,n=this._id;typeof e!="function"&&(e=Ns(e));for(var r=this._groups,o=r.length,i=new Array(o),s=0;s<o;++s)for(var a=r[s],l=a.length,u=i[s]=new Array(l),c,f,d=0;d<l;++d)(c=a[d])&&(f=e.call(c,c.__data__,d,a))&&("__data__"in c&&(f.__data__=c.__data__),u[d]=f,$i(u[d],t,n,d,u,an(c,n)));return new Ln(i,this._parents,t,n)}function mv(e){var t=this._name,n=this._id;typeof e!="function"&&(e=kl(e));for(var r=this._groups,o=r.length,i=[],s=[],a=0;a<o;++a)for(var l=r[a],u=l.length,c,f=0;f<u;++f)if(c=l[f]){for(var d=e.call(c,c.__data__,f,l),g,p=an(c,n),x=0,C=d.length;x<C;++x)(g=d[x])&&$i(g,t,n,x,d,p);i.push(d),s.push(c)}return new Ln(i,s,t,n)}var yv=lo.prototype.constructor;function wv(){return new yv(this._groups,this._parents)}function _v(e,t){var n,r,o;return function(){var i=Tr(this,e),s=(this.style.removeProperty(e),Tr(this,e));return i===s?null:i===n&&s===r?o:o=t(n=i,r=s)}}function uu(e){return function(){this.style.removeProperty(e)}}function xv(e,t,n){var r,o=n+"",i;return function(){var s=Tr(this,e);return s===o?null:s===r?i:i=t(r=s,n)}}function bv(e,t,n){var r,o,i;return function(){var s=Tr(this,e),a=n(this),l=a+"";return a==null&&(l=a=(this.style.removeProperty(e),Tr(this,e))),s===l?null:s===r&&l===o?i:(o=l,i=t(r=s,a))}}function Cv(e,t){var n,r,o,i="style."+t,s="end."+i,a;return function(){var l=xn(this,e),u=l.on,c=l.value[i]==null?a||(a=uu(t)):void 0;(u!==n||o!==c)&&(r=(n=u).copy()).on(s,o=c),l.on=r}}function kv(e,t,n){var r=(e+="")=="transform"?kh:lu;return t==null?this.styleTween(e,_v(e,r)).on("end.style."+e,uu(e)):typeof t=="function"?this.styleTween(e,bv(e,r,Xs(this,"style."+e,t))).each(Cv(this._id,e)):this.styleTween(e,xv(e,r,t),n).on("end.style."+e,null)}function $v(e,t,n){return function(r){this.style.setProperty(e,t.call(this,r),n)}}function Ev(e,t,n){var r,o;function i(){var s=t.apply(this,arguments);return s!==o&&(r=(o=s)&&$v(e,s,n)),r}return i._value=t,i}function Sv(e,t,n){var r="style."+(e+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(t==null)return this.tween(r,null);if(typeof t!="function")throw new Error;return this.tween(r,Ev(e,t,n??""))}function Pv(e){return function(){this.textContent=e}}function Nv(e){return function(){var t=e(this);this.textContent=t??""}}function Tv(e){return this.tween("text",typeof e=="function"?Nv(Xs(this,"text",e)):Pv(e==null?"":e+""))}function Mv(e){return function(t){this.textContent=e.call(this,t)}}function Hv(e){var t,n;function r(){var o=e.apply(this,arguments);return o!==n&&(t=(n=o)&&Mv(o)),t}return r._value=e,r}function Vv(e){var t="text";if(arguments.length<1)return(t=this.tween(t))&&t._value;if(e==null)return this.tween(t,null);if(typeof e!="function")throw new Error;return this.tween(t,Hv(e))}function Dv(){for(var e=this._name,t=this._id,n=cu(),r=this._groups,o=r.length,i=0;i<o;++i)for(var s=r[i],a=s.length,l,u=0;u<a;++u)if(l=s[u]){var c=an(l,t);$i(l,e,n,u,s,{time:c.time+c.delay+c.duration,delay:0,duration:c.duration,ease:c.ease})}return new Ln(r,this._parents,e,n)}function Av(){var e,t,n=this,r=n._id,o=n.size();return new Promise(function(i,s){var a={value:s},l={value:function(){--o===0&&i()}};n.each(function(){var u=xn(this,r),c=u.on;c!==e&&(t=(e=c).copy(),t._.cancel.push(a),t._.interrupt.push(a),t._.end.push(l)),u.on=t}),o===0&&i()})}var Lv=0;function Ln(e,t,n,r){this._groups=e,this._parents=t,this._name=n,this._id=r}function cu(){return++Lv}var On=lo.prototype;Ln.prototype={constructor:Ln,select:pv,selectAll:mv,selectChild:On.selectChild,selectChildren:On.selectChildren,filter:uv,merge:cv,selection:wv,transition:Dv,call:On.call,nodes:On.nodes,node:On.node,size:On.size,empty:On.empty,each:On.each,on:gv,attr:Kh,attrTween:Jh,style:kv,styleTween:Sv,text:Tv,textTween:Vv,remove:vv,tween:Rh,delay:tv,duration:ov,ease:sv,easeVarying:lv,end:Av,[Symbol.iterator]:On[Symbol.iterator]};function Ov(e){return((e*=2)<=1?e*e*e:(e-=2)*e*e+2)/2}var Iv={time:null,delay:0,duration:250,ease:Ov};function zv(e,t){for(var n;!(n=e.__transition)||!(n=n[t]);)if(!(e=e.parentNode))throw new Error(`transition ${t} not found`);return n}function Rv(e){var t,n;e instanceof Ln?(t=e._id,e=e._name):(t=cu(),(n=Iv).time=zs(),e=e==null?null:e+"");for(var r=this._groups,o=r.length,i=0;i<o;++i)for(var s=r[i],a=s.length,l,u=0;u<a;++u)(l=s[u])&&$i(l,e,t,u,s,n||zv(l,t));return new Ln(r,this._parents,e,t)}lo.prototype.interrupt=Oh,lo.prototype.transition=Rv;const Si=e=>()=>e;function Bv(e,{sourceEvent:t,target:n,transform:r,dispatch:o}){Object.defineProperties(this,{type:{value:e,enumerable:!0,configurable:!0},sourceEvent:{value:t,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:o}})}function In(e,t,n){this.k=e,this.x=t,this.y=n}In.prototype={constructor:In,scale:function(e){return e===1?this:new In(this.k*e,this.x,this.y)},translate:function(e,t){return e===0&t===0?this:new In(this.k,this.x+this.k*e,this.y+this.k*t)},apply:function(e){return[e[0]*this.k+this.x,e[1]*this.k+this.y]},applyX:function(e){return e*this.k+this.x},applyY:function(e){return e*this.k+this.y},invert:function(e){return[(e[0]-this.x)/this.k,(e[1]-this.y)/this.k]},invertX:function(e){return(e-this.x)/this.k},invertY:function(e){return(e-this.y)/this.k},rescaleX:function(e){return e.copy().domain(e.range().map(this.invertX,this).map(e.invert,e))},rescaleY:function(e){return e.copy().domain(e.range().map(this.invertY,this).map(e.invert,e))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Pi=new In(1,0,0);du.prototype=In.prototype;function du(e){for(;!e.__zoom;)if(!(e=e.parentNode))return Pi;return e.__zoom}function Fs(e){e.stopImmediatePropagation()}function wo(e){e.preventDefault(),e.stopImmediatePropagation()}function Yv(e){return(!e.ctrlKey||e.type==="wheel")&&!e.button}function Zv(){var e=this;return e instanceof SVGElement?(e=e.ownerSVGElement||e,e.hasAttribute("viewBox")?(e=e.viewBox.baseVal,[[e.x,e.y],[e.x+e.width,e.y+e.height]]):[[0,0],[e.width.baseVal.value,e.height.baseVal.value]]):[[0,0],[e.clientWidth,e.clientHeight]]}function fu(){return this.__zoom||Pi}function Xv(e){return-e.deltaY*(e.deltaMode===1?.05:e.deltaMode?1:.002)*(e.ctrlKey?10:1)}function Fv(){return navigator.maxTouchPoints||"ontouchstart"in this}function Wv(e,t,n){var r=e.invertX(t[0][0])-n[0][0],o=e.invertX(t[1][0])-n[1][0],i=e.invertY(t[0][1])-n[0][1],s=e.invertY(t[1][1])-n[1][1];return e.translate(o>r?(r+o)/2:Math.min(0,r)||Math.max(0,o),s>i?(i+s)/2:Math.min(0,i)||Math.max(0,s))}function gu(){var e=Yv,t=Zv,n=Wv,r=Xv,o=Fv,i=[0,1/0],s=[[-1/0,-1/0],[1/0,1/0]],a=250,l=Nh,u=ui("start","zoom","end"),c,f,d,g=500,p=150,x=0,C=10;function $(S){S.property("__zoom",fu).on("wheel.zoom",T,{passive:!1}).on("mousedown.zoom",D).on("dblclick.zoom",V).filter(o).on("touchstart.zoom",A).on("touchmove.zoom",O).on("touchend.zoom touchcancel.zoom",R).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}$.transform=function(S,M,k,P){var H=S.selection?S.selection():S;H.property("__zoom",fu),S!==H?b(S,M,k,P):H.interrupt().each(function(){N(this,arguments).event(P).start().zoom(null,typeof M=="function"?M.apply(this,arguments):M).end()})},$.scaleBy=function(S,M,k,P){$.scaleTo(S,function(){var H=this.__zoom.k,I=typeof M=="function"?M.apply(this,arguments):M;return H*I},k,P)},$.scaleTo=function(S,M,k,P){$.transform(S,function(){var H=t.apply(this,arguments),I=this.__zoom,B=k==null?v(H):typeof k=="function"?k.apply(this,arguments):k,F=I.invert(B),K=typeof M=="function"?M.apply(this,arguments):M;return n(_(m(I,K),B,F),H,s)},k,P)},$.translateBy=function(S,M,k,P){$.transform(S,function(){return n(this.__zoom.translate(typeof M=="function"?M.apply(this,arguments):M,typeof k=="function"?k.apply(this,arguments):k),t.apply(this,arguments),s)},null,P)},$.translateTo=function(S,M,k,P,H){$.transform(S,function(){var I=t.apply(this,arguments),B=this.__zoom,F=P==null?v(I):typeof P=="function"?P.apply(this,arguments):P;return n(Pi.translate(F[0],F[1]).scale(B.k).translate(typeof M=="function"?-M.apply(this,arguments):-M,typeof k=="function"?-k.apply(this,arguments):-k),I,s)},P,H)};function m(S,M){return M=Math.max(i[0],Math.min(i[1],M)),M===S.k?S:new In(M,S.x,S.y)}function _(S,M,k){var P=M[0]-k[0]*S.k,H=M[1]-k[1]*S.k;return P===S.x&&H===S.y?S:new In(S.k,P,H)}function v(S){return[(+S[0][0]+ +S[1][0])/2,(+S[0][1]+ +S[1][1])/2]}function b(S,M,k,P){S.on("start.zoom",function(){N(this,arguments).event(P).start()}).on("interrupt.zoom end.zoom",function(){N(this,arguments).event(P).end()}).tween("zoom",function(){var H=this,I=arguments,B=N(H,I).event(P),F=t.apply(H,I),K=k==null?v(F):typeof k=="function"?k.apply(H,I):k,se=Math.max(F[1][0]-F[0][0],F[1][1]-F[0][1]),ee=H.__zoom,W=typeof M=="function"?M.apply(H,I):M,fe=l(ee.invert(K).concat(se/ee.k),W.invert(K).concat(se/W.k));return function(me){if(me===1)me=W;else{var Ce=fe(me),he=se/Ce[2];me=new In(he,K[0]-Ce[0]*he,K[1]-Ce[1]*he)}B.zoom(null,me)}})}function N(S,M,k){return!k&&S.__zooming||new E(S,M)}function E(S,M){this.that=S,this.args=M,this.active=0,this.sourceEvent=null,this.extent=t.apply(S,M),this.taps=0}E.prototype={event:function(S){return S&&(this.sourceEvent=S),this},start:function(){return++this.active===1&&(this.that.__zooming=this,this.emit("start")),this},zoom:function(S,M){return this.mouse&&S!=="mouse"&&(this.mouse[1]=M.invert(this.mouse[0])),this.touch0&&S!=="touch"&&(this.touch0[1]=M.invert(this.touch0[0])),this.touch1&&S!=="touch"&&(this.touch1[1]=M.invert(this.touch1[0])),this.that.__zoom=M,this.emit("zoom"),this},end:function(){return--this.active===0&&(delete this.that.__zooming,this.emit("end")),this},emit:function(S){var M=Ut(this.that).datum();u.call(S,this.that,new Bv(S,{sourceEvent:this.sourceEvent,target:$,transform:this.that.__zoom,dispatch:u}),M)}};function T(S,...M){if(!e.apply(this,arguments))return;var k=N(this,M).event(S),P=this.__zoom,H=Math.max(i[0],Math.min(i[1],P.k*Math.pow(2,r.apply(this,arguments)))),I=on(S);if(k.wheel)(k.mouse[0][0]!==I[0]||k.mouse[0][1]!==I[1])&&(k.mouse[1]=P.invert(k.mouse[0]=I)),clearTimeout(k.wheel);else{if(P.k===H)return;k.mouse=[I,P.invert(I)],Ei(this),k.start()}wo(S),k.wheel=setTimeout(B,p),k.zoom("mouse",n(_(m(P,H),k.mouse[0],k.mouse[1]),k.extent,s));function B(){k.wheel=null,k.end()}}function D(S,...M){if(d||!e.apply(this,arguments))return;var k=S.currentTarget,P=N(this,M,!0).event(S),H=Ut(S.view).on("mousemove.zoom",K,!0).on("mouseup.zoom",se,!0),I=on(S,k),B=S.clientX,F=S.clientY;Al(S.view),Fs(S),P.mouse=[I,this.__zoom.invert(I)],Ei(this),P.start();function K(ee){if(wo(ee),!P.moved){var W=ee.clientX-B,fe=ee.clientY-F;P.moved=W*W+fe*fe>x}P.event(ee).zoom("mouse",n(_(P.that.__zoom,P.mouse[0]=on(ee,k),P.mouse[1]),P.extent,s))}function se(ee){H.on("mousemove.zoom mouseup.zoom",null),Ll(ee.view,P.moved),wo(ee),P.event(ee).end()}}function V(S,...M){if(e.apply(this,arguments)){var k=this.__zoom,P=on(S.changedTouches?S.changedTouches[0]:S,this),H=k.invert(P),I=k.k*(S.shiftKey?.5:2),B=n(_(m(k,I),P,H),t.apply(this,M),s);wo(S),a>0?Ut(this).transition().duration(a).call(b,B,P,S):Ut(this).call($.transform,B,P,S)}}function A(S,...M){if(e.apply(this,arguments)){var k=S.touches,P=k.length,H=N(this,M,S.changedTouches.length===P).event(S),I,B,F,K;for(Fs(S),B=0;B<P;++B)F=k[B],K=on(F,this),K=[K,this.__zoom.invert(K),F.identifier],H.touch0?!H.touch1&&H.touch0[2]!==K[2]&&(H.touch1=K,H.taps=0):(H.touch0=K,I=!0,H.taps=1+!!c);c&&(c=clearTimeout(c)),I&&(H.taps<2&&(f=K[0],c=setTimeout(function(){c=null},g)),Ei(this),H.start())}}function O(S,...M){if(this.__zooming){var k=N(this,M).event(S),P=S.changedTouches,H=P.length,I,B,F,K;for(wo(S),I=0;I<H;++I)B=P[I],F=on(B,this),k.touch0&&k.touch0[2]===B.identifier?k.touch0[0]=F:k.touch1&&k.touch1[2]===B.identifier&&(k.touch1[0]=F);if(B=k.that.__zoom,k.touch1){var se=k.touch0[0],ee=k.touch0[1],W=k.touch1[0],fe=k.touch1[1],me=(me=W[0]-se[0])*me+(me=W[1]-se[1])*me,Ce=(Ce=fe[0]-ee[0])*Ce+(Ce=fe[1]-ee[1])*Ce;B=m(B,Math.sqrt(me/Ce)),F=[(se[0]+W[0])/2,(se[1]+W[1])/2],K=[(ee[0]+fe[0])/2,(ee[1]+fe[1])/2]}else if(k.touch0)F=k.touch0[0],K=k.touch0[1];else return;k.zoom("touch",n(_(B,F,K),k.extent,s))}}function R(S,...M){if(this.__zooming){var k=N(this,M).event(S),P=S.changedTouches,H=P.length,I,B;for(Fs(S),d&&clearTimeout(d),d=setTimeout(function(){d=null},g),I=0;I<H;++I)B=P[I],k.touch0&&k.touch0[2]===B.identifier?delete k.touch0:k.touch1&&k.touch1[2]===B.identifier&&delete k.touch1;if(k.touch1&&!k.touch0&&(k.touch0=k.touch1,delete k.touch1),k.touch0)k.touch0[1]=this.__zoom.invert(k.touch0[0]);else if(k.end(),k.taps===2&&(B=on(B,this),Math.hypot(f[0]-B[0],f[1]-B[1])<C)){var F=Ut(this).on("dblclick.zoom");F&&F.apply(this,arguments)}}}return $.wheelDelta=function(S){return arguments.length?(r=typeof S=="function"?S:Si(+S),$):r},$.filter=function(S){return arguments.length?(e=typeof S=="function"?S:Si(!!S),$):e},$.touchable=function(S){return arguments.length?(o=typeof S=="function"?S:Si(!!S),$):o},$.extent=function(S){return arguments.length?(t=typeof S=="function"?S:Si([[+S[0][0],+S[0][1]],[+S[1][0],+S[1][1]]]),$):t},$.scaleExtent=function(S){return arguments.length?(i[0]=+S[0],i[1]=+S[1],$):[i[0],i[1]]},$.translateExtent=function(S){return arguments.length?(s[0][0]=+S[0][0],s[1][0]=+S[1][0],s[0][1]=+S[0][1],s[1][1]=+S[1][1],$):[[s[0][0],s[0][1]],[s[1][0],s[1][1]]]},$.constrain=function(S){return arguments.length?(n=S,$):n},$.duration=function(S){return arguments.length?(a=+S,$):a},$.interpolate=function(S){return arguments.length?(l=S,$):l},$.on=function(){var S=u.on.apply(u,arguments);return S===u?$:S},$.clickDistance=function(S){return arguments.length?(x=(S=+S)*S,$):Math.sqrt(x)},$.tapDistance=function(S){return arguments.length?(C=+S,$):C},$}const Dr={error001:()=>"[React Flow]: Seems like you have not used zustand provider as an ancestor. Help: https://reactflow.dev/error#001",error002:()=>"It looks like you've created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them.",error003:e=>`Node type "${e}" not found. Using fallback type "default".`,error004:()=>"The React Flow parent container needs a width and a height to render the graph.",error005:()=>"Only child nodes can use a parent extent.",error006:()=>"Can't create edge. An edge needs a source and a target.",error007:e=>`The old edge with id=${e} does not exist.`,error009:e=>`Marker type "${e}" doesn't exist.`,error008:(e,{id:t,sourceHandle:n,targetHandle:r})=>`Couldn't create edge for ${e} handle id: "${e==="source"?n:r}", edge id: ${t}.`,error010:()=>"Handle: No node id found. Make sure to only use a Handle inside a custom Node.",error011:e=>`Edge type "${e}" not found. Using fallback type "default".`,error012:e=>`Node with id "${e}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,error013:(e="react")=>`It seems that you haven't loaded the styles. Please import '@xyflow/${e}/dist/style.css' or base.css to make sure everything is working properly.`,error014:()=>"useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.",error015:()=>"It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs."},Ni=[[Number.NEGATIVE_INFINITY,Number.NEGATIVE_INFINITY],[Number.POSITIVE_INFINITY,Number.POSITIVE_INFINITY]];var hr;(function(e){e.Strict="strict",e.Loose="loose"})(hr||(hr={}));var Jn;(function(e){e.Free="free",e.Vertical="vertical",e.Horizontal="horizontal"})(Jn||(Jn={}));var Ti;(function(e){e.Partial="partial",e.Full="full"})(Ti||(Ti={}));const Ws={inProgress:!1,isValid:null,from:null,fromHandle:null,fromPosition:null,fromNode:null,to:null,toHandle:null,toPosition:null,toNode:null};var Ar;(function(e){e.Bezier="default",e.Straight="straight",e.Step="step",e.SmoothStep="smoothstep",e.SimpleBezier="simplebezier"})(Ar||(Ar={}));var _o;(function(e){e.Arrow="arrow",e.ArrowClosed="arrowclosed"})(_o||(_o={}));var $e;(function(e){e.Left="left",e.Top="top",e.Right="right",e.Bottom="bottom"})($e||($e={}));const hu={[$e.Left]:$e.Right,[$e.Right]:$e.Left,[$e.Top]:$e.Bottom,[$e.Bottom]:$e.Top};function Kv(e,t){if(!e&&!t)return!0;if(!e||!t||e.size!==t.size)return!1;if(!e.size&&!t.size)return!0;for(const n of e.keys())if(!t.has(n))return!1;return!0}function vu(e,t,n){if(!n)return;const r=[];e.forEach((o,i)=>{t!=null&&t.has(i)||r.push(o)}),r.length&&n(r)}function qv(e){return e===null?null:e?"valid":"invalid"}const Gv=e=>"id"in e&&"source"in e&&"target"in e,Uv=e=>"id"in e&&"position"in e&&!("source"in e)&&!("target"in e),Ks=e=>"id"in e&&"internals"in e&&!("source"in e)&&!("target"in e),xo=(e,t=[0,0])=>{const{width:n,height:r}=Qn(e),o=e.origin??t,i=n*o[0],s=r*o[1];return{x:e.position.x-i,y:e.position.y-s}},jv=(e,t={nodeOrigin:[0,0],nodeLookup:void 0})=>{if(e.length===0)return{x:0,y:0,width:0,height:0};const n=e.reduce((r,o)=>{const i=typeof o=="string";let s=!t.nodeLookup&&!i?o:void 0;t.nodeLookup&&(s=i?t.nodeLookup.get(o):Ks(o)?o:t.nodeLookup.get(o.id));const a=s?Vi(s,t.nodeOrigin):{x:0,y:0,x2:0,y2:0};return Mi(r,a)},{x:1/0,y:1/0,x2:-1/0,y2:-1/0});return Hi(n)},bo=(e,t={})=>{if(e.size===0)return{x:0,y:0,width:0,height:0};let n={x:1/0,y:1/0,x2:-1/0,y2:-1/0};return e.forEach(r=>{if(t.filter===void 0||t.filter(r)){const o=Vi(r);n=Mi(n,o)}}),Hi(n)},pu=(e,t,[n,r,o]=[0,0,1],i=!1,s=!1)=>{const a={...ko(t,[n,r,o]),width:t.width/o,height:t.height/o},l=[];for(const u of e.values()){const{measured:c,selectable:f=!0,hidden:d=!1}=u;if(s&&!f||d)continue;const g=c.width??u.width??u.initialWidth??null,p=c.height??u.height??u.initialHeight??null,x=Co(a,Or(u)),C=(g??0)*(p??0),$=i&&x>0;(!u.internals.handleBounds||$||x>=C||u.dragging)&&l.push(u)}return l},qs=(e,t)=>{const n=new Set;return e.forEach(r=>{n.add(r.id)}),t.filter(r=>n.has(r.source)||n.has(r.target))};function mu(e,t){const n=new Map,r=t!=null&&t.nodes?new Set(t.nodes.map(o=>o.id)):null;return e.forEach(o=>{o.measured.width&&o.measured.height&&((t==null?void 0:t.includeHiddenNodes)||!o.hidden)&&(!r||r.has(o.id))&&n.set(o.id,o)}),n}async function yu({nodes:e,width:t,height:n,panZoom:r,minZoom:o,maxZoom:i},s){if(e.size===0)return Promise.resolve(!1);const a=bo(e),l=js(a,t,n,(s==null?void 0:s.minZoom)??o,(s==null?void 0:s.maxZoom)??i,(s==null?void 0:s.padding)??.1);return await r.setViewport(l,{duration:s==null?void 0:s.duration}),Promise.resolve(!0)}function Jv({nodeId:e,nextPosition:t,nodeLookup:n,nodeOrigin:r=[0,0],nodeExtent:o,onError:i}){const s=n.get(e),a=s.parentId?n.get(s.parentId):void 0,{x:l,y:u}=a?a.internals.positionAbsolute:{x:0,y:0},c=s.origin??r;let f=o;if(s.extent==="parent"&&!s.expandParent)if(!a)i==null||i("005",Dr.error005());else{const g=a.measured.width,p=a.measured.height;g&&p&&(f=[[l,u],[l+g,u+p]])}else a&&Ir(s.extent)&&(f=[[s.extent[0][0]+l,s.extent[0][1]+u],[s.extent[1][0]+l,s.extent[1][1]+u]]);const d=Ir(f)?vr(t,f,s.measured):t;return(s.measured.width===void 0||s.measured.height===void 0)&&(i==null||i("015",Dr.error015())),{position:{x:d.x-l+(s.measured.width??0)*c[0],y:d.y-u+(s.measured.height??0)*c[1]},positionAbsolute:d}}async function wu({nodesToRemove:e=[],edgesToRemove:t=[],nodes:n,edges:r,onBeforeDelete:o}){const i=new Set(e.map(d=>d.id)),s=[];for(const d of n){if(d.deletable===!1)continue;const g=i.has(d.id),p=!g&&d.parentId&&s.find(x=>x.id===d.parentId);(g||p)&&s.push(d)}const a=new Set(t.map(d=>d.id)),l=r.filter(d=>d.deletable!==!1),c=qs(s,l);for(const d of l)a.has(d.id)&&!c.find(p=>p.id===d.id)&&c.push(d);if(!o)return{edges:c,nodes:s};const f=await o({nodes:s,edges:c});return typeof f=="boolean"?f?{edges:c,nodes:s}:{edges:[],nodes:[]}:f}const Lr=(e,t=0,n=1)=>Math.min(Math.max(e,t),n),vr=(e={x:0,y:0},t,n)=>({x:Lr(e.x,t[0][0],t[1][0]-((n==null?void 0:n.width)??0)),y:Lr(e.y,t[0][1],t[1][1]-((n==null?void 0:n.height)??0))});function _u(e,t,n){const{width:r,height:o}=Qn(n),{x:i,y:s}=n.internals.positionAbsolute;return vr(e,[[i,s],[i+r,s+o]],t)}const xu=(e,t,n)=>e<t?Lr(Math.abs(e-t),1,t)/t:e>n?-Lr(Math.abs(e-n),1,t)/t:0,bu=(e,t,n=15,r=40)=>{const o=xu(e.x,r,t.width-r)*n,i=xu(e.y,r,t.height-r)*n;return[o,i]},Mi=(e,t)=>({x:Math.min(e.x,t.x),y:Math.min(e.y,t.y),x2:Math.max(e.x2,t.x2),y2:Math.max(e.y2,t.y2)}),Gs=({x:e,y:t,width:n,height:r})=>({x:e,y:t,x2:e+n,y2:t+r}),Hi=({x:e,y:t,x2:n,y2:r})=>({x:e,y:t,width:n-e,height:r-t}),Or=(e,t=[0,0])=>{var o,i;const{x:n,y:r}=Ks(e)?e.internals.positionAbsolute:xo(e,t);return{x:n,y:r,width:((o=e.measured)==null?void 0:o.width)??e.width??e.initialWidth??0,height:((i=e.measured)==null?void 0:i.height)??e.height??e.initialHeight??0}},Vi=(e,t=[0,0])=>{var o,i;const{x:n,y:r}=Ks(e)?e.internals.positionAbsolute:xo(e,t);return{x:n,y:r,x2:n+(((o=e.measured)==null?void 0:o.width)??e.width??e.initialWidth??0),y2:r+(((i=e.measured)==null?void 0:i.height)??e.height??e.initialHeight??0)}},Cu=(e,t)=>Hi(Mi(Gs(e),Gs(t))),Co=(e,t)=>{const n=Math.max(0,Math.min(e.x+e.width,t.x+t.width)-Math.max(e.x,t.x)),r=Math.max(0,Math.min(e.y+e.height,t.y+t.height)-Math.max(e.y,t.y));return Math.ceil(n*r)},ku=e=>zn(e.width)&&zn(e.height)&&zn(e.x)&&zn(e.y),zn=e=>!isNaN(e)&&isFinite(e),Qv=(e,t)=>{},Us=(e,t=[1,1])=>({x:t[0]*Math.round(e.x/t[0]),y:t[1]*Math.round(e.y/t[1])}),ko=({x:e,y:t},[n,r,o],i=!1,s=[1,1])=>{const a={x:(e-n)/o,y:(t-r)/o};return i?Us(a,s):a},$u=({x:e,y:t},[n,r,o])=>({x:e*o+n,y:t*o+r}),js=(e,t,n,r,o,i)=>{const s=t/(e.width*(1+i)),a=n/(e.height*(1+i)),l=Math.min(s,a),u=Lr(l,r,o),c=e.x+e.width/2,f=e.y+e.height/2,d=t/2-c*u,g=n/2-f*u;return{x:d,y:g,zoom:u}},Di=()=>{var e;return typeof navigator<"u"&&((e=navigator==null?void 0:navigator.userAgent)==null?void 0:e.indexOf("Mac"))>=0};function Ir(e){return e!==void 0&&e!=="parent"}function Qn(e){var t,n;return{width:((t=e.measured)==null?void 0:t.width)??e.width??e.initialWidth??0,height:((n=e.measured)==null?void 0:n.height)??e.height??e.initialHeight??0}}function Eu(e){var t,n;return(((t=e.measured)==null?void 0:t.width)??e.width??e.initialWidth)!==void 0&&(((n=e.measured)==null?void 0:n.height)??e.height??e.initialHeight)!==void 0}function e0(e,t={width:0,height:0},n,r,o){const i={...e},s=r.get(n);if(s){const a=s.origin||o;i.x+=s.internals.positionAbsolute.x-(t.width??0)*a[0],i.y+=s.internals.positionAbsolute.y-(t.height??0)*a[1]}return i}function Js(e,{snapGrid:t=[0,0],snapToGrid:n=!1,transform:r,containerBounds:o}){const{x:i,y:s}=Rn(e),a=ko({x:i-((o==null?void 0:o.left)??0),y:s-((o==null?void 0:o.top)??0)},r),{x:l,y:u}=n?Us(a,t):a;return{xSnapped:l,ySnapped:u,...a}}const Qs=e=>({width:e.offsetWidth,height:e.offsetHeight}),t0=e=>{var t;return((t=e==null?void 0:e.getRootNode)==null?void 0:t.call(e))||(window==null?void 0:window.document)},n0=["INPUT","SELECT","TEXTAREA"];function r0(e){var r,o;const t=((o=(r=e.composedPath)==null?void 0:r.call(e))==null?void 0:o[0])||e.target;return(t==null?void 0:t.nodeType)!==1?!1:n0.includes(t.nodeName)||t.hasAttribute("contenteditable")||!!t.closest(".nokey")}const Su=e=>"clientX"in e,Rn=(e,t)=>{var i,s;const n=Su(e),r=n?e.clientX:(i=e.touches)==null?void 0:i[0].clientX,o=n?e.clientY:(s=e.touches)==null?void 0:s[0].clientY;return{x:r-((t==null?void 0:t.left)??0),y:o-((t==null?void 0:t.top)??0)}},Pu=(e,t,n,r,o)=>{const i=t.querySelectorAll(`.${e}`);return!i||!i.length?null:Array.from(i).map(s=>{const a=s.getBoundingClientRect();return{id:s.getAttribute("data-handleid"),type:e,nodeId:o,position:s.getAttribute("data-handlepos"),x:(a.left-n.left)/r,y:(a.top-n.top)/r,...Qs(s)}})};function o0({sourceX:e,sourceY:t,targetX:n,targetY:r,sourceControlX:o,sourceControlY:i,targetControlX:s,targetControlY:a}){const l=e*.125+o*.375+s*.375+n*.125,u=t*.125+i*.375+a*.375+r*.125,c=Math.abs(l-e),f=Math.abs(u-t);return[l,u,c,f]}function Ai(e,t){return e>=0?.5*e:t*25*Math.sqrt(-e)}function Nu({pos:e,x1:t,y1:n,x2:r,y2:o,c:i}){switch(e){case $e.Left:return[t-Ai(t-r,i),n];case $e.Right:return[t+Ai(r-t,i),n];case $e.Top:return[t,n-Ai(n-o,i)];case $e.Bottom:return[t,n+Ai(o-n,i)]}}function Tu({sourceX:e,sourceY:t,sourcePosition:n=$e.Bottom,targetX:r,targetY:o,targetPosition:i=$e.Top,curvature:s=.25}){const[a,l]=Nu({pos:n,x1:e,y1:t,x2:r,y2:o,c:s}),[u,c]=Nu({pos:i,x1:r,y1:o,x2:e,y2:t,c:s}),[f,d,g,p]=o0({sourceX:e,sourceY:t,targetX:r,targetY:o,sourceControlX:a,sourceControlY:l,targetControlX:u,targetControlY:c});return[`M${e},${t} C${a},${l} ${u},${c} ${r},${o}`,f,d,g,p]}function Mu({sourceX:e,sourceY:t,targetX:n,targetY:r}){const o=Math.abs(n-e)/2,i=n<e?n+o:n-o,s=Math.abs(r-t)/2,a=r<t?r+s:r-s;return[i,a,o,s]}function i0({sourceNode:e,targetNode:t,selected:n=!1,zIndex:r=0,elevateOnSelect:o=!1}){if(!o)return r;const i=n||t.selected||e.selected,s=Math.max(e.internals.z||0,t.internals.z||0,1e3);return r+(i?s:0)}function s0({sourceNode:e,targetNode:t,width:n,height:r,transform:o}){const i=Mi(Vi(e),Vi(t));i.x===i.x2&&(i.x2+=1),i.y===i.y2&&(i.y2+=1);const s={x:-o[0]/o[2],y:-o[1]/o[2],width:n/o[2],height:r/o[2]};return Co(s,Hi(i))>0}const a0=({source:e,sourceHandle:t,target:n,targetHandle:r})=>`xy-edge__${e}${t||""}-${n}${r||""}`,l0=(e,t)=>t.some(n=>n.source===e.source&&n.target===e.target&&(n.sourceHandle===e.sourceHandle||!n.sourceHandle&&!e.sourceHandle)&&(n.targetHandle===e.targetHandle||!n.targetHandle&&!e.targetHandle)),u0=(e,t)=>{if(!e.source||!e.target)return t;let n;return Gv(e)?n={...e}:n={...e,id:a0(e)},l0(n,t)?t:(n.sourceHandle===null&&delete n.sourceHandle,n.targetHandle===null&&delete n.targetHandle,t.concat(n))};function ea({sourceX:e,sourceY:t,targetX:n,targetY:r}){const[o,i,s,a]=Mu({sourceX:e,sourceY:t,targetX:n,targetY:r});return[`M ${e},${t}L ${n},${r}`,o,i,s,a]}const Hu={[$e.Left]:{x:-1,y:0},[$e.Right]:{x:1,y:0},[$e.Top]:{x:0,y:-1},[$e.Bottom]:{x:0,y:1}},c0=({source:e,sourcePosition:t=$e.Bottom,target:n})=>t===$e.Left||t===$e.Right?e.x<n.x?{x:1,y:0}:{x:-1,y:0}:e.y<n.y?{x:0,y:1}:{x:0,y:-1},Vu=(e,t)=>Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2));function d0({source:e,sourcePosition:t=$e.Bottom,target:n,targetPosition:r=$e.Top,center:o,offset:i}){const s=Hu[t],a=Hu[r],l={x:e.x+s.x*i,y:e.y+s.y*i},u={x:n.x+a.x*i,y:n.y+a.y*i},c=c0({source:l,sourcePosition:t,target:u}),f=c.x!==0?"x":"y",d=c[f];let g=[],p,x;const C={x:0,y:0},$={x:0,y:0},[m,_,v,b]=Mu({sourceX:e.x,sourceY:e.y,targetX:n.x,targetY:n.y});if(s[f]*a[f]===-1){p=o.x??m,x=o.y??_;const E=[{x:p,y:l.y},{x:p,y:u.y}],T=[{x:l.x,y:x},{x:u.x,y:x}];s[f]===d?g=f==="x"?E:T:g=f==="x"?T:E}else{const E=[{x:l.x,y:u.y}],T=[{x:u.x,y:l.y}];if(f==="x"?g=s.x===d?T:E:g=s.y===d?E:T,t===r){const R=Math.abs(e[f]-n[f]);if(R<=i){const S=Math.min(i-1,i-R);s[f]===d?C[f]=(l[f]>e[f]?-1:1)*S:$[f]=(u[f]>n[f]?-1:1)*S}}if(t!==r){const R=f==="x"?"y":"x",S=s[f]===a[R],M=l[R]>u[R],k=l[R]<u[R];(s[f]===1&&(!S&&M||S&&k)||s[f]!==1&&(!S&&k||S&&M))&&(g=f==="x"?E:T)}const D={x:l.x+C.x,y:l.y+C.y},V={x:u.x+$.x,y:u.y+$.y},A=Math.max(Math.abs(D.x-g[0].x),Math.abs(V.x-g[0].x)),O=Math.max(Math.abs(D.y-g[0].y),Math.abs(V.y-g[0].y));A>=O?(p=(D.x+V.x)/2,x=g[0].y):(p=g[0].x,x=(D.y+V.y)/2)}return[[e,{x:l.x+C.x,y:l.y+C.y},...g,{x:u.x+$.x,y:u.y+$.y},n],p,x,v,b]}function f0(e,t,n,r){const o=Math.min(Vu(e,t)/2,Vu(t,n)/2,r),{x:i,y:s}=t;if(e.x===i&&i===n.x||e.y===s&&s===n.y)return`L${i} ${s}`;if(e.y===s){const u=e.x<n.x?-1:1,c=e.y<n.y?1:-1;return`L ${i+o*u},${s}Q ${i},${s} ${i},${s+o*c}`}const a=e.x<n.x?1:-1,l=e.y<n.y?-1:1;return`L ${i},${s+o*l}Q ${i},${s} ${i+o*a},${s}`}function Li({sourceX:e,sourceY:t,sourcePosition:n=$e.Bottom,targetX:r,targetY:o,targetPosition:i=$e.Top,borderRadius:s=5,centerX:a,centerY:l,offset:u=20}){const[c,f,d,g,p]=d0({source:{x:e,y:t},sourcePosition:n,target:{x:r,y:o},targetPosition:i,center:{x:a,y:l},offset:u});return[c.reduce((C,$,m)=>{let _="";return m>0&&m<c.length-1?_=f0(c[m-1],$,c[m+1],s):_=`${m===0?"M":"L"}${$.x} ${$.y}`,C+=_,C},""),f,d,g,p]}function Du(e){var t;return e&&!!(e.internals.handleBounds||(t=e.handles)!=null&&t.length)&&!!(e.measured.width||e.width||e.initialWidth)}function g0(e){var f;const{sourceNode:t,targetNode:n}=e;if(!Du(t)||!Du(n))return null;const r=t.internals.handleBounds||Au(t.handles),o=n.internals.handleBounds||Au(n.handles),i=Lu((r==null?void 0:r.source)??[],e.sourceHandle),s=Lu(e.connectionMode===hr.Strict?(o==null?void 0:o.target)??[]:((o==null?void 0:o.target)??[]).concat((o==null?void 0:o.source)??[]),e.targetHandle);if(!i||!s)return(f=e.onError)==null||f.call(e,"008",Dr.error008(i?"target":"source",{id:e.id,sourceHandle:e.sourceHandle,targetHandle:e.targetHandle})),null;const a=(i==null?void 0:i.position)||$e.Bottom,l=(s==null?void 0:s.position)||$e.Top,u=$o(t,i,a),c=$o(n,s,l);return{sourceX:u.x,sourceY:u.y,targetX:c.x,targetY:c.y,sourcePosition:a,targetPosition:l}}function Au(e){if(!e)return null;const t=[],n=[];for(const r of e)r.width=r.width??1,r.height=r.height??1,r.type==="source"?t.push(r):r.type==="target"&&n.push(r);return{source:t,target:n}}function $o(e,t,n=$e.Left,r=!1){const o=((t==null?void 0:t.x)??0)+e.internals.positionAbsolute.x,i=((t==null?void 0:t.y)??0)+e.internals.positionAbsolute.y,{width:s,height:a}=t??Qn(e);if(r)return{x:o+s/2,y:i+a/2};switch((t==null?void 0:t.position)??n){case $e.Top:return{x:o+s/2,y:i};case $e.Right:return{x:o+s,y:i+a/2};case $e.Bottom:return{x:o+s/2,y:i+a};case $e.Left:return{x:o,y:i+a/2}}}function Lu(e,t){return e&&(t?e.find(n=>n.id===t):e[0])||null}function ta(e,t){return e?typeof e=="string"?e:`${t?`${t}__`:""}${Object.keys(e).sort().map(r=>`${r}=${e[r]}`).join("&")}`:""}function h0(e,{id:t,defaultColor:n,defaultMarkerStart:r,defaultMarkerEnd:o}){const i=new Set;return e.reduce((s,a)=>([a.markerStart||r,a.markerEnd||o].forEach(l=>{if(l&&typeof l=="object"){const u=ta(l,t);i.has(u)||(s.push({id:u,color:l.color||n,...l}),i.add(u))}}),s),[]).sort((s,a)=>s.id.localeCompare(a.id))}function v0(e,t,n,r,o){let i=.5;o==="start"?i=0:o==="end"&&(i=1);let s=[(e.x+e.width*i)*t.zoom+t.x,e.y*t.zoom+t.y-r],a=[-100*i,-100];switch(n){case $e.Right:s=[(e.x+e.width)*t.zoom+t.x+r,(e.y+e.height*i)*t.zoom+t.y],a=[0,-100*i];break;case $e.Bottom:s[1]=(e.y+e.height)*t.zoom+t.y+r,a[1]=0;break;case $e.Left:s=[e.x*t.zoom+t.x-r,(e.y+e.height*i)*t.zoom+t.y],a=[-100,-100*i];break}return`translate(${s[0]}px, ${s[1]}px) translate(${a[0]}%, ${a[1]}%)`}const na={nodeOrigin:[0,0],nodeExtent:Ni,elevateNodesOnSelect:!0,defaults:{}},p0={...na,checkEquality:!0};function ra(e,t){const n={...e};for(const r in t)t[r]!==void 0&&(n[r]=t[r]);return n}function m0(e,t,n){const r=ra(na,n);for(const o of e.values())if(o.parentId)oa(o,e,t,r);else{const i=xo(o,r.nodeOrigin),s=Ir(o.extent)?o.extent:r.nodeExtent,a=vr(i,s,Qn(o));o.internals.positionAbsolute=a}}function Ou(e,t,n,r){var a,l;const o=ra(p0,r),i=new Map(t),s=o!=null&&o.elevateNodesOnSelect?1e3:0;t.clear(),n.clear();for(const u of e){let c=i.get(u.id);if(o.checkEquality&&u===(c==null?void 0:c.internals.userNode))t.set(u.id,c);else{const f=xo(u,o.nodeOrigin),d=Ir(u.extent)?u.extent:o.nodeExtent,g=vr(f,d,Qn(u));c={...o.defaults,...u,measured:{width:(a=u.measured)==null?void 0:a.width,height:(l=u.measured)==null?void 0:l.height},internals:{positionAbsolute:g,handleBounds:u.measured?c==null?void 0:c.internals.handleBounds:void 0,z:Iu(u,s),userNode:u}},t.set(u.id,c)}u.parentId&&oa(c,t,n,r)}}function y0(e,t){if(!e.parentId)return;const n=t.get(e.parentId);n?n.set(e.id,e):t.set(e.parentId,new Map([[e.id,e]]))}function oa(e,t,n,r){const{elevateNodesOnSelect:o,nodeOrigin:i,nodeExtent:s}=ra(na,r),a=e.parentId,l=t.get(a);if(!l){console.warn(`Parent node ${a} not found. Please make sure that parent nodes are in front of their child nodes in the nodes array.`);return}y0(e,n);const u=o?1e3:0,{x:c,y:f,z:d}=w0(e,l,i,s,u),{positionAbsolute:g}=e.internals,p=c!==g.x||f!==g.y;(p||d!==e.internals.z)&&t.set(e.id,{...e,internals:{...e.internals,positionAbsolute:p?{x:c,y:f}:g,z:d}})}function Iu(e,t){return(zn(e.zIndex)?e.zIndex:0)+(e.selected?t:0)}function w0(e,t,n,r,o){const{x:i,y:s}=t.internals.positionAbsolute,a=Qn(e),l=xo(e,n),u=Ir(e.extent)?vr(l,e.extent,a):l;let c=vr({x:i+u.x,y:s+u.y},r,a);e.extent==="parent"&&(c=_u(c,a,t));const f=Iu(e,o),d=t.internals.z??0;return{x:c.x,y:c.y,z:d>f?d:f}}function _0(e,t,n,r=[0,0]){var s;const o=[],i=new Map;for(const a of e){const l=t.get(a.parentId);if(!l)continue;const u=((s=i.get(a.parentId))==null?void 0:s.expandedRect)??Or(l),c=Cu(u,a.rect);i.set(a.parentId,{expandedRect:c,parent:l})}return i.size>0&&i.forEach(({expandedRect:a,parent:l},u)=>{var _;const c=l.internals.positionAbsolute,f=Qn(l),d=l.origin??r,g=a.x<c.x?Math.round(Math.abs(c.x-a.x)):0,p=a.y<c.y?Math.round(Math.abs(c.y-a.y)):0,x=Math.max(f.width,Math.round(a.width)),C=Math.max(f.height,Math.round(a.height)),$=(x-f.width)*d[0],m=(C-f.height)*d[1];(g>0||p>0||$||m)&&(o.push({id:u,type:"position",position:{x:l.position.x-g+$,y:l.position.y-p+m}}),(_=n.get(u))==null||_.forEach(v=>{e.some(b=>b.id===v.id)||o.push({id:v.id,type:"position",position:{x:v.position.x+g,y:v.position.y+p}})})),(f.width<a.width||f.height<a.height||g||p)&&o.push({id:u,type:"dimensions",setAttributes:!0,dimensions:{width:x+(g?d[0]*g-$:0),height:C+(p?d[1]*p-m:0)}})}),o}function x0(e,t,n,r,o,i){const s=r==null?void 0:r.querySelector(".xyflow__viewport");let a=!1;if(!s)return{changes:[],updatedInternals:a};const l=[],u=window.getComputedStyle(s),{m22:c}=new window.DOMMatrixReadOnly(u.transform),f=[];for(const d of e.values()){const g=t.get(d.id);if(!g)continue;if(g.hidden){t.set(g.id,{...g,internals:{...g.internals,handleBounds:void 0}}),a=!0;continue}const p=Qs(d.nodeElement),x=g.measured.width!==p.width||g.measured.height!==p.height;if(!!(p.width&&p.height&&(x||!g.internals.handleBounds||d.force))){const $=d.nodeElement.getBoundingClientRect(),m=Ir(g.extent)?g.extent:i;let{positionAbsolute:_}=g.internals;g.parentId&&g.extent==="parent"?_=_u(_,p,t.get(g.parentId)):m&&(_=vr(_,m,p));const v={...g,measured:p,internals:{...g.internals,positionAbsolute:_,handleBounds:{source:Pu("source",d.nodeElement,$,c,g.id),target:Pu("target",d.nodeElement,$,c,g.id)}}};t.set(g.id,v),g.parentId&&oa(v,t,n,{nodeOrigin:o}),a=!0,x&&(l.push({id:g.id,type:"dimensions",dimensions:p}),g.expandParent&&g.parentId&&f.push({id:g.id,parentId:g.parentId,rect:Or(v,o)}))}}if(f.length>0){const d=_0(f,t,n,o);l.push(...d)}return{changes:l,updatedInternals:a}}async function b0({delta:e,panZoom:t,transform:n,translateExtent:r,width:o,height:i}){if(!t||!e.x&&!e.y)return Promise.resolve(!1);const s=await t.setViewportConstrained({x:n[0]+e.x,y:n[1]+e.y,zoom:n[2]},[[0,0],[o,i]],r),a=!!s&&(s.x!==n[0]||s.y!==n[1]||s.k!==n[2]);return Promise.resolve(a)}function zu(e,t,n,r,o,i){let s=o;const a=r.get(s)||new Map;r.set(s,a.set(n,t)),s=`${o}-${e}`;const l=r.get(s)||new Map;if(r.set(s,l.set(n,t)),i){s=`${o}-${e}-${i}`;const u=r.get(s)||new Map;r.set(s,u.set(n,t))}}function Ru(e,t,n){e.clear(),t.clear();for(const r of n){const{source:o,target:i,sourceHandle:s=null,targetHandle:a=null}=r,l={edgeId:r.id,source:o,target:i,sourceHandle:s,targetHandle:a},u=`${o}-${s}--${i}-${a}`,c=`${i}-${a}--${o}-${s}`;zu("source",l,c,e,o,s),zu("target",l,u,e,i,a),t.set(r.id,r)}}function C0(e,t){if(e===null||t===null)return!1;const n=Array.isArray(e)?e:[e],r=Array.isArray(t)?t:[t];if(n.length!==r.length)return!1;for(let o=0;o<n.length;o++)if(n[o].id!==r[o].id||n[o].type!==r[o].type||!Object.is(n[o].data,r[o].data))return!1;return!0}function Bu(e,t){if(!e.parentId)return!1;const n=t.get(e.parentId);return n?n.selected?!0:Bu(n,t):!1}function Yu(e,t,n){var o;let r=e;do{if((o=r==null?void 0:r.matches)!=null&&o.call(r,t))return!0;if(r===n)return!1;r=r==null?void 0:r.parentElement}while(r);return!1}function k0(e,t,n,r){const o=new Map;for(const[i,s]of e)if((s.selected||s.id===r)&&(!s.parentId||!Bu(s,e))&&(s.draggable||t&&typeof s.draggable>"u")){const a=e.get(i);a&&o.set(i,{id:i,position:a.position||{x:0,y:0},distance:{x:n.x-a.internals.positionAbsolute.x,y:n.y-a.internals.positionAbsolute.y},extent:a.extent,parentId:a.parentId,origin:a.origin,expandParent:a.expandParent,internals:{positionAbsolute:a.internals.positionAbsolute||{x:0,y:0}},measured:{width:a.measured.width??0,height:a.measured.height??0}})}return o}function ia({nodeId:e,dragItems:t,nodeLookup:n,dragging:r=!0}){var s,a,l;const o=[];for(const[u,c]of t){const f=(s=n.get(u))==null?void 0:s.internals.userNode;f&&o.push({...f,position:c.position,dragging:r})}if(!e)return[o[0],o];const i=(a=n.get(e))==null?void 0:a.internals.userNode;return[i?{...i,position:((l=t.get(e))==null?void 0:l.position)||i.position,dragging:r}:o[0],o]}function $0({onNodeMouseDown:e,getStoreItems:t,onDragStart:n,onDrag:r,onDragStop:o}){let i={x:null,y:null},s=0,a=new Map,l=!1,u={x:0,y:0},c=null,f=!1,d=null,g=!1;function p({noDragClassName:C,handleSelector:$,domNode:m,isSelectable:_,nodeId:v,nodeClickDistance:b=0}){d=Ut(m);function N({x:V,y:A},O){const{nodeLookup:R,nodeExtent:S,snapGrid:M,snapToGrid:k,nodeOrigin:P,onNodeDrag:H,onSelectionDrag:I,onError:B,updateNodePositions:F}=t();i={x:V,y:A};let K=!1,se={x:0,y:0,x2:0,y2:0};if(a.size>1&&S){const ee=bo(a);se=Gs(ee)}for(const[ee,W]of a){if(!R.has(ee))continue;let fe={x:V-W.distance.x,y:A-W.distance.y};k&&(fe=Us(fe,M));let me=[[S[0][0],S[0][1]],[S[1][0],S[1][1]]];if(a.size>1&&S&&!W.extent){const{positionAbsolute:ze}=W.internals,G=ze.x-se.x+S[0][0],ae=ze.x+W.measured.width-se.x2+S[1][0],Me=ze.y-se.y+S[0][1],Le=ze.y+W.measured.height-se.y2+S[1][1];me=[[G,Me],[ae,Le]]}const{position:Ce,positionAbsolute:he}=Jv({nodeId:ee,nextPosition:fe,nodeLookup:R,nodeExtent:me,nodeOrigin:P,onError:B});K=K||W.position.x!==Ce.x||W.position.y!==Ce.y,W.position=Ce,W.internals.positionAbsolute=he}if(K&&(F(a,!0),O&&(r||H||!v&&I))){const[ee,W]=ia({nodeId:v,dragItems:a,nodeLookup:R});r==null||r(O,a,ee,W),H==null||H(O,ee,W),v||I==null||I(O,W)}}async function E(){if(!c)return;const{transform:V,panBy:A,autoPanSpeed:O,autoPanOnNodeDrag:R}=t();if(!R){l=!1,cancelAnimationFrame(s);return}const[S,M]=bu(u,c,O);(S!==0||M!==0)&&(i.x=(i.x??0)-S/V[2],i.y=(i.y??0)-M/V[2],await A({x:S,y:M})&&N(i,null)),s=requestAnimationFrame(E)}function T(V){var K;const{nodeLookup:A,multiSelectionActive:O,nodesDraggable:R,transform:S,snapGrid:M,snapToGrid:k,selectNodesOnDrag:P,onNodeDragStart:H,onSelectionDragStart:I,unselectNodesAndEdges:B}=t();f=!0,(!P||!_)&&!O&&v&&((K=A.get(v))!=null&&K.selected||B()),_&&P&&v&&(e==null||e(v));const F=Js(V.sourceEvent,{transform:S,snapGrid:M,snapToGrid:k,containerBounds:c});if(i=F,a=k0(A,R,F,v),a.size>0&&(n||H||!v&&I)){const[se,ee]=ia({nodeId:v,dragItems:a,nodeLookup:A});n==null||n(V.sourceEvent,a,se,ee),H==null||H(V.sourceEvent,se,ee),v||I==null||I(V.sourceEvent,ee)}}const D=rh().clickDistance(b).on("start",V=>{const{domNode:A,nodeDragThreshold:O,transform:R,snapGrid:S,snapToGrid:M}=t();c=(A==null?void 0:A.getBoundingClientRect())||null,g=!1,O===0&&T(V),i=Js(V.sourceEvent,{transform:R,snapGrid:S,snapToGrid:M,containerBounds:c}),u=Rn(V.sourceEvent,c)}).on("drag",V=>{const{autoPanOnNodeDrag:A,transform:O,snapGrid:R,snapToGrid:S,nodeDragThreshold:M,nodeLookup:k}=t(),P=Js(V.sourceEvent,{transform:O,snapGrid:R,snapToGrid:S,containerBounds:c});if((V.sourceEvent.type==="touchmove"&&V.sourceEvent.touches.length>1||v&&!k.has(v))&&(g=!0),!g){if(!l&&A&&f&&(l=!0,E()),!f){const H=P.xSnapped-(i.x??0),I=P.ySnapped-(i.y??0);Math.sqrt(H*H+I*I)>M&&T(V)}(i.x!==P.xSnapped||i.y!==P.ySnapped)&&a&&f&&(u=Rn(V.sourceEvent,c),N(P,V.sourceEvent))}}).on("end",V=>{if(!(!f||g)&&(l=!1,f=!1,cancelAnimationFrame(s),a.size>0)){const{nodeLookup:A,updateNodePositions:O,onNodeDragStop:R,onSelectionDragStop:S}=t();if(O(a,!1),o||R||!v&&S){const[M,k]=ia({nodeId:v,dragItems:a,nodeLookup:A,dragging:!1});o==null||o(V.sourceEvent,a,M,k),R==null||R(V.sourceEvent,M,k),v||S==null||S(V.sourceEvent,k)}}}).filter(V=>{const A=V.target;return!V.button&&(!C||!Yu(A,`.${C}`,m))&&(!$||Yu(A,$,m))});d.call(D)}function x(){d==null||d.on(".drag",null)}return{update:p,destroy:x}}function E0(e,t,n){const r=[],o={x:e.x-n,y:e.y-n,width:n*2,height:n*2};for(const i of t.values())Co(o,Or(i))>0&&r.push(i);return r}const S0=250;function P0(e,t,n,r){var a,l;let o=[],i=1/0;const s=E0(e,n,t+S0);for(const u of s){const c=[...((a=u.internals.handleBounds)==null?void 0:a.source)??[],...((l=u.internals.handleBounds)==null?void 0:l.target)??[]];for(const f of c){if(r.nodeId===f.nodeId&&r.type===f.type&&r.id===f.id)continue;const{x:d,y:g}=$o(u,f,f.position,!0),p=Math.sqrt(Math.pow(d-e.x,2)+Math.pow(g-e.y,2));p>t||(p<i?(o=[{...f,x:d,y:g}],i=p):p===i&&o.push({...f,x:d,y:g}))}}if(!o.length)return null;if(o.length>1){const u=r.type==="source"?"target":"source";return o.find(c=>c.type===u)??o[0]}return o[0]}function Zu(e,t,n,r,o,i=!1){var u,c,f;const s=r.get(e);if(!s)return null;const a=o==="strict"?(u=s.internals.handleBounds)==null?void 0:u[t]:[...((c=s.internals.handleBounds)==null?void 0:c.source)??[],...((f=s.internals.handleBounds)==null?void 0:f.target)??[]],l=(n?a==null?void 0:a.find(d=>d.id===n):a==null?void 0:a[0])??null;return l&&i?{...l,...$o(s,l,l.position,!0)}:l}function Xu(e,t){return e||(t!=null&&t.classList.contains("target")?"target":t!=null&&t.classList.contains("source")?"source":null)}function N0(e,t){let n=null;return t?n=!0:e&&!t&&(n=!1),n}const Fu=()=>!0;function T0(e,{connectionMode:t,connectionRadius:n,handleId:r,nodeId:o,edgeUpdaterType:i,isTarget:s,domNode:a,nodeLookup:l,lib:u,autoPanOnConnect:c,flowId:f,panBy:d,cancelConnection:g,onConnectStart:p,onConnect:x,onConnectEnd:C,isValidConnection:$=Fu,onReconnectEnd:m,updateConnection:_,getTransform:v,getFromHandle:b,autoPanSpeed:N}){const E=t0(e.target);let T=0,D;const{x:V,y:A}=Rn(e),O=E==null?void 0:E.elementFromPoint(V,A),R=Xu(i,O),S=a==null?void 0:a.getBoundingClientRect();if(!S||!R)return;const M=Zu(o,R,r,l,t);if(!M)return;let k=Rn(e,S),P=!1,H=null,I=!1,B=null;function F(){if(!c||!S)return;const[he,ze]=bu(k,S,N);d({x:he,y:ze}),T=requestAnimationFrame(F)}const K={...M,nodeId:o,type:R,position:M.position},se=l.get(o),W={inProgress:!0,isValid:null,from:$o(se,K,$e.Left,!0),fromHandle:K,fromPosition:K.position,fromNode:se,to:k,toHandle:null,toPosition:hu[K.position],toNode:null};_(W);let fe=W;p==null||p(e,{nodeId:o,handleId:r,handleType:R});function me(he){if(!b()||!K){Ce(he);return}const ze=v();k=Rn(he,S),D=P0(ko(k,ze,!1,[1,1]),n,l,K),P||(F(),P=!0);const G=Wu(he,{handle:D,connectionMode:t,fromNodeId:o,fromHandleId:r,fromType:s?"target":"source",isValidConnection:$,doc:E,lib:u,flowId:f,nodeLookup:l});B=G.handleDomNode,H=G.connection,I=N0(!!D,G.isValid);const ae={...fe,isValid:I,to:D&&I?$u({x:D.x,y:D.y},ze):k,toHandle:G.toHandle,toPosition:I&&G.toHandle?G.toHandle.position:hu[K.position],toNode:G.toHandle?l.get(G.toHandle.nodeId):null};I&&D&&fe.toHandle&&ae.toHandle&&fe.toHandle.type===ae.toHandle.type&&fe.toHandle.nodeId===ae.toHandle.nodeId&&fe.toHandle.id===ae.toHandle.id&&fe.to.x===ae.to.x&&fe.to.y===ae.to.y||(_(ae),fe=ae)}function Ce(he){(D||B)&&H&&I&&(x==null||x(H));const{inProgress:ze,...G}=fe,ae={...G,toPosition:fe.toHandle?fe.toPosition:null};C==null||C(he,ae),i&&(m==null||m(he,ae)),g(),cancelAnimationFrame(T),P=!1,I=!1,H=null,B=null,E.removeEventListener("mousemove",me),E.removeEventListener("mouseup",Ce),E.removeEventListener("touchmove",me),E.removeEventListener("touchend",Ce)}E.addEventListener("mousemove",me),E.addEventListener("mouseup",Ce),E.addEventListener("touchmove",me),E.addEventListener("touchend",Ce)}function Wu(e,{handle:t,connectionMode:n,fromNodeId:r,fromHandleId:o,fromType:i,doc:s,lib:a,flowId:l,isValidConnection:u=Fu,nodeLookup:c}){const f=i==="target",d=t?s.querySelector(`.${a}-flow__handle[data-id="${l}-${t==null?void 0:t.nodeId}-${t==null?void 0:t.id}-${t==null?void 0:t.type}"]`):null,{x:g,y:p}=Rn(e),x=s.elementFromPoint(g,p),C=x!=null&&x.classList.contains(`${a}-flow__handle`)?x:d,$={handleDomNode:C,isValid:!1,connection:null,toHandle:null};if(C){const m=Xu(void 0,C),_=C.getAttribute("data-nodeid"),v=C.getAttribute("data-handleid"),b=C.classList.contains("connectable"),N=C.classList.contains("connectableend");if(!_||!m)return $;const E={source:f?_:r,sourceHandle:f?v:o,target:f?r:_,targetHandle:f?o:v};$.connection=E;const D=b&&N&&(n===hr.Strict?f&&m==="source"||!f&&m==="target":_!==r||v!==o);$.isValid=D&&u(E),$.toHandle=Zu(_,m,v,c,n,!1)}return $}const M0={onPointerDown:T0,isValid:Wu};function H0({domNode:e,panZoom:t,getTransform:n,getViewScale:r}){const o=Ut(e);function i({translateExtent:a,width:l,height:u,zoomStep:c=10,pannable:f=!0,zoomable:d=!0,inversePan:g=!1}){const p=_=>{const v=n();if(_.sourceEvent.type!=="wheel"||!t)return;const b=-_.sourceEvent.deltaY*(_.sourceEvent.deltaMode===1?.05:_.sourceEvent.deltaMode?1:.002)*c,N=v[2]*Math.pow(2,b);t.scaleTo(N)};let x=[0,0];const C=_=>{(_.sourceEvent.type==="mousedown"||_.sourceEvent.type==="touchstart")&&(x=[_.sourceEvent.clientX??_.sourceEvent.touches[0].clientX,_.sourceEvent.clientY??_.sourceEvent.touches[0].clientY])},$=_=>{const v=n();if(_.sourceEvent.type!=="mousemove"&&_.sourceEvent.type!=="touchmove"||!t)return;const b=[_.sourceEvent.clientX??_.sourceEvent.touches[0].clientX,_.sourceEvent.clientY??_.sourceEvent.touches[0].clientY],N=[b[0]-x[0],b[1]-x[1]];x=b;const E=r()*Math.max(v[2],Math.log(v[2]))*(g?-1:1),T={x:v[0]-N[0]*E,y:v[1]-N[1]*E},D=[[0,0],[l,u]];t.setViewportConstrained({x:T.x,y:T.y,zoom:v[2]},D,a)},m=gu().on("start",C).on("zoom",f?$:null).on("zoom.wheel",d?p:null);o.call(m,{})}function s(){o.on("zoom",null)}return{update:i,destroy:s,pointer:on}}const V0=(e,t)=>e.x!==t.x||e.y!==t.y||e.zoom!==t.k,Oi=e=>({x:e.x,y:e.y,zoom:e.k}),sa=({x:e,y:t,zoom:n})=>Pi.translate(e,t).scale(n),zr=(e,t)=>e.target.closest(`.${t}`),Ku=(e,t)=>t===2&&Array.isArray(e)&&e.includes(2),aa=(e,t=0,n=()=>{})=>{const r=typeof t=="number"&&t>0;return r||n(),r?e.transition().duration(t).on("end",n):e},qu=e=>{const t=e.ctrlKey&&Di()?10:1;return-e.deltaY*(e.deltaMode===1?.05:e.deltaMode?1:.002)*t};function D0({zoomPanValues:e,noWheelClassName:t,d3Selection:n,d3Zoom:r,panOnScrollMode:o,panOnScrollSpeed:i,zoomOnPinch:s,onPanZoomStart:a,onPanZoom:l,onPanZoomEnd:u}){return c=>{if(zr(c,t))return!1;c.preventDefault(),c.stopImmediatePropagation();const f=n.property("__zoom").k||1;if(c.ctrlKey&&s){const C=on(c),$=qu(c),m=f*Math.pow(2,$);r.scaleTo(n,m,C,c);return}const d=c.deltaMode===1?20:1;let g=o===Jn.Vertical?0:c.deltaX*d,p=o===Jn.Horizontal?0:c.deltaY*d;!Di()&&c.shiftKey&&o!==Jn.Vertical&&(g=c.deltaY*d,p=0),r.translateBy(n,-(g/f)*i,-(p/f)*i,{internal:!0});const x=Oi(n.property("__zoom"));clearTimeout(e.panScrollTimeout),e.isPanScrolling||(e.isPanScrolling=!0,a==null||a(c,x)),e.isPanScrolling&&(l==null||l(c,x),e.panScrollTimeout=setTimeout(()=>{u==null||u(c,x),e.isPanScrolling=!1},150))}}function A0({noWheelClassName:e,preventScrolling:t,d3ZoomHandler:n}){return function(r,o){if(!t&&r.type==="wheel"&&!r.ctrlKey||zr(r,e))return null;r.preventDefault(),n.call(this,r,o)}}function L0({zoomPanValues:e,onDraggingChange:t,onPanZoomStart:n}){return r=>{var i,s,a;if((i=r.sourceEvent)!=null&&i.internal)return;const o=Oi(r.transform);e.mouseButton=((s=r.sourceEvent)==null?void 0:s.button)||0,e.isZoomingOrPanning=!0,e.prevViewport=o,((a=r.sourceEvent)==null?void 0:a.type)==="mousedown"&&t(!0),n&&(n==null||n(r.sourceEvent,o))}}function O0({zoomPanValues:e,panOnDrag:t,onPaneContextMenu:n,onTransformChange:r,onPanZoom:o}){return i=>{var s,a;e.usedRightMouseButton=!!(n&&Ku(t,e.mouseButton??0)),(s=i.sourceEvent)!=null&&s.sync||r([i.transform.x,i.transform.y,i.transform.k]),o&&!((a=i.sourceEvent)!=null&&a.internal)&&(o==null||o(i.sourceEvent,Oi(i.transform)))}}function I0({zoomPanValues:e,panOnDrag:t,panOnScroll:n,onDraggingChange:r,onPanZoomEnd:o,onPaneContextMenu:i}){return s=>{var a;if(!((a=s.sourceEvent)!=null&&a.internal)&&(e.isZoomingOrPanning=!1,i&&Ku(t,e.mouseButton??0)&&!e.usedRightMouseButton&&s.sourceEvent&&i(s.sourceEvent),e.usedRightMouseButton=!1,r(!1),o&&V0(e.prevViewport,s.transform))){const l=Oi(s.transform);e.prevViewport=l,clearTimeout(e.timerId),e.timerId=setTimeout(()=>{o==null||o(s.sourceEvent,l)},n?150:0)}}}function z0({zoomActivationKeyPressed:e,zoomOnScroll:t,zoomOnPinch:n,panOnDrag:r,panOnScroll:o,zoomOnDoubleClick:i,userSelectionActive:s,noWheelClassName:a,noPanClassName:l,lib:u}){return c=>{var p;const f=e||t,d=n&&c.ctrlKey;if(c.button===1&&c.type==="mousedown"&&(zr(c,`${u}-flow__node`)||zr(c,`${u}-flow__edge`)))return!0;if(!r&&!f&&!o&&!i&&!n||s||zr(c,a)&&c.type==="wheel"||zr(c,l)&&(c.type!=="wheel"||o&&c.type==="wheel"&&!e)||!n&&c.ctrlKey&&c.type==="wheel")return!1;if(!n&&c.type==="touchstart"&&((p=c.touches)==null?void 0:p.length)>1)return c.preventDefault(),!1;if(!f&&!o&&!d&&c.type==="wheel"||!r&&(c.type==="mousedown"||c.type==="touchstart")||Array.isArray(r)&&!r.includes(c.button)&&c.type==="mousedown")return!1;const g=Array.isArray(r)&&r.includes(c.button)||!c.button||c.button<=1;return(!c.ctrlKey||c.type==="wheel")&&g}}function R0({domNode:e,minZoom:t,maxZoom:n,paneClickDistance:r,translateExtent:o,viewport:i,onPanZoom:s,onPanZoomStart:a,onPanZoomEnd:l,onDraggingChange:u}){const c={isZoomingOrPanning:!1,usedRightMouseButton:!1,prevViewport:{x:0,y:0,zoom:0},mouseButton:0,timerId:void 0,panScrollTimeout:void 0,isPanScrolling:!1},f=e.getBoundingClientRect(),d=gu().clickDistance(!zn(r)||r<0?0:r).scaleExtent([t,n]).translateExtent(o),g=Ut(e).call(d);_({x:i.x,y:i.y,zoom:Lr(i.zoom,t,n)},[[0,0],[f.width,f.height]],o);const p=g.on("wheel.zoom"),x=g.on("dblclick.zoom");d.wheelDelta(qu);function C(O,R){return g?new Promise(S=>{d==null||d.transform(aa(g,R==null?void 0:R.duration,()=>S(!0)),O)}):Promise.resolve(!1)}function $({noWheelClassName:O,noPanClassName:R,onPaneContextMenu:S,userSelectionActive:M,panOnScroll:k,panOnDrag:P,panOnScrollMode:H,panOnScrollSpeed:I,preventScrolling:B,zoomOnPinch:F,zoomOnScroll:K,zoomOnDoubleClick:se,zoomActivationKeyPressed:ee,lib:W,onTransformChange:fe}){M&&!c.isZoomingOrPanning&&m();const Ce=k&&!ee&&!M?D0({zoomPanValues:c,noWheelClassName:O,d3Selection:g,d3Zoom:d,panOnScrollMode:H,panOnScrollSpeed:I,zoomOnPinch:F,onPanZoomStart:a,onPanZoom:s,onPanZoomEnd:l}):A0({noWheelClassName:O,preventScrolling:B,d3ZoomHandler:p});if(g.on("wheel.zoom",Ce,{passive:!1}),!M){const ze=L0({zoomPanValues:c,onDraggingChange:u,onPanZoomStart:a});d.on("start",ze);const G=O0({zoomPanValues:c,panOnDrag:P,onPaneContextMenu:!!S,onPanZoom:s,onTransformChange:fe});d.on("zoom",G);const ae=I0({zoomPanValues:c,panOnDrag:P,panOnScroll:k,onPaneContextMenu:S,onPanZoomEnd:l,onDraggingChange:u});d.on("end",ae)}const he=z0({zoomActivationKeyPressed:ee,panOnDrag:P,zoomOnScroll:K,panOnScroll:k,zoomOnDoubleClick:se,zoomOnPinch:F,userSelectionActive:M,noPanClassName:R,noWheelClassName:O,lib:W});d.filter(he),se?g.on("dblclick.zoom",x):g.on("dblclick.zoom",null)}function m(){d.on("zoom",null)}async function _(O,R,S){const M=sa(O),k=d==null?void 0:d.constrain()(M,R,S);return k&&await C(k),new Promise(P=>P(k))}async function v(O,R){const S=sa(O);return await C(S,R),new Promise(M=>M(S))}function b(O){if(g){const R=sa(O),S=g.property("__zoom");(S.k!==O.zoom||S.x!==O.x||S.y!==O.y)&&(d==null||d.transform(g,R,null,{sync:!0}))}}function N(){const O=g?du(g.node()):{x:0,y:0,k:1};return{x:O.x,y:O.y,zoom:O.k}}function E(O,R){return g?new Promise(S=>{d==null||d.scaleTo(aa(g,R==null?void 0:R.duration,()=>S(!0)),O)}):Promise.resolve(!1)}function T(O,R){return g?new Promise(S=>{d==null||d.scaleBy(aa(g,R==null?void 0:R.duration,()=>S(!0)),O)}):Promise.resolve(!1)}function D(O){d==null||d.scaleExtent(O)}function V(O){d==null||d.translateExtent(O)}function A(O){const R=!zn(O)||O<0?0:O;d==null||d.clickDistance(R)}return{update:$,destroy:m,setViewport:v,setViewportConstrained:_,getViewport:N,scaleTo:E,scaleBy:T,setScaleExtent:D,setTranslateExtent:V,syncViewport:b,setClickDistance:A}}var Gu;(function(e){e.Line="line",e.Handle="handle"})(Gu||(Gu={}));var B0=ne('<div role="button" tabindex="-1"><!></div>');function er(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(se,"$connectable",n),i=()=>Q(Ce,"$connectionRadius",n),s=()=>Q(fe,"$domNode",n),a=()=>Q(me,"$nodeLookup",n),l=()=>Q(W,"$connectionMode",n),u=()=>Q(G,"$lib",n),c=()=>Q(Fe,"$autoPanOnConnect",n),f=()=>Q(Ie,"$flowId",n),d=()=>Q(ze,"$isValidConnectionStore",n),g=()=>Q(Me,"$onedgecreate",n),p=()=>Q(oe,"$onConnectAction",n),x=()=>Q(pe,"$onConnectStartAction",n),C=()=>Q(be,"$onConnectEndAction",n),$=()=>Q(he,"$viewport",n),m=()=>Q(ht,"$connection",n),_=()=>Q(Oe,"$edges",n),v=()=>Q(rt,"$connectionLookup",n),b=re(),N=re(),E=re(),T=re(),D=re(),V=re(),A=re(),O=re();let R=w(t,"id",12,void 0),S=w(t,"type",12,"source"),M=w(t,"position",28,()=>$e.Top),k=w(t,"style",12,void 0),P=w(t,"isValidConnection",12,void 0),H=w(t,"onconnect",12,void 0),I=w(t,"ondisconnect",12,void 0),B=w(t,"isConnectable",12,void 0),F=w(t,"class",12,void 0);const K=ur("svelteflow__node_id"),se=ur("svelteflow__node_connectable"),ee=Ue(),{connectionMode:W,domNode:fe,nodeLookup:me,connectionRadius:Ce,viewport:he,isValidConnection:ze,lib:G,addEdge:ae,onedgecreate:Me,panBy:Le,cancelConnection:Xe,updateConnection:te,autoPanOnConnect:Fe,edges:Oe,connectionLookup:rt,onconnect:oe,onconnectstart:pe,onconnectend:be,flowId:Ie,connection:ht}=ee;function dt(Te){const st=Su(Te);(st&&Te.button===0||!st)&&M0.onPointerDown(Te,{handleId:h(E),nodeId:K,isTarget:h(b),connectionRadius:i(),domNode:s(),nodeLookup:a(),connectionMode:l(),lib:u(),autoPanOnConnect:c(),flowId:f(),isValidConnection:P()??d(),updateConnection:te,cancelConnection:Xe,panBy:Le,onConnect:ye=>{var ct;const lt=g()?g()(ye):ye;lt&&(ae(lt),(ct=p())==null||ct(ye))},onConnectStart:(ye,lt)=>{var ct;(ct=x())==null||ct(ye,{nodeId:lt.nodeId,handleId:lt.handleId,handleType:lt.handleType})},onConnectEnd:(ye,lt)=>{var ct;(ct=C())==null||ct(ye,lt)},getTransform:()=>[$().x,$().y,$().zoom],getFromHandle:()=>m().fromHandle})}let J=re(null),Re=re();ge(()=>j(S()),()=>{U(b,S()==="target")}),ge(()=>(j(B()),o()),()=>{U(N,B()!==void 0?B():o())}),ge(()=>j(R()),()=>{U(E,R()||null)}),ge(()=>(j(H()),j(I()),_(),v(),j(S()),j(R())),()=>{(H()||I())&&(_(),U(Re,v().get(`${K}-${S()}${R()?`-${R()}`:""}`)))}),ge(()=>(h(J),h(Re),j(I()),j(H())),()=>{if(h(J)&&!Kv(h(Re),h(J))){const Te=h(Re)??new Map;vu(h(J),Te,I()),vu(Te,h(J),H())}U(J,h(Re)??new Map)}),ge(()=>m(),()=>{U(T,!!m().fromHandle)}),ge(()=>(m(),j(S()),h(E)),()=>{var Te,st,ye;U(D,((Te=m().fromHandle)==null?void 0:Te.nodeId)===K&&((st=m().fromHandle)==null?void 0:st.type)===S()&&((ye=m().fromHandle)==null?void 0:ye.id)===h(E))}),ge(()=>(m(),j(S()),h(E)),()=>{var Te,st,ye;U(V,((Te=m().toHandle)==null?void 0:Te.nodeId)===K&&((st=m().toHandle)==null?void 0:st.type)===S()&&((ye=m().toHandle)==null?void 0:ye.id)===h(E))}),ge(()=>(l(),m(),j(S()),h(E)),()=>{var Te,st,ye;U(A,l()===hr.Strict?((Te=m().fromHandle)==null?void 0:Te.type)!==S():K!==((st=m().fromHandle)==null?void 0:st.nodeId)||h(E)!==((ye=m().fromHandle)==null?void 0:ye.id))}),ge(()=>(h(V),m()),()=>{U(O,h(V)&&m().isValid)}),vt(),He();var le=B0();de(le,"data-nodeid",K);let $n;var fn=X(le);wt(fn,t,"default",{}),Z(le),Ee(Te=>{de(le,"data-handleid",h(E)),de(le,"data-handlepos",M()),de(le,"data-id",`${f()??""}-${K??""}-${R()||""}-${S()??""}`),$n=$t(le,1,wn(Te),null,$n,{valid:h(O),connectingto:h(V),connectingfrom:h(D),source:!h(b),target:h(b),connectablestart:h(N),connectableend:h(N),connectable:h(N),connectionindicator:h(N)&&(!h(T)||h(A))}),de(le,"style",k())},[()=>Et(["svelte-flow__handle",`svelte-flow__handle-${M()}`,"nodrag","nopan",M(),F()])],ve),Ze("mousedown",le,dt),Ze("touchstart",le,dt),L(e,le);var En=ce({get id(){return R()},set id(Te){R(Te),y()},get type(){return S()},set type(Te){S(Te),y()},get position(){return M()},set position(Te){M(Te),y()},get style(){return k()},set style(Te){k(Te),y()},get isValidConnection(){return P()},set isValidConnection(Te){P(Te),y()},get onconnect(){return H()},set onconnect(Te){H(Te),y()},get ondisconnect(){return I()},set ondisconnect(Te){I(Te),y()},get isConnectable(){return B()},set isConnectable(Te){B(Te),y()},get class(){return F()},set class(Te){F(Te),y()}});return r(),En}ie(er,{id:{},type:{},position:{},style:{},isValidConnection:{},onconnect:{},ondisconnect:{},isConnectable:{},class:{}},["default"],[],!0);var Y0=ne("<!> <!>",1);function Ii(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["data","targetPosition","sourcePosition"]),ue(t,!1);let r=w(t,"data",28,()=>({label:"Node"})),o=w(t,"targetPosition",12,void 0),i=w(t,"sourcePosition",12,void 0);He();var s=Y0(),a=xe(s);const l=ve(()=>o()??$e.Top);er(a,{type:"target",get position(){return h(l)}});var u=z(a),c=z(u);const f=ve(()=>i()??$e.Bottom);return er(c,{type:"source",get position(){return h(f)}}),Ee(()=>{var d;return Bt(u,` ${((d=r())==null?void 0:d.label)??""} `)}),L(e,s),ce({get data(){return r()},set data(d){r(d),y()},get targetPosition(){return o()},set targetPosition(d){o(d),y()},get sourcePosition(){return i()},set sourcePosition(d){i(d),y()}})}ie(Ii,{data:{},targetPosition:{},sourcePosition:{}},[],[],!0);var Z0=ne(" <!>",1);function Uu(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["data","sourcePosition"]),ue(t,!1);let r=w(t,"data",28,()=>({label:"Node"})),o=w(t,"sourcePosition",12,void 0);He(),Pe();var i=Z0(),s=xe(i),a=z(s);const l=ve(()=>o()??$e.Bottom);return er(a,{type:"source",get position(){return h(l)}}),Ee(()=>{var u;return Bt(s,`${((u=r())==null?void 0:u.label)??""} `)}),L(e,i),ce({get data(){return r()},set data(u){r(u),y()},get sourcePosition(){return o()},set sourcePosition(u){o(u),y()}})}ie(Uu,{data:{},sourcePosition:{}},[],[],!0);var X0=ne(" <!>",1);function ju(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["data","targetPosition"]),ue(t,!1);let r=w(t,"data",28,()=>({label:"Node"})),o=w(t,"targetPosition",12,void 0);He(),Pe();var i=X0(),s=xe(i),a=z(s);const l=ve(()=>o()??$e.Top);return er(a,{type:"target",get position(){return h(l)}}),Ee(()=>{var u;return Bt(s,`${((u=r())==null?void 0:u.label)??""} `)}),L(e,i),ce({get data(){return r()},set data(u){r(u),y()},get targetPosition(){return o()},set targetPosition(u){o(u),y()}})}ie(ju,{data:{},targetPosition:{}},[],[],!0);function Ju(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,[])}ie(Ju,{},[],[],!0);function Qu(e,t,n){if(!t)return;const r=n?t.querySelector(n):t;r&&r.appendChild(e)}function Rr(e,{target:t,domNode:n}){return Qu(e,n,t),{async update({target:r,domNode:o}){Qu(e,o,r)},destroy(){e.parentNode&&e.parentNode.removeChild(e)}}}var F0=ne("<div><!></div>");function ec(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(i,"$domNode",n),{domNode:i}=Ue();He();var s=F0(),a=X(s);wt(a,t,"default",{}),Z(s),_t(s,(l,u)=>Rr==null?void 0:Rr(l,u),()=>({target:".svelte-flow__edgelabel-renderer",domNode:o()})),L(e,s),ce(),r()}ie(ec,{},["default"],[],!0);function tc(){const{edgeLookup:e,selectionRect:t,selectionRectMode:n,multiselectionKeyPressed:r,addSelectedEdges:o,unselectNodesAndEdges:i,elementsSelectable:s}=Ue();return a=>{const l=q(e).get(a);if(!l){console.warn("012",Dr.error012(a));return}(l.selectable||q(s)&&typeof l.selectable>"u")&&(t.set(null),n.set(null),l.selected?l.selected&&q(r)&&i({nodes:[],edges:[l]}):o([a]))}}var W0=ne('<div class="svelte-flow__edge-label" role="button" tabindex="-1"><!></div>');function nc(e,t){ue(t,!1);let n=w(t,"style",12,void 0),r=w(t,"x",12,void 0),o=w(t,"y",12,void 0);const i=tc(),s=ur("svelteflow__edge_id");return He(),ec(e,{children:(a,l)=>{var u=W0(),c=X(u);wt(c,t,"default",{}),Z(u),Ee(()=>{de(u,"style","pointer-events: all;"+n()),at(u,"transform",`translate(-50%, -50%) translate(${r()??""}px,${o()??""}px)`)}),Ze("keyup",u,()=>{}),Ze("click",u,()=>{s&&i(s)}),L(a,u)},$$slots:{default:!0}}),ce({get style(){return n()},set style(a){n(a),y()},get x(){return r()},set x(a){r(a),y()},get y(){return o()},set y(a){o(a),y()}})}ie(nc,{style:{},x:{},y:{}},["default"],[],!0);var K0=_e('<path fill="none" class="svelte-flow__edge-interaction"></path>'),q0=_e('<path fill="none"></path><!><!>',1);function Eo(e,t){ue(t,!1);let n=w(t,"id",12,void 0),r=w(t,"path",12),o=w(t,"label",12,void 0),i=w(t,"labelX",12,void 0),s=w(t,"labelY",12,void 0),a=w(t,"labelStyle",12,void 0),l=w(t,"markerStart",12,void 0),u=w(t,"markerEnd",12,void 0),c=w(t,"style",12,void 0),f=w(t,"interactionWidth",12,20),d=w(t,"class",12,void 0),g=f()===void 0?20:f();He();var p=q0(),x=xe(p),C=z(x);{var $=v=>{var b=K0();de(b,"stroke-opacity",0),de(b,"stroke-width",g),Ee(()=>de(b,"d",r())),L(v,b)};ke(C,v=>{g&&v($)})}var m=z(C);{var _=v=>{nc(v,{get x(){return i()},get y(){return s()},get style(){return a()},children:(b,N)=>{Pe();var E=Ae();Ee(()=>Bt(E,o())),L(b,E)},$$slots:{default:!0}})};ke(m,v=>{o()&&v(_)})}return Ee(v=>{de(x,"id",n()),de(x,"d",r()),$t(x,0,wn(v)),de(x,"marker-start",l()),de(x,"marker-end",u()),de(x,"style",c())},[()=>Et(["svelte-flow__edge-path",d()])],ve),L(e,p),ce({get id(){return n()},set id(v){n(v),y()},get path(){return r()},set path(v){r(v),y()},get label(){return o()},set label(v){o(v),y()},get labelX(){return i()},set labelX(v){i(v),y()},get labelY(){return s()},set labelY(v){s(v),y()},get labelStyle(){return a()},set labelStyle(v){a(v),y()},get markerStart(){return l()},set markerStart(v){l(v),y()},get markerEnd(){return u()},set markerEnd(v){u(v),y()},get style(){return c()},set style(v){c(v),y()},get interactionWidth(){return f()},set interactionWidth(v){f(v),y()},get class(){return d()},set class(v){d(v),y()}})}ie(Eo,{id:{},path:{},label:{},labelX:{},labelY:{},labelStyle:{},markerStart:{},markerEnd:{},style:{},interactionWidth:{},class:{}},[],[],!0);function zi(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["label","labelStyle","style","markerStart","markerEnd","interactionWidth","sourceX","sourceY","sourcePosition","targetX","targetY","targetPosition"]),ue(t,!1);const r=re(),o=re(),i=re();let s=w(t,"label",12,void 0),a=w(t,"labelStyle",12,void 0),l=w(t,"style",12,void 0),u=w(t,"markerStart",12,void 0),c=w(t,"markerEnd",12,void 0),f=w(t,"interactionWidth",12,void 0),d=w(t,"sourceX",12),g=w(t,"sourceY",12),p=w(t,"sourcePosition",12),x=w(t,"targetX",12),C=w(t,"targetY",12),$=w(t,"targetPosition",12);return ge(()=>(h(r),h(o),h(i),j(d()),j(g()),j(x()),j(C()),j(p()),j($())),()=>{(m=>(U(r,m[0]),U(o,m[1]),U(i,m[2])))(Tu({sourceX:d(),sourceY:g(),targetX:x(),targetY:C(),sourcePosition:p(),targetPosition:$()}))}),vt(),He(),Eo(e,{get path(){return h(r)},get labelX(){return h(o)},get labelY(){return h(i)},get label(){return s()},get labelStyle(){return a()},get markerStart(){return u()},get markerEnd(){return c()},get interactionWidth(){return f()},get style(){return l()}}),ce({get label(){return s()},set label(m){s(m),y()},get labelStyle(){return a()},set labelStyle(m){a(m),y()},get style(){return l()},set style(m){l(m),y()},get markerStart(){return u()},set markerStart(m){u(m),y()},get markerEnd(){return c()},set markerEnd(m){c(m),y()},get interactionWidth(){return f()},set interactionWidth(m){f(m),y()},get sourceX(){return d()},set sourceX(m){d(m),y()},get sourceY(){return g()},set sourceY(m){g(m),y()},get sourcePosition(){return p()},set sourcePosition(m){p(m),y()},get targetX(){return x()},set targetX(m){x(m),y()},get targetY(){return C()},set targetY(m){C(m),y()},get targetPosition(){return $()},set targetPosition(m){$(m),y()}})}ie(zi,{label:{},labelStyle:{},style:{},markerStart:{},markerEnd:{},interactionWidth:{},sourceX:{},sourceY:{},sourcePosition:{},targetX:{},targetY:{},targetPosition:{}},[],[],!0);function rc(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["label","labelStyle","style","markerStart","markerEnd","interactionWidth","sourceX","sourceY","sourcePosition","targetX","targetY","targetPosition"]),ue(t,!1);const r=re(),o=re(),i=re();let s=w(t,"label",12,void 0),a=w(t,"labelStyle",12,void 0),l=w(t,"style",12,void 0),u=w(t,"markerStart",12,void 0),c=w(t,"markerEnd",12,void 0),f=w(t,"interactionWidth",12,void 0),d=w(t,"sourceX",12),g=w(t,"sourceY",12),p=w(t,"sourcePosition",12),x=w(t,"targetX",12),C=w(t,"targetY",12),$=w(t,"targetPosition",12);return ge(()=>(h(r),h(o),h(i),j(d()),j(g()),j(x()),j(C()),j(p()),j($())),()=>{(m=>(U(r,m[0]),U(o,m[1]),U(i,m[2])))(Li({sourceX:d(),sourceY:g(),targetX:x(),targetY:C(),sourcePosition:p(),targetPosition:$()}))}),vt(),He(),Eo(e,{get path(){return h(r)},get labelX(){return h(o)},get labelY(){return h(i)},get label(){return s()},get labelStyle(){return a()},get markerStart(){return u()},get markerEnd(){return c()},get interactionWidth(){return f()},get style(){return l()}}),ce({get label(){return s()},set label(m){s(m),y()},get labelStyle(){return a()},set labelStyle(m){a(m),y()},get style(){return l()},set style(m){l(m),y()},get markerStart(){return u()},set markerStart(m){u(m),y()},get markerEnd(){return c()},set markerEnd(m){c(m),y()},get interactionWidth(){return f()},set interactionWidth(m){f(m),y()},get sourceX(){return d()},set sourceX(m){d(m),y()},get sourceY(){return g()},set sourceY(m){g(m),y()},get sourcePosition(){return p()},set sourcePosition(m){p(m),y()},get targetX(){return x()},set targetX(m){x(m),y()},get targetY(){return C()},set targetY(m){C(m),y()},get targetPosition(){return $()},set targetPosition(m){$(m),y()}})}ie(rc,{label:{},labelStyle:{},style:{},markerStart:{},markerEnd:{},interactionWidth:{},sourceX:{},sourceY:{},sourcePosition:{},targetX:{},targetY:{},targetPosition:{}},[],[],!0);function oc(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["label","labelStyle","style","markerStart","markerEnd","interactionWidth","sourceX","sourceY","targetX","targetY"]),ue(t,!1);const r=re(),o=re(),i=re();let s=w(t,"label",12,void 0),a=w(t,"labelStyle",12,void 0),l=w(t,"style",12,void 0),u=w(t,"markerStart",12,void 0),c=w(t,"markerEnd",12,void 0),f=w(t,"interactionWidth",12,void 0),d=w(t,"sourceX",12),g=w(t,"sourceY",12),p=w(t,"targetX",12),x=w(t,"targetY",12);return ge(()=>(h(r),h(o),h(i),j(d()),j(g()),j(p()),j(x())),()=>{(C=>(U(r,C[0]),U(o,C[1]),U(i,C[2])))(ea({sourceX:d(),sourceY:g(),targetX:p(),targetY:x()}))}),vt(),He(),Eo(e,{get path(){return h(r)},get labelX(){return h(o)},get labelY(){return h(i)},get label(){return s()},get labelStyle(){return a()},get markerStart(){return u()},get markerEnd(){return c()},get interactionWidth(){return f()},get style(){return l()}}),ce({get label(){return s()},set label(C){s(C),y()},get labelStyle(){return a()},set labelStyle(C){a(C),y()},get style(){return l()},set style(C){l(C),y()},get markerStart(){return u()},set markerStart(C){u(C),y()},get markerEnd(){return c()},set markerEnd(C){c(C),y()},get interactionWidth(){return f()},set interactionWidth(C){f(C),y()},get sourceX(){return d()},set sourceX(C){d(C),y()},get sourceY(){return g()},set sourceY(C){g(C),y()},get targetX(){return p()},set targetX(C){p(C),y()},get targetY(){return x()},set targetY(C){x(C),y()}})}ie(oc,{label:{},labelStyle:{},style:{},markerStart:{},markerEnd:{},interactionWidth:{},sourceX:{},sourceY:{},targetX:{},targetY:{}},[],[],!0);function ic(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]);it(n,["label","labelStyle","style","markerStart","markerEnd","interactionWidth","sourceX","sourceY","sourcePosition","targetX","targetY","targetPosition"]),ue(t,!1);const r=re(),o=re(),i=re();let s=w(t,"label",12,void 0),a=w(t,"labelStyle",12,void 0),l=w(t,"style",12,void 0),u=w(t,"markerStart",12,void 0),c=w(t,"markerEnd",12,void 0),f=w(t,"interactionWidth",12,void 0),d=w(t,"sourceX",12),g=w(t,"sourceY",12),p=w(t,"sourcePosition",12),x=w(t,"targetX",12),C=w(t,"targetY",12),$=w(t,"targetPosition",12);return ge(()=>(h(r),h(o),h(i),j(d()),j(g()),j(x()),j(C()),j(p()),j($())),()=>{(m=>(U(r,m[0]),U(o,m[1]),U(i,m[2])))(Li({sourceX:d(),sourceY:g(),targetX:x(),targetY:C(),sourcePosition:p(),targetPosition:$(),borderRadius:0}))}),vt(),He(),Eo(e,{get path(){return h(r)},get labelX(){return h(o)},get labelY(){return h(i)},get label(){return s()},get labelStyle(){return a()},get markerStart(){return u()},get markerEnd(){return c()},get interactionWidth(){return f()},get style(){return l()}}),ce({get label(){return s()},set label(m){s(m),y()},get labelStyle(){return a()},set labelStyle(m){a(m),y()},get style(){return l()},set style(m){l(m),y()},get markerStart(){return u()},set markerStart(m){u(m),y()},get markerEnd(){return c()},set markerEnd(m){c(m),y()},get interactionWidth(){return f()},set interactionWidth(m){f(m),y()},get sourceX(){return d()},set sourceX(m){d(m),y()},get sourceY(){return g()},set sourceY(m){g(m),y()},get sourcePosition(){return p()},set sourcePosition(m){p(m),y()},get targetX(){return x()},set targetX(m){x(m),y()},get targetY(){return C()},set targetY(m){C(m),y()},get targetPosition(){return $()},set targetPosition(m){$(m),y()}})}ie(ic,{label:{},labelStyle:{},style:{},markerStart:{},markerEnd:{},interactionWidth:{},sourceX:{},sourceY:{},sourcePosition:{},targetX:{},targetY:{},targetPosition:{}},[],[],!0);function G0(e,t){const n=e.set,r=t.set,o=q(e),i=q(t);let a=o.length===0&&i.length>0?i:o;e.set(a);const l=u=>{const c=n(u);return a=c,r(a),c};e.set=t.set=l,e.update=t.update=u=>l(u(a))}function U0(e,t){const n=e.set,r=t.set;let o=q(t);e.set(o);const i=s=>{n(s),r(s),o=s};e.set=t.set=i,e.update=t.update=s=>i(s(o))}const j0=(e,t,n)=>{if(!n)return;const r=q(e),o=t.set,i=n.set;let s=n?q(n):{x:0,y:0,zoom:1};t.set(s),t.set=a=>(o(a),i(a),s=a,a),n.set=a=>(r==null||r.syncViewport(a),o(a),i(a),s=a,a),t.update=a=>{t.set(a(s))},n.update=a=>{n.set(a(s))}},J0=(e,t,n,r=[0,0],o=Ni)=>{const{subscribe:i,set:s,update:a}=we([]);let l=e,u={},c=!0;const f=x=>(Ou(x,t,n,{elevateNodesOnSelect:c,nodeOrigin:r,nodeExtent:o,defaults:u,checkEquality:!1}),l=x,s(l),l),d=x=>f(x(l)),g=x=>{u=x},p=x=>{c=x.elevateNodesOnSelect??c};return f(l),{subscribe:i,set:f,update:d,setDefaultOptions:g,setOptions:p}},Q0=(e,t,n,r)=>{const{subscribe:o,set:i,update:s}=we([]);let a=e,l={};const u=d=>{const g=l?d.map(p=>({...l,...p})):d;Ru(t,n,g),a=g,i(a)},c=d=>u(d(a)),f=d=>{l=d};return u(a),{subscribe:o,set:u,update:c,setDefaultOptions:f}},sc={input:Uu,output:ju,default:Ii,group:Ju},ac={straight:oc,smoothstep:rc,default:zi,step:ic},e2=({nodes:e=[],edges:t=[],width:n,height:r,fitView:o,nodeOrigin:i,nodeExtent:s})=>{const a=new Map,l=new Map,u=new Map,c=new Map,f=i??[0,0],d=s??Ni;Ou(e,a,l,{nodeExtent:d,nodeOrigin:f,elevateNodesOnSelect:!1,checkEquality:!1}),Ru(u,c,t);let g={x:0,y:0,zoom:1};if(o&&n&&r){const p=bo(a,{filter:x=>!!((x.width||x.initialWidth)&&(x.height||x.initialHeight))});g=js(p,n,r,.5,2,.1)}return{flowId:we(null),nodes:J0(e,a,l,f,d),nodeLookup:Gt(a),parentLookup:Gt(l),edgeLookup:Gt(c),visibleNodes:Gt([]),edges:Q0(t,u,c),visibleEdges:Gt([]),connectionLookup:Gt(u),height:we(500),width:we(500),minZoom:we(.5),maxZoom:we(2),nodeOrigin:we(f),nodeDragThreshold:we(1),nodeExtent:we(d),translateExtent:we(Ni),autoPanOnNodeDrag:we(!0),autoPanOnConnect:we(!0),fitViewOnInit:we(!1),fitViewOnInitDone:we(!1),fitViewOptions:we(void 0),panZoom:we(null),snapGrid:we(null),dragging:we(!1),selectionRect:we(null),selectionKeyPressed:we(!1),multiselectionKeyPressed:we(!1),deleteKeyPressed:we(!1),panActivationKeyPressed:we(!1),zoomActivationKeyPressed:we(!1),selectionRectMode:we(null),selectionMode:we(Ti.Partial),nodeTypes:we(sc),edgeTypes:we(ac),viewport:we(g),connectionMode:we(hr.Strict),domNode:we(null),connection:Gt(Ws),connectionLineType:we(Ar.Bezier),connectionRadius:we(20),isValidConnection:we(()=>!0),nodesDraggable:we(!0),nodesConnectable:we(!0),elementsSelectable:we(!0),selectNodesOnDrag:we(!0),markers:Gt([]),defaultMarkerColor:we("#b1b1b7"),lib:Gt("svelte"),onlyRenderVisibleElements:we(!1),onerror:we(Qv),ondelete:we(void 0),onedgecreate:we(void 0),onconnect:we(void 0),onconnectstart:we(void 0),onconnectend:we(void 0),onbeforedelete:we(void 0),nodesInitialized:we(!1),edgesInitialized:we(!1),viewportInitialized:we(!1),initialized:Gt(!1)}};function t2(e){const t=Un([e.edges,e.nodes,e.nodeLookup,e.onlyRenderVisibleElements,e.viewport,e.width,e.height],([n,,r,o,i,s,a])=>o&&s&&a?n.filter(u=>{const c=r.get(u.source),f=r.get(u.target);return c&&f&&s0({sourceNode:c,targetNode:f,width:s,height:a,transform:[i.x,i.y,i.zoom]})}):n);return Un([t,e.nodes,e.nodeLookup,e.connectionMode,e.onerror],([n,,r,o,i])=>n.reduce((a,l)=>{const u=r.get(l.source),c=r.get(l.target);if(!u||!c)return a;const f=g0({id:l.id,sourceNode:u,targetNode:c,sourceHandle:l.sourceHandle||null,targetHandle:l.targetHandle||null,connectionMode:o,onError:i});return f&&a.push({...l,zIndex:i0({selected:l.selected,zIndex:l.zIndex,sourceNode:u,targetNode:c,elevateOnSelect:!1}),...f}),a},[]))}function n2(e){return Un([e.nodeLookup,e.onlyRenderVisibleElements,e.width,e.height,e.viewport,e.nodes],([t,n,r,o,i])=>{const s=[i.x,i.y,i.zoom];return n?pu(t,{x:0,y:0,width:r,height:o},s,!0):Array.from(t.values())})}const Ri=Symbol();function lc({nodes:e,edges:t,width:n,height:r,fitView:o,nodeOrigin:i,nodeExtent:s}){const a=e2({nodes:e,edges:t,width:n,height:r,fitView:o,nodeOrigin:i,nodeExtent:s});function l(k){a.nodeTypes.set({...sc,...k})}function u(k){a.edgeTypes.set({...ac,...k})}function c(k){const P=q(a.edges);a.edges.set(u0(k,P))}const f=(k,P=!1)=>{var I;const H=q(a.nodeLookup);for(const[B,F]of k){const K=(I=H.get(B))==null?void 0:I.internals.userNode;K&&(K.position=F.position,K.dragging=P)}a.nodes.update(B=>B)};function d(k){var F,K,se;const P=q(a.nodeLookup),H=q(a.parentLookup),{changes:I,updatedInternals:B}=x0(k,P,q(a.parentLookup),q(a.domNode),q(a.nodeOrigin));if(B){if(m0(P,H,{nodeOrigin:i,nodeExtent:s}),!q(a.fitViewOnInitDone)&&q(a.fitViewOnInit)){const ee=q(a.fitViewOptions),W=p({...ee,nodes:ee==null?void 0:ee.nodes});a.fitViewOnInitDone.set(W)}for(const ee of I){const W=(F=P.get(ee.id))==null?void 0:F.internals.userNode;if(W)switch(ee.type){case"dimensions":{const fe={...W.measured,...ee.dimensions};ee.setAttributes&&(W.width=((K=ee.dimensions)==null?void 0:K.width)??W.width,W.height=((se=ee.dimensions)==null?void 0:se.height)??W.height),W.measured=fe;break}case"position":W.position=ee.position??W.position;break}}a.nodes.update(ee=>ee),q(a.nodesInitialized)||a.nodesInitialized.set(!0)}}function g(k){const P=q(a.panZoom),H=q(a.domNode);if(!P||!H)return Promise.resolve(!1);const{width:I,height:B}=Qs(H),F=mu(q(a.nodeLookup),k);return yu({nodes:F,width:I,height:B,minZoom:q(a.minZoom),maxZoom:q(a.maxZoom),panZoom:P},k)}function p(k){const P=q(a.panZoom);if(!P)return!1;const H=mu(q(a.nodeLookup),k);return yu({nodes:H,width:q(a.width),height:q(a.height),minZoom:q(a.minZoom),maxZoom:q(a.maxZoom),panZoom:P},k),H.size>0}function x(k,P){const H=q(a.panZoom);return H?H.scaleBy(k,P):Promise.resolve(!1)}function C(k){return x(1.2,k)}function $(k){return x(1/1.2,k)}function m(k){const P=q(a.panZoom);P&&(P.setScaleExtent([k,q(a.maxZoom)]),a.minZoom.set(k))}function _(k){const P=q(a.panZoom);P&&(P.setScaleExtent([q(a.minZoom),k]),a.maxZoom.set(k))}function v(k){const P=q(a.panZoom);P&&(P.setTranslateExtent(k),a.translateExtent.set(k))}function b(k){let P=!1;return k.forEach(H=>{H.selected&&(H.selected=!1,P=!0)}),P}function N(k){var P;(P=q(a.panZoom))==null||P.setClickDistance(k)}function E(k){b((k==null?void 0:k.nodes)||q(a.nodes))&&a.nodes.set(q(a.nodes)),b((k==null?void 0:k.edges)||q(a.edges))&&a.edges.set(q(a.edges))}a.deleteKeyPressed.subscribe(async k=>{var P;if(k){const H=q(a.nodes),I=q(a.edges),B=H.filter(ee=>ee.selected),F=I.filter(ee=>ee.selected),{nodes:K,edges:se}=await wu({nodesToRemove:B,edgesToRemove:F,nodes:H,edges:I,onBeforeDelete:q(a.onbeforedelete)});(K.length||se.length)&&(a.nodes.update(ee=>ee.filter(W=>!K.some(fe=>fe.id===W.id))),a.edges.update(ee=>ee.filter(W=>!se.some(fe=>fe.id===W.id))),(P=q(a.ondelete))==null||P({nodes:K,edges:se}))}});function T(k){const P=q(a.multiselectionKeyPressed);a.nodes.update(H=>H.map(I=>{const B=k.includes(I.id),F=P&&I.selected||B;return I.selected=F,I})),P||a.edges.update(H=>H.map(I=>(I.selected=!1,I)))}function D(k){const P=q(a.multiselectionKeyPressed);a.edges.update(H=>H.map(I=>{const B=k.includes(I.id),F=P&&I.selected||B;return I.selected=F,I})),P||a.nodes.update(H=>H.map(I=>(I.selected=!1,I)))}function V(k){var H;const P=(H=q(a.nodes))==null?void 0:H.find(I=>I.id===k);if(!P){console.warn("012",Dr.error012(k));return}a.selectionRect.set(null),a.selectionRectMode.set(null),P.selected?P.selected&&q(a.multiselectionKeyPressed)&&E({nodes:[P],edges:[]}):T([k])}function A(k){const P=q(a.viewport);return b0({delta:k,panZoom:q(a.panZoom),transform:[P.x,P.y,P.zoom],translateExtent:q(a.translateExtent),width:q(a.width),height:q(a.height)})}const O=we(Ws),R=k=>{O.set({...k})};function S(){O.set(Ws)}function M(){a.fitViewOnInitDone.set(!1),a.selectionRect.set(null),a.selectionRectMode.set(null),a.snapGrid.set(null),a.isValidConnection.set(()=>!0),E(),S()}return{...a,visibleEdges:t2(a),visibleNodes:n2(a),connection:Un([O,a.viewport],([k,P])=>k.inProgress?{...k,to:ko(k.to,[P.x,P.y,P.zoom])}:{...k}),markers:Un([a.edges,a.defaultMarkerColor,a.flowId],([k,P,H])=>h0(k,{defaultColor:P,id:H})),initialized:(()=>{let k=!1;const P=q(a.nodes).length,H=q(a.edges).length;return Un([a.nodesInitialized,a.edgesInitialized,a.viewportInitialized],([I,B,F])=>k||(P===0?k=F:H===0?k=F&&I:k=F&&I&&B,k))})(),syncNodeStores:k=>G0(a.nodes,k),syncEdgeStores:k=>U0(a.edges,k),syncViewport:k=>j0(a.panZoom,a.viewport,k),setNodeTypes:l,setEdgeTypes:u,addEdge:c,updateNodePositions:f,updateNodeInternals:d,zoomIn:C,zoomOut:$,fitView:k=>g(k),setMinZoom:m,setMaxZoom:_,setTranslateExtent:v,setPaneClickDistance:N,unselectNodesAndEdges:E,addSelectedNodes:T,addSelectedEdges:D,handleNodeSelection:V,panBy:A,updateConnection:R,cancelConnection:S,reset:M}}function Ue(){const e=ur(Ri);if(!e)throw new Error("In order to use useStore you need to wrap your component in a <SvelteFlowProvider />");return e.getStore()}function r2({nodes:e,edges:t,width:n,height:r,fitView:o,nodeOrigin:i,nodeExtent:s}){const a=lc({nodes:e,edges:t,width:n,height:r,fitView:o,nodeOrigin:i,nodeExtent:s});return Sr(Ri,{getStore:()=>a}),a}function la(e,t){const{panZoom:n,minZoom:r,maxZoom:o,initialViewport:i,viewport:s,dragging:a,translateExtent:l,paneClickDistance:u}=t,c=R0({domNode:e,minZoom:r,maxZoom:o,translateExtent:l,viewport:i,paneClickDistance:u,onDraggingChange:a.set}),f=c.getViewport();return s.set(f),n.set(c),c.update(t),{update(d){c.update(d)}}}var o2=ne('<div class="svelte-flow__zoom svelte-4xkw84"><!></div>');const i2={hash:"svelte-4xkw84",code:".svelte-flow__zoom.svelte-4xkw84 {width:100%;height:100%;position:absolute;top:0;left:0;z-index:4;}"};function uc(e,t){ue(t,!1),et(e,i2);const[n,r]=nt(),o=()=>Q(H,"$panActivationKeyPressed",n),i=()=>Q(R,"$minZoom",n),s=()=>Q(S,"$maxZoom",n),a=()=>Q(I,"$zoomActivationKeyPressed",n),l=()=>Q(O,"$selectionRect",n),u=()=>Q(k,"$translateExtent",n),c=()=>Q(P,"$lib",n),f=re(),d=re(),g=re();let p=w(t,"initialViewport",12,void 0),x=w(t,"onMoveStart",12,void 0),C=w(t,"onMove",12,void 0),$=w(t,"onMoveEnd",12,void 0),m=w(t,"panOnScrollMode",12),_=w(t,"preventScrolling",12),v=w(t,"zoomOnScroll",12),b=w(t,"zoomOnDoubleClick",12),N=w(t,"zoomOnPinch",12),E=w(t,"panOnDrag",12),T=w(t,"panOnScroll",12),D=w(t,"paneClickDistance",12);const{viewport:V,panZoom:A,selectionRect:O,minZoom:R,maxZoom:S,dragging:M,translateExtent:k,lib:P,panActivationKeyPressed:H,zoomActivationKeyPressed:I,viewportInitialized:B}=Ue(),F=W=>V.set({x:W[0],y:W[1],zoom:W[2]});rn(()=>{ai(B,!0)}),ge(()=>j(p()),()=>{U(f,p()||{x:0,y:0,zoom:1})}),ge(()=>(o(),j(E())),()=>{U(d,o()||E())}),ge(()=>(o(),j(T())),()=>{U(g,o()||T())}),vt(),He();var K=o2(),se=X(K);wt(se,t,"default",{}),Z(K),_t(K,(W,fe)=>la==null?void 0:la(W,fe),()=>({viewport:V,minZoom:i(),maxZoom:s(),initialViewport:h(f),dragging:M,panZoom:A,onPanZoomStart:x(),onPanZoom:C(),onPanZoomEnd:$(),zoomOnScroll:v(),zoomOnDoubleClick:b(),zoomOnPinch:N(),panOnScroll:h(g),panOnDrag:h(d),panOnScrollSpeed:.5,panOnScrollMode:m()||Jn.Free,zoomActivationKeyPressed:a(),preventScrolling:typeof _()=="boolean"?_():!0,noPanClassName:"nopan",noWheelClassName:"nowheel",userSelectionActive:!!l(),translateExtent:u(),lib:c(),paneClickDistance:D(),onTransformChange:F})),L(e,K);var ee=ce({get initialViewport(){return p()},set initialViewport(W){p(W),y()},get onMoveStart(){return x()},set onMoveStart(W){x(W),y()},get onMove(){return C()},set onMove(W){C(W),y()},get onMoveEnd(){return $()},set onMoveEnd(W){$(W),y()},get panOnScrollMode(){return m()},set panOnScrollMode(W){m(W),y()},get preventScrolling(){return _()},set preventScrolling(W){_(W),y()},get zoomOnScroll(){return v()},set zoomOnScroll(W){v(W),y()},get zoomOnDoubleClick(){return b()},set zoomOnDoubleClick(W){b(W),y()},get zoomOnPinch(){return N()},set zoomOnPinch(W){N(W),y()},get panOnDrag(){return E()},set panOnDrag(W){E(W),y()},get panOnScroll(){return T()},set panOnScroll(W){T(W),y()},get paneClickDistance(){return D()},set paneClickDistance(W){D(W),y()}});return r(),ee}ie(uc,{initialViewport:{},onMoveStart:{},onMove:{},onMoveEnd:{},panOnScrollMode:{},preventScrolling:{},zoomOnScroll:{},zoomOnDoubleClick:{},zoomOnPinch:{},panOnDrag:{},panOnScroll:{},paneClickDistance:{}},["default"],[],!0);function cc(e,t){return n=>{n.target===t&&(e==null||e(n))}}function dc(e){return t=>{const n=e.includes(t.id);return t.selected!==n&&(t.selected=n),t}}var s2=ne("<div><!></div>");const a2={hash:"svelte-1esy7hx",code:".svelte-flow__pane.svelte-1esy7hx {position:absolute;top:0;left:0;width:100%;height:100%;}"};function fc(e,t){ue(t,!1),et(e,a2);const[n,r]=nt(),o=()=>Q(S,"$panActivationKeyPressed",n),i=()=>Q(O,"$selectionKeyPressed",n),s=()=>Q(V,"$selectionRect",n),a=()=>Q(D,"$elementsSelectable",n),l=()=>Q(A,"$selectionRectMode",n),u=()=>Q(N,"$edges",n),c=()=>Q(b,"$nodeLookup",n),f=()=>Q(E,"$viewport",n),d=()=>Q(R,"$selectionMode",n),g=()=>Q(T,"$dragging",n),p=re(),x=re(),C=re();let $=w(t,"panOnDrag",12,void 0),m=w(t,"selectionOnDrag",12,void 0);const _=ii(),{nodes:v,nodeLookup:b,edges:N,viewport:E,dragging:T,elementsSelectable:D,selectionRect:V,selectionRectMode:A,selectionKeyPressed:O,selectionMode:R,panActivationKeyPressed:S,unselectNodesAndEdges:M}=Ue();let k=re(),P=null,H=[],I=!1;function B(G){if(I){I=!1;return}_("paneclick",{event:G}),M(),A.set(null)}function F(G){var Le,Xe;if(P=h(k).getBoundingClientRect(),!D||!h(x)||G.button!==0||G.target!==h(k)||!P)return;(Xe=(Le=G.target)==null?void 0:Le.setPointerCapture)==null||Xe.call(Le,G.pointerId);const{x:ae,y:Me}=Rn(G,P);M(),V.set({width:0,height:0,startX:ae,startY:Me,x:ae,y:Me})}function K(G){if(!h(x)||!P||!s())return;I=!0;const ae=Rn(G,P),Me=s().startX??0,Le=s().startY??0,Xe={...s(),x:ae.x<Me?ae.x:Me,y:ae.y<Le?ae.y:Le,width:Math.abs(ae.x-Me),height:Math.abs(ae.y-Le)},te=H.map(oe=>oe.id),Fe=qs(H,u()).map(oe=>oe.id);H=pu(c(),Xe,[f().x,f().y,f().zoom],d()===Ti.Partial,!0);const Oe=qs(H,u()).map(oe=>oe.id),rt=H.map(oe=>oe.id);(te.length!==rt.length||rt.some(oe=>!te.includes(oe)))&&v.update(oe=>oe.map(dc(rt))),(Fe.length!==Oe.length||Oe.some(oe=>!Fe.includes(oe)))&&N.update(oe=>oe.map(dc(Oe))),A.set("user"),V.set(Xe)}function se(G){var ae,Me;G.button===0&&((Me=(ae=G.target)==null?void 0:ae.releasePointerCapture)==null||Me.call(ae,G.pointerId),!h(x)&&l()==="user"&&G.target===h(k)&&(B==null||B(G)),V.set(null),H.length>0&&ai(A,"nodes"),i()&&(I=!1))}const ee=G=>{var ae;if(Array.isArray(h(p))&&((ae=h(p))!=null&&ae.includes(2))){G.preventDefault();return}_("panecontextmenu",{event:G})};ge(()=>(o(),j($())),()=>{U(p,o()||$())}),ge(()=>(i(),s(),j(m()),h(p)),()=>{U(x,i()||s()||m()&&h(p)!==!0)}),ge(()=>(a(),h(x),l()),()=>{U(C,a()&&(h(x)||l()==="user"))}),vt(),He();var W=s2(),fe=Ne(()=>h(C)?void 0:cc(B,h(k))),me=Ne(()=>cc(ee,h(k)));let Ce;var he=X(W);wt(he,t,"default",{}),Z(W),An(W,G=>U(k,G),()=>h(k)),Ee(G=>Ce=$t(W,1,"svelte-flow__pane svelte-1esy7hx",null,Ce,{draggable:G,dragging:g(),selection:h(x)}),[()=>$()===!0||Array.isArray($())&&$().includes(0)],ve),Ze("click",W,function(...G){var ae;(ae=h(fe))==null||ae.apply(this,G)}),Ze("pointerdown",W,function(...G){var ae;(ae=h(C)?F:void 0)==null||ae.apply(this,G)}),Ze("pointermove",W,function(...G){var ae;(ae=h(C)?K:void 0)==null||ae.apply(this,G)}),Ze("pointerup",W,function(...G){var ae;(ae=h(C)?se:void 0)==null||ae.apply(this,G)}),Ze("contextmenu",W,function(...G){var ae;(ae=h(me))==null||ae.apply(this,G)}),L(e,W);var ze=ce({get panOnDrag(){return $()},set panOnDrag(G){$(G),y()},get selectionOnDrag(){return m()},set selectionOnDrag(G){m(G),y()}});return r(),ze}ie(fc,{panOnDrag:{},selectionOnDrag:{}},["default"],[],!0);var l2=ne('<div class="svelte-flow__viewport xyflow__viewport svelte-1floaup"><!></div>');const u2={hash:"svelte-1floaup",code:".svelte-flow__viewport.svelte-1floaup {width:100%;height:100%;position:absolute;top:0;left:0;}"};function gc(e,t){ue(t,!1),et(e,u2);const[n,r]=nt(),o=()=>Q(i,"$viewport",n),{viewport:i}=Ue();He();var s=l2(),a=X(s);wt(a,t,"default",{}),Z(s),Ee(()=>de(s,"style",`transform: translate(${o().x??""}px, ${o().y??""}px) scale(${o().zoom??""})`)),L(e,s),ce(),r()}ie(gc,{},["default"],[],!0);function Br(e,t){const{store:n,onDrag:r,onDragStart:o,onDragStop:i,onNodeMouseDown:s}=t,a=$0({onDrag:r,onDragStart:o,onDragStop:i,onNodeMouseDown:s,getStoreItems:()=>{const u=q(n.snapGrid),c=q(n.viewport);return{nodes:q(n.nodes),nodeLookup:q(n.nodeLookup),edges:q(n.edges),nodeExtent:q(n.nodeExtent),snapGrid:u||[0,0],snapToGrid:!!u,nodeOrigin:q(n.nodeOrigin),multiSelectionActive:q(n.multiselectionKeyPressed),domNode:q(n.domNode),transform:[c.x,c.y,c.zoom],autoPanOnNodeDrag:q(n.autoPanOnNodeDrag),nodesDraggable:q(n.nodesDraggable),selectNodesOnDrag:q(n.selectNodesOnDrag),nodeDragThreshold:q(n.nodeDragThreshold),unselectNodesAndEdges:n.unselectNodesAndEdges,updateNodePositions:n.updateNodePositions,panBy:n.panBy}}});function l(u,c){if(c.disabled){a.destroy();return}a.update({domNode:u,noDragClassName:c.noDragClass,handleSelector:c.handleSelector,nodeId:c.nodeId,isSelectable:c.isSelectable,nodeClickDistance:c.nodeClickDistance})}return l(e,t),{update(u){l(e,u)},destroy(){a.destroy()}}}function c2({width:e,height:t,initialWidth:n,initialHeight:r,measuredWidth:o,measuredHeight:i}){if(o===void 0&&i===void 0){const s=e??n,a=t??r;return{width:s?`width:${s}px;`:"",height:a?`height:${a}px;`:""}}return{width:e?`width:${e}px;`:"",height:t?`height:${t}px;`:""}}var d2=ne("<div><!></div>");function hc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(me,"$nodeTypes",n),i=()=>Q(ae,"$elementsSelectable",n),s=()=>Q(Me,"$nodesDraggable",n),a=()=>Q(Fe,"$connectableStore",n),l=re(void 0,!0),u=re(void 0,!0),c=re(void 0,!0),f=re(void 0,!0);let d=w(t,"node",13),g=w(t,"id",13),p=w(t,"data",29,()=>({})),x=w(t,"selected",13,!1),C=w(t,"draggable",13,void 0),$=w(t,"selectable",13,void 0),m=w(t,"connectable",13,!0),_=w(t,"deletable",13,!0),v=w(t,"hidden",13,!1),b=w(t,"dragging",13,!1),N=w(t,"resizeObserver",13,null),E=w(t,"style",13,void 0),T=w(t,"type",13,"default"),D=w(t,"isParent",13,!1),V=w(t,"positionX",13),A=w(t,"positionY",13),O=w(t,"sourcePosition",13,void 0),R=w(t,"targetPosition",13,void 0),S=w(t,"zIndex",13),M=w(t,"measuredWidth",13,void 0),k=w(t,"measuredHeight",13,void 0),P=w(t,"initialWidth",13,void 0),H=w(t,"initialHeight",13,void 0),I=w(t,"width",13,void 0),B=w(t,"height",13,void 0),F=w(t,"dragHandle",13,void 0),K=w(t,"initialized",13,!1),se=w(t,"parentId",13,void 0),ee=w(t,"nodeClickDistance",13,void 0),W=w(t,"class",13,"");const fe=Ue(),{nodeTypes:me,nodeDragThreshold:Ce,selectNodesOnDrag:he,handleNodeSelection:ze,updateNodeInternals:G,elementsSelectable:ae,nodesDraggable:Me}=fe;let Le=re(void 0,!0),Xe=re(null,!0);const te=ii(),Fe=we(m());let Oe=re(void 0,!0),rt=re(void 0,!0),oe=re(void 0,!0);Sr("svelteflow__node_id",g()),Sr("svelteflow__node_connectable",Fe),$s(()=>{var J;h(Xe)&&((J=N())==null||J.unobserve(h(Xe)))});function pe(J){$()&&(!q(he)||!C()||q(Ce)>0)&&ze(g()),te("nodeclick",{node:d().internals.userNode,event:J})}ge(()=>j(T()),()=>{U(l,T()||"default")}),ge(()=>(o(),h(l)),()=>{U(u,!!o()[h(l)])}),ge(()=>(o(),h(l),Ii),()=>{U(c,o()[h(l)]||Ii)}),ge(()=>(h(u),j(T())),()=>{h(u)||console.warn("003",Dr.error003(T()))}),ge(()=>(j(I()),j(B()),j(P()),j(H()),j(M()),j(k())),()=>{U(f,c2({width:I(),height:B(),initialWidth:P(),initialHeight:H(),measuredWidth:M(),measuredHeight:k()}))}),ge(()=>j(m()),()=>{Fe.set(!!m())}),ge(()=>(h(Oe),h(l),h(rt),j(O()),h(oe),j(R()),j(g()),h(Le)),()=>{(h(Oe)&&h(l)!==h(Oe)||h(rt)&&O()!==h(rt)||h(oe)&&R()!==h(oe))&&requestAnimationFrame(()=>G(new Map([[g(),{id:g(),nodeElement:h(Le),force:!0}]]))),U(Oe,h(l)),U(rt,O()),U(oe,R())}),ge(()=>(j(N()),h(Le),h(Xe),j(K())),()=>{N()&&(h(Le)!==h(Xe)||!K())&&(h(Xe)&&N().unobserve(h(Xe)),h(Le)&&N().observe(h(Le)),U(Xe,h(Le)))}),vt(),He(!0);var be=tt(),Ie=xe(be);{var ht=J=>{var Re=d2();let le;var $n=X(Re);const fn=ve(()=>x()??!1),En=ve(()=>$()??i()??!0),Te=ve(()=>_()??!0),st=ve(()=>C()??s()??!0);fl($n,()=>h(c),(ye,lt)=>{lt(ye,{get data(){return p()},get id(){return g()},get selected(){return h(fn)},get selectable(){return h(En)},get deletable(){return h(Te)},get sourcePosition(){return O()},get targetPosition(){return R()},get zIndex(){return S()},get dragging(){return b()},get draggable(){return h(st)},get dragHandle(){return F()},get parentId(){return se()},get type(){return h(l)},get isConnectable(){return a()},get positionAbsoluteX(){return V()},get positionAbsoluteY(){return A()},get width(){return I()},get height(){return B()}})}),Z(Re),_t(Re,(ye,lt)=>Br==null?void 0:Br(ye,lt),()=>({nodeId:g(),isSelectable:$(),disabled:!1,handleSelector:F(),noDragClass:"nodrag",nodeClickDistance:ee(),onNodeMouseDown:ze,onDrag:(ye,lt,ct,Jt)=>{te("nodedrag",{event:ye,targetNode:ct,nodes:Jt})},onDragStart:(ye,lt,ct,Jt)=>{te("nodedragstart",{event:ye,targetNode:ct,nodes:Jt})},onDragStop:(ye,lt,ct,Jt)=>{te("nodedragstop",{event:ye,targetNode:ct,nodes:Jt})},store:fe})),An(Re,ye=>U(Le,ye),()=>h(Le)),Rt(()=>Ze("click",Re,pe)),Rt(()=>Ze("mouseenter",Re,ye=>te("nodemouseenter",{node:d(),event:ye}))),Rt(()=>Ze("mouseleave",Re,ye=>te("nodemouseleave",{node:d(),event:ye}))),Rt(()=>Ze("mousemove",Re,ye=>te("nodemousemove",{node:d(),event:ye}))),Rt(()=>Ze("contextmenu",Re,ye=>te("nodecontextmenu",{node:d(),event:ye}))),Ee(ye=>{de(Re,"data-id",g()),le=$t(Re,1,wn(ye),null,le,{dragging:b(),selected:x(),draggable:C(),connectable:m(),selectable:$(),nopan:C(),parent:D()}),de(Re,"style",`${E()??""};${h(f).width??""}${h(f).height??""}`),at(Re,"z-index",S()),at(Re,"transform",`translate(${V()??""}px, ${A()??""}px)`),at(Re,"visibility",K()?"visible":"hidden")},[()=>Et(["svelte-flow__node",`svelte-flow__node-${h(l)}`,W()])],ve),L(J,Re)};ke(Ie,J=>{v()||J(ht)})}L(e,be);var dt=ce({get node(){return d()},set node(J){d(J),y()},get id(){return g()},set id(J){g(J),y()},get data(){return p()},set data(J){p(J),y()},get selected(){return x()},set selected(J){x(J),y()},get draggable(){return C()},set draggable(J){C(J),y()},get selectable(){return $()},set selectable(J){$(J),y()},get connectable(){return m()},set connectable(J){m(J),y()},get deletable(){return _()},set deletable(J){_(J),y()},get hidden(){return v()},set hidden(J){v(J),y()},get dragging(){return b()},set dragging(J){b(J),y()},get resizeObserver(){return N()},set resizeObserver(J){N(J),y()},get style(){return E()},set style(J){E(J),y()},get type(){return T()},set type(J){T(J),y()},get isParent(){return D()},set isParent(J){D(J),y()},get positionX(){return V()},set positionX(J){V(J),y()},get positionY(){return A()},set positionY(J){A(J),y()},get sourcePosition(){return O()},set sourcePosition(J){O(J),y()},get targetPosition(){return R()},set targetPosition(J){R(J),y()},get zIndex(){return S()},set zIndex(J){S(J),y()},get measuredWidth(){return M()},set measuredWidth(J){M(J),y()},get measuredHeight(){return k()},set measuredHeight(J){k(J),y()},get initialWidth(){return P()},set initialWidth(J){P(J),y()},get initialHeight(){return H()},set initialHeight(J){H(J),y()},get width(){return I()},set width(J){I(J),y()},get height(){return B()},set height(J){B(J),y()},get dragHandle(){return F()},set dragHandle(J){F(J),y()},get initialized(){return K()},set initialized(J){K(J),y()},get parentId(){return se()},set parentId(J){se(J),y()},get nodeClickDistance(){return ee()},set nodeClickDistance(J){ee(J),y()},get class(){return W()},set class(J){W(J),y()}});return r(),dt}ie(hc,{node:{},id:{},data:{},selected:{},draggable:{},selectable:{},connectable:{},deletable:{},hidden:{},dragging:{},resizeObserver:{},style:{},type:{},isParent:{},positionX:{},positionY:{},sourcePosition:{},targetPosition:{},zIndex:{},measuredWidth:{},measuredHeight:{},initialWidth:{},initialHeight:{},width:{},height:{},dragHandle:{},initialized:{},parentId:{},nodeClickDistance:{},class:{}},[],[],!0);var f2=ne('<div class="svelte-flow__nodes svelte-tf4uy4"></div>');const g2={hash:"svelte-tf4uy4",code:".svelte-flow__nodes.svelte-tf4uy4 {width:100%;height:100%;position:absolute;left:0;top:0;}"};function vc(e,t){ue(t,!1),et(e,g2);const[n,r]=nt(),o=()=>Q(c,"$visibleNodes",n),i=()=>Q(f,"$nodesDraggable",n),s=()=>Q(g,"$elementsSelectable",n),a=()=>Q(d,"$nodesConnectable",n),l=()=>Q(x,"$parentLookup",n);let u=w(t,"nodeClickDistance",12,0);const{visibleNodes:c,nodesDraggable:f,nodesConnectable:d,elementsSelectable:g,updateNodeInternals:p,parentLookup:x}=Ue(),C=typeof ResizeObserver>"u"?null:new ResizeObserver(_=>{const v=new Map;_.forEach(b=>{const N=b.target.getAttribute("data-id");v.set(N,{id:N,nodeElement:b.target,force:!0})}),p(v)});$s(()=>{C==null||C.disconnect()}),He();var $=f2();Yt($,5,o,_=>_.id,(_,v)=>{const b=ve(()=>!!h(v).selected),N=ve(()=>!!h(v).hidden),E=ve(()=>!!(h(v).draggable||i()&&typeof h(v).draggable>"u")),T=ve(()=>!!(h(v).selectable||s()&&typeof h(v).selectable>"u")),D=ve(()=>!!(h(v).connectable||a()&&typeof h(v).connectable>"u")),V=ve(()=>h(v).deletable??!0),A=ve(()=>l().has(h(v).id)),O=ve(()=>h(v).type??"default"),R=ve(()=>h(v).internals.z??0),S=ve(()=>Eu(h(v)));hc(_,{get node(){return h(v)},get id(){return h(v).id},get data(){return h(v).data},get selected(){return h(b)},get hidden(){return h(N)},get draggable(){return h(E)},get selectable(){return h(T)},get connectable(){return h(D)},get deletable(){return h(V)},get positionX(){return h(v).internals.positionAbsolute.x},get positionY(){return h(v).internals.positionAbsolute.y},get isParent(){return h(A)},get style(){return h(v).style},get class(){return h(v).class},get type(){return h(O)},get sourcePosition(){return h(v).sourcePosition},get targetPosition(){return h(v).targetPosition},get dragging(){return h(v).dragging},get zIndex(){return h(R)},get dragHandle(){return h(v).dragHandle},get initialized(){return h(S)},get width(){return h(v).width},get height(){return h(v).height},get initialWidth(){return h(v).initialWidth},get initialHeight(){return h(v).initialHeight},get measuredWidth(){return h(v).measured.width},get measuredHeight(){return h(v).measured.height},get parentId(){return h(v).parentId},resizeObserver:C,get nodeClickDistance(){return u()},$$events:{nodeclick(M){De.call(this,t,M)},nodemouseenter(M){De.call(this,t,M)},nodemousemove(M){De.call(this,t,M)},nodemouseleave(M){De.call(this,t,M)},nodedrag(M){De.call(this,t,M)},nodedragstart(M){De.call(this,t,M)},nodedragstop(M){De.call(this,t,M)},nodecontextmenu(M){De.call(this,t,M)}}})}),Z($),L(e,$);var m=ce({get nodeClickDistance(){return u()},set nodeClickDistance(_){u(_),y()}});return r(),m}ie(vc,{nodeClickDistance:{}},[],[],!0);var h2=_e('<svg><g role="img"><!></g></svg>');function pc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(W,"$edgeTypes",n),i=()=>Q(fe,"$flowId",n),s=()=>Q(me,"$elementsSelectable",n),a=()=>Q(ee,"$edgeLookup",n),l=re(void 0,!0),u=re(void 0,!0),c=re(void 0,!0),f=re(void 0,!0),d=re(void 0,!0);let g=w(t,"id",13),p=w(t,"type",13,"default"),x=w(t,"source",13,""),C=w(t,"target",13,""),$=w(t,"data",29,()=>({})),m=w(t,"style",13,void 0),_=w(t,"zIndex",13,void 0),v=w(t,"animated",13,!1),b=w(t,"selected",13,!1),N=w(t,"selectable",13,void 0),E=w(t,"deletable",13,void 0),T=w(t,"hidden",13,!1),D=w(t,"label",13,void 0),V=w(t,"labelStyle",13,void 0),A=w(t,"markerStart",13,void 0),O=w(t,"markerEnd",13,void 0),R=w(t,"sourceHandle",13,void 0),S=w(t,"targetHandle",13,void 0),M=w(t,"sourceX",13),k=w(t,"sourceY",13),P=w(t,"targetX",13),H=w(t,"targetY",13),I=w(t,"sourcePosition",13),B=w(t,"targetPosition",13),F=w(t,"ariaLabel",13,void 0),K=w(t,"interactionWidth",13,void 0),se=w(t,"class",13,"");Sr("svelteflow__edge_id",g());const{edgeLookup:ee,edgeTypes:W,flowId:fe,elementsSelectable:me}=Ue(),Ce=ii(),he=tc();function ze(te){const Fe=a().get(g());Fe&&(he(g()),Ce("edgeclick",{event:te,edge:Fe}))}function G(te,Fe){const Oe=a().get(g());Oe&&Ce(Fe,{event:te,edge:Oe})}ge(()=>j(p()),()=>{U(l,p()||"default")}),ge(()=>(o(),h(l),zi),()=>{U(u,o()[h(l)]||zi)}),ge(()=>(j(A()),i()),()=>{U(c,A()?`url('#${ta(A(),i())}')`:void 0)}),ge(()=>(j(O()),i()),()=>{U(f,O()?`url('#${ta(O(),i())}')`:void 0)}),ge(()=>(j(N()),s()),()=>{U(d,N()??s())}),vt(),He(!0);var ae=tt(),Me=xe(ae);{var Le=te=>{var Fe=h2(),Oe=X(Fe);let rt;var oe=X(Oe);const pe=ve(()=>E()??!0);fl(oe,()=>h(u),(be,Ie)=>{Ie(be,{get id(){return g()},get source(){return x()},get target(){return C()},get sourceX(){return M()},get sourceY(){return k()},get targetX(){return P()},get targetY(){return H()},get sourcePosition(){return I()},get targetPosition(){return B()},get animated(){return v()},get selected(){return b()},get label(){return D()},get labelStyle(){return V()},get data(){return $()},get style(){return m()},get interactionWidth(){return K()},get selectable(){return h(d)},get deletable(){return h(pe)},get type(){return h(l)},get sourceHandleId(){return R()},get targetHandleId(){return S()},get markerStart(){return h(c)},get markerEnd(){return h(f)}})}),Z(Oe),Z(Fe),Ee(be=>{at(Fe,"z-index",_()),rt=$t(Oe,0,wn(be),null,rt,{animated:v(),selected:b(),selectable:h(d)}),de(Oe,"data-id",g()),de(Oe,"aria-label",F()===null?void 0:F()?F():`Edge from ${x()} to ${C()}`)},[()=>Et(["svelte-flow__edge",se()])],ve),Ze("click",Oe,ze),Ze("contextmenu",Oe,be=>{G(be,"edgecontextmenu")}),Ze("mouseenter",Oe,be=>{G(be,"edgemouseenter")}),Ze("mouseleave",Oe,be=>{G(be,"edgemouseleave")}),L(te,Fe)};ke(Me,te=>{T()||te(Le)})}L(e,ae);var Xe=ce({get id(){return g()},set id(te){g(te),y()},get type(){return p()},set type(te){p(te),y()},get source(){return x()},set source(te){x(te),y()},get target(){return C()},set target(te){C(te),y()},get data(){return $()},set data(te){$(te),y()},get style(){return m()},set style(te){m(te),y()},get zIndex(){return _()},set zIndex(te){_(te),y()},get animated(){return v()},set animated(te){v(te),y()},get selected(){return b()},set selected(te){b(te),y()},get selectable(){return N()},set selectable(te){N(te),y()},get deletable(){return E()},set deletable(te){E(te),y()},get hidden(){return T()},set hidden(te){T(te),y()},get label(){return D()},set label(te){D(te),y()},get labelStyle(){return V()},set labelStyle(te){V(te),y()},get markerStart(){return A()},set markerStart(te){A(te),y()},get markerEnd(){return O()},set markerEnd(te){O(te),y()},get sourceHandle(){return R()},set sourceHandle(te){R(te),y()},get targetHandle(){return S()},set targetHandle(te){S(te),y()},get sourceX(){return M()},set sourceX(te){M(te),y()},get sourceY(){return k()},set sourceY(te){k(te),y()},get targetX(){return P()},set targetX(te){P(te),y()},get targetY(){return H()},set targetY(te){H(te),y()},get sourcePosition(){return I()},set sourcePosition(te){I(te),y()},get targetPosition(){return B()},set targetPosition(te){B(te),y()},get ariaLabel(){return F()},set ariaLabel(te){F(te),y()},get interactionWidth(){return K()},set interactionWidth(te){K(te),y()},get class(){return se()},set class(te){se(te),y()}});return r(),Xe}ie(pc,{id:{},type:{},source:{},target:{},data:{},style:{},zIndex:{},animated:{},selected:{},selectable:{},deletable:{},hidden:{},label:{},labelStyle:{},markerStart:{},markerEnd:{},sourceHandle:{},targetHandle:{},sourceX:{},sourceY:{},targetX:{},targetY:{},sourcePosition:{},targetPosition:{},ariaLabel:{},interactionWidth:{},class:{}},[],[],!0);function mc(e,t){ue(t,!1);let n=w(t,"onMount",12,void 0),r=w(t,"onDestroy",12,void 0);return rn(()=>{var o;return(o=n())==null||o(),r()}),He(),ce({get onMount(){return n()},set onMount(o){n(o),y()},get onDestroy(){return r()},set onDestroy(o){r(o),y()}})}ie(mc,{onMount:{},onDestroy:{}},[],[],!0);var v2=_e("<defs></defs>");function yc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(i,"$markers",n),{markers:i}=Ue();He();var s=v2();Yt(s,5,o,a=>a.id,(a,l)=>{wc(a,ft(()=>h(l)))}),Z(s),L(e,s),ce(),r()}ie(yc,{},[],[],!0);var p2=_e('<polyline stroke-linecap="round" stroke-linejoin="round" fill="none" points="-5,-4 0,0 -5,4"></polyline>'),m2=_e('<polyline stroke-linecap="round" stroke-linejoin="round" points="-5,-4 0,0 -5,4 -5,-4"></polyline>'),y2=_e('<marker class="svelte-flow__arrowhead" viewBox="-10 -10 20 20" refX="0" refY="0"><!></marker>');function wc(e,t){ue(t,!1);let n=w(t,"id",12),r=w(t,"type",12),o=w(t,"width",12,12.5),i=w(t,"height",12,12.5),s=w(t,"markerUnits",12,"strokeWidth"),a=w(t,"orient",12,"auto-start-reverse"),l=w(t,"color",12,void 0),u=w(t,"strokeWidth",12,void 0);He();var c=y2(),f=X(c);{var d=p=>{var x=p2();Ee(()=>{de(x,"stroke",l()),de(x,"stroke-width",u())}),L(p,x)},g=(p,x)=>{{var C=$=>{var m=m2();Ee(()=>{de(m,"stroke",l()),de(m,"stroke-width",u()),de(m,"fill",l())}),L($,m)};ke(p,$=>{r()===_o.ArrowClosed&&$(C)},x)}};ke(f,p=>{r()===_o.Arrow?p(d):p(g,!1)})}return Z(c),Ee(()=>{de(c,"id",n()),de(c,"markerWidth",`${o()}`),de(c,"markerHeight",`${i()}`),de(c,"markerUnits",s()),de(c,"orient",a())}),L(e,c),ce({get id(){return n()},set id(p){n(p),y()},get type(){return r()},set type(p){r(p),y()},get width(){return o()},set width(p){o(p),y()},get height(){return i()},set height(p){i(p),y()},get markerUnits(){return s()},set markerUnits(p){s(p),y()},get orient(){return a()},set orient(p){a(p),y()},get color(){return l()},set color(p){l(p),y()},get strokeWidth(){return u()},set strokeWidth(p){u(p),y()}})}ie(wc,{id:{},type:{},width:{},height:{},markerUnits:{},orient:{},color:{},strokeWidth:{}},[],[],!0);var w2=ne('<div class="svelte-flow__edges"><svg class="svelte-flow__marker"><!></svg> <!> <!></div>');function _c(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(a,"$visibleEdges",n),i=()=>Q(c,"$elementsSelectable",n);let s=w(t,"defaultEdgeOptions",12);const{visibleEdges:a,edgesInitialized:l,edges:{setDefaultOptions:u},elementsSelectable:c}=Ue();rn(()=>{s()&&u(s())}),He();var f=w2(),d=X(f),g=X(d);yc(g,{}),Z(d);var p=z(d,2);Yt(p,1,o,m=>m.id,(m,_)=>{const v=ve(()=>h(_).selectable??i()),b=ve(()=>h(_).type||"default");pc(m,{get id(){return h(_).id},get source(){return h(_).source},get target(){return h(_).target},get data(){return h(_).data},get style(){return h(_).style},get animated(){return h(_).animated},get selected(){return h(_).selected},get selectable(){return h(v)},get deletable(){return h(_).deletable},get hidden(){return h(_).hidden},get label(){return h(_).label},get labelStyle(){return h(_).labelStyle},get markerStart(){return h(_).markerStart},get markerEnd(){return h(_).markerEnd},get sourceHandle(){return h(_).sourceHandle},get targetHandle(){return h(_).targetHandle},get sourceX(){return h(_).sourceX},get sourceY(){return h(_).sourceY},get targetX(){return h(_).targetX},get targetY(){return h(_).targetY},get sourcePosition(){return h(_).sourcePosition},get targetPosition(){return h(_).targetPosition},get ariaLabel(){return h(_).ariaLabel},get interactionWidth(){return h(_).interactionWidth},get class(){return h(_).class},get type(){return h(b)},get zIndex(){return h(_).zIndex},$$events:{edgeclick(N){De.call(this,t,N)},edgecontextmenu(N){De.call(this,t,N)},edgemouseenter(N){De.call(this,t,N)},edgemouseleave(N){De.call(this,t,N)}}})});var x=z(p,2);{var C=m=>{mc(m,{onMount:()=>{ai(l,!0)},onDestroy:()=>{ai(l,!1)}})};ke(x,m=>{o().length>0&&m(C)})}Z(f),L(e,f);var $=ce({get defaultEdgeOptions(){return s()},set defaultEdgeOptions(m){s(m),y()}});return r(),$}ie(_c,{defaultEdgeOptions:{}},[],[],!0);var _2=ne('<div class="svelte-flow__selection svelte-1iugwpu"></div>');const x2={hash:"svelte-1iugwpu",code:".svelte-flow__selection.svelte-1iugwpu {position:absolute;top:0;left:0;}"};function ua(e,t){ue(t,!1),et(e,x2);let n=w(t,"x",12,0),r=w(t,"y",12,0),o=w(t,"width",12,0),i=w(t,"height",12,0),s=w(t,"isVisible",12,!0);var a=tt(),l=xe(a);{var u=c=>{var f=_2();Ee(()=>{at(f,"width",typeof o()=="string"?o():`${o()}px`),at(f,"height",typeof i()=="string"?i():`${i()}px`),at(f,"transform",`translate(${n()}px, ${r()}px)`)}),L(c,f)};ke(l,c=>{s()&&c(u)})}return L(e,a),ce({get x(){return n()},set x(c){n(c),y()},get y(){return r()},set y(c){r(c),y()},get width(){return o()},set width(c){o(c),y()},get height(){return i()},set height(c){i(c),y()},get isVisible(){return s()},set isVisible(c){s(c),y()}})}ie(ua,{x:{},y:{},width:{},height:{},isVisible:{}},[],[],!0);function xc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(s,"$selectionRect",n),i=()=>Q(a,"$selectionRectMode",n),{selectionRect:s,selectionRectMode:a}=Ue();He();const l=ve(()=>!!(o()&&i()==="user")),u=ve(()=>{var g;return(g=o())==null?void 0:g.width}),c=ve(()=>{var g;return(g=o())==null?void 0:g.height}),f=ve(()=>{var g;return(g=o())==null?void 0:g.x}),d=ve(()=>{var g;return(g=o())==null?void 0:g.y});ua(e,{get isVisible(){return h(l)},get width(){return h(u)},get height(){return h(c)},get x(){return h(f)},get y(){return h(d)}}),ce(),r()}ie(xc,{},[],[],!0);var b2=ne('<div class="selection-wrapper nopan svelte-5pxri" role="button" tabindex="-1"><!></div>');const C2={hash:"svelte-5pxri",code:".selection-wrapper.svelte-5pxri {position:absolute;top:0;left:0;z-index:7;pointer-events:all;}"};function bc(e,t){ue(t,!1),et(e,C2);const[n,r]=nt(),o=()=>Q(l,"$selectionRectMode",n),i=()=>Q(c,"$nodeLookup",n),s=()=>Q(u,"$nodes",n),a=Ue(),{selectionRectMode:l,nodes:u,nodeLookup:c}=a,f=ii();let d=re(null);function g(m){const _=s().filter(v=>v.selected);f("selectioncontextmenu",{nodes:_,event:m})}function p(m){const _=s().filter(v=>v.selected);f("selectionclick",{nodes:_,event:m})}ge(()=>(o(),i(),s()),()=>{o()==="nodes"&&(U(d,bo(i(),{filter:m=>!!m.selected})),s())}),vt(),He();var x=tt(),C=xe(x);{var $=m=>{var _=b2(),v=X(_);ua(v,{width:"100%",height:"100%",x:0,y:0}),Z(_),_t(_,(b,N)=>Br==null?void 0:Br(b,N),()=>({disabled:!1,store:a,onDrag:(b,N,E,T)=>{f("nodedrag",{event:b,targetNode:null,nodes:T})},onDragStart:(b,N,E,T)=>{f("nodedragstart",{event:b,targetNode:null,nodes:T})},onDragStop:(b,N,E,T)=>{f("nodedragstop",{event:b,targetNode:null,nodes:T})}})),Rt(()=>Ze("contextmenu",_,g)),Rt(()=>Ze("click",_,p)),Rt(()=>Ze("keyup",_,()=>{})),Ee(()=>de(_,"style",`width: ${h(d).width??""}px; height: ${h(d).height??""}px; transform: translate(${h(d).x??""}px, ${h(d).y??""}px)`)),L(m,_)};ke(C,m=>{o()==="nodes"&&h(d)&&zn(h(d).x)&&zn(h(d).y)&&m($)})}L(e,x),ce(),r()}ie(bc,{},[],[],!0);function qe(e,t){let{enabled:n=!0,trigger:r,type:o="keydown"}=t;function i(s){const a=Array.isArray(r)?r:[r],l={alt:s.altKey,ctrl:s.ctrlKey,shift:s.shiftKey,meta:s.metaKey};for(const u of a){const c={modifier:[],preventDefault:!1,enabled:!0,...u},{modifier:f,key:d,callback:g,preventDefault:p,enabled:x}=c;if(x){if(f.length&&!(Array.isArray(f)?f:[f]).map(m=>typeof m=="string"?[m]:m).some(m=>m.every(_=>l[_])))continue;if(s.key===d){p&&s.preventDefault();const C={node:e,trigger:c,originalEvent:s};e.dispatchEvent(new CustomEvent("shortcut",{detail:C})),g==null||g(C)}}}}return n&&e.addEventListener(o,i),{update:s=>{const{enabled:a=!0,type:l="keydown"}=s;n&&(!a||o!==l)?e.removeEventListener(o,i):!n&&a&&e.addEventListener(l,i),n=a,o=l,r=s.trigger},destroy:()=>{e.removeEventListener(o,i)}}}function Cc(e,t){ue(t,!1);let n=w(t,"selectionKey",12,"Shift"),r=w(t,"multiSelectionKey",28,()=>Di()?"Meta":"Control"),o=w(t,"deleteKey",12,"Backspace"),i=w(t,"panActivationKey",12," "),s=w(t,"zoomActivationKey",28,()=>Di()?"Meta":"Control");const{selectionKeyPressed:a,multiselectionKeyPressed:l,deleteKeyPressed:u,panActivationKeyPressed:c,zoomActivationKeyPressed:f,selectionRect:d}=Ue();function g(m){return m!==null&&typeof m=="object"}function p(m){return g(m)?m.modifier||[]:[]}function x(m){return m==null?"":g(m)?m.key:m}function C(m,_){return(Array.isArray(m)?m:[m]).map(b=>{const N=x(b);return{key:N,modifier:p(b),enabled:N!==null,callback:_}})}function $(){d.set(null),a.set(!1),l.set(!1),u.set(!1),c.set(!1),f.set(!1)}return He(),Ze("blur",Vt,$),Ze("contextmenu",Vt,$),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(n(),()=>a.set(!0)),type:"keydown"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(n(),()=>a.set(!1)),type:"keyup"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(r(),()=>l.set(!0)),type:"keydown"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(r(),()=>l.set(!1)),type:"keyup"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(o(),m=>{!(m.originalEvent.ctrlKey||m.originalEvent.metaKey||m.originalEvent.shiftKey)&&!r0(m.originalEvent)&&u.set(!0)}),type:"keydown"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(o(),()=>u.set(!1)),type:"keyup"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(i(),()=>c.set(!0)),type:"keydown"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(i(),()=>c.set(!1)),type:"keyup"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(s(),()=>f.set(!0)),type:"keydown"})),_t(Vt,(m,_)=>qe==null?void 0:qe(m,_),()=>({trigger:C(s(),()=>f.set(!1)),type:"keyup"})),ce({get selectionKey(){return n()},set selectionKey(m){n(m),y()},get multiSelectionKey(){return r()},set multiSelectionKey(m){r(m),y()},get deleteKey(){return o()},set deleteKey(m){o(m),y()},get panActivationKey(){return i()},set panActivationKey(m){i(m),y()},get zoomActivationKey(){return s()},set zoomActivationKey(m){s(m),y()}})}ie(Cc,{selectionKey:{},multiSelectionKey:{},deleteKey:{},panActivationKey:{},zoomActivationKey:{}},[],[],!0);var k2=_e('<path fill="none" class="svelte-flow__connection-path"></path>'),$2=_e('<svg class="svelte-flow__connectionline"><g><!><!></g></svg>');function kc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(g,"$connection",n),i=()=>Q(p,"$connectionLineType",n),s=()=>Q(f,"$width",n),a=()=>Q(d,"$height",n);let l=w(t,"containerStyle",12,""),u=w(t,"style",12,""),c=w(t,"isCustomComponent",12,!1);const{width:f,height:d,connection:g,connectionLineType:p}=Ue();let x=re(null);ge(()=>(o(),j(c()),i(),h(x),ea),()=>{if(o().inProgress&&!c()){const{from:v,to:b,fromPosition:N,toPosition:E}=o(),T={sourceX:v.x,sourceY:v.y,sourcePosition:N,targetX:b.x,targetY:b.y,targetPosition:E};switch(i()){case Ar.Bezier:(D=>U(x,D[0]))(Tu(T));break;case Ar.Step:(D=>U(x,D[0]))(Li({...T,borderRadius:0}));break;case Ar.SmoothStep:(D=>U(x,D[0]))(Li(T));break;default:(D=>U(x,D[0]))(ea(T))}}}),vt(),He();var C=tt(),$=xe(C);{var m=v=>{var b=$2(),N=X(b),E=X(N);wt(E,t,"connectionLine",{});var T=z(E);{var D=V=>{var A=k2();Ee(()=>{de(A,"d",h(x)),de(A,"style",u())}),L(V,A)};ke(T,V=>{c()||V(D)})}Z(N),Z(b),Ee(V=>{de(b,"width",s()),de(b,"height",a()),de(b,"style",l()),$t(N,0,wn(V))},[()=>Et(["svelte-flow__connection",qv(o().isValid)])],ve),L(v,b)};ke($,v=>{o().inProgress&&v(m)})}L(e,C);var _=ce({get containerStyle(){return l()},set containerStyle(v){l(v),y()},get style(){return u()},set style(v){u(v),y()},get isCustomComponent(){return c()},set isCustomComponent(v){c(v),y()}});return r(),_}ie(kc,{containerStyle:{},style:{},isCustomComponent:{}},["connectionLine"],[],!0);var E2=ne("<div><!></div>");function So(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]),r=it(n,["position","style","class"]);ue(t,!1);const[o,i]=nt(),s=()=>Q(f,"$selectionRectMode",o),a=re();let l=w(t,"position",12,"top-right"),u=w(t,"style",12,void 0),c=w(t,"class",12,void 0);const{selectionRectMode:f}=Ue();ge(()=>j(l()),()=>{U(a,`${l()}`.split("-"))}),vt(),He();var d=E2();let g;var p=X(d);wt(p,t,"default",{}),Z(d),Ee(C=>{g=nn(d,g,{class:C,style:u(),...r}),at(d,"pointer-events",s()?"none":"")},[()=>Et(["svelte-flow__panel",c(),...h(a)])],ve),L(e,d);var x=ce({get position(){return l()},set position(C){l(C),y()},get style(){return u()},set style(C){u(C),y()},get class(){return c()},set class(C){c(C),y()}});return i(),x}ie(So,{position:{},style:{},class:{}},["default"],[],!0);var S2=ne('<a href="https://svelteflow.dev" target="_blank" rel="noopener noreferrer" aria-label="Svelte Flow attribution">Svelte Flow</a>');function $c(e,t){ue(t,!1);let n=w(t,"proOptions",12,void 0),r=w(t,"position",12,"bottom-right");He();var o=tt(),i=xe(o);{var s=a=>{So(a,{get position(){return r()},class:"svelte-flow__attribution","data-message":"Feel free to remove the attribution or check out how you could support us: https://svelteflow.dev/support-us",children:(l,u)=>{var c=S2();L(l,c)},$$slots:{default:!0}})};ke(i,a=>{var l;(l=n())!=null&&l.hideAttribution||a(s)})}return L(e,o),ce({get proOptions(){return n()},set proOptions(a){n(a),y()},get position(){return r()},set position(a){r(a),y()}})}ie($c,{proOptions:{},position:{}},[],[],!0);function Ec(e,{nodeTypes:t,edgeTypes:n,minZoom:r,maxZoom:o,translateExtent:i,paneClickDistance:s}){t!==void 0&&e.setNodeTypes(t),n!==void 0&&e.setEdgeTypes(n),r!==void 0&&e.setMinZoom(r),o!==void 0&&e.setMaxZoom(o),i!==void 0&&e.setTranslateExtent(i),s!==void 0&&e.setPaneClickDistance(s)}const P2=e=>Object.keys(e);function Sc(e,t){P2(t).forEach(n=>{const r=t[n];r!==void 0&&e[n].set(r)})}function N2(){return typeof window>"u"||!window.matchMedia?null:window.matchMedia("(prefers-color-scheme: dark)")}function T2(e="light"){return Gt("light",n=>{if(e!=="system"){n(e);return}const r=N2(),o=()=>n(r!=null&&r.matches?"dark":"light");return n(r!=null&&r.matches?"dark":"light"),r==null||r.addEventListener("change",o),()=>{r==null||r.removeEventListener("change",o)}})}var M2=ne('<!> <!> <div class="svelte-flow__edgelabel-renderer"></div> <div class="svelte-flow__viewport-portal"></div> <!> <!>',1),H2=ne("<!> <!>",1),V2=ne("<div><!> <!> <!> <!></div>");const D2={hash:"svelte-12wlba6",code:".svelte-flow.svelte-12wlba6 {width:100%;height:100%;overflow:hidden;position:relative;z-index:0;background-color:var(--background-color, var(--background-color-default));}:root {--background-color-default: #fff;--background-pattern-color-default: #ddd;--minimap-mask-color-default: rgb(240, 240, 240, 0.6);--minimap-mask-stroke-color-default: none;--minimap-mask-stroke-width-default: 1;--controls-button-background-color-default: #fefefe;--controls-button-background-color-hover-default: #f4f4f4;--controls-button-color-default: inherit;--controls-button-color-hover-default: inherit;--controls-button-border-color-default: #eee;}"};function Pc(e,t){const n=e1(t),r=it(t,["children","$$slots","$$events","$$legacy","$$host"]),o=it(r,["id","nodes","edges","fitView","fitViewOptions","minZoom","maxZoom","initialViewport","viewport","nodeTypes","edgeTypes","selectionKey","selectionMode","panActivationKey","multiSelectionKey","zoomActivationKey","nodesDraggable","nodesConnectable","nodeDragThreshold","elementsSelectable","snapGrid","deleteKey","connectionRadius","connectionLineType","connectionMode","connectionLineStyle","connectionLineContainerStyle","onMoveStart","onMove","onMoveEnd","isValidConnection","translateExtent","nodeExtent","onlyRenderVisibleElements","panOnScrollMode","preventScrolling","zoomOnScroll","zoomOnDoubleClick","zoomOnPinch","panOnScroll","panOnDrag","selectionOnDrag","autoPanOnConnect","autoPanOnNodeDrag","onerror","ondelete","onedgecreate","attributionPosition","proOptions","defaultEdgeOptions","width","height","colorMode","onconnect","onconnectstart","onconnectend","onbeforedelete","oninit","nodeOrigin","paneClickDistance","nodeClickDistance","defaultMarkerColor","style","class"]);ue(t,!1),et(e,D2);const[i,s]=nt(),a=()=>Q(_(),"$viewport",i),l=()=>Q(xa,"$initialized",i),u=()=>Q(h(c),"$colorModeClass",i),c=re();let f=w(t,"id",12,"1"),d=w(t,"nodes",12),g=w(t,"edges",12),p=w(t,"fitView",12,void 0),x=w(t,"fitViewOptions",12,void 0),C=w(t,"minZoom",12,void 0),$=w(t,"maxZoom",12,void 0),m=w(t,"initialViewport",12,void 0),_=w(t,"viewport",12,void 0),v=w(t,"nodeTypes",12,void 0),b=w(t,"edgeTypes",12,void 0),N=w(t,"selectionKey",12,void 0),E=w(t,"selectionMode",12,void 0),T=w(t,"panActivationKey",12,void 0),D=w(t,"multiSelectionKey",12,void 0),V=w(t,"zoomActivationKey",12,void 0),A=w(t,"nodesDraggable",12,void 0),O=w(t,"nodesConnectable",12,void 0),R=w(t,"nodeDragThreshold",12,void 0),S=w(t,"elementsSelectable",12,void 0),M=w(t,"snapGrid",12,void 0),k=w(t,"deleteKey",12,void 0),P=w(t,"connectionRadius",12,void 0),H=w(t,"connectionLineType",12,void 0),I=w(t,"connectionMode",28,()=>hr.Strict),B=w(t,"connectionLineStyle",12,""),F=w(t,"connectionLineContainerStyle",12,""),K=w(t,"onMoveStart",12,void 0),se=w(t,"onMove",12,void 0),ee=w(t,"onMoveEnd",12,void 0),W=w(t,"isValidConnection",12,void 0),fe=w(t,"translateExtent",12,void 0),me=w(t,"nodeExtent",12,void 0),Ce=w(t,"onlyRenderVisibleElements",12,void 0),he=w(t,"panOnScrollMode",28,()=>Jn.Free),ze=w(t,"preventScrolling",12,!0),G=w(t,"zoomOnScroll",12,!0),ae=w(t,"zoomOnDoubleClick",12,!0),Me=w(t,"zoomOnPinch",12,!0),Le=w(t,"panOnScroll",12,!1),Xe=w(t,"panOnDrag",12,!0),te=w(t,"selectionOnDrag",12,void 0),Fe=w(t,"autoPanOnConnect",12,!0),Oe=w(t,"autoPanOnNodeDrag",12,!0),rt=w(t,"onerror",12,void 0),oe=w(t,"ondelete",12,void 0),pe=w(t,"onedgecreate",12,void 0),be=w(t,"attributionPosition",12,void 0),Ie=w(t,"proOptions",12,void 0),ht=w(t,"defaultEdgeOptions",12,void 0),dt=w(t,"width",12,void 0),J=w(t,"height",12,void 0),Re=w(t,"colorMode",12,"light"),le=w(t,"onconnect",12,void 0),$n=w(t,"onconnectstart",12,void 0),fn=w(t,"onconnectend",12,void 0),En=w(t,"onbeforedelete",12,void 0),Te=w(t,"oninit",12,void 0),st=w(t,"nodeOrigin",12,void 0),ye=w(t,"paneClickDistance",12,0),lt=w(t,"nodeClickDistance",12,0),ct=w(t,"defaultMarkerColor",12,"#b1b1b7"),Jt=w(t,"style",12,void 0),Oo=w(t,"class",12,void 0),Wt=re(),Ot=re(),Sn=re();const gn=a()||m(),mt=Df(Ri)?Ue():r2({nodes:q(d()),edges:q(g()),width:dt(),height:J(),fitView:p(),nodeOrigin:st(),nodeExtent:me()});rn(()=>(mt.width.set(h(Ot)),mt.height.set(h(Sn)),mt.domNode.set(h(Wt)),mt.syncNodeStores(d()),mt.syncEdgeStores(g()),mt.syncViewport(_()),p()!==void 0&&mt.fitViewOnInit.set(p()),x()&&mt.fitViewOptions.set(x()),Ec(mt,{nodeTypes:v(),edgeTypes:b(),minZoom:C(),maxZoom:$(),translateExtent:fe(),paneClickDistance:ye()}),()=>{mt.reset()}));const{initialized:xa}=mt;let yr=re(!1);ge(()=>(h(Ot),h(Sn)),()=>{h(Ot)!==void 0&&h(Sn)!==void 0&&(mt.width.set(h(Ot)),mt.height.set(h(Sn)))}),ge(()=>(h(yr),l(),j(Te())),()=>{var Y;!h(yr)&&l()&&((Y=Te())==null||Y(),U(yr,!0))}),ge(()=>(j(f()),j(H()),j(P()),j(E()),j(M()),j(ct()),j(A()),j(O()),j(S()),j(Ce()),j(W()),j(Fe()),j(Oe()),j(rt()),j(oe()),j(pe()),j(I()),j(R()),j(le()),j($n()),j(fn()),j(En()),j(st()),Sc),()=>{const Y={flowId:f(),connectionLineType:H(),connectionRadius:P(),selectionMode:E(),snapGrid:M(),defaultMarkerColor:ct(),nodesDraggable:A(),nodesConnectable:O(),elementsSelectable:S(),onlyRenderVisibleElements:Ce(),isValidConnection:W(),autoPanOnConnect:Fe(),autoPanOnNodeDrag:Oe(),onerror:rt(),ondelete:oe(),onedgecreate:pe(),connectionMode:I(),nodeDragThreshold:R(),onconnect:le(),onconnectstart:$n(),onconnectend:fn(),onbeforedelete:En(),nodeOrigin:st()};Sc(mt,Y)}),ge(()=>(j(v()),j(b()),j(C()),j($()),j(fe()),j(ye())),()=>{Ec(mt,{nodeTypes:v(),edgeTypes:b(),minZoom:C(),maxZoom:$(),translateExtent:fe(),paneClickDistance:ye()})}),ge(()=>j(Re()),()=>{l1(U(c,T2(Re())),"$colorModeClass",i)}),vt(),He();var hn=V2();let ji;var Ji=X(hn);Cc(Ji,{get selectionKey(){return N()},get deleteKey(){return k()},get panActivationKey(){return T()},get multiSelectionKey(){return D()},get zoomActivationKey(){return V()}});var Qi=z(Ji,2);const jy=ve(()=>he()===void 0?Jn.Free:he()),Jy=ve(()=>ze()===void 0?!0:ze()),Qy=ve(()=>G()===void 0?!0:G()),ew=ve(()=>ae()===void 0?!0:ae()),tw=ve(()=>Me()===void 0?!0:Me()),nw=ve(()=>Le()===void 0?!1:Le()),rw=ve(()=>Xe()===void 0?!0:Xe()),ow=ve(()=>ye()===void 0?0:ye());uc(Qi,{initialViewport:gn,get onMoveStart(){return K()},get onMove(){return se()},get onMoveEnd(){return ee()},get panOnScrollMode(){return h(jy)},get preventScrolling(){return h(Jy)},get zoomOnScroll(){return h(Qy)},get zoomOnDoubleClick(){return h(ew)},get zoomOnPinch(){return h(tw)},get panOnScroll(){return h(nw)},get panOnDrag(){return h(rw)},get paneClickDistance(){return h(ow)},children:(Y,pw)=>{const aw=ve(()=>Xe()===void 0?!0:Xe());fc(Y,{get panOnDrag(){return h(aw)},get selectionOnDrag(){return te()},$$events:{paneclick(Io){De.call(this,t,Io)},panecontextmenu(Io){De.call(this,t,Io)}},children:(Io,mw)=>{var Vd=H2(),Dd=xe(Vd);gc(Dd,{children:(uw,yw)=>{var Ad=M2(),Ld=xe(Ad);_c(Ld,{get defaultEdgeOptions(){return ht()},$$events:{edgeclick(Be){De.call(this,t,Be)},edgecontextmenu(Be){De.call(this,t,Be)},edgemouseenter(Be){De.call(this,t,Be)},edgemouseleave(Be){De.call(this,t,Be)}}});var Od=z(Ld,2);kc(Od,{get containerStyle(){return F()},get style(){return B()},isCustomComponent:n.connectionLine,$$slots:{connectionLine:(Be,ww)=>{var zd=tt(),dw=xe(zd);wt(dw,t,"connectionLine",{}),L(Be,zd)}}});var Id=z(Od,6);vc(Id,{get nodeClickDistance(){return lt()},$$events:{nodeclick(Be){De.call(this,t,Be)},nodemouseenter(Be){De.call(this,t,Be)},nodemousemove(Be){De.call(this,t,Be)},nodemouseleave(Be){De.call(this,t,Be)},nodedragstart(Be){De.call(this,t,Be)},nodedrag(Be){De.call(this,t,Be)},nodedragstop(Be){De.call(this,t,Be)},nodecontextmenu(Be){De.call(this,t,Be)}}});var cw=z(Id,2);bc(cw,{$$events:{selectionclick(Be){De.call(this,t,Be)},selectioncontextmenu(Be){De.call(this,t,Be)},nodedragstart(Be){De.call(this,t,Be)},nodedrag(Be){De.call(this,t,Be)},nodedragstop(Be){De.call(this,t,Be)}}}),L(uw,Ad)},$$slots:{default:!0}});var lw=z(Dd,2);xc(lw,{}),L(Io,Vd)},$$slots:{default:!0}})},$$slots:{default:!0}});var Hd=z(Qi,2);$c(Hd,{get proOptions(){return Ie()},get position(){return be()}});var iw=z(Hd,2);wt(iw,t,"default",{}),Z(hn),An(hn,Y=>U(Wt,Y),()=>h(Wt)),Ee(Y=>ji=nn(hn,ji,{style:Jt(),class:Y,"data-testid":"svelte-flow__wrapper",...o,role:"application"},"svelte-12wlba6"),[()=>Et(["svelte-flow",Oo(),u()])],ve),ml(hn,"clientWidth",Y=>U(Ot,Y)),ml(hn,"clientHeight",Y=>U(Sn,Y)),Ze("dragover",hn,function(Y){De.call(this,t,Y)}),Ze("drop",hn,function(Y){De.call(this,t,Y)}),L(e,hn);var sw=ce({get id(){return f()},set id(Y){f(Y),y()},get nodes(){return d()},set nodes(Y){d(Y),y()},get edges(){return g()},set edges(Y){g(Y),y()},get fitView(){return p()},set fitView(Y){p(Y),y()},get fitViewOptions(){return x()},set fitViewOptions(Y){x(Y),y()},get minZoom(){return C()},set minZoom(Y){C(Y),y()},get maxZoom(){return $()},set maxZoom(Y){$(Y),y()},get initialViewport(){return m()},set initialViewport(Y){m(Y),y()},get viewport(){return _()},set viewport(Y){_(Y),y()},get nodeTypes(){return v()},set nodeTypes(Y){v(Y),y()},get edgeTypes(){return b()},set edgeTypes(Y){b(Y),y()},get selectionKey(){return N()},set selectionKey(Y){N(Y),y()},get selectionMode(){return E()},set selectionMode(Y){E(Y),y()},get panActivationKey(){return T()},set panActivationKey(Y){T(Y),y()},get multiSelectionKey(){return D()},set multiSelectionKey(Y){D(Y),y()},get zoomActivationKey(){return V()},set zoomActivationKey(Y){V(Y),y()},get nodesDraggable(){return A()},set nodesDraggable(Y){A(Y),y()},get nodesConnectable(){return O()},set nodesConnectable(Y){O(Y),y()},get nodeDragThreshold(){return R()},set nodeDragThreshold(Y){R(Y),y()},get elementsSelectable(){return S()},set elementsSelectable(Y){S(Y),y()},get snapGrid(){return M()},set snapGrid(Y){M(Y),y()},get deleteKey(){return k()},set deleteKey(Y){k(Y),y()},get connectionRadius(){return P()},set connectionRadius(Y){P(Y),y()},get connectionLineType(){return H()},set connectionLineType(Y){H(Y),y()},get connectionMode(){return I()},set connectionMode(Y){I(Y),y()},get connectionLineStyle(){return B()},set connectionLineStyle(Y){B(Y),y()},get connectionLineContainerStyle(){return F()},set connectionLineContainerStyle(Y){F(Y),y()},get onMoveStart(){return K()},set onMoveStart(Y){K(Y),y()},get onMove(){return se()},set onMove(Y){se(Y),y()},get onMoveEnd(){return ee()},set onMoveEnd(Y){ee(Y),y()},get isValidConnection(){return W()},set isValidConnection(Y){W(Y),y()},get translateExtent(){return fe()},set translateExtent(Y){fe(Y),y()},get nodeExtent(){return me()},set nodeExtent(Y){me(Y),y()},get onlyRenderVisibleElements(){return Ce()},set onlyRenderVisibleElements(Y){Ce(Y),y()},get panOnScrollMode(){return he()},set panOnScrollMode(Y){he(Y),y()},get preventScrolling(){return ze()},set preventScrolling(Y){ze(Y),y()},get zoomOnScroll(){return G()},set zoomOnScroll(Y){G(Y),y()},get zoomOnDoubleClick(){return ae()},set zoomOnDoubleClick(Y){ae(Y),y()},get zoomOnPinch(){return Me()},set zoomOnPinch(Y){Me(Y),y()},get panOnScroll(){return Le()},set panOnScroll(Y){Le(Y),y()},get panOnDrag(){return Xe()},set panOnDrag(Y){Xe(Y),y()},get selectionOnDrag(){return te()},set selectionOnDrag(Y){te(Y),y()},get autoPanOnConnect(){return Fe()},set autoPanOnConnect(Y){Fe(Y),y()},get autoPanOnNodeDrag(){return Oe()},set autoPanOnNodeDrag(Y){Oe(Y),y()},get onerror(){return rt()},set onerror(Y){rt(Y),y()},get ondelete(){return oe()},set ondelete(Y){oe(Y),y()},get onedgecreate(){return pe()},set onedgecreate(Y){pe(Y),y()},get attributionPosition(){return be()},set attributionPosition(Y){be(Y),y()},get proOptions(){return Ie()},set proOptions(Y){Ie(Y),y()},get defaultEdgeOptions(){return ht()},set defaultEdgeOptions(Y){ht(Y),y()},get width(){return dt()},set width(Y){dt(Y),y()},get height(){return J()},set height(Y){J(Y),y()},get colorMode(){return Re()},set colorMode(Y){Re(Y),y()},get onconnect(){return le()},set onconnect(Y){le(Y),y()},get onconnectstart(){return $n()},set onconnectstart(Y){$n(Y),y()},get onconnectend(){return fn()},set onconnectend(Y){fn(Y),y()},get onbeforedelete(){return En()},set onbeforedelete(Y){En(Y),y()},get oninit(){return Te()},set oninit(Y){Te(Y),y()},get nodeOrigin(){return st()},set nodeOrigin(Y){st(Y),y()},get paneClickDistance(){return ye()},set paneClickDistance(Y){ye(Y),y()},get nodeClickDistance(){return lt()},set nodeClickDistance(Y){lt(Y),y()},get defaultMarkerColor(){return ct()},set defaultMarkerColor(Y){ct(Y),y()},get style(){return Jt()},set style(Y){Jt(Y),y()},get class(){return Oo()},set class(Y){Oo(Y),y()}});return s(),sw}ie(Pc,{id:{},nodes:{},edges:{},fitView:{},fitViewOptions:{},minZoom:{},maxZoom:{},initialViewport:{},viewport:{},nodeTypes:{},edgeTypes:{},selectionKey:{},selectionMode:{},panActivationKey:{},multiSelectionKey:{},zoomActivationKey:{},nodesDraggable:{},nodesConnectable:{},nodeDragThreshold:{},elementsSelectable:{},snapGrid:{},deleteKey:{},connectionRadius:{},connectionLineType:{},connectionMode:{},connectionLineStyle:{},connectionLineContainerStyle:{},onMoveStart:{},onMove:{},onMoveEnd:{},isValidConnection:{},translateExtent:{},nodeExtent:{},onlyRenderVisibleElements:{},panOnScrollMode:{},preventScrolling:{},zoomOnScroll:{},zoomOnDoubleClick:{},zoomOnPinch:{},panOnScroll:{},panOnDrag:{},selectionOnDrag:{},autoPanOnConnect:{},autoPanOnNodeDrag:{},onerror:{},ondelete:{},onedgecreate:{},attributionPosition:{},proOptions:{},defaultEdgeOptions:{},width:{},height:{},colorMode:{},onconnect:{},onconnectstart:{},onconnectend:{},onbeforedelete:{},oninit:{},nodeOrigin:{},paneClickDistance:{},nodeClickDistance:{},defaultMarkerColor:{},style:{},class:{}},["connectionLine","default"],[],!0);function Nc(e,t){ue(t,!1);let n=w(t,"initialNodes",12,void 0),r=w(t,"initialEdges",12,void 0),o=w(t,"initialWidth",12,void 0),i=w(t,"initialHeight",12,void 0),s=w(t,"fitView",12,void 0),a=w(t,"nodeOrigin",12,void 0);const l=lc({nodes:n(),edges:r(),width:o(),height:i(),nodeOrigin:a(),fitView:s()});Sr(Ri,{getStore:()=>l}),$s(()=>{l.reset()}),He();var u=tt(),c=xe(u);return wt(c,t,"default",{}),L(e,u),ce({get initialNodes(){return n()},set initialNodes(f){n(f),y()},get initialEdges(){return r()},set initialEdges(f){r(f),y()},get initialWidth(){return o()},set initialWidth(f){o(f),y()},get initialHeight(){return i()},set initialHeight(f){i(f),y()},get fitView(){return s()},set fitView(f){s(f),y()},get nodeOrigin(){return a()},set nodeOrigin(f){a(f),y()}})}ie(Nc,{initialNodes:{},initialEdges:{},initialWidth:{},initialHeight:{},fitView:{},nodeOrigin:{}},["default"],[],!0);var A2=ne("<button><!></button>");function Po(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]),r=it(n,["class","bgColor","bgColorHover","color","colorHover","borderColor"]);ue(t,!1);let o=w(t,"class",12,void 0),i=w(t,"bgColor",12,void 0),s=w(t,"bgColorHover",12,void 0),a=w(t,"color",12,void 0),l=w(t,"colorHover",12,void 0),u=w(t,"borderColor",12,void 0);He();var c=A2();let f;var d=X(c);return wt(d,t,"default",{class:"button-svg"}),Z(c),Ee(g=>{f=nn(c,f,{type:"button",class:g,...r}),at(c,"--xy-controls-button-background-color-props",i()),at(c,"--xy-controls-button-background-color-hover-props",s()),at(c,"--xy-controls-button-color-props",a()),at(c,"--xy-controls-button-color-hover-props",l()),at(c,"--xy-controls-button-border-color-props",u())},[()=>Et(["svelte-flow__controls-button",o()])],ve),Ze("click",c,function(g){De.call(this,t,g)}),L(e,c),ce({get class(){return o()},set class(g){o(g),y()},get bgColor(){return i()},set bgColor(g){i(g),y()},get bgColorHover(){return s()},set bgColorHover(g){s(g),y()},get color(){return a()},set color(g){a(g),y()},get colorHover(){return l()},set colorHover(g){l(g),y()},get borderColor(){return u()},set borderColor(g){u(g),y()}})}ie(Po,{class:{},bgColor:{},bgColorHover:{},color:{},colorHover:{},borderColor:{}},["default"],[],!0);var L2=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"></path></svg>');function Tc(e){var t=L2();L(e,t)}ie(Tc,{},[],[],!0);var O2=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5"><path d="M0 0h32v4.2H0z"></path></svg>');function Mc(e){var t=O2();L(e,t)}ie(Mc,{},[],[],!0);var I2=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30"><path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z"></path></svg>');function Hc(e){var t=I2();L(e,t)}ie(Hc,{},[],[],!0);var z2=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32"><path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z"></path></svg>');function Vc(e){var t=z2();L(e,t)}ie(Vc,{},[],[],!0);var R2=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32"><path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z"></path></svg>');function Dc(e){var t=R2();L(e,t)}ie(Dc,{},[],[],!0);var B2=ne("<!> <!>",1),Y2=ne("<!> <!> <!> <!> <!> <!>",1);function Ac(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(H,"$nodesDraggable",n),i=()=>Q(I,"$nodesConnectable",n),s=()=>Q(B,"$elementsSelectable",n),a=()=>Q(M,"$viewport",n),l=()=>Q(k,"$minZoom",n),u=()=>Q(P,"$maxZoom",n),c=re(),f=re(),d=re(),g=re();let p=w(t,"position",12,"bottom-left"),x=w(t,"showZoom",12,!0),C=w(t,"showFitView",12,!0),$=w(t,"showLock",12,!0),m=w(t,"buttonBgColor",12,void 0),_=w(t,"buttonBgColorHover",12,void 0),v=w(t,"buttonColor",12,void 0),b=w(t,"buttonColorHover",12,void 0),N=w(t,"buttonBorderColor",12,void 0),E=w(t,"ariaLabel",12,void 0),T=w(t,"style",12,void 0),D=w(t,"orientation",12,"vertical"),V=w(t,"fitViewOptions",12,void 0),A=w(t,"class",12,"");const{zoomIn:O,zoomOut:R,fitView:S,viewport:M,minZoom:k,maxZoom:P,nodesDraggable:H,nodesConnectable:I,elementsSelectable:B}=Ue(),F={bgColor:m(),bgColorHover:_(),color:v(),colorHover:b(),borderColor:N()},K=()=>{O()},se=()=>{R()},ee=()=>{S(V())},W=()=>{U(c,!h(c)),H.set(h(c)),I.set(h(c)),B.set(h(c))};ge(()=>(o(),i(),s()),()=>{U(c,o()||i()||s())}),ge(()=>(a(),l()),()=>{U(f,a().zoom<=l())}),ge(()=>(a(),u()),()=>{U(d,a().zoom>=u())}),ge(()=>j(D()),()=>{U(g,D()==="horizontal"?"horizontal":"vertical")}),vt(),He();const fe=ve(()=>Et(["svelte-flow__controls",h(g),A()])),me=ve(()=>E()??"Svelte Flow controls");So(e,{get class(){return h(fe)},get position(){return p()},"data-testid":"svelte-flow__controls",get"aria-label"(){return h(me)},get style(){return T()},children:(he,ze)=>{var G=Y2(),ae=xe(G);wt(ae,t,"before",{});var Me=z(ae,2);{var Le=pe=>{var be=B2(),Ie=xe(be);Po(Ie,ft({class:"svelte-flow__controls-zoomin",title:"zoom in","aria-label":"zoom in",get disabled(){return h(d)}},F,{$$events:{click:K},children:(dt,J)=>{Tc(dt)},$$slots:{default:!0}}));var ht=z(Ie,2);Po(ht,ft({class:"svelte-flow__controls-zoomout",title:"zoom out","aria-label":"zoom out",get disabled(){return h(f)}},F,{$$events:{click:se},children:(dt,J)=>{Mc(dt)},$$slots:{default:!0}})),L(pe,be)};ke(Me,pe=>{x()&&pe(Le)})}var Xe=z(Me,2);{var te=pe=>{Po(pe,ft({class:"svelte-flow__controls-fitview",title:"fit view","aria-label":"fit view"},F,{$$events:{click:ee},children:(be,Ie)=>{Hc(be)},$$slots:{default:!0}}))};ke(Xe,pe=>{C()&&pe(te)})}var Fe=z(Xe,2);{var Oe=pe=>{Po(pe,ft({class:"svelte-flow__controls-interactive",title:"toggle interactivity","aria-label":"toggle interactivity"},F,{$$events:{click:W},children:(be,Ie)=>{var ht=tt(),dt=xe(ht);{var J=le=>{Dc(le)},Re=le=>{Vc(le)};ke(dt,le=>{h(c)?le(J):le(Re,!1)})}L(be,ht)},$$slots:{default:!0}}))};ke(Fe,pe=>{$()&&pe(Oe)})}var rt=z(Fe,2);wt(rt,t,"default",{});var oe=z(rt,2);wt(oe,t,"after",{}),L(he,G)},$$slots:{default:!0}});var Ce=ce({get position(){return p()},set position(he){p(he),y()},get showZoom(){return x()},set showZoom(he){x(he),y()},get showFitView(){return C()},set showFitView(he){C(he),y()},get showLock(){return $()},set showLock(he){$(he),y()},get buttonBgColor(){return m()},set buttonBgColor(he){m(he),y()},get buttonBgColorHover(){return _()},set buttonBgColorHover(he){_(he),y()},get buttonColor(){return v()},set buttonColor(he){v(he),y()},get buttonColorHover(){return b()},set buttonColorHover(he){b(he),y()},get buttonBorderColor(){return N()},set buttonBorderColor(he){N(he),y()},get ariaLabel(){return E()},set ariaLabel(he){E(he),y()},get style(){return T()},set style(he){T(he),y()},get orientation(){return D()},set orientation(he){D(he),y()},get fitViewOptions(){return V()},set fitViewOptions(he){V(he),y()},get class(){return A()},set class(he){A(he),y()}});return r(),Ce}ie(Ac,{position:{},showZoom:{},showFitView:{},showLock:{},buttonBgColor:{},buttonBgColorHover:{},buttonColor:{},buttonColorHover:{},buttonBorderColor:{},ariaLabel:{},style:{},orientation:{},fitViewOptions:{},class:{}},["before","default","after"],[],!0);var tr;(function(e){e.Lines="lines",e.Dots="dots",e.Cross="cross"})(tr||(tr={}));var Z2=_e("<circle></circle>");function Lc(e,t){ue(t,!1);let n=w(t,"radius",12,5),r=w(t,"class",12,"");He();var o=Z2();return Ee(i=>{de(o,"cx",n()),de(o,"cy",n()),de(o,"r",n()),$t(o,0,wn(i))},[()=>Et(["svelte-flow__background-pattern","dots",r()])],ve),L(e,o),ce({get radius(){return n()},set radius(i){n(i),y()},get class(){return r()},set class(i){r(i),y()}})}ie(Lc,{radius:{},class:{}},[],[],!0);var X2=_e("<path></path>");function Oc(e,t){ue(t,!1);let n=w(t,"lineWidth",12,1),r=w(t,"dimensions",12),o=w(t,"variant",12,void 0),i=w(t,"class",12,"");He();var s=X2();return Ee(a=>{de(s,"stroke-width",n()),de(s,"d",`M${r()[0]/2} 0 V${r()[1]} M0 ${r()[1]/2} H${r()[0]}`),$t(s,0,wn(a))},[()=>Et(["svelte-flow__background-pattern",o(),i()])],ve),L(e,s),ce({get lineWidth(){return n()},set lineWidth(a){n(a),y()},get dimensions(){return r()},set dimensions(a){r(a),y()},get variant(){return o()},set variant(a){o(a),y()},get class(){return i()},set class(a){i(a),y()}})}ie(Oc,{lineWidth:{},dimensions:{},variant:{},class:{}},[],[],!0);const F2={[tr.Dots]:1,[tr.Lines]:1,[tr.Cross]:6};var W2=_e('<svg data-testid="svelte-flow__background"><pattern patternUnits="userSpaceOnUse"><!></pattern><rect x="0" y="0" width="100%" height="100%"></rect></svg>');const K2={hash:"svelte-1r7pe8d",code:".svelte-flow__background.svelte-1r7pe8d {position:absolute;width:100%;height:100%;top:0;left:0;}"};function Ic(e,t){ue(t,!1),et(e,K2);const[n,r]=nt(),o=()=>Q(b,"$flowId",n),i=()=>Q(v,"$viewport",n),s=re(),a=re(),l=re(),u=re(),c=re();let f=w(t,"id",12,void 0),d=w(t,"variant",28,()=>tr.Dots),g=w(t,"gap",12,20),p=w(t,"size",12,1),x=w(t,"lineWidth",12,1),C=w(t,"bgColor",12,void 0),$=w(t,"patternColor",12,void 0),m=w(t,"patternClass",12,void 0),_=w(t,"class",12,"");const{viewport:v,flowId:b}=Ue(),N=p()||F2[d()],E=d()===tr.Dots,T=d()===tr.Cross,D=Array.isArray(g())?g():[g(),g()];ge(()=>(o(),j(f())),()=>{U(s,`background-pattern-${o()}-${f()?f():""}`)}),ge(()=>i(),()=>{U(a,[D[0]*i().zoom||1,D[1]*i().zoom||1])}),ge(()=>i(),()=>{U(l,N*i().zoom)}),ge(()=>(h(l),h(a)),()=>{U(u,T?[h(l),h(l)]:h(a))}),ge(()=>(h(l),h(u)),()=>{U(c,E?[h(l)/2,h(l)/2]:[h(u)[0]/2,h(u)[1]/2])}),vt(),He();var V=W2(),A=X(V),O=X(A);{var R=P=>{const H=ve(()=>h(l)/2);Lc(P,{get radius(){return h(H)},get class(){return m()}})},S=P=>{Oc(P,{get dimensions(){return h(u)},get variant(){return d()},get lineWidth(){return x()},get class(){return m()}})};ke(O,P=>{E?P(R):P(S,!1)})}Z(A);var M=z(A);Z(V),Ee(P=>{$t(V,0,wn(P),"svelte-1r7pe8d"),at(V,"--xy-background-color-props",C()),at(V,"--xy-background-pattern-color-props",$()),de(A,"id",h(s)),de(A,"x",i().x%h(a)[0]),de(A,"y",i().y%h(a)[1]),de(A,"width",h(a)[0]),de(A,"height",h(a)[1]),de(A,"patternTransform",`translate(-${h(c)[0]},-${h(c)[1]})`),de(M,"fill",`url(#${h(s)})`)},[()=>Et(["svelte-flow__background",_()])],ve),L(e,V);var k=ce({get id(){return f()},set id(P){f(P),y()},get variant(){return d()},set variant(P){d(P),y()},get gap(){return g()},set gap(P){g(P),y()},get size(){return p()},set size(P){p(P),y()},get lineWidth(){return x()},set lineWidth(P){x(P),y()},get bgColor(){return C()},set bgColor(P){C(P),y()},get patternColor(){return $()},set patternColor(P){$(P),y()},get patternClass(){return m()},set patternClass(P){m(P),y()},get class(){return _()},set class(P){_(P),y()}});return r(),k}ie(Ic,{id:{},variant:{},gap:{},size:{},lineWidth:{},bgColor:{},patternColor:{},patternClass:{},class:{}},[],[],!0);var q2=_e("<rect></rect>");function zc(e,t){ue(t,!1);let n=w(t,"x",12),r=w(t,"y",12),o=w(t,"width",12,0),i=w(t,"height",12,0),s=w(t,"borderRadius",12,5),a=w(t,"color",12,void 0),l=w(t,"shapeRendering",12),u=w(t,"strokeColor",12,void 0),c=w(t,"strokeWidth",12,2),f=w(t,"selected",12,!1),d=w(t,"class",12,"");He();var g=q2();let p;return Ee(x=>{p=$t(g,0,wn(x),null,p,{selected:f()}),de(g,"x",n()),de(g,"y",r()),de(g,"rx",s()),de(g,"ry",s()),de(g,"width",o()),de(g,"height",i()),de(g,"style",`${a()?`fill: ${a()};`:""}${u()?`stroke: ${u()};`:""}${c()?`stroke-width: ${c()};`:""}`),de(g,"shape-rendering",l())},[()=>Et(["svelte-flow__minimap-node",d()])],ve),L(e,g),ce({get x(){return n()},set x(x){n(x),y()},get y(){return r()},set y(x){r(x),y()},get width(){return o()},set width(x){o(x),y()},get height(){return i()},set height(x){i(x),y()},get borderRadius(){return s()},set borderRadius(x){s(x),y()},get color(){return a()},set color(x){a(x),y()},get shapeRendering(){return l()},set shapeRendering(x){l(x),y()},get strokeColor(){return u()},set strokeColor(x){u(x),y()},get strokeWidth(){return c()},set strokeWidth(x){c(x),y()},get selected(){return f()},set selected(x){f(x),y()},get class(){return d()},set class(x){d(x),y()}})}ie(zc,{x:{},y:{},width:{},height:{},borderRadius:{},color:{},shapeRendering:{},strokeColor:{},strokeWidth:{},selected:{},class:{}},[],[],!0);function ca(e,t){const n=H0({domNode:e,panZoom:t.panZoom,getTransform:()=>{const o=q(t.viewport);return[o.x,o.y,o.zoom]},getViewScale:t.getViewScale});function r(o){n.update({translateExtent:o.translateExtent,width:o.width,height:o.height,inversePan:o.inversePan,zoomStep:o.zoomStep,pannable:o.pannable,zoomable:o.zoomable})}return{update:r,destroy(){n.destroy()}}}const da=e=>e instanceof Function?e:()=>e;var G2=_e("<title> </title>"),U2=_e('<svg class="svelte-flow__minimap-svg" role="img"><!><!><path class="svelte-flow__minimap-mask" fill-rule="evenodd" pointer-events="none"></path></svg>');function Rc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(Xe,"$flowId",n),i=()=>Q(ae,"$viewport",n),s=()=>Q(Me,"$containerWidth",n),a=()=>Q(Le,"$containerHeight",n),l=()=>Q(G,"$nodeLookup",n),u=()=>Q(ze,"$nodes",n),c=()=>Q(te,"$panZoom",n),f=()=>Q(Fe,"$translateExtent",n),d=re(),g=re(),p=re(),x=re(),C=re(),$=re(),m=re(),_=re(),v=re(),b=re(),N=re(),E=re(),T=re();let D=w(t,"position",12,"bottom-right"),V=w(t,"ariaLabel",12,"Mini map"),A=w(t,"nodeStrokeColor",12,"transparent"),O=w(t,"nodeColor",12,void 0),R=w(t,"nodeClass",12,""),S=w(t,"nodeBorderRadius",12,5),M=w(t,"nodeStrokeWidth",12,2),k=w(t,"bgColor",12,void 0),P=w(t,"maskColor",12,void 0),H=w(t,"maskStrokeColor",12,void 0),I=w(t,"maskStrokeWidth",12,void 0),B=w(t,"width",12,void 0),F=w(t,"height",12,void 0),K=w(t,"pannable",12,!0),se=w(t,"zoomable",12,!0),ee=w(t,"inversePan",12,void 0),W=w(t,"zoomStep",12,void 0),fe=w(t,"style",12,""),me=w(t,"class",12,"");const Ce=200,he=150,{nodes:ze,nodeLookup:G,viewport:ae,width:Me,height:Le,flowId:Xe,panZoom:te,translateExtent:Fe}=Ue(),Oe=O()===void 0?void 0:da(O()),rt=da(A()),oe=da(R()),pe=typeof window>"u"||window.chrome?"crispEdges":"geometricPrecision",be=`svelte-flow__minimap-desc-${o()}`;let Ie=re(h(d));const ht=()=>h($);ge(()=>(i(),s(),a()),()=>{U(d,{x:-i().x/i().zoom,y:-i().y/i().zoom,width:s()/i().zoom,height:a()/i().zoom})}),ge(()=>(l(),h(d),u()),()=>{U(Ie,l().size>0?Cu(bo(l()),h(d)):h(d)),u()}),ge(()=>j(B()),()=>{U(g,B()??Ce)}),ge(()=>j(F()),()=>{U(p,F()??he)}),ge(()=>(h(Ie),h(g)),()=>{U(x,h(Ie).width/h(g))}),ge(()=>(h(Ie),h(p)),()=>{U(C,h(Ie).height/h(p))}),ge(()=>(h(x),h(C)),()=>{U($,Math.max(h(x),h(C)))}),ge(()=>(h($),h(g)),()=>{U(m,h($)*h(g))}),ge(()=>(h($),h(p)),()=>{U(_,h($)*h(p))}),ge(()=>h($),()=>{U(v,5*h($))}),ge(()=>(h(Ie),h(m),h(v)),()=>{U(b,h(Ie).x-(h(m)-h(Ie).width)/2-h(v))}),ge(()=>(h(Ie),h(_),h(v)),()=>{U(N,h(Ie).y-(h(_)-h(Ie).height)/2-h(v))}),ge(()=>(h(m),h(v)),()=>{U(E,h(m)+h(v)*2)}),ge(()=>(h(_),h(v)),()=>{U(T,h(_)+h(v)*2)}),vt(),He();const dt=ve(()=>fe()+(k()?`;--xy-minimap-background-color-props:${k()}`:"")),J=ve(()=>Et(["svelte-flow__minimap",me()]));So(e,{get position(){return D()},get style(){return h(dt)},get class(){return h(J)},"data-testid":"svelte-flow__minimap",children:(le,$n)=>{var fn=tt(),En=xe(fn);{var Te=st=>{var ye=U2();de(ye,"aria-labelledby",be);var lt=X(ye);{var ct=Wt=>{var Ot=G2();de(Ot,"id",be);var Sn=X(Ot,!0);Z(Ot),Ee(()=>Bt(Sn,V())),L(Wt,Ot)};ke(lt,Wt=>{V()&&Wt(ct)})}var Jt=z(lt);Yt(Jt,1,u,Wt=>Wt.id,(Wt,Ot)=>{var Sn=tt();const gn=ve(()=>l().get(h(Ot).id));var mt=xe(Sn);{var xa=yr=>{const hn=ve(()=>Qn(h(gn))),ji=ve(()=>Oe==null?void 0:Oe(h(gn))),Ji=ve(()=>rt(h(gn))),Qi=ve(()=>oe(h(gn)));zc(yr,ft({get x(){return h(gn).internals.positionAbsolute.x},get y(){return h(gn).internals.positionAbsolute.y}},()=>h(hn),{get selected(){return h(gn).selected},get color(){return h(ji)},get borderRadius(){return S()},get strokeColor(){return h(Ji)},get strokeWidth(){return M()},shapeRendering:pe,get class(){return h(Qi)}}))};ke(mt,yr=>{h(gn)&&Eu(h(gn))&&yr(xa)})}L(Wt,Sn)});var Oo=z(Jt);Z(ye),_t(ye,(Wt,Ot)=>ca==null?void 0:ca(Wt,Ot),()=>({panZoom:c(),viewport:ae,getViewScale:ht,translateExtent:f(),width:s(),height:a(),inversePan:ee(),zoomStep:W(),pannable:K(),zoomable:se()})),Ee(()=>{de(ye,"width",h(g)),de(ye,"height",h(p)),de(ye,"viewBox",`${h(b)??""} ${h(N)??""} ${h(E)??""} ${h(T)??""}`),at(ye,"--xy-minimap-mask-background-color-props",P()),at(ye,"--xy-minimap-mask-stroke-color-props",H()),at(ye,"--xy-minimap-mask-stroke-width-props",I()?I()*h($):void 0),de(Oo,"d",`M${h(b)-h(v)},${h(N)-h(v)}h${h(E)+h(v)*2}v${h(T)+h(v)*2}h${-h(E)-h(v)*2}z
+ M${h(d).x??""},${h(d).y??""}h${h(d).width??""}v${h(d).height??""}h${-h(d).width}z`)}),L(st,ye)};ke(En,st=>{c()&&st(Te)})}L(le,fn)},$$slots:{default:!0}});var Re=ce({get position(){return D()},set position(le){D(le),y()},get ariaLabel(){return V()},set ariaLabel(le){V(le),y()},get nodeStrokeColor(){return A()},set nodeStrokeColor(le){A(le),y()},get nodeColor(){return O()},set nodeColor(le){O(le),y()},get nodeClass(){return R()},set nodeClass(le){R(le),y()},get nodeBorderRadius(){return S()},set nodeBorderRadius(le){S(le),y()},get nodeStrokeWidth(){return M()},set nodeStrokeWidth(le){M(le),y()},get bgColor(){return k()},set bgColor(le){k(le),y()},get maskColor(){return P()},set maskColor(le){P(le),y()},get maskStrokeColor(){return H()},set maskStrokeColor(le){H(le),y()},get maskStrokeWidth(){return I()},set maskStrokeWidth(le){I(le),y()},get width(){return B()},set width(le){B(le),y()},get height(){return F()},set height(le){F(le),y()},get pannable(){return K()},set pannable(le){K(le),y()},get zoomable(){return se()},set zoomable(le){se(le),y()},get inversePan(){return ee()},set inversePan(le){ee(le),y()},get zoomStep(){return W()},set zoomStep(le){W(le),y()},get style(){return fe()},set style(le){fe(le),y()},get class(){return me()},set class(le){me(le),y()}});return r(),Re}ie(Rc,{position:{},ariaLabel:{},nodeStrokeColor:{},nodeColor:{},nodeClass:{},nodeBorderRadius:{},nodeStrokeWidth:{},bgColor:{},maskColor:{},maskStrokeColor:{},maskStrokeWidth:{},width:{},height:{},pannable:{},zoomable:{},inversePan:{},zoomStep:{},style:{},class:{}},[],[],!0);const Bc=e=>Uv(e);function Lt(){const{zoomIn:e,zoomOut:t,fitView:n,onbeforedelete:r,snapGrid:o,viewport:i,width:s,height:a,minZoom:l,maxZoom:u,panZoom:c,nodes:f,edges:d,domNode:g,nodeLookup:p,nodeOrigin:x,edgeLookup:C,connectionLookup:$}=Ue(),m=b=>{var V,A;const N=q(p),E=Bc(b)?b:N.get(b.id),T=E.parentId?e0(E.position,E.measured,E.parentId,N,q(x)):E.position,D={...E,position:T,width:((V=E.measured)==null?void 0:V.width)??E.width,height:((A=E.measured)==null?void 0:A.height)??E.height};return Or(D)},_=(b,N,E={replace:!1})=>{var V;const T=(V=q(p).get(b))==null?void 0:V.internals.userNode;if(!T)return;const D=typeof N=="function"?N(T):N;E.replace?f.update(A=>A.map(O=>O.id===b?Bc(D)?D:{...O,...D}:O)):(Object.assign(T,D),f.update(A=>A))},v=b=>q(p).get(b);return{zoomIn:e,zoomOut:t,getInternalNode:v,getNode:b=>{var N;return(N=v(b))==null?void 0:N.internals.userNode},getNodes:b=>b===void 0?q(f):Yc(q(p),b),getEdge:b=>q(C).get(b),getEdges:b=>b===void 0?q(d):Yc(q(C),b),setZoom:(b,N)=>{const E=q(c);return E?E.scaleTo(b,{duration:N==null?void 0:N.duration}):Promise.resolve(!1)},getZoom:()=>q(i).zoom,setViewport:async(b,N)=>{const E=q(i),T=q(c);return T?(await T.setViewport({x:b.x??E.x,y:b.y??E.y,zoom:b.zoom??E.zoom},{duration:N==null?void 0:N.duration}),Promise.resolve(!0)):Promise.resolve(!1)},getViewport:()=>q(i),setCenter:async(b,N,E)=>{const T=typeof(E==null?void 0:E.zoom)<"u"?E.zoom:q(u),D=q(c);return D?(await D.setViewport({x:q(s)/2-b*T,y:q(a)/2-N*T,zoom:T},{duration:E==null?void 0:E.duration}),Promise.resolve(!0)):Promise.resolve(!1)},fitView:n,fitBounds:async(b,N)=>{const E=q(c);if(!E)return Promise.resolve(!1);const T=js(b,q(s),q(a),q(l),q(u),(N==null?void 0:N.padding)??.1);return await E.setViewport(T,{duration:N==null?void 0:N.duration}),Promise.resolve(!0)},getIntersectingNodes:(b,N=!0,E)=>{const T=ku(b),D=T?b:m(b);return D?(E||q(f)).filter(V=>{const A=q(p).get(V.id);if(!A||!T&&V.id===b.id)return!1;const O=Or(A),R=Co(O,D);return N&&R>0||R>=D.width*D.height}):[]},isNodeIntersecting:(b,N,E=!0)=>{const D=ku(b)?b:m(b);if(!D)return!1;const V=Co(D,N);return E&&V>0||V>=D.width*D.height},deleteElements:async({nodes:b=[],edges:N=[]})=>{const{nodes:E,edges:T}=await wu({nodesToRemove:b,edgesToRemove:N,nodes:q(f),edges:q(d),onBeforeDelete:q(r)});return E&&f.update(D=>D.filter(V=>!E.some(({id:A})=>A===V.id))),T&&d.update(D=>D.filter(V=>!T.some(({id:A})=>A===V.id))),{deletedNodes:E,deletedEdges:T}},screenToFlowPosition:(b,N={snapToGrid:!0})=>{const E=q(g);if(!E)return b;const T=N.snapToGrid?q(o):!1,{x:D,y:V,zoom:A}=q(i),{x:O,y:R}=E.getBoundingClientRect(),S={x:b.x-O,y:b.y-R};return ko(S,[D,V,A],T!==null,T||[1,1])},flowToScreenPosition:b=>{const N=q(g);if(!N)return b;const{x:E,y:T,zoom:D}=q(i),{x:V,y:A}=N.getBoundingClientRect(),O=$u(b,[E,T,D]);return{x:O.x+V,y:O.y+A}},toObject:()=>({nodes:q(f).map(b=>({...b,position:{...b.position},data:{...b.data}})),edges:q(d).map(b=>({...b})),viewport:{...q(i)}}),updateNode:_,updateNodeData:(b,N,E)=>{var V;const T=(V=q(p).get(b))==null?void 0:V.internals.userNode;if(!T)return;const D=typeof N=="function"?N(T):N;T.data=E!=null&&E.replace?D:{...T.data,...D},f.update(A=>A)},getNodesBounds:b=>{const N=q(p),E=q(x);return jv(b,{nodeLookup:N,nodeOrigin:E})},getHandleConnections:({type:b,id:N,nodeId:E})=>{var T;return Array.from(((T=q($).get(`${E}-${b}-${N??null}`))==null?void 0:T.values())??[])},viewport:i}}function Yc(e,t){var r;const n=[];for(const o of t){const i=e.get(o);if(i){const s="internals"in i?(r=i.internals)==null?void 0:r.userNode:i;n.push(s)}}return n}var j2=ne('<div class="svelte-flow__node-toolbar"><!></div>');function Zc(e,t){ue(t,!1);const[n,r]=nt(),o=()=>Q(_,"$nodes",n),i=()=>Q(m,"$nodeLookup",n),s=()=>Q($,"$viewport",n),a=()=>Q(C,"$domNode",n),l=re(),u=re(),c=re();let f=w(t,"nodeId",12,void 0),d=w(t,"position",12,void 0),g=w(t,"align",12,void 0),p=w(t,"offset",12,void 0),x=w(t,"isVisible",12,void 0);const{domNode:C,viewport:$,nodeLookup:m,nodes:_}=Ue(),{getNodesBounds:v}=Lt(),b=ur("svelteflow__node_id");let N=re(),E=re([]),T=p()!==void 0?p():10,D=d()!==void 0?d():$e.Top,V=g()!==void 0?g():"center";ge(()=>(o(),j(f()),i()),()=>{o();const M=Array.isArray(f())?f():[f()||b];U(E,M.reduce((k,P)=>{const H=i().get(P);return H&&k.push(H),k},[]))}),ge(()=>(h(E),s()),()=>{const M=v(h(E));M&&U(N,v0(M,s(),D,T,V))}),ge(()=>h(E),()=>{U(l,h(E).length===0?1:Math.max(...h(E).map(M=>(M.internals.z||5)+1)))}),ge(()=>o(),()=>{U(u,o().filter(M=>M.selected).length)}),ge(()=>(j(x()),h(E),h(u)),()=>{U(c,typeof x()=="boolean"?x():h(E).length===1&&h(E)[0].selected&&h(u)===1)}),vt(),He();var A=tt(),O=xe(A);{var R=M=>{var k=j2(),P=X(k);wt(P,t,"default",{}),Z(k),_t(k,(H,I)=>Rr==null?void 0:Rr(H,I),()=>({domNode:a()})),Ee(H=>{de(k,"data-id",H),at(k,"position","absolute"),at(k,"transform",h(N)),at(k,"z-index",h(l))},[()=>h(E).reduce((H,I)=>`${H}${I.id} `,"").trim()],ve),L(M,k)};ke(O,M=>{a()&&h(c)&&h(E)&&M(R)})}L(e,A);var S=ce({get nodeId(){return f()},set nodeId(M){f(M),y()},get position(){return d()},set position(M){d(M),y()},get align(){return g()},set align(M){g(M),y()},get offset(){return p()},set offset(M){p(M),y()},get isVisible(){return x()},set isVisible(M){x(M),y()}});return r(),S}ie(Zc,{nodeId:{},position:{},align:{},offset:{},isVisible:{}},["default"],[],!0);function pr(e){const{nodes:t,nodeLookup:n}=Ue();let r=[],o=!0;return Un([t,n],([,i],s)=>{var c;const a=[],l=Array.isArray(e),u=l?e:[e];for(const f of u){const d=(c=i.get(f))==null?void 0:c.internals.userNode;d&&a.push({id:d.id,type:d.type,data:d.data})}(!C0(a,r)||o)&&(r=a,s(l?a:a[0]??null),o=!1)})}const Xc="tinyflow-component";class J2{constructor(t){Nt(this,"options");Nt(this,"rootEl");Nt(this,"svelteFlowInstance");if(typeof t.element!="string"&&!(t.element instanceof Element))throw new Error("element must be a string or Element");this._setOptions(t),this._init()}_init(){if(typeof this.options.element=="string"){if(this.rootEl=document.querySelector(this.options.element),!this.rootEl)throw new Error(`element not found by document.querySelector('${this.options.element}')`)}else if(this.options.element instanceof Element)this.rootEl=this.options.element;else throw new Error("element must be a string or Element");const t=document.createElement(Xc);t.style.display="block",t.style.width="100%",t.style.height="100%",t.classList.add("tf-theme-light"),t.options=this.options,t.onInit=n=>{this.svelteFlowInstance=n},this.rootEl.appendChild(t)}_setOptions(t){this.options={...t}}getOptions(){return this.options}getData(){return this.svelteFlowInstance.toObject()}setData(t){this.options.data=t;const n=document.createElement(Xc);n.style.display="block",n.style.width="100%",n.style.height="100%",n.classList.add("tf-theme-light"),n.options=this.options,n.onInit=r=>{this.svelteFlowInstance=r},this.destroy(),this.rootEl.appendChild(n)}destroy(){for(;this.rootEl.firstChild;)this.rootEl.removeChild(this.rootEl.firstChild)}}const Bi=(()=>{const e=we([]),t=we([]),n=we({x:250,y:100,zoom:1});return{nodes:e,edges:t,viewport:n,init:(r,o)=>{e.set(r),t.set(o)},addNode:r=>{e.update(o=>[...o,r])},removeNode:r=>{e.update(o=>o.filter(i=>i.id!==r))},updateNode:(r,o)=>{e.update(i=>i.map(s=>s.id===r?o:s))},updateNodeData:(r,o)=>{e.update(i=>i.map(s=>s.id===r?{...s,data:{...s.data,...o}}:s))},selectNodeOnly:r=>{e.update(o=>o.map(i=>i.id===r?{...i,selected:!0}:{...i,selected:!1}))},addEdge:r=>{t.update(o=>[...o,r])},removeEdge:r=>{t.update(o=>o.filter(i=>i.id!==r))},updateEdge:(r,o)=>{t.update(i=>i.map(s=>s.id===r?o:s))},updateEdgeData:(r,o)=>{t.update(i=>i.map(s=>s.id===r?{...s,data:o}:s))}}})();var Q2=ne("<button><!></button>");function Ge(e,t){ue(t,!0);const n=w(t,"children",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","children"]);var o=Q2();let i;var s=X(o);return cr(s,()=>n()??gt),Z(o),Ee(()=>i=nn(o,i,{type:"button",...r,class:`tf-btn nopan nodrag ${t.class??""}`})),L(e,o),ce({get children(){return n()},set children(a){n(a),y()}})}ie(Ge,{children:{}},[],[],!0);var ep=ne("<input>");function Fc(e,t){ue(t,!0);const n=xt(t,["$$slots","$$events","$$legacy","$$host"]);var r=ep();ao(r);let o;Ee(()=>o=nn(r,o,{type:"checkbox",...n,class:`tf-checkbox nopan nodrag ${t.class??""}`})),L(e,r),ce()}ie(Fc,{},[],[],!0);var tp=ne("<input>");function St(e,t){ue(t,!0);const n=xt(t,["$$slots","$$events","$$legacy","$$host"]);var r=tp();ao(r);let o;Ee(()=>o=nn(r,o,{type:"text",...n,class:`tf-input nopan nodrag ${t.class??""}`})),L(e,r),ce()}ie(St,{},[],[],!0);var np=ne("<textarea></textarea>");function Pt(e,t){ue(t,!0);const n=xt(t,["$$slots","$$events","$$legacy","$$host"]);var r=np();Wf(r);let o;Ee(()=>o=nn(r,o,{...n,class:`tf-textarea nodrag ${t.class??""}`})),L(e,r),ce()}ie(Pt,{},[],[],!0);var rp=ne('<div role="button"><!></div>'),op=ne("<div></div>");function Wc(e,t){const n=it(t,["children","$$slots","$$events","$$legacy","$$host"]),r=it(n,["items","onChange","activeIndex"]);ue(t,!1);let o=w(t,"items",28,()=>[]),i=w(t,"onChange",12,()=>{}),s=w(t,"activeIndex",12,0);function a(c,f){var d;s(f),(d=i())==null||d(c,f)}He();var l=op();let u;return Yt(l,5,o,oi,(c,f,d)=>{var g=rp();de(g,"tabindex",d),g.__click=()=>a(h(f),d),g.__keydown=$=>{($.key==="Enter"||$.key===" ")&&($.preventDefault(),a(h(f),d))};var p=X(g);{var x=$=>{var m=Ae();Ee(()=>Bt(m,h(f).label)),L($,m)},C=$=>{var m=tt(),_=xe(m);cr(_,()=>h(f).label??gt),L($,m)};ke(p,$=>{typeof h(f).label=="string"?$(x):$(C,!1)})}Z(g),Ee(()=>$t(g,1,`tf-tabs-item ${(d===s()?"active":"")??""}`)),L(c,g)}),Z(l),Ee(()=>u=nn(l,u,{...r,class:`tf-tabs ${r.class??""}`})),L(e,l),ce({get items(){return o()},set items(c){o(c),y()},get onChange(){return i()},set onChange(c){i(c),y()},get activeIndex(){return s()},set activeIndex(c){s(c),y()}})}ri(["click","keydown"]),ie(Wc,{items:{},onChange:{},activeIndex:{}},[],[],!0);var ip=(e,t,n)=>t(h(n)),sp=(e,t,n)=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),t(h(n)))},ap=ne('<span class="tf-collapse-item-title-icon"><!></span>'),lp=ne('<div class="tf-collapse-item-description"><!></div>'),up=ne('<div class="tf-collapse-item-content"><!></div>'),cp=ne('<div class="tf-collapse-item"><div class="tf-collapse-item-title" role="button"><!> <!> <span><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"></path></svg></span></div> <!> <!></div>'),dp=ne("<div></div>");const fp={hash:"svelte-1jfktzw",code:`\r
+ /* 瀹氫箟鏃嬭浆鐨� CSS 绫� */.rotate-90.svelte-1jfktzw {transform:rotate(90deg);transition:transform 0.3s ease;}`};function Kc(e,t){ue(t,!0),et(e,fp);let n=w(t,"items",7),r=w(t,"onChange",7),o=w(t,"activeKeys",31,()=>Ht([]));function i(a){var l;o().includes(a.key)?o(o().filter(u=>u!==a.key)):(o().push(a.key),o(o())),(l=r())==null||l(a,o())}var s=dp();return Yt(s,21,n,oi,(a,l,u)=>{var c=cp(),f=X(c);de(f,"tabindex",u),f.__click=[ip,i,l],f.__keydown=[sp,i,l];var d=X(f);{var g=v=>{var b=ap(),N=X(b);nr(N,{get target(){return h(l).icon}}),Z(b),L(v,b)};ke(d,v=>{h(l).icon&&v(g)})}var p=z(d,2);nr(p,{get target(){return h(l).title}});var x=z(p,2);Z(f);var C=z(f,2);{var $=v=>{var b=lp(),N=X(b);nr(N,{get target(){return h(l).description}}),Z(b),L(v,b)};ke(C,v=>{h(l).description&&v($)})}var m=z(C,2);{var _=v=>{var b=up(),N=X(b);nr(N,{get target(){return h(l).content}}),Z(b),L(v,b)};ke(m,v=>{o().includes(h(l).key)&&v(_)})}Z(c),Ee(v=>$t(x,1,`tf-collapse-item-title-arrow ${v??""}`,"svelte-1jfktzw"),[()=>o().includes(h(l).key)?"rotate-90":""]),L(a,c)}),Z(s),Ee(()=>{de(s,"style",t.style),$t(s,1,`tf-collapse ${t.class??""}`,"svelte-1jfktzw")}),L(e,s),ce({get items(){return n()},set items(a){n(a),y()},get onChange(){return r()},set onChange(a){r(a),y()},get activeKeys(){return o()},set activeKeys(a=[]){o(a),y()}})}ri(["click","keydown"]),ie(Kc,{items:{},onChange:{},activeKeys:{}},[],[],!0);function nr(e,t){ue(t,!0);let n=w(t,"target",7);typeof n()>"u"&&n("undefined");var r=tt(),o=xe(r);{var i=a=>{var l=tt(),u=xe(l);cr(u,()=>n()??gt),L(a,l)},s=a=>{var l=tt(),u=xe(l);dl(u,n),L(a,l)};ke(o,a=>{typeof n()=="function"?a(i):a(s,!1)})}return L(e,r),ce({get target(){return n()},set target(a){n(a),y()}})}ie(nr,{target:{}},[],[],!0);var gp=(e,t,n)=>t(h(n)),hp=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 14L8 10H16L12 14Z"></path></svg>'),vp=ne('<div class="tf-select-content-children"><!></div>'),pp=ne('<button class="tf-select-content-item"><span><!></span> <!></button> <!>',1),mp=ne('<div class="tf-select-content nopan nodrag"><!></div>'),yp=ne("<!> <!>",1),wp=ne('<div class="tf-select-input-placeholder"> </div>'),_p=ne('<button><div class="tf-select-input-value"></div> <div class="tf-select-input-arrow"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.9999 13.1714L16.9497 8.22168L18.3639 9.63589L11.9999 15.9999L5.63599 9.63589L7.0502 8.22168L11.9999 13.1714Z"></path></svg></div></button>'),xp=ne("<div><!></div>");function ln(e,t){ue(t,!0);const n=(_,v=gt)=>{var b=tt(),N=xe(b);Yt(N,19,v,(E,T)=>`${T}_${E.value}`,(E,T)=>{var D=pp(),V=xe(D);V.__click=[gp,x,T];var A=X(V),O=X(A);{var R=P=>{var H=hp();L(P,H)};ke(O,P=>{h(T).children&&h(T).children.length>0&&P(R)})}Z(A);var S=z(A,2);nr(S,{get target(){return h(T).label}}),Z(V);var M=z(V,2);{var k=P=>{var H=vp(),I=X(H);n(I,()=>h(T).children),Z(H),L(P,H)};ke(M,P=>{h(T).children&&h(T).children.length>0&&(l()||c().includes(h(T).value))&&P(k)})}L(E,D)}),L(_,b)};let r=w(t,"items",7),o=w(t,"onExpand",7),i=w(t,"onSelect",7),s=w(t,"value",23,()=>[]),a=w(t,"defaultValue",23,()=>[]),l=w(t,"expandAll",7,!0),u=w(t,"multiple",7,!1),c=w(t,"expandValue",23,()=>[]),f=w(t,"placeholder",7),d=xt(t,["$$slots","$$events","$$legacy","$$host","items","onExpand","onSelect","value","defaultValue","expandAll","multiple","expandValue","placeholder"]),g=Ne(()=>{const _=[],v=b=>{for(let N of b)s().length>0?s().includes(N.value)&&_.push(N):a().includes(N.value)&&_.push(N),N.children&&N.children.length>0&&v(N.children)};return v(r()),_}),p;function x(_){var v,b;if(_.children&&_.children.length>0){(v=o())==null||v(_);return}else p==null||p.hide(),(b=i())==null||b(_)}var C=xp();let $;var m=X(C);return An(Do(m,{floating:v=>{var b=mp(),N=X(b);n(N,r),Z(b),L(v,b)},children:(v,b)=>{var N=_p();let E;var T=X(N);Yt(T,23,()=>h(g),(D,V)=>`${V}_${D.value}`,(D,V,A)=>{var O=tt(),R=xe(O);{var S=k=>{var P=tt(),H=xe(P);{var I=B=>{nr(B,{get target(){return h(V).label}})};ke(H,B=>{h(A)===0&&B(I)})}L(k,P)},M=k=>{var P=yp(),H=xe(P);nr(H,{get target(){return h(V).label}});var I=z(H,2);{var B=F=>{var K=Ae(",");L(F,K)};ke(I,F=>{h(A)<h(g).length-1&&F(B)})}L(k,P)};ke(R,k=>{u()?k(M,!1):k(S)})}L(D,O)},D=>{var V=wp(),A=X(V,!0);Z(V),Ee(()=>Bt(A,f())),L(D,V)}),Z(T),Pe(2),Z(N),Ee(()=>E=nn(N,E,{class:"tf-select-input nopan nodrag",...d})),L(v,N)},$$slots:{floating:!0,default:!0}}),v=>p=v,()=>p),Z(C),Ee(()=>$=nn(C,$,{...d,class:`tf-select ${d.class??""}`})),L(e,C),ce({get items(){return r()},set items(_){r(_),y()},get onExpand(){return o()},set onExpand(_){o(_),y()},get onSelect(){return i()},set onSelect(_){i(_),y()},get value(){return s()},set value(_=[]){s(_),y()},get defaultValue(){return a()},set defaultValue(_=[]){a(_),y()},get expandAll(){return l()},set expandAll(_=!0){l(_),y()},get multiple(){return u()},set multiple(_=!1){u(_),y()},get expandValue(){return c()},set expandValue(_=[]){c(_),y()},get placeholder(){return f()},set placeholder(_){f(_),y()}})}ri(["click"]),ie(ln,{items:{},onExpand:{},onSelect:{},value:{},defaultValue:{},expandAll:{},multiple:{},expandValue:{},placeholder:{}},[],[],!0);const No=Math.min,Yr=Math.max,Yi=Math.round,bn=e=>({x:e,y:e}),bp={left:"right",right:"left",bottom:"top",top:"bottom"},Cp={start:"end",end:"start"};function fa(e,t,n){return Yr(e,No(t,n))}function To(e,t){return typeof e=="function"?e(t):e}function mr(e){return e.split("-")[0]}function Mo(e){return e.split("-")[1]}function qc(e){return e==="x"?"y":"x"}function ga(e){return e==="y"?"height":"width"}function Zr(e){return["top","bottom"].includes(mr(e))?"y":"x"}function ha(e){return qc(Zr(e))}function kp(e,t,n){n===void 0&&(n=!1);const r=Mo(e),o=ha(e),i=ga(o);let s=o==="x"?r===(n?"end":"start")?"right":"left":r==="start"?"bottom":"top";return t.reference[i]>t.floating[i]&&(s=Zi(s)),[s,Zi(s)]}function $p(e){const t=Zi(e);return[va(e),t,va(t)]}function va(e){return e.replace(/start|end/g,t=>Cp[t])}function Ep(e,t,n){const r=["left","right"],o=["right","left"],i=["top","bottom"],s=["bottom","top"];switch(e){case"top":case"bottom":return n?t?o:r:t?r:o;case"left":case"right":return t?i:s;default:return[]}}function Sp(e,t,n,r){const o=Mo(e);let i=Ep(mr(e),n==="start",r);return o&&(i=i.map(s=>s+"-"+o),t&&(i=i.concat(i.map(va)))),i}function Zi(e){return e.replace(/left|right|bottom|top/g,t=>bp[t])}function Pp(e){return{top:0,right:0,bottom:0,left:0,...e}}function Gc(e){return typeof e!="number"?Pp(e):{top:e,right:e,bottom:e,left:e}}function Xi(e){const{x:t,y:n,width:r,height:o}=e;return{width:r,height:o,top:n,left:t,right:t+r,bottom:n+o,x:t,y:n}}function Uc(e,t,n){let{reference:r,floating:o}=e;const i=Zr(t),s=ha(t),a=ga(s),l=mr(t),u=i==="y",c=r.x+r.width/2-o.width/2,f=r.y+r.height/2-o.height/2,d=r[a]/2-o[a]/2;let g;switch(l){case"top":g={x:c,y:r.y-o.height};break;case"bottom":g={x:c,y:r.y+r.height};break;case"right":g={x:r.x+r.width,y:f};break;case"left":g={x:r.x-o.width,y:f};break;default:g={x:r.x,y:r.y}}switch(Mo(t)){case"start":g[s]-=d*(n&&u?-1:1);break;case"end":g[s]+=d*(n&&u?-1:1);break}return g}const Np=async(e,t,n)=>{const{placement:r="bottom",strategy:o="absolute",middleware:i=[],platform:s}=n,a=i.filter(Boolean),l=await(s.isRTL==null?void 0:s.isRTL(t));let u=await s.getElementRects({reference:e,floating:t,strategy:o}),{x:c,y:f}=Uc(u,r,l),d=r,g={},p=0;for(let x=0;x<a.length;x++){const{name:C,fn:$}=a[x],{x:m,y:_,data:v,reset:b}=await $({x:c,y:f,initialPlacement:r,placement:d,strategy:o,middlewareData:g,rects:u,platform:s,elements:{reference:e,floating:t}});c=m??c,f=_??f,g={...g,[C]:{...g[C],...v}},b&&p<=50&&(p++,typeof b=="object"&&(b.placement&&(d=b.placement),b.rects&&(u=b.rects===!0?await s.getElementRects({reference:e,floating:t,strategy:o}):b.rects),{x:c,y:f}=Uc(u,d,l)),x=-1)}return{x:c,y:f,placement:d,strategy:o,middlewareData:g}};async function jc(e,t){var n;t===void 0&&(t={});const{x:r,y:o,platform:i,rects:s,elements:a,strategy:l}=e,{boundary:u="clippingAncestors",rootBoundary:c="viewport",elementContext:f="floating",altBoundary:d=!1,padding:g=0}=To(t,e),p=Gc(g),C=a[d?f==="floating"?"reference":"floating":f],$=Xi(await i.getClippingRect({element:(n=await(i.isElement==null?void 0:i.isElement(C)))==null||n?C:C.contextElement||await(i.getDocumentElement==null?void 0:i.getDocumentElement(a.floating)),boundary:u,rootBoundary:c,strategy:l})),m=f==="floating"?{x:r,y:o,width:s.floating.width,height:s.floating.height}:s.reference,_=await(i.getOffsetParent==null?void 0:i.getOffsetParent(a.floating)),v=await(i.isElement==null?void 0:i.isElement(_))?await(i.getScale==null?void 0:i.getScale(_))||{x:1,y:1}:{x:1,y:1},b=Xi(i.convertOffsetParentRelativeRectToViewportRelativeRect?await i.convertOffsetParentRelativeRectToViewportRelativeRect({elements:a,rect:m,offsetParent:_,strategy:l}):m);return{top:($.top-b.top+p.top)/v.y,bottom:(b.bottom-$.bottom+p.bottom)/v.y,left:($.left-b.left+p.left)/v.x,right:(b.right-$.right+p.right)/v.x}}const Tp=e=>({name:"arrow",options:e,async fn(t){const{x:n,y:r,placement:o,rects:i,platform:s,elements:a,middlewareData:l}=t,{element:u,padding:c=0}=To(e,t)||{};if(u==null)return{};const f=Gc(c),d={x:n,y:r},g=ha(o),p=ga(g),x=await s.getDimensions(u),C=g==="y",$=C?"top":"left",m=C?"bottom":"right",_=C?"clientHeight":"clientWidth",v=i.reference[p]+i.reference[g]-d[g]-i.floating[p],b=d[g]-i.reference[g],N=await(s.getOffsetParent==null?void 0:s.getOffsetParent(u));let E=N?N[_]:0;(!E||!await(s.isElement==null?void 0:s.isElement(N)))&&(E=a.floating[_]||i.floating[p]);const T=v/2-b/2,D=E/2-x[p]/2-1,V=No(f[$],D),A=No(f[m],D),O=V,R=E-x[p]-A,S=E/2-x[p]/2+T,M=fa(O,S,R),k=!l.arrow&&Mo(o)!=null&&S!==M&&i.reference[p]/2-(S<O?V:A)-x[p]/2<0,P=k?S<O?S-O:S-R:0;return{[g]:d[g]+P,data:{[g]:M,centerOffset:S-M-P,...k&&{alignmentOffset:P}},reset:k}}}),Mp=function(e){return e===void 0&&(e={}),{name:"flip",options:e,async fn(t){var n,r;const{placement:o,middlewareData:i,rects:s,initialPlacement:a,platform:l,elements:u}=t,{mainAxis:c=!0,crossAxis:f=!0,fallbackPlacements:d,fallbackStrategy:g="bestFit",fallbackAxisSideDirection:p="none",flipAlignment:x=!0,...C}=To(e,t);if((n=i.arrow)!=null&&n.alignmentOffset)return{};const $=mr(o),m=Zr(a),_=mr(a)===a,v=await(l.isRTL==null?void 0:l.isRTL(u.floating)),b=d||(_||!x?[Zi(a)]:$p(a)),N=p!=="none";!d&&N&&b.push(...Sp(a,x,p,v));const E=[a,...b],T=await jc(t,C),D=[];let V=((r=i.flip)==null?void 0:r.overflows)||[];if(c&&D.push(T[$]),f){const S=kp(o,s,v);D.push(T[S[0]],T[S[1]])}if(V=[...V,{placement:o,overflows:D}],!D.every(S=>S<=0)){var A,O;const S=(((A=i.flip)==null?void 0:A.index)||0)+1,M=E[S];if(M)return{data:{index:S,overflows:V},reset:{placement:M}};let k=(O=V.filter(P=>P.overflows[0]<=0).sort((P,H)=>P.overflows[1]-H.overflows[1])[0])==null?void 0:O.placement;if(!k)switch(g){case"bestFit":{var R;const P=(R=V.filter(H=>{if(N){const I=Zr(H.placement);return I===m||I==="y"}return!0}).map(H=>[H.placement,H.overflows.filter(I=>I>0).reduce((I,B)=>I+B,0)]).sort((H,I)=>H[1]-I[1])[0])==null?void 0:R[0];P&&(k=P);break}case"initialPlacement":k=a;break}if(o!==k)return{reset:{placement:k}}}return{}}}};async function Hp(e,t){const{placement:n,platform:r,elements:o}=e,i=await(r.isRTL==null?void 0:r.isRTL(o.floating)),s=mr(n),a=Mo(n),l=Zr(n)==="y",u=["left","top"].includes(s)?-1:1,c=i&&l?-1:1,f=To(t,e);let{mainAxis:d,crossAxis:g,alignmentAxis:p}=typeof f=="number"?{mainAxis:f,crossAxis:0,alignmentAxis:null}:{mainAxis:f.mainAxis||0,crossAxis:f.crossAxis||0,alignmentAxis:f.alignmentAxis};return a&&typeof p=="number"&&(g=a==="end"?p*-1:p),l?{x:g*c,y:d*u}:{x:d*u,y:g*c}}const Vp=function(e){return e===void 0&&(e=0),{name:"offset",options:e,async fn(t){var n,r;const{x:o,y:i,placement:s,middlewareData:a}=t,l=await Hp(t,e);return s===((n=a.offset)==null?void 0:n.placement)&&(r=a.arrow)!=null&&r.alignmentOffset?{}:{x:o+l.x,y:i+l.y,data:{...l,placement:s}}}}},Dp=function(e){return e===void 0&&(e={}),{name:"shift",options:e,async fn(t){const{x:n,y:r,placement:o}=t,{mainAxis:i=!0,crossAxis:s=!1,limiter:a={fn:C=>{let{x:$,y:m}=C;return{x:$,y:m}}},...l}=To(e,t),u={x:n,y:r},c=await jc(t,l),f=Zr(mr(o)),d=qc(f);let g=u[d],p=u[f];if(i){const C=d==="y"?"top":"left",$=d==="y"?"bottom":"right",m=g+c[C],_=g-c[$];g=fa(m,g,_)}if(s){const C=f==="y"?"top":"left",$=f==="y"?"bottom":"right",m=p+c[C],_=p-c[$];p=fa(m,p,_)}const x=a.fn({...t,[d]:g,[f]:p});return{...x,data:{x:x.x-n,y:x.y-r,enabled:{[d]:i,[f]:s}}}}}};function Fi(){return typeof window<"u"}function Xr(e){return Jc(e)?(e.nodeName||"").toLowerCase():"#document"}function Xt(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function Bn(e){var t;return(t=(Jc(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function Jc(e){return Fi()?e instanceof Node||e instanceof Xt(e).Node:!1}function un(e){return Fi()?e instanceof Element||e instanceof Xt(e).Element:!1}function Cn(e){return Fi()?e instanceof HTMLElement||e instanceof Xt(e).HTMLElement:!1}function Qc(e){return!Fi()||typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot||e instanceof Xt(e).ShadowRoot}function Ho(e){const{overflow:t,overflowX:n,overflowY:r,display:o}=cn(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&!["inline","contents"].includes(o)}function Ap(e){return["table","td","th"].includes(Xr(e))}function Wi(e){return[":popover-open",":modal"].some(t=>{try{return e.matches(t)}catch{return!1}})}function pa(e){const t=ma(),n=un(e)?cn(e):e;return["transform","translate","scale","rotate","perspective"].some(r=>n[r]?n[r]!=="none":!1)||(n.containerType?n.containerType!=="normal":!1)||!t&&(n.backdropFilter?n.backdropFilter!=="none":!1)||!t&&(n.filter?n.filter!=="none":!1)||["transform","translate","scale","rotate","perspective","filter"].some(r=>(n.willChange||"").includes(r))||["paint","layout","strict","content"].some(r=>(n.contain||"").includes(r))}function Lp(e){let t=rr(e);for(;Cn(t)&&!Fr(t);){if(pa(t))return t;if(Wi(t))return null;t=rr(t)}return null}function ma(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}function Fr(e){return["html","body","#document"].includes(Xr(e))}function cn(e){return Xt(e).getComputedStyle(e)}function Ki(e){return un(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function rr(e){if(Xr(e)==="html")return e;const t=e.assignedSlot||e.parentNode||Qc(e)&&e.host||Bn(e);return Qc(t)?t.host:t}function ed(e){const t=rr(e);return Fr(t)?e.ownerDocument?e.ownerDocument.body:e.body:Cn(t)&&Ho(t)?t:ed(t)}function td(e,t,n){var r;t===void 0&&(t=[]);const o=ed(e),i=o===((r=e.ownerDocument)==null?void 0:r.body),s=Xt(o);return i?(ya(s),t.concat(s,s.visualViewport||[],Ho(o)?o:[],[])):t.concat(o,td(o,[]))}function ya(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function nd(e){const t=cn(e);let n=parseFloat(t.width)||0,r=parseFloat(t.height)||0;const o=Cn(e),i=o?e.offsetWidth:n,s=o?e.offsetHeight:r,a=Yi(n)!==i||Yi(r)!==s;return a&&(n=i,r=s),{width:n,height:r,$:a}}function rd(e){return un(e)?e:e.contextElement}function Wr(e){const t=rd(e);if(!Cn(t))return bn(1);const n=t.getBoundingClientRect(),{width:r,height:o,$:i}=nd(t);let s=(i?Yi(n.width):n.width)/r,a=(i?Yi(n.height):n.height)/o;return(!s||!Number.isFinite(s))&&(s=1),(!a||!Number.isFinite(a))&&(a=1),{x:s,y:a}}const Op=bn(0);function od(e){const t=Xt(e);return!ma()||!t.visualViewport?Op:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function Ip(e,t,n){return t===void 0&&(t=!1),!n||t&&n!==Xt(e)?!1:t}function Vo(e,t,n,r){t===void 0&&(t=!1),n===void 0&&(n=!1);const o=e.getBoundingClientRect(),i=rd(e);let s=bn(1);t&&(r?un(r)&&(s=Wr(r)):s=Wr(e));const a=Ip(i,n,r)?od(i):bn(0);let l=(o.left+a.x)/s.x,u=(o.top+a.y)/s.y,c=o.width/s.x,f=o.height/s.y;if(i){const d=Xt(i),g=r&&un(r)?Xt(r):r;let p=d,x=ya(p);for(;x&&r&&g!==p;){const C=Wr(x),$=x.getBoundingClientRect(),m=cn(x),_=$.left+(x.clientLeft+parseFloat(m.paddingLeft))*C.x,v=$.top+(x.clientTop+parseFloat(m.paddingTop))*C.y;l*=C.x,u*=C.y,c*=C.x,f*=C.y,l+=_,u+=v,p=Xt(x),x=ya(p)}}return Xi({width:c,height:f,x:l,y:u})}function wa(e,t){const n=Ki(e).scrollLeft;return t?t.left+n:Vo(Bn(e)).left+n}function id(e,t,n){n===void 0&&(n=!1);const r=e.getBoundingClientRect(),o=r.left+t.scrollLeft-(n?0:wa(e,r)),i=r.top+t.scrollTop;return{x:o,y:i}}function zp(e){let{elements:t,rect:n,offsetParent:r,strategy:o}=e;const i=o==="fixed",s=Bn(r),a=t?Wi(t.floating):!1;if(r===s||a&&i)return n;let l={scrollLeft:0,scrollTop:0},u=bn(1);const c=bn(0),f=Cn(r);if((f||!f&&!i)&&((Xr(r)!=="body"||Ho(s))&&(l=Ki(r)),Cn(r))){const g=Vo(r);u=Wr(r),c.x=g.x+r.clientLeft,c.y=g.y+r.clientTop}const d=s&&!f&&!i?id(s,l,!0):bn(0);return{width:n.width*u.x,height:n.height*u.y,x:n.x*u.x-l.scrollLeft*u.x+c.x+d.x,y:n.y*u.y-l.scrollTop*u.y+c.y+d.y}}function Rp(e){return Array.from(e.getClientRects())}function Bp(e){const t=Bn(e),n=Ki(e),r=e.ownerDocument.body,o=Yr(t.scrollWidth,t.clientWidth,r.scrollWidth,r.clientWidth),i=Yr(t.scrollHeight,t.clientHeight,r.scrollHeight,r.clientHeight);let s=-n.scrollLeft+wa(e);const a=-n.scrollTop;return cn(r).direction==="rtl"&&(s+=Yr(t.clientWidth,r.clientWidth)-o),{width:o,height:i,x:s,y:a}}function Yp(e,t){const n=Xt(e),r=Bn(e),o=n.visualViewport;let i=r.clientWidth,s=r.clientHeight,a=0,l=0;if(o){i=o.width,s=o.height;const u=ma();(!u||u&&t==="fixed")&&(a=o.offsetLeft,l=o.offsetTop)}return{width:i,height:s,x:a,y:l}}function Zp(e,t){const n=Vo(e,!0,t==="fixed"),r=n.top+e.clientTop,o=n.left+e.clientLeft,i=Cn(e)?Wr(e):bn(1),s=e.clientWidth*i.x,a=e.clientHeight*i.y,l=o*i.x,u=r*i.y;return{width:s,height:a,x:l,y:u}}function sd(e,t,n){let r;if(t==="viewport")r=Yp(e,n);else if(t==="document")r=Bp(Bn(e));else if(un(t))r=Zp(t,n);else{const o=od(e);r={x:t.x-o.x,y:t.y-o.y,width:t.width,height:t.height}}return Xi(r)}function ad(e,t){const n=rr(e);return n===t||!un(n)||Fr(n)?!1:cn(n).position==="fixed"||ad(n,t)}function Xp(e,t){const n=t.get(e);if(n)return n;let r=td(e,[]).filter(a=>un(a)&&Xr(a)!=="body"),o=null;const i=cn(e).position==="fixed";let s=i?rr(e):e;for(;un(s)&&!Fr(s);){const a=cn(s),l=pa(s);!l&&a.position==="fixed"&&(o=null),(i?!l&&!o:!l&&a.position==="static"&&!!o&&["absolute","fixed"].includes(o.position)||Ho(s)&&!l&&ad(e,s))?r=r.filter(c=>c!==s):o=a,s=rr(s)}return t.set(e,r),r}function Fp(e){let{element:t,boundary:n,rootBoundary:r,strategy:o}=e;const s=[...n==="clippingAncestors"?Wi(t)?[]:Xp(t,this._c):[].concat(n),r],a=s[0],l=s.reduce((u,c)=>{const f=sd(t,c,o);return u.top=Yr(f.top,u.top),u.right=No(f.right,u.right),u.bottom=No(f.bottom,u.bottom),u.left=Yr(f.left,u.left),u},sd(t,a,o));return{width:l.right-l.left,height:l.bottom-l.top,x:l.left,y:l.top}}function Wp(e){const{width:t,height:n}=nd(e);return{width:t,height:n}}function Kp(e,t,n){const r=Cn(t),o=Bn(t),i=n==="fixed",s=Vo(e,!0,i,t);let a={scrollLeft:0,scrollTop:0};const l=bn(0);if(r||!r&&!i)if((Xr(t)!=="body"||Ho(o))&&(a=Ki(t)),r){const d=Vo(t,!0,i,t);l.x=d.x+t.clientLeft,l.y=d.y+t.clientTop}else o&&(l.x=wa(o));const u=o&&!r&&!i?id(o,a):bn(0),c=s.left+a.scrollLeft-l.x-u.x,f=s.top+a.scrollTop-l.y-u.y;return{x:c,y:f,width:s.width,height:s.height}}function _a(e){return cn(e).position==="static"}function ld(e,t){if(!Cn(e)||cn(e).position==="fixed")return null;if(t)return t(e);let n=e.offsetParent;return Bn(e)===n&&(n=n.ownerDocument.body),n}function ud(e,t){const n=Xt(e);if(Wi(e))return n;if(!Cn(e)){let o=rr(e);for(;o&&!Fr(o);){if(un(o)&&!_a(o))return o;o=rr(o)}return n}let r=ld(e,t);for(;r&&Ap(r)&&_a(r);)r=ld(r,t);return r&&Fr(r)&&_a(r)&&!pa(r)?n:r||Lp(e)||n}const qp=async function(e){const t=this.getOffsetParent||ud,n=this.getDimensions,r=await n(e.floating);return{reference:Kp(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,width:r.width,height:r.height}}};function Gp(e){return cn(e).direction==="rtl"}const Up={convertOffsetParentRelativeRectToViewportRelativeRect:zp,getDocumentElement:Bn,getClippingRect:Fp,getOffsetParent:ud,getElementRects:qp,getClientRects:Rp,getDimensions:Wp,getScale:Wr,isElement:un,isRTL:Gp},jp=Vp,Jp=Dp,Qp=Mp,em=Tp,tm=(e,t,n)=>{const r=new Map,o={platform:Up,...n},i={...o.platform,_c:r};return Np(e,t,{...o,platform:i})},nm=({trigger:e,triggerEvent:t,floatContent:n,placement:r="bottom",offsetOptions:o,flipOptions:i,shiftOptions:s,interactive:a,showArrow:l})=>{if(typeof e=="string"){const $=document.querySelector(e);if($)e=$;else throw new Error("element not found by document.querySelector('"+e+"')")}let u;if(typeof n=="string"){const $=document.querySelector(n);if($)u=$;else throw new Error("element not found by document.querySelector('"+n+"')")}else u=n;let c;l&&(c=document.createElement("div"),c.style.position="absolute",c.style.backgroundColor="#222",c.style.width="8px",c.style.height="8px",c.style.transform="rotate(45deg)",c.style.display="none",u.firstElementChild.before(c));function f(){tm(e,u,{placement:r,middleware:[jp(o),Qp(i),Jp(s),...l?[em({element:c})]:[]]}).then(({x:$,y:m,placement:_,middlewareData:v})=>{if(Object.assign(u.style,{left:`${$}px`,top:`${m}px`}),l){const{x:b,y:N}=v.arrow,E={top:"bottom",right:"left",bottom:"top",left:"right"}[_.split("-")[0]];Object.assign(c.style,{zIndex:-1,left:b!=null?`${b}px`:"",top:N!=null?`${N}px`:"",right:"",bottom:"",[E]:"2px"})}})}let d=!1;function g(){u.style.display="block",u.style.visibility="block",u.style.position="absolute",l&&(c.style.display="block"),d=!0,f()}function p(){u.style.display="none",l&&(c.style.display="none"),d=!1}function x($){$.stopPropagation(),d?p():g()}function C($){u.contains($.target)||p()}return(!t||t.length==0)&&(t=["click"]),t.forEach($=>{e.addEventListener($,x)}),document.addEventListener("click",C),{destroy(){t.forEach($=>{e.removeEventListener($,x)}),document.removeEventListener("click",C)},hide(){p()},isVisible(){return d}}};var rm=ne('<div style="position: relative"><div><!></div> <div style="display: none; width: 100%;z-index: 9999"><!></div></div>');function Do(e,t){ue(t,!0);const n=w(t,"children",7),r=w(t,"floating",7),o=w(t,"placement",7,"bottom");let i,s,a;rn(()=>(a=nm({trigger:i,floatContent:s,interactive:!0,placement:o()}),()=>{a.destroy()}));function l(){a.hide()}var u=rm(),c=X(u),f=X(c);cr(f,n),Z(c),An(c,p=>i=p,()=>i);var d=z(c,2),g=X(d);return cr(g,r),Z(d),An(d,p=>s=p,()=>s),Z(u),L(e,u),ce({hide:l,get children(){return n()},set children(p){n(p),y()},get floating(){return r()},set floating(p){r(p),y()},get placement(){return o()},set placement(p="bottom"){o(p),y()}})}ie(Do,{children:{},floating:{},placement:{}},[],["hide"],!0);function je(e,t){ue(t,!0);const n=w(t,"children",7),r=w(t,"level",7,1),o=w(t,"mt",7),i=w(t,"mb",7);var s=tt(),a=xe(s);return t1(a,()=>`h${r()}`,!1,(l,u)=>{let c;Ee(()=>c=nn(l,c,{class:"tf-heading",style:`margin-top:${o()||"0"};margin-bottom:${i()||"0"}`},void 0,l.namespaceURI===Ea,l.nodeName.includes("-")));var f=tt(),d=xe(f);cr(d,()=>n()??gt),L(u,f)}),L(e,s),ce({get children(){return n()},set children(l){n(l),y()},get level(){return r()},set level(l=1){r(l),y()},get mt(){return o()},set mt(l){o(l),y()},get mb(){return i()},set mb(l){i(l),y()}})}ie(je,{children:{},level:{},mt:{},mb:{}},[],[],!0);var om=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="svelte-1rvn4a8"><path d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z" class="svelte-1rvn4a8"></path></svg>');const im={hash:"svelte-1rvn4a8",code:".input-btn-more {border:1px solid transparent;padding:3px;&:hover {background:#eee;border:1px solid transparent;}}"};function qi(e,t){ue(t,!0),et(e,im);const n=xt(t,["$$slots","$$events","$$legacy","$$host"]);Ge(e,ft(()=>n,{get class(){return`input-btn-more ${t.class??""}`},children:(r,o)=>{var i=om();L(r,i)},$$slots:{default:!0}})),ce()}ie(qi,{},[],[],!0);const sm=()=>{const e=Ue();return{deleteNode:n=>{e.nodes.update(r=>r.filter(o=>o.id!==n)),e.edges.update(r=>r.filter(o=>o.source!==n&&o.target!==n))}}},Kr=(e=16)=>{const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",n=new Uint8Array(e);return crypto.getRandomValues(n),Array.from(n,r=>t[r%t.length]).join("")},am=()=>{const{nodes:e,nodeLookup:t}=Ue();return{copyNode:r=>{var s;const i=(s=q(t).get(r))==null?void 0:s.internals.userNode;if(i){const a=Kr(),l={...i,id:a,position:{x:i.position.x+50,y:i.position.y+50}};e.update(u=>[...u,l]),e.update(u=>u.map(c=>c.id===a?{...c,selected:!0}:{...c,selected:!1}))}}}};var lm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M8 18.3915V5.60846L18.2264 12L8 18.3915ZM6 3.80421V20.1957C6 20.9812 6.86395 21.46 7.53 21.0437L20.6432 12.848C21.2699 12.4563 21.2699 11.5436 20.6432 11.152L7.53 2.95621C6.86395 2.53993 6 3.01878 6 3.80421Z"></path></svg>'),um=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6Z"></path></svg>'),cm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"></path></svg>'),dm=ne('<div class="tf-node-toolbar svelte-44dmwv"><!> <!> <!></div>'),fm=ne('<!> <div class="tf-node-wrapper"><div class="tf-node-wrapper-title">TinyFlow.ai</div> <div class="tf-node-wrapper-body"><!></div></div> <!> <!> <!>',1);const gm={hash:"svelte-44dmwv",code:".tf-node-toolbar.svelte-44dmwv {display:flex;gap:5px;padding:5px;border-radius:5px;background:#fff;border:1px solid #eee;box-shadow:0 0 5px rgba(0, 0, 0, 0.1);}.tf-node-toolbar-item {border:1px solid transparent;}"};function dn(e,t){ue(t,!0),et(e,gm);const n=w(t,"data",7),r=w(t,"id",7,""),o=w(t,"icon",7),i=w(t,"handle",7),s=w(t,"children",7),a=w(t,"allowExecute",7,!0),l=w(t,"allowCopy",7,!0),u=w(t,"allowDelete",7,!0),c=w(t,"showSourceHandle",7,!0),f=w(t,"showTargetHandle",7,!0);let d=n().expand?["key"]:[];const{updateNodeData:g}=Lt(),p=[{key:"key",icon:o(),title:n().title,description:n().description,content:s()}],{deleteNode:x}=sm(),{copyNode:C}=am();var $=fm(),m=xe($);{var _=O=>{Zc(O,{get position(){return $e.Top},align:"end",children:(R,S)=>{var M=dm(),k=X(M);{var P=K=>{Ge(K,{class:"tf-node-toolbar-item",children:(se,ee)=>{var W=lm();L(se,W)},$$slots:{default:!0}})};ke(k,K=>{a()&&K(P)})}var H=z(k,2);{var I=K=>{Ge(K,{class:"tf-node-toolbar-item",onclick:()=>{C(r())},children:(se,ee)=>{var W=um();L(se,W)},$$slots:{default:!0}})};ke(H,K=>{l()&&K(I)})}var B=z(H,2);{var F=K=>{Ge(K,{class:"tf-node-toolbar-item",onclick:()=>{x(r())},children:(se,ee)=>{var W=cm();L(se,W)},$$slots:{default:!0}})};ke(B,K=>{u()&&K(F)})}Z(M),L(R,M)},$$slots:{default:!0}})};ke(m,O=>{(a()||l()||u())&&O(_)})}var v=z(m,2),b=z(X(v),2),N=X(b);Kc(N,{items:p,activeKeys:d,onChange:(O,R)=>{g(r(),{expand:R==null?void 0:R.includes("key")})}}),Z(b),Z(v);var E=z(v,2);{var T=O=>{er(O,{type:"target",get position(){return $e.Left},style:" left: -12px;top: 20px"})};ke(E,O=>{f()&&O(T)})}var D=z(E,2);{var V=O=>{er(O,{type:"source",get position(){return $e.Right},style:"right: -12px;top: 20px"})};ke(D,O=>{c()&&O(V)})}var A=z(D,2);return cr(A,()=>i()??gt),L(e,$),ce({get data(){return n()},set data(O){n(O),y()},get id(){return r()},set id(O=""){r(O),y()},get icon(){return o()},set icon(O){o(O),y()},get handle(){return i()},set handle(O){i(O),y()},get children(){return s()},set children(O){s(O),y()},get allowExecute(){return a()},set allowExecute(O=!0){a(O),y()},get allowCopy(){return l()},set allowCopy(O=!0){l(O),y()},get allowDelete(){return u()},set allowDelete(O=!0){u(O),y()},get showSourceHandle(){return c()},set showSourceHandle(O=!0){c(O),y()},get showTargetHandle(){return f()},set showTargetHandle(O=!0){f(O),y()}})}ie(dn,{data:{},id:{},icon:{},handle:{},children:{},allowExecute:{},allowCopy:{},allowDelete:{},showSourceHandle:{},showTargetHandle:{}},[],[],!0);function pt(){return ur("svelteflow__node_id")}const cd=[{value:"String",label:"String"},{value:"Number",label:"Number"},{value:"Boolean",label:"Boolean"},{value:"File",label:"File"},{value:"Object",label:"Object"},{value:"Array",label:"Array"}],hm=[{value:"ref",label:"寮曠敤"},{value:"input",label:"鍥哄畾鍊�"}];var vm=ne('<div class="input-more-setting svelte-laou7w"><div class="input-more-item svelte-laou7w">鍙傛暟绫诲瀷锛� <!></div> <div class="input-more-item svelte-laou7w">榛樿鍊硷細 <!></div> <div class="input-more-item svelte-laou7w">鍙傛暟鎻忚堪锛� <!></div> <div class="input-more-item svelte-laou7w"><!></div></div>'),pm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4.5 10.5C3.675 10.5 3 11.175 3 12C3 12.825 3.675 13.5 4.5 13.5C5.325 13.5 6 12.825 6 12C6 11.175 5.325 10.5 4.5 10.5ZM19.5 10.5C18.675 10.5 18 11.175 18 12C18 12.825 18.675 13.5 19.5 13.5C20.325 13.5 21 12.825 21 12C21 11.175 20.325 10.5 19.5 10.5ZM12 10.5C11.175 10.5 10.5 11.175 10.5 12C10.5 12.825 11.175 13.5 12 13.5C12.825 13.5 13.5 12.825 13.5 12C13.5 11.175 12.825 10.5 12 10.5Z"></path></svg>'),mm=ne('<div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div>',1);const ym={hash:"svelte-laou7w",code:".input-item.svelte-laou7w {display:flex;align-items:center;}.input-more-setting.svelte-laou7w {display:flex;flex-direction:column;gap:10px;padding:10px;background:#fff;border:1px solid #ddd;border-radius:5px;width:200px;box-shadow:0 0 10px 2px rgba(0, 0, 0, 0.1);}.input-more-setting.svelte-laou7w .input-more-item:where(.svelte-laou7w) {display:flex;flex-direction:column;gap:3px;font-size:12px;color:#666;}"};function dd(e,t){ue(t,!0),et(e,ym);const[n,r]=nt(),o=()=>Q(h(l),"$node",n),i=w(t,"parameter",7),s=w(t,"index",7);let a=pt(),l=Ne(()=>pr(a)),u=Ne(()=>{var T,D;return{...i(),...(D=(T=o())==null?void 0:T.data)==null?void 0:D.parameters[s()]}});const{updateNodeData:c}=Lt(),f=T=>{const D=T.target.value;c(a,V=>{let A=V.data.parameters;return A[s()].name=D,{parameters:A}})},d=T=>{const D=T.target.checked;c(a,V=>{let A=V.data.parameters;return A[s()].required=D,{parameters:A}})},g=T=>{const D=T.value;D&&c(a,V=>{let A=V.data.parameters;return A[s()].dataType=D,{parameters:A}})};let p;const x=()=>{c(a,T=>{let D=T.data.parameters;return D.splice(s(),1),{parameters:[...D]}}),p==null||p.hide()};var C=mm(),$=xe(C),m=X($);St(m,{style:"width: 100%;",get value(){return h(u).name},placeholder:"璇疯緭鍏ュ弬鏁板悕绉�",oninput:f}),Z($);var _=z($,2),v=X(_);Fc(v,{get checked(){return h(u).required},onchange:d}),Z(_);var b=z(_,2),N=X(b);An(Do(N,{placement:"bottom",floating:D=>{var V=vm(),A=X(V),O=z(X(A));const R=Ne(()=>h(u).dataType?[h(u).dataType]:["String"]);ln(O,{items:cd,style:"width: 100%",onSelect:g,get value(){return h(R)}}),Z(A);var S=z(A,2),M=z(X(S));Pt(M,{rows:1,style:"width: 100%;"}),Z(S);var k=z(S,2),P=z(X(k));Pt(P,{rows:3,style:"width: 100%;"}),Z(k);var H=z(k,2),I=X(H);Ge(I,{onclick:x,children:(B,F)=>{Pe();var K=Ae("鍒犻櫎");L(B,K)},$$slots:{default:!0}}),Z(H),Z(V),L(D,V)},children:(D,V)=>{Ge(D,{class:"input-btn-more",children:(A,O)=>{var R=pm();L(A,R)},$$slots:{default:!0}})},$$slots:{floating:!0,default:!0}}),D=>p=D,()=>p),Z(b),L(e,C);var E=ce({get parameter(){return i()},set parameter(T){i(T),y()},get index(){return s()},set index(T){s(T),y()}});return r(),E}ie(dd,{parameter:{},index:{}},[],[],!0);var wm=ne('<div class="input-header svelte-3n0wca">鍙傛暟鍚嶇О</div> <div class="input-header svelte-3n0wca">蹇呭~</div> <div class="input-header svelte-3n0wca"></div>',1),_m=ne('<div class="none-params svelte-3n0wca">鏃犺緭鍏ュ弬鏁�</div>'),xm=ne('<div class="input-container svelte-3n0wca"><!> <!></div>');const bm={hash:"svelte-3n0wca",code:`.input-container.svelte-3n0wca {display:grid;grid-template-columns:80% 10% 10%;row-gap:5px;column-gap:3px;}.input-container.svelte-3n0wca .none-params:where(.svelte-3n0wca) {font-size:12px;background:#f8f8f8;height:40px;display:flex;justify-content:center;align-items:center;border-radius:5px;width:calc(100% - 5px);grid-column:1 / -1;
+ /* 浠庣涓�鍒楀紑濮嬪埌鏈�鍚庝竴鍒楃粨鏉� */}.input-container.svelte-3n0wca .input-header:where(.svelte-3n0wca) {font-size:12px;color:#666;}`};function fd(e,t){ue(t,!0),et(e,bm);const[n,r]=nt(),o=()=>Q(h(s),"$node",n);let i=pt(),s=Ne(()=>pr(i)),a=Ne(()=>{var d,g;return[...((g=(d=o())==null?void 0:d.data)==null?void 0:g.parameters)||[]]});var l=xm(),u=X(l);{var c=d=>{var g=wm();Pe(4),L(d,g)};ke(u,d=>{h(a).length!==0&&d(c)})}var f=z(u,2);Yt(f,19,()=>h(a),d=>d.id,(d,g,p)=>{dd(d,{get parameter(){return h(g)},get index(){return h(p)}})},d=>{var g=_m();L(d,g)}),Z(l),L(e,l),ce(),r()}ie(fd,{},[],[],!0);const gd=e=>{!e||e.length==0||e.forEach(t=>{t.id||(t.id=Kr()),gd(t.children)})},kn=()=>{const{updateNodeData:e}=Lt();return{addParameter:(t,n="parameters",r)=>{gd(r==null?void 0:r.children);const o={...r,id:Kr()};e(t,i=>{let s=i.data[n];return s?s.push(o):s=[o],{[n]:[...s]}})}}};var Cm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>'),km=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),$m=ne('<div class="heading svelte-r5g35l"><!> <!></div> <!>',1);const Em={hash:"svelte-r5g35l",code:".heading.svelte-r5g35l {display:flex;margin-bottom:10px;}.input-btn-more {border:1px solid transparent;padding:3px;}.input-btn-more:hover {background:#eee;border:1px solid transparent;}"};function hd(e,t){ue(t,!0),et(e,Em);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn();return dn(e,ft(()=>r,{get data(){return n()},allowExecute:!1,showTargetHandle:!1,icon:a=>{var l=Cm();L(a,l)},children:(a,l)=>{var u=$m(),c=xe(u),f=X(c);je(f,{level:3,children:(p,x)=>{Pe();var C=Ae("杈撳叆鍙傛暟");L(p,C)},$$slots:{default:!0}});var d=z(f,2);Ge(d,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(p,x)=>{var C=km();L(p,C)},$$slots:{default:!0}}),Z(c);var g=z(c,2);fd(g,{}),L(a,u)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(s){n(s),y()}})}ie(hd,{data:{}},[],[],!0);const vd=(e,t,n)=>{for(let r of n)r.target===t&&r.source&&(e.push(r.source),vd(e,r.source,n))},pd=(e,t)=>{if(e.type==="startNode"){const n=e.data.parameters,r=[];if(n)for(const o of n)r.push({label:o.name+(t?` (Array<${o.dataType||"String"}>)`:` (${o.dataType||"String"})`),value:e.id+"."+o.name});return{label:e.data.title,value:e.id,children:r}}else{if(e.type==="loopNode"&&t)return{label:e.data.title,value:e.id,children:[{label:"loopItem",value:e.id+".loop"},{label:"index (Number)",value:e.id+".index"}]};{const n=e.data.outputDefs;if(n){const r=(o,i)=>!o||o.length===0?[]:o.map(s=>({label:s.name+(t?` (Array<${s.dataType||"String"}>)`:` (${s.dataType||"String"})`),value:i+"."+s.name,children:r(s.children,i+"."+s.name)}));return{label:e.data.title,value:e.id,children:r(n,e.id)}}}}},Sm=(e=!1)=>{const t=pt(),n=pr(t),{nodes:r,edges:o}=Ue();return Un([n,r,o],([i,s,a])=>{const l=[];if(e){for(let u of s)if(u.parentId===i.id){const c=pd(u,u.parentId===i.id);c&&l.push(c)}}else{const u=[];vd(u,t,a);for(let c of s)if(u.includes(c.id)){const f=pd(c,c.parentId===i.id);f&&l.push(f)}}return l})};var Pm=ne('<div class="input-more-setting svelte-laou7w"><div class="input-more-item svelte-laou7w">鏁版嵁鏉ユ簮锛� <!></div> <div class="input-more-item svelte-laou7w">榛樿鍊硷細 <!></div> <div class="input-more-item svelte-laou7w">鍙傛暟鎻忚堪锛� <!></div> <div class="input-more-item svelte-laou7w"><!></div></div>'),Nm=ne('<div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div> <div class="input-item svelte-laou7w"><!></div>',1);const Tm={hash:"svelte-laou7w",code:".input-item.svelte-laou7w {display:flex;align-items:center;}.input-more-setting.svelte-laou7w {display:flex;flex-direction:column;gap:10px;padding:10px;background:#fff;border:1px solid #ddd;border-radius:5px;width:200px;box-shadow:0 0 10px 2px rgba(0, 0, 0, 0.1);}.input-more-setting.svelte-laou7w .input-more-item:where(.svelte-laou7w) {display:flex;flex-direction:column;gap:3px;font-size:12px;color:#666;}"};function md(e,t){ue(t,!0),et(e,Tm);const[n,r]=nt(),o=()=>Q(h(c),"$node",n),i=()=>Q(v,"$selectItems",n),s=w(t,"parameter",7),a=w(t,"index",7),l=w(t,"dataKeyName",7);let u=pt(),c=Ne(()=>pr(u)),f=Ne(()=>{var M;return{...s(),...(M=o())==null?void 0:M.data[l()][a()]}});const{updateNodeData:d}=Lt(),g=(M,k)=>{d(u,P=>{let H=P.data[l()];return H[a()]={...H[a()],[M]:k},{[l()]:H}})},p=M=>{const k=M.target.value;g("name",k)},x=M=>{const k=M.target.value;g("value",k)},C=M=>{const k=M.value;g("ref",k)},$=M=>{const k=M.value;g("refType",k)};let m;const _=()=>{d(u,M=>{let k=M.data[l()];return k.splice(a(),1),{[l()]:[...k]}}),m==null||m.hide()},v=Sm();var b=Nm(),N=xe(b),E=X(N);St(E,{style:"width: 100%;",get value(){return h(f).name},placeholder:"璇疯緭鍏ュ弬鏁板悕绉�",oninput:p}),Z(N);var T=z(N,2),D=X(T);{var V=M=>{St(M,{get value(){return h(f).value},placeholder:"璇疯緭鍏ュ弬鏁板��",oninput:x})},A=M=>{const k=Ne(()=>[h(f).ref]);ln(M,{get items(){return i()},style:"width: 100%",defaultValue:["ref"],get value(){return h(k)},expandAll:!0,onSelect:C})};ke(D,M=>{h(f).refType==="input"?M(V):M(A,!1)})}Z(T);var O=z(T,2),R=X(O);An(Do(R,{placement:"bottom",floating:k=>{var P=Pm(),H=X(P),I=z(X(H));const B=Ne(()=>h(f).refType?[h(f).refType]:[]);ln(I,{items:hm,style:"width: 100%",defaultValue:["ref"],get value(){return h(B)},onSelect:$}),Z(H);var F=z(H,2),K=z(X(F));Pt(K,{rows:1,style:"width: 100%;",onchange:me=>{const Ce=me.target.value;g("defaultValue",Ce)}}),Z(F);var se=z(F,2),ee=z(X(se));Pt(ee,{rows:3,style:"width: 100%;",onchange:me=>{const Ce=me.target.value;g("description",Ce)}}),Z(se);var W=z(se,2),fe=X(W);Ge(fe,{onclick:_,children:(me,Ce)=>{Pe();var he=Ae("鍒犻櫎");L(me,he)},$$slots:{default:!0}}),Z(W),Z(P),L(k,P)},children:(k,P)=>{qi(k,{})},$$slots:{floating:!0,default:!0}}),k=>m=k,()=>m),Z(O),L(e,b);var S=ce({get parameter(){return s()},set parameter(M){s(M),y()},get index(){return a()},set index(M){a(M),y()},get dataKeyName(){return l()},set dataKeyName(M){l(M),y()}});return r(),S}ie(md,{parameter:{},index:{},dataKeyName:{}},[],[],!0);var Mm=ne('<div class="input-header svelte-1sm1mgi">鍙傛暟鍚嶇О</div> <div class="input-header svelte-1sm1mgi">鍙傛暟鍊�</div> <div class="input-header svelte-1sm1mgi"></div>',1),Hm=ne('<div class="none-params svelte-1sm1mgi"> </div>'),Vm=ne('<div class="input-container svelte-1sm1mgi"><!> <!></div>');const Dm={hash:"svelte-1sm1mgi",code:`.input-container.svelte-1sm1mgi {display:grid;grid-template-columns:40% 50% 10%;row-gap:5px;column-gap:3px;}.input-container.svelte-1sm1mgi .none-params:where(.svelte-1sm1mgi) {font-size:12px;background:#f8f8f8;height:40px;display:flex;justify-content:center;align-items:center;border-radius:5px;width:calc(100% - 5px);grid-column:1 / -1;
+ /* 浠庣涓�鍒楀紑濮嬪埌鏈�鍚庝竴鍒楃粨鏉� */}.input-container.svelte-1sm1mgi .input-header:where(.svelte-1sm1mgi) {font-size:12px;color:#666;}`};function Ft(e,t){ue(t,!0),et(e,Dm);const[n,r]=nt(),o=()=>Q(h(l),"$node",n),i=w(t,"noneParameterText",7,"鏃犺緭鍏ュ弬鏁�"),s=w(t,"dataKeyName",7,"parameters");let a=pt(),l=Ne(()=>pr(a)),u=Ne(()=>{var x;return[...((x=o())==null?void 0:x.data[s()])||[]]});var c=Vm(),f=X(c);{var d=x=>{var C=Mm();Pe(4),L(x,C)};ke(f,x=>{h(u).length!==0&&x(d)})}var g=z(f,2);Yt(g,19,()=>h(u),x=>x.id,(x,C,$)=>{md(x,{get parameter(){return h(C)},get index(){return h($)},get dataKeyName(){return s()}})},x=>{var C=Hm(),$=X(C,!0);Z(C),Ee(()=>Bt($,i())),L(x,C)}),Z(c),L(e,c);var p=ce({get noneParameterText(){return i()},set noneParameterText(x="鏃犺緭鍏ュ弬鏁�"){i(x),y()},get dataKeyName(){return s()},set dataKeyName(x="parameters"){s(x),y()}});return r(),p}ie(Ft,{noneParameterText:{},dataKeyName:{}},[],[],!0);var Am=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>'),Lm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),Om=ne('<div class="heading svelte-11h445j"><!> <!></div> <!>',1);const Im={hash:"svelte-11h445j",code:".heading.svelte-11h445j {display:flex;margin-bottom:10px;}"};function yd(e,t){ue(t,!0),et(e,Im);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn();return dn(e,ft({get data(){return n()}},()=>r,{allowExecute:!1,showSourceHandle:!1,icon:a=>{var l=Am();L(a,l)},children:(a,l)=>{var u=Om(),c=xe(u),f=X(c);je(f,{level:3,children:(p,x)=>{Pe();var C=Ae("杈撳嚭鍙傛暟");L(p,C)},$$slots:{default:!0}});var d=z(f,2);Ge(d,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o,"outputDefs")},children:(p,x)=>{var C=Lm();L(p,C)},$$slots:{default:!0}}),Z(c);var g=z(c,2);Ft(g,{noneParameterText:"鏃犺緭鍑哄弬鏁�",dataKeyName:"outputDefs"}),L(a,u)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(s){n(s),y()}})}ie(yd,{data:{}},[],[],!0);const Ao=()=>ur("tinyflow_options");var zm=_e('<svg style="transform: scaleY(-1)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 8V16C13 17.6569 11.6569 19 10 19H7.82929C7.41746 20.1652 6.30622 21 5 21C3.34315 21 2 19.6569 2 18C2 16.3431 3.34315 15 5 15C6.30622 15 7.41746 15.8348 7.82929 17H10C10.5523 17 11 16.5523 11 16V8C11 6.34315 12.3431 5 14 5H17V2L22 6L17 10V7H14C13.4477 7 13 7.44772 13 8ZM5 19C5.55228 19 6 18.5523 6 18C6 17.4477 5.55228 17 5 17C4.44772 17 4 17.4477 4 18C4 18.5523 4.44772 19 5 19Z"></path></svg>'),Rm=ne('<div class="input-more-item svelte-1cfeest"><!></div>'),Bm=ne('<div class="input-more-setting svelte-1cfeest"><div class="input-more-item svelte-1cfeest">榛樿鍊硷細 <!></div> <div class="input-more-item svelte-1cfeest">鍙傛暟鎻忚堪锛� <!></div> <!></div>'),Ym=ne('<div class="input-item svelte-1cfeest"><!> <!></div> <div class="input-item svelte-1cfeest"><!> <!></div> <div class="input-item svelte-1cfeest"><!></div>',1);const Zm={hash:"svelte-1cfeest",code:".input-item.svelte-1cfeest {display:flex;align-items:center;gap:2px;}.input-more-setting.svelte-1cfeest {display:flex;flex-direction:column;gap:10px;padding:10px;background:#fff;border:1px solid #ddd;border-radius:5px;width:200px;box-shadow:0 0 10px 2px rgba(0, 0, 0, 0.1);}.input-more-setting.svelte-1cfeest .input-more-item:where(.svelte-1cfeest) {display:flex;flex-direction:column;gap:3px;font-size:12px;color:#666;}"};function wd(e,t){ue(t,!0),et(e,Zm);const[n,r]=nt(),o=()=>Q(h(u),"$node",n),i=w(t,"parameter",7),s=w(t,"position",7),a=w(t,"dataKeyName",7);let l=pt(),u=Ne(()=>pr(l)),c=Ne(()=>{var I;let P=(I=o())==null?void 0:I.data[a()],H;if(P&&s().length>0){let B=P;for(let F=0;F<s().length;F++){const K=s()[F];F==s().length-1?H=B[K]:B=B[K].children}}return{...i(),...H}});const{updateNodeData:f}=Lt(),d=(P,H)=>{f(l,I=>{const B=I.data[a()];if(B&&s().length>0){let F=B;for(let K=0;K<s().length;K++){const se=s()[K];K==s().length-1?F[se]={...F[se],[P]:H}:F=B[se].children}}return{[a()]:B}})},g=P=>{const H=P.target.value;d("name",H)},p=P=>{const H=P.value;d("dataType",H)};let x;const C=()=>{f(l,P=>{let H=P.data[a()];if(H&&s().length>0){let I=H;for(let B=0;B<s().length;B++){const F=s()[B];B==s().length-1?I.splice(F,1):I=I[F].children}}return{[a()]:[...H]}}),x==null||x.hide()},$=()=>{f(l,P=>{let H=P.data[a()];if(H&&s().length>0){let I=H;for(let B=0;B<s().length;B++){const F=s()[B];B==s().length-1?I[F].children?I[F].children.push({id:Kr(),name:"newParam",dataType:"String"}):I[F].children=[{id:Kr(),name:"newParam",dataType:"String"}]:I=I[F].children}}return{[a()]:[...H]}})};var m=Ym(),_=xe(m),v=X(_);{var b=P=>{var H=tt(),I=xe(H);Yt(I,17,s,oi,(B,F)=>{Pe();var K=Ae("聽");L(B,K)}),L(P,H)};ke(v,P=>{s().length>1&&P(b)})}var N=z(v,2);const E=Ne(()=>h(c).nameDisabled===!0);St(N,{style:"width: 100%;",get value(){return h(c).name},placeholder:"璇疯緭鍏ュ弬鏁板悕绉�",oninput:g,get disabled(){return h(E)}}),Z(_);var T=z(_,2),D=X(T);const V=Ne(()=>h(c).dataType?[h(c).dataType]:[]),A=Ne(()=>h(c).dataTypeDisabled===!0);ln(D,{items:cd,style:"width: 100%",defaultValue:["String"],get value(){return h(V)},get disabled(){return h(A)},onSelect:p});var O=z(D,2);{var R=P=>{Ge(P,{class:"input-btn-more",style:"margin-left: auto",onclick:$,children:(H,I)=>{var B=zm();L(H,B)},$$slots:{default:!0}})};ke(O,P=>{(h(c).dataType==="Object"||h(c).dataType==="Array")&&h(c).addChildDisabled!==!0&&P(R)})}Z(T);var S=z(T,2),M=X(S);An(Do(M,{placement:"bottom",floating:H=>{var I=Bm(),B=X(I),F=z(X(B));Pt(F,{rows:1,style:"width: 100%;",onchange:fe=>{const me=fe.target.value;d("defaultValue",me)}}),Z(B);var K=z(B,2),se=z(X(K));Pt(se,{rows:3,style:"width: 100%;",onchange:fe=>{const me=fe.target.value;d("description",me)}}),Z(K);var ee=z(K,2);{var W=fe=>{var me=Rm(),Ce=X(me);Ge(Ce,{onclick:C,children:(he,ze)=>{Pe();var G=Ae("鍒犻櫎");L(he,G)},$$slots:{default:!0}}),Z(me),L(fe,me)};ke(ee,fe=>{h(c).deleteDisabled!==!0&&fe(W)})}Z(I),L(H,I)},children:(H,I)=>{qi(H,{})},$$slots:{floating:!0,default:!0}}),H=>x=H,()=>x),Z(S),L(e,m);var k=ce({get parameter(){return i()},set parameter(P){i(P),y()},get position(){return s()},set position(P){s(P),y()},get dataKeyName(){return a()},set dataKeyName(P){a(P),y()}});return r(),k}ie(wd,{parameter:{},position:{},dataKeyName:{}},[],[],!0);var Xm=ne("<!> <!>",1),Fm=ne('<div class="none-params svelte-1sm1mgi"> </div>'),Wm=ne('<div class="input-header svelte-1sm1mgi">鍙傛暟鍚嶇О</div> <div class="input-header svelte-1sm1mgi">鍙傛暟绫诲瀷</div> <div class="input-header svelte-1sm1mgi"></div>',1),Km=ne('<div class="input-container svelte-1sm1mgi"><!> <!></div>');const qm={hash:"svelte-1sm1mgi",code:`.input-container.svelte-1sm1mgi {display:grid;grid-template-columns:40% 50% 10%;row-gap:5px;column-gap:3px;}.input-container.svelte-1sm1mgi .none-params:where(.svelte-1sm1mgi) {font-size:12px;background:#f8f8f8;height:40px;display:flex;justify-content:center;align-items:center;border-radius:5px;width:calc(100% - 5px);grid-column:1 / -1;
+ /* 浠庣涓�鍒楀紑濮嬪埌鏈�鍚庝竴鍒楃粨鏉� */}.input-container.svelte-1sm1mgi .input-header:where(.svelte-1sm1mgi) {font-size:12px;color:#666;}`};function Yn(e,t){ue(t,!0),et(e,qm);const[n,r]=nt(),o=()=>Q(h(u),"$node",n),i=(C,$=gt,m=gt)=>{var _=tt(),v=xe(_);Yt(v,19,$,b=>`${b.id}_${b.children?b.children.length:0}`,(b,N,E)=>{var T=Xm(),D=xe(T);const V=Ne(()=>[...m(),h(E)]);wd(D,{get parameter(){return h(N)},get position(){return h(V)},get dataKeyName(){return a()}});var A=z(D,2);{var O=R=>{var S=ve(()=>[...m(),h(E)]);i(R,()=>h(N).children,()=>h(S))};ke(A,R=>{h(N).children&&R(O)})}L(b,T)},b=>{var N=tt(),E=xe(N);{var T=D=>{var V=Fm(),A=X(V,!0);Z(V),Ee(()=>Bt(A,s())),L(D,V)};ke(E,D=>{m().length===0&&D(T)})}L(b,N)}),L(C,_)},s=w(t,"noneParameterText",7,"鏃犺緭鍑哄弬鏁�"),a=w(t,"dataKeyName",7,"outputDefs");let l=pt(),u=Ne(()=>pr(l)),c=Ne(()=>{var C;return[...((C=o())==null?void 0:C.data[a()])||[]]});var f=Km(),d=X(f);{var g=C=>{var $=Wm();Pe(4),L(C,$)};ke(d,C=>{h(c).length!==0&&C(g)})}var p=z(d,2);i(p,()=>h(c)||[],()=>[]),Z(f),L(e,f);var x=ce({get noneParameterText(){return s()},set noneParameterText(C="鏃犺緭鍑哄弬鏁�"){s(C),y()},get dataKeyName(){return a()},set dataKeyName(C="outputDefs"){a(C),y()}});return r(),x}ie(Yn,{noneParameterText:{},dataKeyName:{}},[],[],!0);var Gm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>'),Um=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),jm=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),Jm=ne('<div class="heading svelte-wn2kra"><!> <!></div> <!> <!> <div class="setting-title svelte-wn2kra">妯″瀷</div> <div class="setting-item svelte-wn2kra"><!> <!></div> <div class="setting-title svelte-wn2kra">閲囨牱鍙傛暟</div> <div class="setting-item svelte-wn2kra"><div class="slider-container svelte-wn2kra"><label class="svelte-wn2kra"> </label> <input type="range" min="0" max="1" step="0.1" class="svelte-wn2kra"></div></div> <div class="setting-item svelte-wn2kra"><div class="slider-container svelte-wn2kra"><label class="svelte-wn2kra"> </label> <input type="range" min="0" max="1" step="0.1" class="svelte-wn2kra"></div></div> <div class="setting-item svelte-wn2kra"><div class="slider-container svelte-wn2kra"><label class="svelte-wn2kra"> </label> <input type="range" min="0" max="100" step="1" class="svelte-wn2kra"></div></div> <div class="setting-title svelte-wn2kra">绯荤粺鎻愮ず璇�</div> <div class="setting-item svelte-wn2kra"><!></div> <div class="setting-title svelte-wn2kra">鐢ㄦ埛鎻愮ず璇�</div> <div class="setting-item svelte-wn2kra"><!></div> <div class="heading svelte-wn2kra"><!> <!></div> <!>',1);const Qm={hash:"svelte-wn2kra",code:`.heading.svelte-wn2kra {display:flex;margin-bottom:10px;}.setting-title.svelte-wn2kra {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-wn2kra {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}\r
+ /* 鏂板鏍峰紡 */.slider-container.svelte-wn2kra {width:100%;display:flex;flex-direction:column;gap:4px;}.slider-container.svelte-wn2kra label:where(.svelte-wn2kra) {font-size:12px;color:#666;display:flex;justify-content:space-between;align-items:center;}input[type="range"].svelte-wn2kra {width:100%;height:4px;background:#ddd;border-radius:2px;outline:none;-webkit-appearance:none;}input[type="range"].svelte-wn2kra::-webkit-slider-thumb {-webkit-appearance:none;width:14px;height:14px;background:#007bff;border-radius:50%;cursor:pointer;}`};function _d(e,t){ue(t,!0),et(e,Qm);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),s=Ao();let a=Fn(Ht([]));rn(async()=>{var c,f;const u=await((f=(c=s.provider)==null?void 0:c.llm)==null?void 0:f.call(c));h(a).push(...u||[])});const{updateNodeData:l}=Lt();return dn(e,ft({get data(){return n()}},()=>r,{icon:c=>{var f=Gm();L(c,f)},children:(c,f)=>{var d=Jm(),g=xe(d),p=X(g);je(p,{level:3,children:(G,ae)=>{Pe();var Me=Ae("杈撳叆鍙傛暟");L(G,Me)},$$slots:{default:!0}});var x=z(p,2);Ge(x,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(G,ae)=>{var Me=Um();L(G,Me)},$$slots:{default:!0}}),Z(g);var C=z(g,2);Ft(C,{});var $=z(C,2);je($,{level:3,mt:"10px",children:(G,ae)=>{Pe();var Me=Ae("妯″瀷璁剧疆");L(G,Me)},$$slots:{default:!0}});var m=z($,4),_=X(m);const v=Ne(()=>n().llmId?[n().llmId]:[]);ln(_,{get items(){return h(a)},style:"width: 100%",placeholder:"璇烽�夋嫨妯″瀷",onSelect:G=>{const ae=G.value;l(o,()=>({llmId:ae}))},get value(){return h(v)}});var b=z(_,2);qi(b,{}),Z(m);var N=z(m,4),E=X(N),T=X(E),D=X(T);Z(T);var V=z(T,2);ao(V),Z(E),Z(N);var A=z(N,2),O=X(A),R=X(O),S=X(R);Z(R);var M=z(R,2);ao(M),Z(O),Z(A);var k=z(A,2),P=X(k),H=X(P),I=X(H);Z(H);var B=z(H,2);ao(B),Z(P),Z(k);var F=z(k,4),K=X(F);const se=Ne(()=>n().systemPrompt||"");Pt(K,{rows:5,placeholder:"璇疯緭鍏ョ郴缁熸彁绀鸿瘝",style:"width: 100%",get value(){return h(se)},oninput:G=>{l(o,{systemPrompt:G.target.value})}}),Z(F);var ee=z(F,4),W=X(ee);const fe=Ne(()=>n().userPrompt||"");Pt(W,{rows:5,placeholder:"璇疯緭鍏ョ敤鎴锋彁绀鸿瘝",style:"width: 100%",get value(){return h(fe)},oninput:G=>{l(o,{userPrompt:G.target.value})}}),Z(ee);var me=z(ee,2),Ce=X(me);je(Ce,{level:3,mt:"10px",children:(G,ae)=>{Pe();var Me=Ae("杈撳嚭鍙傛暟");L(G,Me)},$$slots:{default:!0}});var he=z(Ce,2);Ge(he,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o,"outputDefs")},children:(G,ae)=>{var Me=jm();L(G,Me)},$$slots:{default:!0}}),Z(me);var ze=z(me,2);Yn(ze,{}),Ee(()=>{Bt(D,`Temperature: ${n().temperature??.5}`),bs(V,n().temperature??.5),Bt(S,`Top P: ${n().topP??.9}`),bs(M,n().topP??.9),Bt(I,`Top K: ${n().topK??50}`),bs(B,n().topK??50)}),Ze("mousedown",V,ks(function(G){De.call(this,t,G)})),Ze("input",V,G=>l(o,{temperature:parseFloat(G.target.value)})),Ze("mousedown",M,ks(function(G){De.call(this,t,G)})),Ze("input",M,G=>l(o,{topP:parseFloat(G.target.value)})),Ze("mousedown",B,ks(function(G){De.call(this,t,G)})),Ze("input",B,G=>l(o,{topK:parseInt(G.target.value)})),L(c,d)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(u){n(u),y()}})}ie(_d,{data:{}},[],[],!0);var ey=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>'),ty=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),ny=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),ry=ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">鎵ц寮曟搸</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鎵ц浠g爜</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!> <!></div> <!>',1);const oy={hash:"svelte-15t2v24",code:".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"};function xd(e,t){ue(t,!0),et(e,oy);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),{updateNodeData:s}=Lt(),a=[{label:"QLExpress",value:"qlexpress"},{label:"Groovy",value:"groovy"},{label:"JavaScript",value:"js"}];return dn(e,ft({get data(){return n()}},()=>r,{icon:u=>{var c=ey();L(u,c)},children:(u,c)=>{var f=ry(),d=xe(f),g=X(d);je(g,{level:3,children:(A,O)=>{Pe();var R=Ae("杈撳叆鍙傛暟");L(A,R)},$$slots:{default:!0}});var p=z(g,2);Ge(p,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(A,O)=>{var R=ty();L(A,R)},$$slots:{default:!0}}),Z(d);var x=z(d,2);Ft(x,{});var C=z(x,2);je(C,{level:3,mt:"10px",children:(A,O)=>{Pe();var R=Ae("浠g爜");L(A,R)},$$slots:{default:!0}});var $=z(C,4),m=X($);const _=Ne(()=>n().engine?[n().engine]:["qlexpress"]);ln(m,{items:a,style:"width: 100%",placeholder:"璇烽�夋嫨鎵ц寮曟搸",onSelect:A=>{const O=A.value;s(o,()=>({engine:O}))},get value(){return h(_)}}),Z($);var v=z($,4),b=X(v);const N=Ne(()=>n().code||"");Pt(b,{rows:10,placeholder:"璇疯緭鍏ユ墽琛屼唬鐮侊紝娉細杈撳嚭鍐呭闇�娣诲姞鍒癬result涓紝濡傦細_result.put(key, value)",style:"width: 100%",onchange:A=>{s(o,()=>({code:A.target.value}))},get value(){return h(N)}}),Z(v);var E=z(v,2),T=X(E);je(T,{level:3,mt:"10px",children:(A,O)=>{Pe();var R=Ae("杈撳嚭鍙傛暟");L(A,R)},$$slots:{default:!0}});var D=z(T,2);Ge(D,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o,"outputDefs")},children:(A,O)=>{var R=ny();L(A,R)},$$slots:{default:!0}}),Z(E);var V=z(E,2);Yn(V,{}),L(u,f)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(l){n(l),y()}})}ie(xd,{data:{}},[],[],!0);var iy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>'),sy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),ay=ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">鎵ц浠g爜</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!></div> <!>',1);const ly={hash:"svelte-15t2v24",code:".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"};function bd(e,t){ue(t,!0),et(e,ly);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),{updateNodeData:s}=Lt();return kr(()=>{(!n().outputDefs||n().outputDefs.length===0)&&i(o,"outputDefs",{name:"output",dataType:"String",dataTypeDisabled:!0,deleteDisabled:!0})}),dn(e,ft({get data(){return n()}},()=>r,{icon:l=>{var u=iy();L(l,u)},children:(l,u)=>{var c=ay(),f=xe(c),d=X(f);je(d,{level:3,children:(N,E)=>{Pe();var T=Ae("杈撳叆鍙傛暟");L(N,T)},$$slots:{default:!0}});var g=z(d,2);Ge(g,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(N,E)=>{var T=sy();L(N,T)},$$slots:{default:!0}}),Z(f);var p=z(f,2);Ft(p,{});var x=z(p,2);je(x,{level:3,mt:"10px",children:(N,E)=>{Pe();var T=Ae("浠g爜");L(N,T)},$$slots:{default:!0}});var C=z(x,4),$=X(C);const m=Ne(()=>n().template||"");Pt($,{rows:10,placeholder:"璇疯緭鍏ユ墽琛屼唬鐮�",style:"width: 100%",onchange:N=>{s(o,()=>({template:N.target.value}))},get value(){return h(m)}}),Z(C);var _=z(C,2),v=X(_);je(v,{level:3,mt:"10px",children:(N,E)=>{Pe();var T=Ae("杈撳嚭鍙傛暟");L(N,T)},$$slots:{default:!0}}),Z(_);var b=z(_,2);Yn(b,{}),L(l,c)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(a){n(a),y()}})}ie(bd,{data:{}},[],[],!0);var uy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>'),cy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),dy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),fy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),gy=ne('<div class="heading svelte-1vtcqdz" style="padding-top: 10px"><!> <!></div> <!>',1),hy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),vy=ne('<div class="heading svelte-1vtcqdz" style="padding-top: 10px"><!> <!></div> <!>',1),py=ne('<div style="width: 100%"><!></div>'),my=ne('<div style="width: 100%"><!></div>'),yy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),wy=ne('<div style="display: flex;gap: 2px;width: 100%;padding: 10px 0"><div><!></div> <div style="width: 100%"><!></div></div> <div class="heading svelte-1vtcqdz"><!> <!></div> <!> <div class="heading svelte-1vtcqdz" style="padding-top: 10px"><!> <!></div> <!> <!> <div class="radio-group svelte-1vtcqdz"><label class="svelte-1vtcqdz"><!>none</label> <label class="svelte-1vtcqdz"><!>form-data</label> <label class="svelte-1vtcqdz"><!>x-www-form-urlencoded</label> <label class="svelte-1vtcqdz"><!>json</label> <label class="svelte-1vtcqdz"><!>raw</label></div> <!> <!> <!> <!> <div class="heading svelte-1vtcqdz"><!> <!></div> <!>',1);const _y={hash:"svelte-1vtcqdz",code:".heading.svelte-1vtcqdz {display:flex;margin-bottom:10px;}.radio-group.svelte-1vtcqdz {display:flex;margin:10px 0;}.radio-group.svelte-1vtcqdz label:where(.svelte-1vtcqdz) {display:flex;font-size:14px;}"};function Cd(e,t){ue(t,!0),et(e,_y);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=[{value:"get",label:"GET"},{value:"post",label:"POST"},{value:"put",label:"PUT"},{value:"delete",label:"DELETE"},{value:"head",label:"HEAD"},{value:"patch",label:"PATCH"}],i=pt(),{addParameter:s}=kn(),{updateNodeData:a}=Lt();return dn(e,ft({get data(){return n()}},()=>r,{icon:u=>{var c=uy();L(u,c)},children:(u,c)=>{var f=wy(),d=xe(f),g=X(d),p=X(g);const x=Ne(()=>n().method?[n().method]:["get"]);ln(p,{items:o,style:"width: 100%",placeholder:"璇烽�夋嫨璇锋眰鏂瑰紡",onSelect:oe=>{const pe=oe.value;a(i,()=>({method:pe}))},get value(){return h(x)}}),Z(g);var C=z(g,2),$=X(C);const m=Ne(()=>n().url||"");St($,{placeholder:"璇疯緭鍏rl",style:"width: 100%",onchange:oe=>{a(i,()=>({url:oe.target.value}))},get value(){return h(m)}}),Z(C),Z(d);var _=z(d,2),v=X(_);je(v,{level:3,children:(oe,pe)=>{Pe();var be=Ae("Http 澶翠俊鎭�");L(oe,be)},$$slots:{default:!0}});var b=z(v,2);Ge(b,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{s(i,"headers")},children:(oe,pe)=>{var be=cy();L(oe,be)},$$slots:{default:!0}}),Z(_);var N=z(_,2);Ft(N,{dataKeyName:"headers"});var E=z(N,2),T=X(E);je(T,{level:3,children:(oe,pe)=>{Pe();var be=Ae("鍙傛暟");L(oe,be)},$$slots:{default:!0}});var D=z(T,2);Ge(D,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{s(i,"urlParameters")},children:(oe,pe)=>{var be=dy();L(oe,be)},$$slots:{default:!0}}),Z(E);var V=z(E,2);Ft(V,{dataKeyName:"urlParameters"});var A=z(V,2);je(A,{level:3,mt:"10px",children:(oe,pe)=>{Pe();var be=Ae("Body");L(oe,be)},$$slots:{default:!0}});var O=z(A,2),R=X(O),S=X(R);const M=Ne(()=>!n().bodyType);St(S,{type:"radio",name:"bodyType",value:"",get checked(){return h(M)},onchange:oe=>{var pe;(pe=oe.target)!=null&&pe.checked&&a(i,{bodyType:""})}}),Pe(),Z(R);var k=z(R,2),P=X(k);const H=Ne(()=>n().bodyType==="form-data");St(P,{type:"radio",name:"bodyType",value:"form-data",get checked(){return h(H)},onchange:oe=>{var pe;(pe=oe.target)!=null&&pe.checked&&a(i,{bodyType:"form-data"})}}),Pe(),Z(k);var I=z(k,2),B=X(I);const F=Ne(()=>n().bodyType==="x-www-form-urlencoded");St(B,{type:"radio",name:"bodyType",value:"x-www-form-urlencoded",get checked(){return h(F)},onchange:oe=>{var pe;(pe=oe.target)!=null&&pe.checked&&a(i,{bodyType:"x-www-form-urlencoded"})}}),Pe(),Z(I);var K=z(I,2),se=X(K);const ee=Ne(()=>n().bodyType==="json");St(se,{type:"radio",name:"bodyType",value:"json",get checked(){return h(ee)},onchange:oe=>{var pe;(pe=oe.target)!=null&&pe.checked&&a(i,{bodyType:"json"})}}),Pe(),Z(K);var W=z(K,2),fe=X(W);const me=Ne(()=>n().bodyType==="raw");St(fe,{type:"radio",name:"bodyType",value:"raw",get checked(){return h(me)},onchange:oe=>{var pe;(pe=oe.target)!=null&&pe.checked&&a(i,{bodyType:"raw"})}}),Pe(),Z(W),Z(O);var Ce=z(O,2);{var he=oe=>{var pe=gy(),be=xe(pe),Ie=X(be);je(Ie,{level:3,children:(J,Re)=>{Pe();var le=Ae("鍙傛暟");L(J,le)},$$slots:{default:!0}});var ht=z(Ie,2);Ge(ht,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{s(i,"fromData")},children:(J,Re)=>{var le=fy();L(J,le)},$$slots:{default:!0}}),Z(be);var dt=z(be,2);Ft(dt,{dataKeyName:"fromData"}),L(oe,pe)};ke(Ce,oe=>{n().bodyType==="form-data"&&oe(he)})}var ze=z(Ce,2);{var G=oe=>{var pe=vy(),be=xe(pe),Ie=X(be);je(Ie,{level:3,children:(J,Re)=>{Pe();var le=Ae("鍙傛暟");L(J,le)},$$slots:{default:!0}});var ht=z(Ie,2);Ge(ht,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{s(i,"fromUrlencoded")},children:(J,Re)=>{var le=hy();L(J,le)},$$slots:{default:!0}}),Z(be);var dt=z(be,2);Ft(dt,{dataKeyName:"fromUrlencoded"}),L(oe,pe)};ke(ze,oe=>{n().bodyType==="x-www-form-urlencoded"&&oe(G)})}var ae=z(ze,2);{var Me=oe=>{var pe=py(),be=X(pe);Pt(be,{rows:"5",style:"width: 100%",placeholder:"璇疯緭鍏� json 淇℃伅",get value(){return n().bodyJson},oninput:Ie=>{a(i,{bodyJson:Ie.target.value})}}),Z(pe),L(oe,pe)};ke(ae,oe=>{n().bodyType==="json"&&oe(Me)})}var Le=z(ae,2);{var Xe=oe=>{var pe=my(),be=X(pe);Pt(be,{rows:"5",style:"width: 100%",placeholder:"璇疯緭鍏ヨ姹備俊鎭�",get value(){return n().bodyRaw},oninput:Ie=>{a(i,{bodyRaw:Ie.target.value})}}),Z(pe),L(oe,pe)};ke(Le,oe=>{n().bodyType==="raw"&&oe(Xe)})}var te=z(Le,2),Fe=X(te);je(Fe,{level:3,mt:"10px",children:(oe,pe)=>{Pe();var be=Ae("杈撳嚭鍙傛暟");L(oe,be)},$$slots:{default:!0}});var Oe=z(Fe,2);Ge(Oe,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{s(i,"outputDefs")},children:(oe,pe)=>{var be=yy();L(oe,be)},$$slots:{default:!0}}),Z(te);var rt=z(te,2);Yn(rt,{}),L(u,f)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(l){n(l),y()}})}ie(Cd,{data:{}},[],[],!0);var xy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>'),by=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),Cy=ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">鐭ヨ瘑搴�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鑾峰彇鏁版嵁閲�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!></div> <!>',1);const ky={hash:"svelte-15t2v24",code:".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"};function kd(e,t){ue(t,!0),et(e,ky);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),s=Ao();let a=Fn(Ht([]));rn(async()=>{var c,f;const u=await((f=(c=s.provider)==null?void 0:c.knowledge)==null?void 0:f.call(c));h(a).push(...u||[])});const{updateNodeData:l}=Lt();return kr(()=>{(!n().outputDefs||n().outputDefs.length===0)&&i(o,"outputDefs",{name:"documents",dataType:"Array",nameDisabled:!0,dataTypeDisabled:!0,addChildDisabled:!0,children:[{name:"title",dataType:"String",nameDisabled:!0,dataTypeDisabled:!0},{name:"content",dataType:"String",nameDisabled:!0,dataTypeDisabled:!0},{name:"documentId",dataType:"Number",nameDisabled:!0,dataTypeDisabled:!0},{name:"knowledgeId",dataType:"Number",nameDisabled:!0,dataTypeDisabled:!0}]})}),dn(e,ft({get data(){return n()}},()=>r,{icon:c=>{var f=xy();L(c,f)},children:(c,f)=>{var d=Cy(),g=xe(d),p=X(g);je(p,{level:3,children:(V,A)=>{Pe();var O=Ae("杈撳叆鍙傛暟");L(V,O)},$$slots:{default:!0}});var x=z(p,2);Ge(x,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(V,A)=>{var O=by();L(V,O)},$$slots:{default:!0}}),Z(g);var C=z(g,2);Ft(C,{});var $=z(C,2);je($,{level:3,mt:"10px",children:(V,A)=>{Pe();var O=Ae("鐭ヨ瘑搴撹缃�");L(V,O)},$$slots:{default:!0}});var m=z($,4),_=X(m);const v=Ne(()=>n().knowledgeId?[n().knowledgeId]:[]);ln(_,{get items(){return h(a)},style:"width: 100%",placeholder:"璇烽�夋嫨鐭ヨ瘑搴�",onSelect:V=>{const A=V.value;l(o,()=>({knowledgeId:A}))},get value(){return h(v)}}),Z(m);var b=z(m,4),N=X(b);St(N,{placeholder:"鎼滅储鐨勬暟鎹潯鏁�",style:"width: 100%"}),Z(b);var E=z(b,2),T=X(E);je(T,{level:3,mt:"10px",children:(V,A)=>{Pe();var O=Ae("杈撳嚭鍙傛暟");L(V,O)},$$slots:{default:!0}}),Z(E);var D=z(E,2);Yn(D,{}),L(c,d)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(u){n(u),y()}})}ie(kd,{data:{}},[],[],!0);var $y=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>'),Ey=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),Sy=ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">API 鏈嶅姟鍟�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">API Key</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鍏抽敭瀛�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鏁版嵁閲�</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="setting-title svelte-15t2v24">鍏朵粬鍙傛暟</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!></div> <!>',1);const Py={hash:"svelte-15t2v24",code:".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"};function $d(e,t){ue(t,!0),et(e,Py);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),s=Ao();let a=Fn(Ht([]));rn(async()=>{var c;const u=await((c=s.provider)==null?void 0:c.knowledge());h(a).push(...u||[])});const{updateNodeData:l}=Lt();return kr(()=>{(!n().outputDefs||n().outputDefs.length===0)&&i(o,"outputDefs",{name:"documents",dataType:"Array",nameDisabled:!0,dataTypeDisabled:!0,addChildDisabled:!0,children:[{name:"title",dataType:"String",nameDisabled:!0,dataTypeDisabled:!0},{name:"content",dataType:"String",nameDisabled:!0,dataTypeDisabled:!0},{name:"documentId",dataType:"Number",nameDisabled:!0,dataTypeDisabled:!0},{name:"knowledgeId",dataType:"Number",nameDisabled:!0,dataTypeDisabled:!0}]})}),dn(e,ft({get data(){return n()}},()=>r,{icon:c=>{var f=$y();L(c,f)},children:(c,f)=>{var d=Sy(),g=xe(d),p=X(g);je(p,{level:3,children:(k,P)=>{Pe();var H=Ae("杈撳叆鍙傛暟");L(k,H)},$$slots:{default:!0}});var x=z(p,2);Ge(x,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(k,P)=>{var H=Ey();L(k,H)},$$slots:{default:!0}}),Z(g);var C=z(g,2);Ft(C,{});var $=z(C,2);je($,{level:3,mt:"10px",children:(k,P)=>{Pe();var H=Ae("鎼滅储寮曟搸璁剧疆");L(k,H)},$$slots:{default:!0}});var m=z($,4),_=X(m);const v=Ne(()=>n().knowledgeId?[n().knowledgeId]:[]);ln(_,{get items(){return h(a)},style:"width: 100%",placeholder:"璇烽�夋嫨 API 鏈嶅姟鍟�",onSelect:k=>{const P=k.value;l(o,()=>({knowledgeId:P}))},get value(){return h(v)}}),Z(m);var b=z(m,4),N=X(b);St(N,{placeholder:"璇疯緭鍏� API Key",style:"width: 100%"}),Z(b);var E=z(b,4),T=X(E);St(T,{placeholder:"璇疯緭鍏ュ叧閿瓧",style:"width: 100%"}),Z(E);var D=z(E,4),V=X(D);St(V,{placeholder:"鎼滅储鐨勬暟鎹潯鏁�",style:"width: 100%"}),Z(D);var A=z(D,4),O=X(A);Pt(O,{rows:3,placeholder:"璇疯緭鍏ュ叾浠栧弬鏁帮紙Property 鏍煎紡锛�",style:"width: 100%"}),Z(A);var R=z(A,2),S=X(R);je(S,{level:3,mt:"10px",children:(k,P)=>{Pe();var H=Ae("杈撳嚭鍙傛暟");L(k,H)},$$slots:{default:!0}}),Z(R);var M=z(R,2);Yn(M,{}),L(c,d)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(u){n(u),y()}})}ie($d,{data:{}},[],[],!0);var Ny=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>'),Ty=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),My=ne('<div class="heading svelte-md8tgj"><!> <!></div> <!> <div class="heading svelte-md8tgj"><!></div> <!>',1);const Hy={hash:"svelte-md8tgj",code:".heading.svelte-md8tgj {display:flex;margin-bottom:10px;}.loop_handle_wrapper ::after {content:'寰幆浣�';width:100px;height:20px;background:#000;color:#fff;display:flex;justify-content:center;align-items:center;}"};function Ed(e,t){ue(t,!0),et(e,Hy);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),s=Ao();let a=Fn(Ht([]));return rn(async()=>{var u;const l=await((u=s.provider)==null?void 0:u.knowledge());h(a).push(...l||[])}),dn(e,ft({get data(){return n()}},()=>r,{icon:c=>{var f=Ny();L(c,f)},handle:c=>{er(c,{type:"source",get position(){return $e.Bottom},id:"loop_handle",style:"bottom: -12px;width: 100px",class:"loop_handle_wrapper"})},children:(c,f)=>{var d=My(),g=xe(d),p=X(g);je(p,{level:3,children:(v,b)=>{Pe();var N=Ae("寰幆鍙橀噺");L(v,N)},$$slots:{default:!0}});var x=z(p,2);Ge(x,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(v,b)=>{var N=Ty();L(v,N)},$$slots:{default:!0}}),Z(g);var C=z(g,2);Ft(C,{});var $=z(C,2),m=X($);je(m,{level:3,mt:"10px",children:(v,b)=>{Pe();var N=Ae("杈撳嚭鍙傛暟");L(v,N)},$$slots:{default:!0}}),Z($);var _=z($,2);Yn(_,{}),L(c,d)},$$slots:{icon:!0,handle:!0,default:!0}})),ce({get data(){return n()},set data(l){n(l),y()}})}ie(Ed,{data:{}},[],[],!0);var Vy=_e('<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor" p-id="2577" width="200" height="200"><path d="M312.096 408.576l67.84 67.84 45.312-45.216a32 32 0 0 1 45.248 45.248l-45.28 45.248 90.496 90.496 45.28-45.216a32 32 0 0 1 45.248 45.248l-45.248 45.248 67.904 67.872-90.528 90.528a224.064 224.064 0 0 1-292.544 21.024L176.32 906.368a32 32 0 0 1-45.248-45.248l69.504-69.472a224.064 224.064 0 0 1 21.024-292.576l90.496-90.496z m0 90.496L266.848 544.32a160 160 0 0 0-4.8 221.28l4.8 4.992a160 160 0 0 0 221.248 4.8l5.024-4.8 45.248-45.248-226.272-226.24z m610.272-384a32 32 0 0 1 0 45.248l-69.44 69.504a224.064 224.064 0 0 1-21.056 292.544l-90.528 90.528-316.8-316.8 90.56-90.496a224.064 224.064 0 0 1 292.544-21.024l69.44-69.504a32 32 0 0 1 45.28 0zM565.344 246.08l-5.024 4.8-45.248 45.248 226.272 226.272 45.248-45.248a160 160 0 0 0 4.8-221.28l-4.8-4.992a160 160 0 0 0-221.248-4.8z" p-id="2578"></path></svg>'),Dy=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),Ay=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>'),Ly=ne('<div class="heading svelte-15t2v24"><!> <!></div> <!> <!> <div class="setting-title svelte-15t2v24">閫夋嫨鍐呴儴鎺ュ彛</div> <div class="setting-item svelte-15t2v24"><!></div> <div class="heading svelte-15t2v24"><!> <!></div> <!>',1);const Oy={hash:"svelte-15t2v24",code:".heading.svelte-15t2v24 {display:flex;margin-bottom:10px;}.setting-title.svelte-15t2v24 {font-size:12px;color:#999;margin-bottom:4px;margin-top:10px;}.setting-item.svelte-15t2v24 {display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;gap:10px;}"};function Sd(e,t){ue(t,!0),et(e,Oy);const n=w(t,"data",7),r=xt(t,["$$slots","$$events","$$legacy","$$host","data"]),o=pt(),{addParameter:i}=kn(),{updateNodeData:s}=Lt(),a=Ao();let l=Fn(Ht([]));return rn(async()=>{var c,f;const u=await((f=(c=a.provider)==null?void 0:c.internal)==null?void 0:f.call(c));h(l).push(...u||[])}),dn(e,ft({get data(){return n()}},()=>r,{icon:c=>{var f=Vy();L(c,f)},children:(c,f)=>{var d=Ly(),g=xe(d),p=X(g);je(p,{level:3,children:(D,V)=>{Pe();var A=Ae("杈撳叆鍙傛暟");L(D,A)},$$slots:{default:!0}});var x=z(p,2);Ge(x,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o)},children:(D,V)=>{var A=Dy();L(D,A)},$$slots:{default:!0}}),Z(g);var C=z(g,2);Ft(C,{});var $=z(C,2);je($,{level:3,mt:"10px",children:(D,V)=>{Pe();var A=Ae("鎺ュ彛");L(D,A)},$$slots:{default:!0}});var m=z($,4),_=X(m);const v=Ne(()=>n().method?[n().method]:[""]);ln(_,{get items(){return h(l)},style:"width: 100%",placeholder:"璇烽�夋嫨鍐呴儴鎺ュ彛",onSelect:D=>{const V=D.value;s(o,()=>({method:V}))},get value(){return h(v)}}),Z(m);var b=z(m,2),N=X(b);je(N,{level:3,mt:"10px",children:(D,V)=>{Pe();var A=Ae("杈撳嚭鍙傛暟");L(D,A)},$$slots:{default:!0}});var E=z(N,2);Ge(E,{class:"input-btn-more",style:"margin-left: auto",onclick:()=>{i(o,"outputDefs")},children:(D,V)=>{var A=Ay();L(D,A)},$$slots:{default:!0}}),Z(b);var T=z(b,2);Yn(T,{}),L(c,d)},$$slots:{icon:!0,default:!0}})),ce({get data(){return n()},set data(u){n(u),y()}})}ie(Sd,{data:{}},[],[],!0);const Iy={startNode:hd,codeNode:xd,llmNode:_d,templateNode:bd,httpNode:Cd,knowledgeNode:kd,searchEngineNode:$d,loopNode:Ed,internalNode:Sd,endNode:yd};var zy=ne("<!> ",1);function Pd(e,t){ue(t,!0);const n=w(t,"icon",7),r=w(t,"title",7),o=w(t,"type",7),i=w(t,"description",7),s=w(t,"extra",7);return Ge(e,{draggable:!0,ondragstart:l=>{if(!l.dataTransfer)return null;const u={type:o(),data:{title:r(),description:i(),systemPrompt:"",userPrompt:"",...s()}};l.dataTransfer.setData("application/tinyflow",JSON.stringify(u)),l.dataTransfer.effectAllowed="move"},children:(l,u)=>{var c=zy(),f=xe(c);dl(f,n);var d=z(f);Ee(()=>Bt(d,` ${r()??""}`)),L(l,c)},$$slots:{default:!0}}),ce({get icon(){return n()},set icon(l){n(l),y()},get title(){return r()},set title(l){r(l),y()},get type(){return o()},set type(l){o(l),y()},get description(){return i()},set description(l){i(l),y()},get extra(){return s()},set extra(l){s(l),y()}})}ie(Pd,{icon:{},title:{},type:{},description:{},extra:{}},[],[],!0);var Ry=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4.83582 12L11.0429 18.2071L12.4571 16.7929L7.66424 12L12.4571 7.20712L11.0429 5.79291L4.83582 12ZM10.4857 12L16.6928 18.2071L18.107 16.7929L13.3141 12L18.107 7.20712L16.6928 5.79291L10.4857 12Z"></path></svg>'),By=_e('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.1642 12L12.9571 5.79291L11.5429 7.20712L16.3358 12L11.5429 16.7929L12.9571 18.2071L19.1642 12ZM13.5143 12L7.30722 5.79291L5.89301 7.20712L10.6859 12L5.89301 16.7929L7.30722 18.2071L13.5143 12Z"></path></svg>'),Yy=ne('<div><div class="tf-toolbar-container "><div class="tf-toolbar-container-header"><!></div> <div class="tf-toolbar-container-body"><div class="tf-toolbar-container-base"></div> <div class="tf-toolbar-container-tools"><!></div></div></div> <!></div>');function Nd(e){let t=Fn("base"),n=Fn("show");const r=[{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12C15 13.6569 13.6569 15 12 15Z"></path></svg>',title:"寮�濮嬭妭鐐�",type:"startNode",description:"寮�濮嬪畾涔夎緭鍏ュ弬鏁�"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5.46257 4.43262C7.21556 2.91688 9.5007 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C9.84982 4 7.89777 4.84827 6.46023 6.22842L5.46257 4.43262ZM18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 9.86386 2.66979 7.88416 3.8108 6.25944L7 12H4C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"></path></svg>',title:"寰幆",type:"loopNode",description:"鐢ㄤ簬寰幆鎵ц浠诲姟"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M20.7134 7.12811L20.4668 7.69379C20.2864 8.10792 19.7136 8.10792 19.5331 7.69379L19.2866 7.12811C18.8471 6.11947 18.0555 5.31641 17.0677 4.87708L16.308 4.53922C15.8973 4.35653 15.8973 3.75881 16.308 3.57612L17.0252 3.25714C18.0384 2.80651 18.8442 1.97373 19.2761 0.930828L19.5293 0.319534C19.7058 -0.106511 20.2942 -0.106511 20.4706 0.319534L20.7238 0.930828C21.1558 1.97373 21.9616 2.80651 22.9748 3.25714L23.6919 3.57612C24.1027 3.75881 24.1027 4.35653 23.6919 4.53922L22.9323 4.87708C21.9445 5.31641 21.1529 6.11947 20.7134 7.12811ZM9 2C13.0675 2 16.426 5.03562 16.9337 8.96494L19.1842 12.5037C19.3324 12.7367 19.3025 13.0847 18.9593 13.2317L17 14.071V17C17 18.1046 16.1046 19 15 19H13.001L13 22H4L4.00025 18.3061C4.00033 17.1252 3.56351 16.0087 2.7555 15.0011C1.65707 13.6313 1 11.8924 1 10C1 5.58172 4.58172 2 9 2ZM9 4C5.68629 4 3 6.68629 3 10C3 11.3849 3.46818 12.6929 4.31578 13.7499C5.40965 15.114 6.00036 16.6672 6.00025 18.3063L6.00013 20H11.0007L11.0017 17H15V12.7519L16.5497 12.0881L15.0072 9.66262L14.9501 9.22118C14.5665 6.25141 12.0243 4 9 4ZM19.4893 16.9929L21.1535 18.1024C22.32 16.3562 23 14.2576 23 12.0001C23 11.317 22.9378 10.6486 22.8186 10L20.8756 10.5C20.9574 10.9878 21 11.489 21 12.0001C21 13.8471 20.4436 15.5642 19.4893 16.9929Z"></path></svg>',title:"澶фā鍨�",type:"llmNode",description:"浣跨敤澶фā鍨嬪鐞嗛棶棰�"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 5C13.567 5 12 6.567 12 8.5C12 10.433 13.567 12 15.5 12C17.433 12 19 10.433 19 8.5C19 6.567 17.433 5 15.5 5ZM10 8.5C10 5.46243 12.4624 3 15.5 3C18.5376 3 21 5.46243 21 8.5C21 9.6575 20.6424 10.7315 20.0317 11.6175L22.7071 14.2929L21.2929 15.7071L18.6175 13.0317C17.7315 13.6424 16.6575 14 15.5 14C12.4624 14 10 11.5376 10 8.5ZM3 4H8V6H3V4ZM3 11H8V13H3V11ZM21 18V20H3V18H21Z"></path></svg>',title:"鐭ヨ瘑搴�",type:"knowledgeNode",description:"閫氳繃鐭ヨ瘑搴撹幏鍙栧唴瀹�"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M18.031 16.6168L22.3137 20.8995L20.8995 22.3137L16.6168 18.031C15.0769 19.263 13.124 20 11 20C6.032 20 2 15.968 2 11C2 6.032 6.032 2 11 2C15.968 2 20 6.032 20 11C20 13.124 19.263 15.0769 18.031 16.6168ZM16.0247 15.8748C17.2475 14.6146 18 12.8956 18 11C18 7.1325 14.8675 4 11 4C7.1325 4 4 7.1325 4 11C4 14.8675 7.1325 18 11 18C12.8956 18 14.6146 17.2475 15.8748 16.0247L16.0247 15.8748Z"></path></svg>',title:"鎼滅储寮曟搸",type:"searchEngineNode",description:"閫氳繃鎼滅储寮曟搸鎼滅储鍐呭"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.23509 6.45329C4.85101 7.89148 4 9.84636 4 12C4 16.4183 7.58172 20 12 20C13.0808 20 14.1116 19.7857 15.0521 19.3972C15.1671 18.6467 14.9148 17.9266 14.8116 17.6746C14.582 17.115 13.8241 16.1582 12.5589 14.8308C12.2212 14.4758 12.2429 14.2035 12.3636 13.3943L12.3775 13.3029C12.4595 12.7486 12.5971 12.4209 14.4622 12.1248C15.4097 11.9746 15.6589 12.3533 16.0043 12.8777C16.0425 12.9358 16.0807 12.9928 16.1198 13.0499C16.4479 13.5297 16.691 13.6394 17.0582 13.8064C17.2227 13.881 17.428 13.9751 17.7031 14.1314C18.3551 14.504 18.3551 14.9247 18.3551 15.8472V15.9518C18.3551 16.3434 18.3168 16.6872 18.2566 16.9859C19.3478 15.6185 20 13.8854 20 12C20 8.70089 18.003 5.8682 15.1519 4.64482C14.5987 5.01813 13.8398 5.54726 13.575 5.91C13.4396 6.09538 13.2482 7.04166 12.6257 7.11976C12.4626 7.14023 12.2438 7.12589 12.012 7.11097C11.3905 7.07058 10.5402 7.01606 10.268 7.75495C10.0952 8.2232 10.0648 9.49445 10.6239 10.1543C10.7134 10.2597 10.7307 10.4547 10.6699 10.6735C10.59 10.9608 10.4286 11.1356 10.3783 11.1717C10.2819 11.1163 10.0896 10.8931 9.95938 10.7412C9.64554 10.3765 9.25405 9.92233 8.74797 9.78176C8.56395 9.73083 8.36166 9.68867 8.16548 9.64736C7.6164 9.53227 6.99443 9.40134 6.84992 9.09302C6.74442 8.8672 6.74488 8.55621 6.74529 8.22764C6.74529 7.8112 6.74529 7.34029 6.54129 6.88256C6.46246 6.70541 6.35689 6.56446 6.23509 6.45329ZM12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22Z"></path></svg>',title:"Http 璇锋眰",type:"httpNode",description:"閫氳繃 HTTP 璇锋眰鑾峰彇鏁版嵁"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M23 12L15.9289 19.0711L14.5147 17.6569L20.1716 12L14.5147 6.34317L15.9289 4.92896L23 12ZM3.82843 12L9.48528 17.6569L8.07107 19.0711L1 12L8.07107 4.92896L9.48528 6.34317L3.82843 12Z"></path></svg>',title:"鍔ㄦ�佷唬鐮�",type:"codeNode",description:"鍔ㄦ�佹墽琛屼唬鐮�"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2 4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4ZM4 5V19H20V5H4ZM7 8H17V11H15V10H13V14H14.5V16H9.5V14H11V10H9V11H7V8Z"></path></svg>',title:"鍐呭妯℃澘",type:"templateNode",description:"閫氳繃妯℃澘寮曟搸鐢熸垚鍐呭"},{icon:'<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor" p-id="2577" width="200" height="200"><path d="M312.096 408.576l67.84 67.84 45.312-45.216a32 32 0 0 1 45.248 45.248l-45.28 45.248 90.496 90.496 45.28-45.216a32 32 0 0 1 45.248 45.248l-45.248 45.248 67.904 67.872-90.528 90.528a224.064 224.064 0 0 1-292.544 21.024L176.32 906.368a32 32 0 0 1-45.248-45.248l69.504-69.472a224.064 224.064 0 0 1 21.024-292.576l90.496-90.496z m0 90.496L266.848 544.32a160 160 0 0 0-4.8 221.28l4.8 4.992a160 160 0 0 0 221.248 4.8l5.024-4.8 45.248-45.248-226.272-226.24z m610.272-384a32 32 0 0 1 0 45.248l-69.44 69.504a224.064 224.064 0 0 1-21.056 292.544l-90.528 90.528-316.8-316.8 90.56-90.496a224.064 224.064 0 0 1 292.544-21.024l69.44-69.504a32 32 0 0 1 45.28 0zM565.344 246.08l-5.024 4.8-45.248 45.248 226.272 226.272 45.248-45.248a160 160 0 0 0 4.8-221.28l-4.8-4.992a160 160 0 0 0-221.248-4.8z" p-id="2578"></path></svg>',title:"鍐呴儴鎺ュ彛",type:"internalNode",description:"鎵ц鍐呴儴鎻愪緵鎺ュ彛"},{icon:'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 5.1438V16.0002H18.3391L6 5.1438ZM4 2.932C4 2.07155 5.01456 1.61285 5.66056 2.18123L21.6501 16.2494C22.3423 16.8584 21.9116 18.0002 20.9896 18.0002H6V22H4V2.932Z"></path></svg>',title:"缁撴潫鑺傜偣",type:"endNode",description:"缁撴潫瀹氫箟杈撳嚭鍙傛暟"}],o=[{label:"鍩虹鑺傜偣",value:"base"},{label:"涓氬姟宸ュ叿",value:"tools"}];var i=Yy(),s=X(i),a=X(s),l=X(a);Wc(l,{style:"width: 100%",items:o,onChange:p=>{U(t,Ht(p.value.toString()))}}),Z(a);var u=z(a,2),c=X(u);Yt(c,21,()=>r,oi,(p,x)=>{Pd(p,ft(()=>h(x)))}),Z(c);var f=z(c,2),d=X(f);Ge(d,{children:(p,x)=>{Pe();var C=Ae("娴嬭瘯涓氬姟鎸夐挳");L(p,C)},$$slots:{default:!0}}),Z(f),Z(u),Z(s);var g=z(s,2);Ge(g,{onclick:()=>{U(n,Ht(h(n)?"":"show"))},children:(p,x)=>{var C=tt(),$=xe(C);{var m=v=>{var b=Ry();L(v,b)},_=v=>{var b=By();L(v,b)};ke($,v=>{h(n)==="show"?v(m):v(_,!1)})}L(p,C)},$$slots:{default:!0}}),Z(i),Ee(()=>{$t(i,1,`tf-toolbar ${h(n)??""}`),de(c,"style",`display: ${(h(t)==="base"?"flex":"none")??""}`),de(f,"style",`display: ${(h(t)!=="base"?"flex":"none")??""}`)}),L(e,i)}ie(Nd,{},[],[],!0);const Zy=()=>{const{nodeLookup:e}=Ue();return{getNode:n=>{var o;return(o=q(e).get(n))==null?void 0:o.internals.userNode}}},Xy=()=>{const{nodes:e}=Ue();return{ensureParentInNodesBefore:(n,r)=>{e.update(o=>{let i=-1;for(let l=0;l<o.length;l++)if(o[l].id===n){i=l;break}if(i<=0)return o;let s=-1;for(let l=0;l<i;l++)if(o[l].parentId===n||o[l].id===r){s=l;break}if(s==-1)return o;const a=o[i];for(let l=i;l>s;l--)o[l]=o[l-1];return o[s]=a,o})}}},Fy=()=>{const{edges:e}=Ue();return{getEdgesByTarget:n=>q(e).filter(o=>o.target===n)}};var Wy=ne('<div class="panel-content svelte-1oe15vw"><div>杈瑰睘鎬ц缃�</div> <div class="setting-title svelte-1oe15vw">杈规潯浠惰缃�</div> <div class="setting-item"><!></div></div>'),Ky=ne("<!> <!> <!> <!>",1),qy=ne('<div style="position: relative; height: 100%; width: 100%"><!> <!></div>');const Gy={hash:"svelte-1oe15vw",code:".panel-content.svelte-1oe15vw {padding:10px;background-color:#fff;border-radius:5px;box-shadow:0 2px 4px rgba(0, 0, 0, 0.1);width:200px;border:1px solid #efefef;}.setting-title.svelte-1oe15vw {margin:10px 0;font-size:12px;color:#999;}"};function Td(e,t){ue(t,!0),et(e,Gy);const n=w(t,"onInit",7),r=Lt();n()(r);let o=Fn(!1);const i=_=>{_.preventDefault(),_.dataTransfer&&(_.dataTransfer.dropEffect="move")},s=_=>{var T;_.preventDefault();const v=r.screenToFlowPosition({x:_.clientX-250,y:_.clientY-100}),b=(T=_.dataTransfer)==null?void 0:T.getData("application/tinyflow"),N=b?JSON.parse(b):{},E={id:`node_${Kr()}`,position:v,data:{},...N};Bi.addNode(E),Bi.selectNodeOnly(E.id)},{getNode:a}=Zy(),l=_=>{const v=a(_.source),b=a(_.target);if(_.sourceHandle==="loop_handle"||v.parentId){const N=r.getEdges();for(let E of N)if(E.target===_.target){const T=a(E.source);if(_.sourceHandle==="loop_handle"&&T.parentId!==v.id||v.parentId&&T.parentId!==v.parentId)return!1}}return!(!v.parentId&&b.parentId&&b.parentId!==v.id)},{ensureParentInNodesBefore:u}=Xy(),c=(_,v)=>{if(!v.isValid)return;const b=v.toNode;if(b.parentId)return;const N=v.fromNode,E=v.fromHandle,T={position:{...b.position}};if(E.id==="loop_handle"?T.parentId=N.id:N.parentId&&(T.parentId=N.parentId),T.parentId){const D=a(T.parentId);T.position={x:b.position.x-D.position.x,y:b.position.y-D.position.y},u(T.parentId,b.id),r.updateNode(b.id,T)}},{getEdgesByTarget:f}=Fy(),d=_=>{_.edges.forEach(b=>{const N=a(b.target);if(N.parentId){const E=f(b.target),T=a(N.parentId);if(E.length===0)r.updateNode(N.id,{parentId:void 0,position:{x:N.position.x+T.position.x,y:N.position.y+T.position.y}});else{let D=!1;for(let V=0;V<E.length;V++){const A=E[V],O=a(A.source);if(O.parentId||O.type==="loopNode"){D=!0;break}}D||r.updateNode(N.id,{parentId:void 0,position:{x:N.position.x+T.position.x,y:N.position.y+T.position.y}})}}})},g=(_,v)=>{console.log("onconnectstart: ",_,v)},p=_=>{console.log("onconnect: ",_)};var x=qy(),C=X(x);Nd(C);var $=z(C,2);const m=Ne(()=>({markerEnd:{type:_o.ArrowClosed,width:20,height:20}}));return Pc($,ft({nodeTypes:Iy},Bi,{class:"tinyflow-logo",isValidConnection:l,onconnectend:c,onconnectstart:g,onconnect:p,connectionRadius:50,ondelete:d,onclick:_=>{const v=_.target;v.classList.contains("svelte-flow__edge-interaction")||v.classList.contains("panel-content")||v.closest(".panel-content")||U(o,!1)},get defaultEdgeOptions(){return h(m)},$$events:{drop:s,dragover:i,edgeclick:()=>{U(o,!0)}},children:(_,v)=>{var b=Ky(),N=xe(b);Ic(N,{});var E=z(N,2);Ac(E,{});var T=z(E,2);Rc(T,{});var D=z(T,2);{var V=A=>{So(A,{children:(O,R)=>{var S=Wy(),M=z(X(S),4),k=X(M);Pt(k,{rows:3,placeholder:"璇疯緭鍏ヨ竟鏉′欢",style:"width: 100%",oninput:P=>{}}),Z(M),Z(S),L(O,S)},$$slots:{default:!0}})};ke(D,A=>{h(o)&&A(V)})}L(_,b)},$$slots:{default:!0}})),Z(x),L(e,x),ce({get onInit(){return n()},set onInit(_){n(_),y()}})}ie(Td,{onInit:{}},[],[],!0);function Uy(e,t){ue(t,!0);const n=w(t,"options",7),r=w(t,"onInit",7),{data:o}=n();return Bi.init((o==null?void 0:o.nodes)||[],(o==null?void 0:o.edges)||[]),Sr("tinyflow_options",n()),Nc(e,{fitView:!0,children:(i,s)=>{Td(i,{get onInit(){return r()}})},$$slots:{default:!0}}),ce({get options(){return n()},set options(i){n(i),y()},get onInit(){return r()},set onInit(i){r(i),y()}})}customElements.define("tinyflow-component",ie(Uy,{options:{},onInit:{}},[],[],!1)),We.Tinyflow=J2,Object.defineProperty(We,Symbol.toStringTag,{value:"Module"})});
+//# sourceMappingURL=index.umd.js.map
diff --git a/src/components/Tooltip/index.ts b/src/components/Tooltip/index.ts
new file mode 100644
index 0000000..ab66ddf
--- /dev/null
+++ b/src/components/Tooltip/index.ts
@@ -0,0 +1,3 @@
+import Tooltip from './src/Tooltip.vue'
+
+export { Tooltip }
diff --git a/src/components/Tooltip/src/Tooltip.vue b/src/components/Tooltip/src/Tooltip.vue
new file mode 100644
index 0000000..1a2e09c
--- /dev/null
+++ b/src/components/Tooltip/src/Tooltip.vue
@@ -0,0 +1,17 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'Tooltip' })
+
+defineProps({
+ title: propTypes.string.def(''),
+ message: propTypes.string.def(''),
+ icon: propTypes.string.def('ep:question-filled')
+})
+</script>
+<template>
+ <span>{{ title }}</span>
+ <ElTooltip :content="message" placement="top">
+ <Icon :icon="icon" class="relative top-1px ml-1px" />
+ </ElTooltip>
+</template>
diff --git a/src/components/UploadFile/index.ts b/src/components/UploadFile/index.ts
new file mode 100644
index 0000000..97c1d66
--- /dev/null
+++ b/src/components/UploadFile/index.ts
@@ -0,0 +1,5 @@
+import UploadImg from './src/UploadImg.vue'
+import UploadImgs from './src/UploadImgs.vue'
+import UploadFile from './src/UploadFile.vue'
+
+export { UploadImg, UploadImgs, UploadFile }
diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue
new file mode 100644
index 0000000..96fe1bc
--- /dev/null
+++ b/src/components/UploadFile/src/UploadFile.vue
@@ -0,0 +1,235 @@
+<template>
+ <div v-if="!disabled" class="upload-file">
+ <el-upload
+ ref="uploadRef"
+ v-model:file-list="fileList"
+ :action="uploadUrl"
+ :auto-upload="autoUpload"
+ :before-upload="beforeUpload"
+ :disabled="disabled"
+ :drag="drag"
+ :http-request="httpRequest"
+ :limit="props.limit"
+ :multiple="props.limit > 1"
+ :on-error="excelUploadError"
+ :on-exceed="handleExceed"
+ :on-preview="handlePreview"
+ :on-remove="handleRemove"
+ :on-success="handleFileSuccess"
+ :show-file-list="true"
+ class="upload-file-uploader"
+ name="file"
+ >
+ <el-button type="primary">
+ <Icon icon="ep:upload-filled" />
+ 閫夊彇鏂囦欢
+ </el-button>
+ <template v-if="isShowTip" #tip>
+ <div style="font-size: 8px">
+ 澶у皬涓嶈秴杩� <b style="color: #f56c6c">{{ fileSize }}MB</b>
+ </div>
+ <div style="font-size: 8px">
+ 鏍煎紡涓� <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 鐨勬枃浠�
+ </div>
+ </template>
+ <template #file="row">
+ <div class="flex items-center">
+ <span>{{ row.file.name }}</span>
+ <div class="ml-10px">
+ <el-link
+ :href="row.file.url"
+ :underline="false"
+ download
+ target="_blank"
+ type="primary"
+ >
+ 涓嬭浇
+ </el-link>
+ </div>
+ <div class="ml-10px">
+ <el-button link type="danger" @click="handleRemove(row.file)"> 鍒犻櫎</el-button>
+ </div>
+ </div>
+ </template>
+ </el-upload>
+ </div>
+
+ <!-- 涓婁紶鎿嶄綔绂佺敤鏃� -->
+ <div v-if="disabled" class="upload-file">
+ <div v-for="(file, index) in fileList" :key="index" class="flex items-center file-list-item">
+ <span>{{ file.name }}</span>
+ <div class="ml-10px">
+ <el-link :href="file.url" :underline="false" download target="_blank" type="primary">
+ 涓嬭浇
+ </el-link>
+ </div>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
+import { isString } from '@/utils/is'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { UploadFile } from 'element-plus/es/components/upload/src/upload'
+
+defineOptions({ name: 'UploadFile' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const emit = defineEmits(['update:modelValue'])
+
+const props = defineProps({
+ modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
+ fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 鏂囦欢绫诲瀷, 渚嬪['png', 'jpg', 'jpeg']
+ fileSize: propTypes.number.def(5), // 澶у皬闄愬埗(MB)
+ limit: propTypes.number.def(5), // 鏁伴噺闄愬埗
+ autoUpload: propTypes.bool.def(true), // 鑷姩涓婁紶
+ drag: propTypes.bool.def(false), // 鎷栨嫿涓婁紶
+ isShowTip: propTypes.bool.def(true), // 鏄惁鏄剧ず鎻愮ず
+ disabled: propTypes.bool.def(false), // 鏄惁绂佺敤涓婁紶缁勪欢 ==> 闈炲繀浼狅紙榛樿涓� false锛�
+ directory: propTypes.string.def(undefined) // 涓婁紶鐩綍 ==> 闈炲繀浼狅紙榛樿涓� undefined锛�
+})
+
+// ========== 涓婁紶鐩稿叧 ==========
+const uploadRef = ref<UploadInstance>()
+const uploadList = ref<UploadUserFile[]>([])
+const fileList = ref<UploadUserFile[]>([])
+const uploadNumber = ref<number>(0)
+
+const { uploadUrl, httpRequest } = useUpload(props.directory)
+
+// 鏂囦欢涓婁紶涔嬪墠鍒ゆ柇
+const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
+ if (fileList.value.length >= props.limit) {
+ message.error(`涓婁紶鏂囦欢鏁伴噺涓嶈兘瓒呰繃${props.limit}涓�!`)
+ return false
+ }
+ let fileExtension = ''
+ if (file.name.lastIndexOf('.') > -1) {
+ fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
+ }
+ const isImg = props.fileType.some((type: string) => {
+ if (file.type.indexOf(type) > -1) return true
+ return !!(fileExtension && fileExtension.indexOf(type) > -1)
+ })
+ const isLimit = file.size < props.fileSize * 1024 * 1024
+ if (!isImg) {
+ message.error(`鏂囦欢鏍煎紡涓嶆纭�, 璇蜂笂浼�${props.fileType.join('/')}鏍煎紡!`)
+ return false
+ }
+ if (!isLimit) {
+ message.error(`涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃${props.fileSize}MB!`)
+ return false
+ }
+ message.success('姝e湪涓婁紶鏂囦欢锛岃绋嶅��...')
+ // 鍙湁鍦ㄩ獙璇侀�氳繃鍚庢墠澧炲姞璁℃暟鍣�
+ uploadNumber.value++
+ return true
+}
+// 澶勭悊涓婁紶鐨勬枃浠跺彂鐢熷彉鍖�
+// const handleFileChange = (uploadFile: UploadFile): void => {
+// uploadRef.value.data.path = uploadFile.name
+// }
+// 鏂囦欢涓婁紶鎴愬姛
+const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
+ message.success('涓婁紶鎴愬姛')
+ // 鍒犻櫎鑷韩
+ const index = fileList.value.findIndex((item) => item.response?.data === res.data)
+ fileList.value.splice(index, 1)
+ uploadList.value.push({ name: res.data, url: res.data })
+ if (uploadList.value.length == uploadNumber.value) {
+ fileList.value.push(...uploadList.value)
+ uploadList.value = []
+ uploadNumber.value = 0
+ emitUpdateModelValue()
+ }
+}
+// 鏂囦欢鏁拌秴鍑烘彁绀�
+const handleExceed: UploadProps['onExceed'] = (): void => {
+ message.error(`涓婁紶鏂囦欢鏁伴噺涓嶈兘瓒呰繃${props.limit}涓�!`)
+}
+// 涓婁紶閿欒鎻愮ず
+const excelUploadError: UploadProps['onError'] = (): void => {
+ message.error('瀵煎叆鏁版嵁澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒')
+ // 涓婁紶澶辫触鏃跺噺灏戣鏁板櫒锛岄伩鍏嶅悗缁笂浼犺闃诲
+ uploadNumber.value = Math.max(0, uploadNumber.value - 1)
+}
+// 鍒犻櫎涓婁紶鏂囦欢
+const handleRemove = (file: UploadFile) => {
+ const index = fileList.value.map((f) => f.name).indexOf(file.name)
+ if (index > -1) {
+ fileList.value.splice(index, 1)
+ emitUpdateModelValue()
+ }
+}
+const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
+ console.log(uploadFile)
+}
+
+// 鐩戝惉妯″瀷缁戝畾鍊煎彉鍔�
+watch(
+ () => props.modelValue,
+ (val: string | string[]) => {
+ if (!val) {
+ fileList.value = [] // fix锛氬鐞嗘帀缂撳瓨锛岃〃鍗曢噸缃悗涓婁紶缁勪欢鐨勫唴瀹瑰苟娌℃湁閲嶇疆
+ return
+ }
+
+ fileList.value = [] // 淇濋殰鏁版嵁涓虹┖
+ // 鎯呭喌1锛氬瓧绗︿覆
+ if (isString(val)) {
+ fileList.value.push(
+ ...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
+ )
+ return
+ }
+ // 鎯呭喌2锛氭暟缁�
+ fileList.value.push(
+ ...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
+ )
+ },
+ { immediate: true, deep: true }
+)
+// 鍙戦�佹枃浠堕摼鎺ュ垪琛ㄦ洿鏂�
+const emitUpdateModelValue = () => {
+ // 鎯呭喌1锛氭暟缁勭粨鏋�
+ let result: string | string[] = fileList.value.map((file) => file.url!)
+ // 鎯呭喌2锛氶�楀彿鍒嗛殧鐨勫瓧绗︿覆
+ if (props.limit === 1 || isString(props.modelValue)) {
+ result = result.join(',')
+ }
+ emit('update:modelValue', result)
+}
+</script>
+<style lang="scss" scoped>
+.upload-file-uploader {
+ margin-bottom: 5px;
+}
+
+:deep(.upload-file-list .el-upload-list__item) {
+ position: relative;
+ margin-bottom: 10px;
+ line-height: 2;
+ border: 1px solid #e4e7ed;
+}
+
+:deep(.el-upload-list__item-file-name) {
+ max-width: 250px;
+}
+
+:deep(.upload-file-list .ele-upload-list__item-content) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: inherit;
+}
+
+:deep(.ele-upload-list__item-content-action .el-link) {
+ margin-right: 10px;
+}
+
+.file-list-item {
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+}
+</style>
diff --git a/src/components/UploadFile/src/UploadImg.vue b/src/components/UploadFile/src/UploadImg.vue
new file mode 100644
index 0000000..66e9d0c
--- /dev/null
+++ b/src/components/UploadFile/src/UploadImg.vue
@@ -0,0 +1,272 @@
+<template>
+ <div class="upload-box">
+ <el-upload
+ :id="uuid"
+ :accept="fileType.join(',')"
+ :action="uploadUrl"
+ :before-upload="beforeUpload"
+ :class="['upload', drag ? 'no-border' : '']"
+ :disabled="disabled"
+ :drag="drag"
+ :http-request="httpRequest"
+ :multiple="false"
+ :on-error="uploadError"
+ :on-success="uploadSuccess"
+ :show-file-list="false"
+ >
+ <template v-if="modelValue">
+ <img :src="modelValue" class="upload-image" />
+ <div class="upload-handle" @click.stop>
+ <div v-if="!disabled" class="handle-icon" @click="editImg">
+ <Icon icon="ep:edit" />
+ <span v-if="showBtnText">{{ t('action.edit') }}</span>
+ </div>
+ <div class="handle-icon" @click="imagePreview(modelValue)">
+ <Icon icon="ep:zoom-in" />
+ <span v-if="showBtnText">{{ t('action.detail') }}</span>
+ </div>
+ <div v-if="showDelete && !disabled" class="handle-icon" @click="deleteImg">
+ <Icon icon="ep:delete" />
+ <span v-if="showBtnText">{{ t('action.del') }}</span>
+ </div>
+ </div>
+ </template>
+ <template v-else>
+ <div class="upload-empty">
+ <slot name="empty">
+ <Icon icon="ep:plus" />
+ <!-- <span>璇蜂笂浼犲浘鐗�</span> -->
+ </slot>
+ </div>
+ </template>
+ </el-upload>
+ <div class="el-upload__tip">
+ <slot name="tip"></slot>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import type { UploadProps } from 'element-plus'
+
+import { generateUUID } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { createImageViewer } from '@/components/ImageViewer'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+
+defineOptions({ name: 'UploadImg' })
+
+type FileTypes =
+ | 'image/apng'
+ | 'image/bmp'
+ | 'image/gif'
+ | 'image/jpeg'
+ | 'image/pjpeg'
+ | 'image/png'
+ | 'image/svg+xml'
+ | 'image/tiff'
+ | 'image/webp'
+ | 'image/x-icon'
+
+// 鎺ュ彈鐖剁粍浠跺弬鏁�
+const props = defineProps({
+ modelValue: propTypes.string.def(''),
+ drag: propTypes.bool.def(true), // 鏄惁鏀寔鎷栨嫿涓婁紶 ==> 闈炲繀浼狅紙榛樿涓� true锛�
+ disabled: propTypes.bool.def(false), // 鏄惁绂佺敤涓婁紶缁勪欢 ==> 闈炲繀浼狅紙榛樿涓� false锛�
+ fileSize: propTypes.number.def(5), // 鍥剧墖澶у皬闄愬埗 ==> 闈炲繀浼狅紙榛樿涓� 5M锛�
+ fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 鍥剧墖绫诲瀷闄愬埗 ==> 闈炲繀浼狅紙榛樿涓� ["image/jpeg", "image/png", "image/gif"]锛�
+ height: propTypes.string.def('150px'), // 缁勪欢楂樺害 ==> 闈炲繀浼狅紙榛樿涓� 150px锛�
+ width: propTypes.string.def('150px'), // 缁勪欢瀹藉害 ==> 闈炲繀浼狅紙榛樿涓� 150px锛�
+ borderradius: propTypes.string.def('8px'), // 缁勪欢杈规鍦嗚 ==> 闈炲繀浼狅紙榛樿涓� 8px锛�
+ showDelete: propTypes.bool.def(true), // 鏄惁鏄剧ず鍒犻櫎鎸夐挳
+ showBtnText: propTypes.bool.def(true), // 鏄惁鏄剧ず鎸夐挳鏂囧瓧
+ directory: propTypes.string.def(undefined) // 涓婁紶鐩綍 ==> 闈炲繀浼狅紙榛樿涓� undefined锛�
+})
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+// 鐢熸垚缁勪欢鍞竴id
+const uuid = ref('id-' + generateUUID())
+// 鏌ョ湅鍥剧墖
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ zIndex: 9999999,
+ urlList: [imgUrl]
+ })
+}
+
+const emit = defineEmits(['update:modelValue'])
+
+const deleteImg = () => {
+ emit('update:modelValue', '')
+}
+
+const { uploadUrl, httpRequest } = useUpload(props.directory)
+
+const editImg = () => {
+ const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
+ dom && dom.dispatchEvent(new MouseEvent('click'))
+}
+
+const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
+ const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
+ const imgType = props.fileType
+ if (!imgType.includes(rawFile.type as FileTypes))
+ message.notifyWarning('涓婁紶鍥剧墖涓嶇鍚堟墍闇�鐨勬牸寮忥紒')
+ if (!imgSize) message.notifyWarning(`涓婁紶鍥剧墖澶у皬涓嶈兘瓒呰繃 ${props.fileSize}M锛乣)
+ return imgType.includes(rawFile.type as FileTypes) && imgSize
+}
+
+// 鍥剧墖涓婁紶鎴愬姛鎻愮ず
+const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
+ message.success('涓婁紶鎴愬姛')
+ emit('update:modelValue', res.data)
+}
+
+// 鍥剧墖涓婁紶閿欒鎻愮ず
+const uploadError = () => {
+ message.notifyError('鍥剧墖涓婁紶澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒')
+}
+</script>
+<style lang="scss" scoped>
+.is-error {
+ .upload {
+ :deep(.el-upload),
+ :deep(.el-upload-dragger) {
+ border: 1px dashed var(--el-color-danger) !important;
+
+ &:hover {
+ border-color: var(--el-color-primary) !important;
+ }
+ }
+ }
+}
+
+:deep(.disabled) {
+ .el-upload,
+ .el-upload-dragger {
+ cursor: not-allowed !important;
+ background: var(--el-disabled-bg-color);
+ border: 1px dashed var(--el-border-color-darker) !important;
+
+ &:hover {
+ border: 1px dashed var(--el-border-color-darker) !important;
+ }
+ }
+}
+
+.upload-box {
+ .no-border {
+ :deep(.el-upload) {
+ border: none !important;
+ }
+ }
+
+ :deep(.upload) {
+ .el-upload {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: v-bind(width);
+ height: v-bind(height);
+ overflow: hidden;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: v-bind(borderradius);
+ transition: var(--el-transition-duration-fast);
+
+ &:hover {
+ border-color: var(--el-color-primary);
+
+ .upload-handle {
+ opacity: 1;
+ }
+ }
+
+ .el-upload-dragger {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ overflow: hidden;
+ background-color: transparent;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: v-bind(borderradius);
+
+ &:hover {
+ border: 1px dashed var(--el-color-primary);
+ }
+ }
+
+ .el-upload-dragger.is-dragover {
+ background-color: var(--el-color-primary-light-9);
+ border: 2px dashed var(--el-color-primary) !important;
+ }
+
+ .upload-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+ .upload-empty {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ line-height: 30px;
+ color: var(--el-color-info);
+
+ .el-icon {
+ font-size: 28px;
+ color: var(--el-text-color-secondary);
+ }
+ }
+
+ .upload-handle {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ background: rgb(0 0 0 / 60%);
+ opacity: 0;
+ box-sizing: border-box;
+ transition: var(--el-transition-duration-fast);
+ align-items: center;
+ justify-content: center;
+
+ .handle-icon {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0 6%;
+ color: aliceblue;
+
+ .el-icon {
+ margin-bottom: 40%;
+ font-size: 130%;
+ line-height: 130%;
+ }
+
+ span {
+ font-size: 85%;
+ line-height: 85%;
+ }
+ }
+ }
+ }
+ }
+
+ .el-upload__tip {
+ line-height: 18px;
+ text-align: center;
+ }
+}
+</style>
diff --git a/src/components/UploadFile/src/UploadImgs.vue b/src/components/UploadFile/src/UploadImgs.vue
new file mode 100644
index 0000000..d1386e4
--- /dev/null
+++ b/src/components/UploadFile/src/UploadImgs.vue
@@ -0,0 +1,329 @@
+<template>
+ <div class="upload-box">
+ <el-upload
+ v-model:file-list="fileList"
+ :accept="fileType.join(',')"
+ :action="uploadUrl"
+ :before-upload="beforeUpload"
+ :class="['upload', drag ? 'no-border' : '']"
+ :disabled="disabled"
+ :drag="drag"
+ :http-request="httpRequest"
+ :limit="limit"
+ :multiple="true"
+ :on-error="uploadError"
+ :on-exceed="handleExceed"
+ :on-success="uploadSuccess"
+ list-type="picture-card"
+ >
+ <div class="upload-empty">
+ <slot name="empty">
+ <Icon icon="ep:plus" />
+ <!-- <span>璇蜂笂浼犲浘鐗�</span> -->
+ </slot>
+ </div>
+ <template #file="{ file }">
+ <img :src="file.url" class="upload-image" />
+ <div class="upload-handle" @click.stop>
+ <div class="handle-icon" @click="imagePreview(file.url!)">
+ <Icon icon="ep:zoom-in" />
+ <span>鏌ョ湅</span>
+ </div>
+ <div v-if="!disabled" class="handle-icon" @click="handleRemove(file)">
+ <Icon icon="ep:delete" />
+ <span>鍒犻櫎</span>
+ </div>
+ </div>
+ </template>
+ </el-upload>
+ <div class="el-upload__tip">
+ <slot name="tip"></slot>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
+import { ElNotification } from 'element-plus'
+import { createImageViewer } from '@/components/ImageViewer'
+
+import { propTypes } from '@/utils/propTypes'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+
+defineOptions({ name: 'UploadImgs' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+// 鏌ョ湅鍥剧墖
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ zIndex: 9999999,
+ urlList: [imgUrl]
+ })
+}
+
+type FileTypes =
+ | 'image/apng'
+ | 'image/bmp'
+ | 'image/gif'
+ | 'image/jpeg'
+ | 'image/pjpeg'
+ | 'image/png'
+ | 'image/svg+xml'
+ | 'image/tiff'
+ | 'image/webp'
+ | 'image/x-icon'
+
+const props = defineProps({
+ modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
+ drag: propTypes.bool.def(true), // 鏄惁鏀寔鎷栨嫿涓婁紶 ==> 闈炲繀浼狅紙榛樿涓� true锛�
+ disabled: propTypes.bool.def(false), // 鏄惁绂佺敤涓婁紶缁勪欢 ==> 闈炲繀浼狅紙榛樿涓� false锛�
+ limit: propTypes.number.def(5), // 鏈�澶у浘鐗囦笂浼犳暟 ==> 闈炲繀浼狅紙榛樿涓� 5寮狅級
+ fileSize: propTypes.number.def(5), // 鍥剧墖澶у皬闄愬埗 ==> 闈炲繀浼狅紙榛樿涓� 5M锛�
+ fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 鍥剧墖绫诲瀷闄愬埗 ==> 闈炲繀浼狅紙榛樿涓� ["image/jpeg", "image/png", "image/gif"]锛�
+ height: propTypes.string.def('150px'), // 缁勪欢楂樺害 ==> 闈炲繀浼狅紙榛樿涓� 150px锛�
+ width: propTypes.string.def('150px'), // 缁勪欢瀹藉害 ==> 闈炲繀浼狅紙榛樿涓� 150px锛�
+ borderradius: propTypes.string.def('8px'), // 缁勪欢杈规鍦嗚 ==> 闈炲繀浼狅紙榛樿涓� 8px锛�
+ directory: propTypes.string.def(undefined) // 涓婁紶鐩綍 ==> 闈炲繀浼狅紙榛樿涓� undefined锛�
+})
+
+const { uploadUrl, httpRequest } = useUpload(props.directory)
+
+const fileList = ref<UploadUserFile[]>([])
+const uploadNumber = ref<number>(0)
+const uploadList = ref<UploadUserFile[]>([])
+/**
+ * @description 鏂囦欢涓婁紶涔嬪墠鍒ゆ柇
+ * @param rawFile 涓婁紶鐨勬枃浠�
+ * */
+const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
+ const imgSize = rawFile.size / 1024 / 1024 < props.fileSize
+ const imgType = props.fileType
+ const isValidType = imgType.includes(rawFile.type as FileTypes)
+ const isValidSize = imgSize
+
+ if (!isValidType)
+ ElNotification({
+ title: '娓╅Θ鎻愮ず',
+ message: '涓婁紶鍥剧墖涓嶇鍚堟墍闇�鐨勬牸寮忥紒',
+ type: 'warning'
+ })
+ if (!isValidSize)
+ ElNotification({
+ title: '娓╅Θ鎻愮ず',
+ message: `涓婁紶鍥剧墖澶у皬涓嶈兘瓒呰繃 ${props.fileSize}M锛乣,
+ type: 'warning'
+ })
+
+ // 鍙湁鍦ㄩ獙璇侀�氳繃鍚庢墠澧炲姞璁℃暟鍣�
+ if (isValidType && isValidSize) {
+ uploadNumber.value++
+ }
+
+ return isValidType && isValidSize
+}
+
+// 鍥剧墖涓婁紶鎴愬姛
+interface UploadEmits {
+ (e: 'update:modelValue', value: string[]): void
+}
+
+const emit = defineEmits<UploadEmits>()
+const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
+ message.success('涓婁紶鎴愬姛')
+ // 鍒犻櫎鑷韩
+ const index = fileList.value.findIndex((item) => item.response?.data === res.data)
+ fileList.value.splice(index, 1)
+ uploadList.value.push({ name: res.data, url: res.data })
+ if (uploadList.value.length == uploadNumber.value) {
+ fileList.value.push(...uploadList.value)
+ uploadList.value = []
+ uploadNumber.value = 0
+ emitUpdateModelValue()
+ }
+}
+
+// 鐩戝惉妯″瀷缁戝畾鍊煎彉鍔�
+watch(
+ () => props.modelValue,
+ (val: string | string[]) => {
+ if (!val) {
+ fileList.value = [] // fix锛氬鐞嗘帀缂撳瓨锛岃〃鍗曢噸缃悗涓婁紶缁勪欢鐨勫唴瀹瑰苟娌℃湁閲嶇疆
+ return
+ }
+
+ fileList.value = [] // 淇濋殰鏁版嵁涓虹┖
+ fileList.value.push(
+ ...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
+ )
+ },
+ { immediate: true, deep: true }
+)
+// 鍙戦�佸浘鐗囬摼鎺ュ垪琛ㄦ洿鏂�
+const emitUpdateModelValue = () => {
+ let result: string[] = fileList.value.map((file) => file.url!)
+ emit('update:modelValue', result)
+}
+// 鍒犻櫎鍥剧墖
+const handleRemove = (uploadFile: UploadFile) => {
+ fileList.value = fileList.value.filter(
+ (item) => item.url !== uploadFile.url || item.name !== uploadFile.name
+ )
+ emit(
+ 'update:modelValue',
+ fileList.value.map((file) => file.url!)
+ )
+}
+
+// 鍥剧墖涓婁紶閿欒鎻愮ず
+const uploadError = () => {
+ ElNotification({
+ title: '娓╅Θ鎻愮ず',
+ message: '鍥剧墖涓婁紶澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒',
+ type: 'error'
+ })
+ // 涓婁紶澶辫触鏃跺噺灏戣鏁板櫒锛岄伩鍏嶅悗缁笂浼犺闃诲
+ uploadNumber.value = Math.max(0, uploadNumber.value - 1)
+}
+
+// 鏂囦欢鏁拌秴鍑烘彁绀�
+const handleExceed = () => {
+ ElNotification({
+ title: '娓╅Θ鎻愮ず',
+ message: `褰撳墠鏈�澶氬彧鑳戒笂浼� ${props.limit} 寮犲浘鐗囷紝璇风Щ闄ゅ悗涓婁紶锛乣,
+ type: 'warning'
+ })
+}
+</script>
+
+<style lang="scss" scoped>
+.is-error {
+ .upload {
+ :deep(.el-upload--picture-card),
+ :deep(.el-upload-dragger) {
+ border: 1px dashed var(--el-color-danger) !important;
+
+ &:hover {
+ border-color: var(--el-color-primary) !important;
+ }
+ }
+ }
+}
+
+:deep(.disabled) {
+ .el-upload--picture-card,
+ .el-upload-dragger {
+ cursor: not-allowed;
+ background: var(--el-disabled-bg-color) !important;
+ border: 1px dashed var(--el-border-color-darker);
+
+ &:hover {
+ border-color: var(--el-border-color-darker) !important;
+ }
+ }
+}
+
+.upload-box {
+ .no-border {
+ :deep(.el-upload--picture-card) {
+ border: none !important;
+ }
+ }
+
+ :deep(.upload) {
+ .el-upload-dragger {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ overflow: hidden;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: v-bind(borderradius);
+
+ &:hover {
+ border: 1px dashed var(--el-color-primary);
+ }
+ }
+
+ .el-upload-dragger.is-dragover {
+ background-color: var(--el-color-primary-light-9);
+ border: 2px dashed var(--el-color-primary) !important;
+ }
+
+ .el-upload-list__item,
+ .el-upload--picture-card {
+ width: v-bind(width);
+ height: v-bind(height);
+ background-color: transparent;
+ border-radius: v-bind(borderradius);
+ }
+
+ .upload-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+ .upload-handle {
+ position: absolute;
+ top: 0;
+ right: 0;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ background: rgb(0 0 0 / 60%);
+ opacity: 0;
+ box-sizing: border-box;
+ transition: var(--el-transition-duration-fast);
+ align-items: center;
+ justify-content: center;
+
+ .handle-icon {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 0 6%;
+ color: aliceblue;
+
+ .el-icon {
+ margin-bottom: 15%;
+ font-size: 140%;
+ }
+
+ span {
+ font-size: 100%;
+ }
+ }
+ }
+
+ .el-upload-list__item {
+ &:hover {
+ .upload-handle {
+ opacity: 1;
+ }
+ }
+ }
+
+ .upload-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ font-size: 12px;
+ line-height: 30px;
+ color: var(--el-color-info);
+
+ .el-icon {
+ font-size: 28px;
+ color: var(--el-text-color-secondary);
+ }
+ }
+ }
+
+ .el-upload__tip {
+ line-height: 15px;
+ text-align: center;
+ }
+}
+</style>
diff --git a/src/components/UploadFile/src/useUpload.ts b/src/components/UploadFile/src/useUpload.ts
new file mode 100644
index 0000000..dddd159
--- /dev/null
+++ b/src/components/UploadFile/src/useUpload.ts
@@ -0,0 +1,102 @@
+import * as FileApi from '@/api/infra/file'
+import {
+ UploadRawFile,
+ UploadRequestOptions,
+ UploadProgressEvent
+} from 'element-plus/es/components/upload/src/upload'
+import axios, { AxiosProgressEvent } from 'axios'
+
+/**
+ * 鑾峰緱涓婁紶 URL
+ */
+export const getUploadUrl = (): string => {
+ return import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/infra/file/upload'
+}
+
+export const useUpload = (directory?: string) => {
+ // 鍚庣涓婁紶鍦板潃
+ const uploadUrl = getUploadUrl()
+ // 鏄惁浣跨敤鍓嶇鐩磋繛涓婁紶
+ const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
+ // 閲嶅啓ElUpload涓婁紶鏂规硶
+ const httpRequest = async (options: UploadRequestOptions) => {
+ // 鏂囦欢涓婁紶杩涘害鐩戝惉
+ const uploadProgressHandler = (evt: AxiosProgressEvent) => {
+ const upEvt: UploadProgressEvent = Object.assign(evt.event)
+ upEvt.percent = evt.progress ? evt.progress * 100 : 0
+ options.onProgress(upEvt) // 瑙﹀彂 el-upload 鐨� on-progress
+ }
+
+ // 妯″紡涓�锛氬墠绔笂浼�
+ if (isClientUpload) {
+ // 1.1 鐢熸垚鏂囦欢鍚嶇О
+ const fileName = options.file.name || options.filename
+ // 1.2 鑾峰彇鏂囦欢棰勭鍚嶅湴鍧�
+ const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
+ // 1.3 涓婁紶鏂囦欢锛堜笉鑳戒娇鐢� ElUpload 鐨� ajaxUpload 鏂规硶鐨勫師鍥狅細鍏朵娇鐢ㄧ殑鏄� FormData 涓婁紶锛孧inio 涓嶆敮鎸侊級
+ return axios
+ .put(presignedInfo.uploadUrl, options.file, {
+ headers: {
+ 'Content-Type': options.file.type || 'application/octet-stream'
+ },
+ onUploadProgress: uploadProgressHandler
+ })
+ .then(() => {
+ // 1.4. 璁板綍鏂囦欢淇℃伅鍒板悗绔紙寮傛锛�
+ createFile(presignedInfo, options.file, fileName)
+ // 閫氱煡鎴愬姛锛屾暟鎹牸寮忎繚鎸佷笌鍚庣涓婁紶鐨勮繑鍥炵粨鏋滀竴鑷�
+ return { data: presignedInfo.url }
+ })
+ } else {
+ // 妯″紡浜岋細鍚庣涓婁紶
+ // 閲嶅啓 el-upload httpRequest 鏂囦欢涓婁紶鎴愬姛浼氳蛋鎴愬姛鐨勯挬瀛愶紝澶辫触璧板け璐ョ殑閽╁瓙
+ return new Promise((resolve, reject) => {
+ FileApi.updateFile({ file: options.file, directory }, uploadProgressHandler)
+ .then((res) => {
+ if (res.code === 0) {
+ resolve(res)
+ } else {
+ reject(res)
+ }
+ })
+ .catch((res) => {
+ reject(res)
+ })
+ })
+ }
+ }
+
+ return {
+ uploadUrl,
+ httpRequest
+ }
+}
+
+/**
+ * 鍒涘缓鏂囦欢淇℃伅
+ * @param vo 鏂囦欢棰勭鍚嶄俊鎭�
+ * @param file 鏂囦欢
+ * @param fileName
+ */
+function createFile(vo: FileApi.FilePresignedUrlRespVO, file: UploadRawFile, fileName: string) {
+ const fileVo = {
+ configId: vo.configId,
+ url: vo.url,
+ path: vo.path,
+ name: fileName,
+ type: file.type || 'application/octet-stream',
+ size: file.size
+ }
+ FileApi.createFile(fileVo)
+ return fileVo
+}
+
+/**
+ * 涓婁紶绫诲瀷
+ */
+enum UPLOAD_TYPE {
+ // 瀹㈡埛绔洿鎺ヤ笂浼狅紙鍙敮鎸丼3鏈嶅姟锛�
+ CLIENT = 'client',
+ // 瀹㈡埛绔彂閫佸埌鍚庣涓婁紶
+ SERVER = 'server'
+}
diff --git a/src/components/UserSelectForm/index.vue b/src/components/UserSelectForm/index.vue
new file mode 100644
index 0000000..5ed99f8
--- /dev/null
+++ b/src/components/UserSelectForm/index.vue
@@ -0,0 +1,171 @@
+<template>
+ <Dialog v-model="dialogVisible" title="浜哄憳閫夋嫨" width="800">
+ <el-row class="gap2" v-loading="formLoading">
+ <el-col :span="6">
+ <ContentWrap class="h-1/1">
+ <el-tree
+ ref="treeRef"
+ :data="deptTree"
+ :expand-on-click-node="false"
+ :props="defaultProps"
+ default-expand-all
+ highlight-current
+ node-key="id"
+ @node-click="handleNodeClick"
+ />
+ </ContentWrap>
+ </el-col>
+ <el-col :span="17">
+ <el-transfer
+ v-model="selectedUserIdList"
+ :titles="['鏈��', '宸查��']"
+ filterable
+ filter-placeholder="鎼滅储鎴愬憳"
+ :data="transferUserList"
+ :props="{ label: 'nickname', key: 'id' }"
+ />
+ </el-col>
+ </el-row>
+ <template #footer>
+ <el-button
+ :disabled="formLoading || !selectedUserIdList?.length"
+ type="primary"
+ @click="submitForm"
+ >
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'UserSelectForm' })
+const emit = defineEmits<{
+ confirm: [id: any, userList: any[]]
+}>()
+const { t } = useI18n() // 鍥介檯
+const message = useMessage() // 娑堟伅寮圭獥
+const deptTree = ref<Tree[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯鍖�
+const deptList = ref<any[]>([]) // 淇濆瓨鎵佸钩鍖栫殑閮ㄩ棬鍒楄〃鏁版嵁
+const userList = ref<UserApi.UserVO[]>([]) // 鎵�鏈夌敤鎴峰垪琛�
+const filteredUserList = ref<UserApi.UserVO[]>([]) // 褰撳墠閮ㄩ棬杩囨护鍚庣殑鐢ㄦ埛鍒楄〃
+const selectedUserIdList: any = ref([]) // 閫変腑鐨勭敤鎴峰垪琛�
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const activityId = ref()
+
+/** 璁$畻灞炴�э細鍚堝苟宸查�夋嫨鐨勭敤鎴峰拰褰撳墠閮ㄩ棬杩囨护鍚庣殑鐢ㄦ埛 */
+const transferUserList = computed(() => {
+ // 1.1 鑾峰彇鎵�鏈夊凡閫夋嫨鐨勭敤鎴�
+ const selectedUsers = userList.value.filter((user: any) =>
+ selectedUserIdList.value.includes(user.id)
+ )
+
+ // 1.2 鑾峰彇褰撳墠閮ㄩ棬杩囨护鍚庣殑鏈�夋嫨鐢ㄦ埛
+ const filteredUnselectedUsers = filteredUserList.value.filter(
+ (user: any) => !selectedUserIdList.value.includes(user.id)
+ )
+
+ // 2. 鍚堝苟骞跺幓閲�
+ return [...selectedUsers, ...filteredUnselectedUsers]
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number, selectedList?: any[]) => {
+ activityId.value = id
+ resetForm()
+
+ // 鍔犺浇閮ㄩ棬銆佺敤鎴峰垪琛�
+ const deptData = await DeptApi.getSimpleDeptList()
+ deptList.value = deptData // 淇濆瓨鎵佸钩缁撴瀯鐨勯儴闂ㄦ暟鎹�
+ deptTree.value = handleTree(deptData) // 杞崲鎴愭爲褰㈢粨鏋�
+ userList.value = await UserApi.getSimpleUserList()
+
+ // 鍒濆鐘舵�佷笅锛岃繃婊ゅ垪琛ㄧ瓑浜庢墍鏈夌敤鎴峰垪琛�
+ filteredUserList.value = [...userList.value]
+ selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
+ dialogVisible.value = true
+}
+
+/** 鑾峰彇鎸囧畾閮ㄩ棬鍙婂叾鎵�鏈夊瓙閮ㄩ棬鐨処D鍒楄〃 */
+const getChildDeptIds = (deptId: number, deptList: any[]): number[] => {
+ const ids = [deptId]
+ const children = deptList.filter((dept) => dept.parentId === deptId)
+ children.forEach((child) => {
+ ids.push(...getChildDeptIds(child.id, deptList))
+ })
+ return ids
+}
+
+/** 鑾峰彇閮ㄩ棬杩囨护鍚庣殑鐢ㄦ埛鍒楄〃 */
+const filterUserList = async (deptId?: number) => {
+ formLoading.value = true
+ try {
+ if (!deptId) {
+ // 濡傛灉娌℃湁閫夋嫨閮ㄩ棬锛屾樉绀烘墍鏈夌敤鎴�
+ filteredUserList.value = [...userList.value]
+ return
+ }
+
+ // 鐩存帴浣跨敤宸蹭繚瀛樼殑閮ㄩ棬鍒楄〃鏁版嵁杩涜杩囨护
+ const deptIds = getChildDeptIds(deptId, deptList.value)
+
+ // 杩囨护鍑鸿繖浜涢儴闂ㄤ笅鐨勭敤鎴�
+ filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId))
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鎻愪氦閫夋嫨 */
+const submitForm = async () => {
+ try {
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 浠庢墍鏈夌敤鎴峰垪琛ㄤ腑绛涢�夊嚭宸查�夋嫨鐨勭敤鎴�
+ const emitUserList = userList.value.filter((user: any) =>
+ selectedUserIdList.value.includes(user.id)
+ )
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('confirm', activityId.value, emitUserList)
+ } finally {
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ deptTree.value = []
+ deptList.value = []
+ userList.value = []
+ filteredUserList.value = []
+ selectedUserIdList.value = []
+}
+
+/** 澶勭悊閮ㄩ棬琚偣鍑� */
+const handleNodeClick = (row: { [key: string]: any }) => {
+ filterUserList(row.id)
+}
+
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
+
+<style lang="scss" scoped>
+:deep() {
+ .el-transfer {
+ display: flex;
+ }
+ .el-transfer__buttons {
+ display: flex !important;
+ flex-direction: column-reverse;
+ justify-content: center;
+ gap: 20px;
+ .el-transfer__button:nth-child(2) {
+ margin: 0;
+ }
+ }
+}
+</style>
diff --git a/src/components/Verifition/index.ts b/src/components/Verifition/index.ts
new file mode 100644
index 0000000..bcfe6d9
--- /dev/null
+++ b/src/components/Verifition/index.ts
@@ -0,0 +1,3 @@
+import Verify from './src/Verify.vue'
+
+export { Verify }
diff --git a/src/components/Verifition/src/Verify.vue b/src/components/Verifition/src/Verify.vue
new file mode 100644
index 0000000..930d0e7
--- /dev/null
+++ b/src/components/Verifition/src/Verify.vue
@@ -0,0 +1,446 @@
+<template>
+ <div v-show="showBox" :class="mode == 'pop' ? 'mask' : ''">
+ <div
+ :class="mode == 'pop' ? 'verifybox' : ''"
+ :style="{ 'max-width': parseInt(imgSize.width) + 20 + 'px' }"
+ >
+ <div v-if="mode == 'pop'" class="verifybox-top">
+ {{ t('captcha.verification') }}
+ <span class="verifybox-close" @click="closeBox">
+ <i class="iconfont icon-close"></i>
+ </span>
+ </div>
+ <div :style="{ padding: mode == 'pop' ? '10px' : '0' }" class="verifybox-bottom">
+ <!-- 楠岃瘉鐮佸鍣� -->
+ <component
+ :is="componentType"
+ v-if="componentType"
+ ref="instance"
+ :arith="arith"
+ :barSize="barSize"
+ :blockSize="blockSize"
+ :captchaType="captchaType"
+ :explain="explain"
+ :figure="figure"
+ :imgSize="imgSize"
+ :mode="mode"
+ :type="verifyType"
+ :vSpace="vSpace"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+<script type="text/babel">
+/**
+ * Verify 楠岃瘉鐮佺粍浠�
+ * @description 鍒嗗彂楠岃瘉鐮佷娇鐢�
+ * */
+import {VerifyPictureWord, VerifyPoints, VerifySlide} from './Verify'
+import { computed, ref, toRefs, watchEffect } from 'vue'
+
+export default {
+ name: 'Vue3Verify',
+ components: {
+ VerifySlide,
+ VerifyPoints,
+ VerifyPictureWord
+ },
+ props: {
+ captchaType: {
+ type: String,
+ required: true
+ },
+ figure: {
+ type: Number
+ },
+ arith: {
+ type: Number
+ },
+ mode: {
+ type: String,
+ default: 'pop'
+ },
+ vSpace: {
+ type: Number
+ },
+ explain: {
+ type: String
+ },
+ imgSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '155px'
+ }
+ }
+ },
+ blockSize: {
+ type: Object
+ },
+ barSize: {
+ type: Object
+ }
+ },
+ setup(props) {
+ const { t } = useI18n()
+ const { captchaType, mode } = toRefs(props)
+ const clickShow = ref(false)
+ const verifyType = ref(undefined)
+ const componentType = ref(undefined)
+
+ const instance = ref({})
+
+ const showBox = computed(() => {
+ if (mode.value == 'pop') {
+ return clickShow.value
+ } else {
+ return true
+ }
+ })
+ /**
+ * refresh
+ * @description 鍒锋柊
+ * */
+ const refresh = () => {
+ if (instance.value.refresh) {
+ instance.value.refresh()
+ }
+ }
+ const closeBox = () => {
+ clickShow.value = false
+ refresh()
+ }
+ const show = () => {
+ if (mode.value == 'pop') {
+ clickShow.value = true
+ }
+ }
+ watchEffect(() => {
+ switch (captchaType.value) {
+ case 'pictureWord':
+ verifyType.value = '3'
+ componentType.value = 'VerifyPictureWord'
+ break
+ case 'blockPuzzle':
+ verifyType.value = '2'
+ componentType.value = 'VerifySlide'
+ break
+ case 'clickWord':
+ verifyType.value = ''
+ componentType.value = 'VerifyPoints'
+ break
+ }
+ })
+
+ return {
+ t,
+ clickShow,
+ verifyType,
+ componentType,
+ instance,
+ showBox,
+ closeBox,
+ show
+ }
+ }
+}
+</script>
+<style>
+.verifybox {
+ position: relative;
+ top: 50%;
+ left: 50%;
+ background-color: #fff;
+ border: 1px solid #e4e7eb;
+ border-radius: 5px;
+ transform: translate(-50%, -50%);
+ box-shadow: 0 0 10px rgb(0 0 0 / 30%);
+ box-sizing: border-box;
+}
+
+.verifybox-top {
+ height: 40px;
+ padding: 0 15px;
+ font-size: 16px;
+ line-height: 40px;
+ color: #45494c;
+ text-align: left;
+ border-bottom: 1px solid #e4e7eb;
+ box-sizing: border-box;
+}
+
+.verifybox-bottom {
+ padding: 10px;
+ box-sizing: border-box;
+}
+
+.verifybox-close {
+ position: absolute;
+ top: 13px;
+ right: 9px;
+ width: 24px;
+ height: 24px;
+ text-align: center;
+ cursor: pointer;
+}
+
+.mask {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1001;
+ width: 100%;
+ height: 100vh;
+ background: rgb(0 0 0 / 30%);
+
+ /* display: none; */
+ transition: all 0.5s;
+}
+
+.verify-tips {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 30px;
+ line-height: 30px;
+ color: #fff;
+ text-indent: 10px;
+}
+
+.suc-bg {
+ background-color: rgb(92 184 92 / 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7f5CB85C, endcolorstr=#7f5CB85C);
+}
+
+.err-bg {
+ background-color: rgb(217 83 79 / 50%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startcolorstr=#7fD9534F, endcolorstr=#7fD9534F);
+}
+
+.tips-enter,
+.tips-leave-to {
+ bottom: -30px;
+}
+
+.tips-enter-active,
+.tips-leave-active {
+ transition: bottom 0.5s;
+}
+
+/* ---------------------------- */
+
+/* 甯歌楠岃瘉鐮� */
+.verify-code {
+ margin-bottom: 5px;
+ font-size: 20px;
+ text-align: center;
+ cursor: pointer;
+ border: 1px solid #ddd;
+}
+
+.cerify-code-panel {
+ height: 100%;
+ overflow: hidden;
+}
+
+.verify-code-area {
+ float: left;
+}
+
+.verify-input-area {
+ float: left;
+ width: 60%;
+ padding-right: 10px;
+}
+
+.verify-change-area {
+ float: left;
+ line-height: 30px;
+}
+
+.varify-input-code {
+ display: inline-block;
+ width: 100%;
+ height: 25px;
+}
+
+.verify-change-code {
+ color: #337ab7;
+ cursor: pointer;
+}
+
+.verify-btn {
+ width: 200px;
+ height: 30px;
+ margin-top: 10px;
+ color: #fff;
+ background-color: #337ab7;
+ border: none;
+ border-radius: 8px;
+}
+
+/* 婊戝姩楠岃瘉鐮� */
+.verify-bar-area {
+ position: relative;
+ text-align: center;
+ background: #fff;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-sizing: content-box;
+}
+
+.verify-bar-area .verify-move-block {
+ position: absolute;
+ top: 0;
+ left: 0;
+ cursor: pointer;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 0 2px #888;
+ box-sizing: content-box;
+}
+
+.verify-bar-area .verify-move-block:hover {
+ color: #fff;
+ background-color: #337ab7;
+}
+
+.verify-bar-area .verify-left-bar {
+ position: absolute;
+ top: -1px;
+ left: -1px;
+ cursor: pointer;
+ background: #f0fff0;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ box-sizing: content-box;
+}
+
+.verify-img-panel {
+ position: relative;
+ margin: 0;
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ border-radius: 3px;
+ box-sizing: content-box;
+}
+
+.verify-img-panel .verify-refresh {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 2;
+ width: 25px;
+ height: 25px;
+ padding: 5px;
+ text-align: center;
+ cursor: pointer;
+}
+
+.verify-img-panel .icon-refresh {
+ font-size: 20px;
+ color: #fff;
+}
+
+.verify-img-panel .verify-gap {
+ position: relative;
+ z-index: 2;
+ background-color: #fff;
+ border: 1px solid #fff;
+}
+
+.verify-bar-area .verify-move-block .verify-sub-block {
+ position: absolute;
+ z-index: 3;
+ text-align: center;
+
+ /* border: 1px solid #fff; */
+}
+
+.verify-bar-area .verify-move-block .verify-icon {
+ font-size: 18px;
+}
+
+.verify-bar-area .verify-msg {
+ z-index: 3;
+}
+
+/* 瀛椾綋鍥炬爣鐨刢ss */
+
+/* @font-face {font-family: "iconfont"; */
+
+/* src: url('../fonts/iconfont.eot?t=1508229193188'); !* IE9*! */
+
+/* src: url('../fonts/iconfont.eot?t=1508229193188#iefix') format('embedded-opentype'), !* IE6-IE8 *! */
+
+/* url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAaAAAsAAAAACUwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFZW7kiSY21hcAAAAYAAAAB3AAABuM+qBlRnbHlmAAAB+AAAAnQAAALYnrUwT2hlYWQAAARsAAAALwAAADYPNwajaGhlYQAABJwAAAAcAAAAJAfeA4dobXR4AAAEuAAAABMAAAAYF+kAAGxvY2EAAATMAAAADgAAAA4CvAGsbWF4cAAABNwAAAAfAAAAIAEVAF1uYW1lAAAE/AAAAUUAAAJtPlT+fXBvc3QAAAZEAAAAPAAAAE3oPPXPeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2Bk/sM4gYGVgYOpk+kMAwNDP4RmfM1gxMjBwMDEwMrMgBUEpLmmMDgwVDxbwtzwv4EhhrmBoQEozAiSAwAw1A0UeJzFkcENgCAMRX8RjCGO4gTe9eQcnhzAfXC2rqG/hYsT8MmD9gdS0gJIAAaykAjIBYHppCvuD8juR6zMJ67A89Zdn/f1aNPikUn8RvYo8G20CjKim6Rf6b9m34+WWd/vBr+oW8V6q3vF5qKlYrPRp4L0Ad5nGL8AeJxFUc9rE0EYnTezu8lMsrvtbrqb3TRt0rS7bdOmdI0JbWmCtiItIv5oi14qevCk9SQVLFiQgqAF8Q9QLKIHLx48FkHo3ZNnFUXwD5C2B6dO6sFhmI83w7z3fe8RnZCjb2yX5YlLhskkmScXCIFRxYBFiyjH9Rqtoqes9/g5i8WVuJyqDNTYLPwBI+cljXrkGynDhoU+nCgnjbhGY5yst+gMEq8IBIXwsjPU67CnEPm4b0su0h309Fd67da4XBhr55KSm17POk7gOE/Shq6nKdVsC7d9j+tcGPKVboc9u/0jtB/ZIA7PXTVLBef6o/paccjnwOYm3ELJetPuDrvV3gg91wlSXWY6H5qVwRzWf2TybrYYfSdqoXOwh/Qa8RWIjBTiSI3h614/vKSNRhONOrsnQi6Xf4nQFQDTmJE1NKbhI6crHEJO/+S5QPxhYJRRyvBFBP+5T9EPpEAIVzzRQIrjmJ6jY1WTo+NXTMchuBsKuS8PRZATSMl9oTA4uNLkeIA0V1UeqOoGQh7IAxGo+7T83fn3T+voqCNPPAUazUYUI7LgKSV1Jk2oUeghYGhZ+cKOe2FjVu5ZKEY2VkE13AK1+jI4r1KLbPlZfrKiPhOXKPRj7q9sj9XJ7LFHNmrKJS3VCdhXGSdKrtmoQaWeMjQVt0KD6sGPOx0oH2fgtzoNROxtNq8F3tzYM/n+TjKSX5qf2jx941276TIr9FjXxKr8eX/6bK4yuopwo9py1sw8F9kdw4AmurRpLUM3tYx5ZnKpfHPi8dzz19vJ6MjyxYUrpqeb1uLs3eGV6vr21pSqpeWkqonAN9oUyIiXpv8XvlN5e3icY2BkYGAA4n0vN4fG89t8ZeBmYQCBa9wPPRH0/wcsDMwmQC4HAxNIFABAfAqaAHicY2BkYGBu+N/AEMPCAAJAkpEBFbABAEcMAm94nGNhYGBgfsnAwMKAigESnwEBAAAAAAAAdgCkANoBCAFsAAB4nGNgZGBgYGMIZGBlAAEmIOYCQgaG/2A+AwARSAFzAHicZY9NTsMwEIVf+gekEqqoYIfkBWIBKP0Rq25YVGr3XXTfpk6bKokjx63UA3AejsAJOALcgDvwSCebNpbH37x5Y08A3OAHHo7fLfeRPVwyO3INF7gXrlN/EG6QX4SbaONVuEX9TdjHM6bCbXRheYPXuGL2hHdhDx18CNdwjU/hOvUv4Qb5W7iJO/wKt9Dx6sI+5l5XuI1HL/bHVi+cXqnlQcWhySKTOb+CmV7vkoWt0uqca1vEJlODoF9JU51pW91T7NdD5yIVWZOqCas6SYzKrdnq0AUb5/JRrxeJHoQm5Vhj/rbGAo5xBYUlDowxQhhkiMro6DtVZvSvsUPCXntWPc3ndFsU1P9zhQEC9M9cU7qy0nk6T4E9XxtSdXQrbsuelDSRXs1JErJCXta2VELqATZlV44RelzRiT8oZ0j/AAlabsgAAAB4nGNgYoAALgbsgI2RiZGZkYWRlZGNkZ2BsYI1OSM1OZs1OSe/OJW1KDM9o4S9KDWtKLU4g4EBAJ79CeQ=') format('woff'), */
+
+/* url('../fonts/iconfont.ttf?t=1508229193188') format('truetype'), !* chrome, firefox, opera, Safari, Android, iOS 4.2+*! */
+
+/* url('../fonts/iconfont.svg?t=1508229193188#iconfont') format('svg'); !* iOS 4.1- *! */
+
+/* } */
+
+.iconfont {
+ font-family: iconfont !important;
+ font-size: 16px;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-style: normal;
+}
+
+.icon-check::before {
+ position: absolute;
+ z-index: 9999;
+ display: block;
+ width: 16px;
+ height: 16px;
+ margin: auto;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAYAAAD9yHLdAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAIlFJREFUeNrt3X1cVNW6B/BnbcS3xJd7fLmSeo+op/Qmyp4BFcQEwpd8Nyc9iZppgUfE49u1tCwlNcMySCM1S81jCoaioiJvKoYgswfUo5wSJ69SZFKCKSAws+4f2/GetFFRYG3g9/2Hz2xj+O2J4Zm19trrIQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgjmOgAAADwOBhz83TzdPNs397qanW1ujJ2s8fNHjd7FBTkhuSG5IbculVdP1kSfeoAAPBwdFzHdXzgQN0S3RLdkpgY2SJbZMvNm9It6ZZ064cfGmQ2yGyQmZfX3KO5R3OPwkJdsi5Zl5yYKIfL4XL4mDHqs7AqGzhgBAIAoFFdI7pGdI1o1KjFlhZbWmxZv149OmXK4z3r4cPEiROfOFExKSbFVFDwqM+EEQgAgMY8y5/lz/LGjZu3bt66eev9+9Wjj1s4bAYNIkaMWHKyx3mP8x7nmzd/1GdyEP1CAQCASifrZJ3s6FjmWuZa5rprF3uLvcXeGjq0en5au3a8nJfz8k6d8lPyU/JTYmIq+wwYgQAAaIIk0WgaTaO/+IJm0SyaNWJEtf/IPMqjvJde0g/QD9APcHOrdGIhrxMAANzGmJwr58q569ZRLMVS7MSJNfajFVJIYYy/wF/gL7z0UmW/vUGNvk4AAHCHTqfT6XQrVtB4Gk/jg4KEBfmBfqAf+vSp7LdhBAIAUMPUwvH66+oj21eBSqmUStu3r+y3oYAAANQQtXDMmKE+WrlSdB4bvpwv58t/+62y34cCAgBQzeSt8lZ568SJFEiBFLh2reg8d2MD2UA28PTpyn4fCggAQDXRh+pD9aEjR1IABVDA5s20ntbTeklzf3eZF/NiXvv2Vfb7NHciAAC1nRwsB8vBvr5Wf6u/1X/nTubO3Jl7A+0tWvImb/LOyemc3zm/c/6ePZX9dmxlAgBQRfTd9N303Tw8rFusW6xbEhPZLDaLzXJyEp3rHjNoBs24dYt/wj/hn3h5mUwmk8mkKJV9GoxAAAAekz5AH6APeOYZ6znrOeu5Awc0WzgCKZACrVZ2hB1hR15++VELhw1GIAAAj0hdVdWli/ooNVX9WvnlsNUflHSk45wbuZEbg4LUwrFhw+M+LUYgAACV1CuoV1CvoCef5Kv4Kr4qIUE9qsHCcRsv4AW8YOHCqiocNtq7qAMAoFHqZoetW9MgGkSDDh+mhbSQFnbuLDrX/YWGmmJMMaaYsLCqfmZMYQEAPIBt23PLp5ZPLZ8mJ9MROkJHdDrRueyKpViKXbdO6aB0UDoEB1fXj8EUFgCAHX0v973c93KTJpbvLd9bvt+3T+uFg0/mk/nkL79UC0dISHX/PIxAAADuYuvLwQ/xQ/zQnj1sKBvKhj7/vOhc9vA4HsfjYmOd2jm1c2o3btxRdpQdZRUV1f1zMQIBALjNYDAYDAYHB9pEm2jTl19qvXBQGIVRWFKSWjgmTKipwmGDi+gAAERExJhZZ9aZdZGRNJ2m0/Tx40UnssuHfMgnPb2koKSgpGD0aIUpTGGlpTUdAwUEAOo9XbguXBf+/vu0lbbS1ldfFZ3HrgE0gAacPu0423G24+xhw5SOSkel440bouKggABAvaXjOq7j77xDetKTfv580Xns8iIv8srNlfKkPClv8OD0jukd0zv++qvoWLiIDgD1jrpnVXAwb86b8+Yffyw6jz18NV/NV+flWQZaBloGenufYqfYKXbxouhcNriIDgD1hi5Zl6xLnjyZL+AL+ILwcNF57OpLfanv1atsPpvP5vv7a61w2GAEAgB1nrpn1ejRPJNn8szoaM1ur05EREVF6ldfX0VRFEUxmUQnskejLyAAwOPT79fv1+9/7jn+E/+J/7Rjh7YLR3ExceLEhw9XTIpJMWm3cNho9IUEAHh08hB5iDykb1/+M/+Z/7x7N0VSJEU2aiQ61z30pCd9WZl1inWKdcoLL2R5ZnlmeR4/LjrWw8I1EACoM+S2clu5rasr+yv7K/vrgQO0jtbRumbNROe6G4/kkTzSYqFMyqTMgAC1cBw6JDpXZaGAAECt1zukd0jvkG7daBftol2HD1MERVBEq1aic93jdl8O9gv7hf0SGKhOVUVHi471qFBAAKDW0hfri/XFHTs6cAfuwBMS2Bw2h81p1050LruepWfp2fnzlaHKUGXopk2i4zwuFBAAqHVcw1zDXMPatrWSlayUkEBplEZp//VfonPZw86ys+zsm28qE5WJysQPPxSdp6qggABAraHuktuiRYOgBkENgg4dYt7Mm3k/9ZToXHZNpIk0MTzcWGosNZYuXy46TlXDfSAAoHnqfRxNm6qP4uPVr/37i85l11gaS2M3b1YWK4uVxa+8oh7kXHSsqoYRCABoVo+oHlE9oho2pME0mAbHxKhHNVw4IimSImNiXLJdsl2yp09XD9a9wmGDAgIAmmPry9G4f+P+jfv/4x8UT/EUP3iw6Fz3d/hwUXpRelH6Sy9FR0dHR0dbLKITVTfcSAgAGsPYhT4X+lzos2EDG8FGsBHjxolOZA9fxBfxRWlpFeYKc4V57NjckNyQ3JBbt0Tnqim4BgIAmiEvkhfJiz78kMWzeBY/Z47oPPbwpXwpX5qdbRlmGWYZ5uOjbnZYWCg6V03DFBYACKdbq1urW7tiheYLRypP5anffluRU5FTkTN4cH0tHDYYgQCAMOqeVX//O7vKrrKra9aIzmMPP86P8+NmM/fjftzP2zsrLSstK+3HH0XnEg0jEACocXJXuavcdepU1ol1Yp00fGNdP+pH/X78UUqSkqQkf38Ujt9DAQGAGqMP0YfoQ154gbbTdtq+cSMppJDCtDcTwokTLyiwvGh50fKiv79xuHG4cbjZLDqW1mjvfxwA1DluZjezm3nECMkgGSTD11+rRx0dRee6G8/gGTzj+nU+gA/gA/z81BGH0Sg6l1ZhBAIA1Ua9g9zHh/3MfmY/R0WpRzVYOE7yk/xkSYmUI+VIOSNHonA8HIxAAKDK6bvpu+m7eXhYt1i3WLckJrJZbBab5eQkOtcfKy9Xv44Zo7aQjYsTnai2cBAdAADqDn2APkAf8Mwz1gRrgjUhIYG9wF5gL7RsKTrXPQIpkAKtVlbMilnxpElKvBKvxO/eLTpWbYMRCAA8NnWqqksXddXSsWN0gk7QCWdn0bnuDao2dOJGbuTGoCCTyWQymTZsEB2rtsI1EAB4ZL2CegX1CnrySb6Kr+KrEhI0Wzhu4wW8gBcsXIjCUTWwFxYAVJral6N1axpEg2jQ4cO0kBbSws6dRee6v9BQU4wpxhQTFiY6SV2BKSwAeGge5z3Oe5xv3tzyreVby7dJSfQ2vU1v6/Wic9kVS7EUu26d0kHpoHQIDhYdp67BFBYAPFDfy30v973cpElFVkVWRdbevZovHJtpM23etk0tHCEhouPUVRiBAIBd6lSVoyMxYsRsq5SGDROdyx4ex+N4XGysUzundk7txo07yo6yo6yiQnSuugojEACwQ5L4dD6dT9+6VX2s3cJBYRRGYUlJauGYMAGFo2bUWAHps73P9j7b27Xr2bNnz549W7USfeIAYA9jslk2y+YNG9gmtoltmjBBdCJ7bA2dypVypVwZNUotHKWlonPVF1U+hfX7PW8CA9UtAnx9mQfzYB5Nmtz5Dz3IgzwKC+k1eo1ei4+naTSNpq1Zo5gUk2LKyBD9wgDUR/I5+Zx87oMP2CQ2iU2aO1d0HnvQ0EkbHruA9OK9eC/esmWD1AapDVK/+orm0ByaM2TIIz9hNEVT9IYNRfuL9hftDwmpby0iAUSQT8on5ZNLlrAZbAabsXSp6Dz28JV8JV/53XcVpypOVZzy9j694PSC0wt+/ll0rvrqkQuI15+8/uT1Jyen0smlk0snHz9Ox+gYHXN1rdp4KSnlE8onlE8YMUL9Rbl5U/QLBlCXqBfJQ0LUi+Th4aLz3N+lS+o2697e6kzFpUuiE9V3j3wNpHR26ezS2ZGR1VM4bHx8HHs59nLsdeBAj6geUT2imjUT9UIB1CVylBwlR738MulJT/qPPhKdxx6+hq/ha65ckWKlWCnW3x+FQ1sqPQJxN7gb3A29e1tbWVtZW5lMNdUQhifxJJ70zTdNujTp0qTL0KHf/PLNL9/88ttvYl42gNrJ7Te339x+GzuW5bAclhMVpU5ZOWhvU9UQCqGQa9es063TrdN9fLLKs8qzyk+dEh0Lfq/SIxBrf2t/a/+JE2u6kxjzY37Mz8ur9OXSl0tfTklRb2z6j/+o2ZcLoHZyi3aLdov285N2Sjulndu3a7ZwEBFRcTFP4Ak8YdQoFA5tq/wU1l/oL/QXLy9hiY/QETqi05U1L2te1vzgQdtFfGF5ADRMX6wv1hd7eqo9vWNjKZIiKbJRI9G57jGDZtCMW7fYUraULR01yrTNtM20LTVVdCy4v0qPINSLbrm56kW3Ll1EnwAtpaW01Ggse6PsjbI3Bg06c+bMmTNnrl0THQtApDtTza2tra2tU1LoJJ2kk9r7oMUzeSbPrKhg7syduRsMakOnPXtE54KHU+kRCF/Gl/FlGrr2cHtPHseVjisdVyYn39klFKAe6h3SO6R3SLduln9Y/mH5x8GDWi0ctr4cLJ7Fs/igIBSO2qnyU1i9qTf1zskRHfxu7G32Nnu7d2+1oCQmopBAfaL+vnfqJIVJYVJYUhLrx/qxfv/5n6Jz2cNSWApLCQlRhipDlaGbNonOA4+m8gWkM3WmzrGxooPbtYyW0bJevdQptuRk1zDXMNewtm1FxwKoDrYtgugNeoPeSExknsyTeXbsKDqXPewsO8vOvvmm8bzxvPH82rWi88DjqXQB6TK6y+guo3ftosW0mBafOyf6BO6vZ0/Hrxy/cvzq6FE3TzdPN0/tdkoDqAx1xNGiRfmI8hHlIw4epPfoPXqvWzfRueyaSBNpYni4sdRYaixdvlx0HKgaj7wMV5ZlWZZ1OsYYY+zYMfVo06aiT8genspTeeq331rmWuZa5vr5nfr01KenPv3hB9G5ACpD7T1ue5/Fx6tf+/cXncuusTSWxm7erCxWFiuLX3lFPci56FhQNR75TnS1p7Ci8Ml8Mp8cEKAeLS8XfUL2MG/mzbyfesphrMNYh7HJybZezqJzATyMrhFdI7pGNGrE5/F5fJ5tClm7hYNP49P4tB071MIxbdrtoygcdUyV3Qioy9Pl6fKef57n8Tye9/XXbCabyWY2biz6BO1aQAtowcWLFeMrxleMt+3mefGi6FgA/85gMBgMBgcH8wXzBfOFr75Sr+0ZDKJz3d/hw0VTiqYUTRk5Epuh1m1Vfie5foN+g37D0KFWV6ur1TUmRvOFxJM8yfN//9fhosNFh4s+Pif3ndx3ct/334uOBfD/fTk2bmQGZmAG2yd57bH15agwV5grzIMGYfPT+qHatiKRF8mL5EWDB1MohVLo7t339APRJNsmbb6+6rr0CxdEJ4L6SX3/fPihep/EnDmi89iDvhz1W7V1JDStMK0wrYiPV+8wHT1abSxVUiL6hO+vUyeextN4WkqKW5pbmlta166iE0H9oivVlepKly/XfOG4vSilIqcipyJn8GAUjvqp2lvaqtsvHz6sbss8ZAjNpJk088YN0Sduj20dPbvFbrFbKSm2O3tF54K6TU6UE+XE2bPJi7zIa9Ei0Xns4cf5cX7cbObP8ef4c76+aOhUv9XYbro2coAcIAd4e9Pf6G/0t7g4NovNYrOcnES/EPbwE/wEP/HTT9Z0a7o13c8ve0D2gOwBWr//BWoLW18OlsgSWeLnn9f0LtcPrR/1o34//siGsCFsiLe3cbhxuHG42Sw6FohV7SOQu9l22WTBLJgFP/88/5h/zD/W0N5ad7FtCSGRRBIlJ7uvdV/rvva//1t0LqjdbH056M/0Z/rzZ59ptnBw4sQLCqSnpaelpwcNQuGAf1fjBcRGndo6flzqLfWWeg8ZwjN4Bs+4fl30C2IPm8PmsDnt2llbWFtYW9g2bezZU3QuqF3U35tBg7Tel8P2frQ2tja2Nh46NDM4Mzgz+OxZ0blAW4QVEBtjU2NTY9O0NPIgD/Lw9eXhPJyH//qr6Fx2fUQf0Udt26pD+qQkua3cVm5bXS19oa6w9eVQf89jYrTal8O22IU5MAfmMGpUVlpWWlaa0Sg6F2iT5obM6lYNsqwWkoQENpvNZrM13HnQ1npzvXW9df2gQXjDwb+rLX05VLadJMaMUZexx8WJTgTaJnwEcjf1F9dkkhZJi6RFzz3H03k6T//lF9G57IqgCIpo1UrqJfWSeiUkuHd27+ze2d1ddCwQSx+qD9WHPvWUdaR1pHVkfLxmC0cgBVKg1cq6s+6s++TJKBxQGZobgdztzie4C9YL1gsJCepWDhru8+FBHuRRWEgZlEEZQ4ao13oyMkTHgpqh36/fr9/v4sIP8UP8UGoqnaATdEKDu0DfbujEjdzIjUFB6t52GzaIjgW1i+YLiI26aqV7d9aINWKNkpO13jBHVVTE2/A2vM2QIaZDpkOmQ+npohNB9bC1C2BJLIklpaay/qw/6+/iIjqXPczMzMy8cKHxmvGa8dr774vOA7WT5qaw7MlyynLKcsrJUQuHj496ND9fdK77a9GCXWVX2dVDh9wC3QLdAvv1E50Iqpat86U0X5ovzU9I0HrhUIWGonBAVag1BcRGnaP917/UR76+thucROe6vxYtJCYxiSUk6LiO6/jAgaITwePxOO9x3uN88+ZqB8yDB2k5LaflPXqIzmVXLMVS7Lp16vtnyRLRcaBuqDVTWPbYLlZyF+7CXZKS6EP6kD7UcJ8Pd3In95s3eQPegDcYOdK01rTWtDY5WXQseDh9L/e93PdykyZlT5Q9UfbEgQPMn/kzfw1/INhMm2nztm1KT6Wn0nPKFPWg1So6FtQNtb6A2Nj2rJLGSGOkMcnJbD6bz+Z36CA61/0VF1tft75ufX3kyCxDliHLkJQkOhH8MXWqytFRXcSxe7d6dNgw0bns4XE8jsfFxjq1c2rn1G7cuKPsKDvKKipE54K6pdZNYdmTHZEdkR1x/rxloGWgZaC3N1/FV/FVWu/r0bSp9J70nvTe3r26Ql2hrtDfX3Qi+COSxKfz6Xz61q3qY+0WDgqjMApLSlILx4QJKBxQnepMAbGxdRbk2TybZ/v42HYPFZ3r/po2pV20i3bt2yevkFfIK4YPF50IiIgY05l1Zp05MpJtYpvYpgkTRCeyy4d8yCc9vaSgpKCkYPRotXCUloqOBXVbnZnCskedeujUSX2UnKxOQXTpIjqXXXrSk76sjHVgHVgHg8H4lvEt41t794qOVd/I8+R58rxVq9gRdoQd+Z//EZ3n/s6ccdzjuMdxz8CB6R3TO6Z31PBWQFCn1LkRyN3UG/kuXWLH2XF23MdH7beQmys6l11GMpKxYUO1t3x0tO5fun/p/jVqlOhY9YW6lc5bb2m+cNz+PZZcJBfJZdAgFA4Qoc4XEBt108bLl6V8KV/K9/amxbSYFmu4r8ftQkJraA2tiYqSw+VwOXzMGNGx6ir5oHxQPvi3v6mPli0Tnccevpqv5qvz8irCK8Irwv39M6MzozOjf/pJdC6on+pNAbGxveEalDYobVDq68vf5e/ydzW8TfXtQsK2sq1s686dd/pIQJVQd1MOCGCX2WV2+eOPReexqy/1pb5Xr6qrC/39bdf6RMeC+q3eFRCbjJcyXsp46coVx2uO1xyv+fnxo/woP/rPf4rOdX+OjiyH5bCcqCh5q7xV3jpxouhEtdWdqcGf6Cf66YsvaD2tp/WSRt8PRUWUTumUPmTI72+kBRBLo2+YmmMrJBWRFZEVkX5+6tEzZ0TnsudOA6Kn6Wl6essW2ydo0blqC7dot2i3aD8/XsgLeeGOHcyduTP3Bg1E5/pjxcW8O+/Ou48YYdulWnQigH9X51dhVVbvY72P9T7Wpo3DbofdDrsTE+kYHaNj2m0YxSN5JI+0WNgNdoPdeOUVxVfxVXxt9yuAjboar08fCqZgCk5MpHW0jtY1ayY61z1ur8KzTrFOsU4ZNSrLM8szy/PQIdGxAP5IvR+B3C17QPaA7AFXr5YlliWWJQ4cSEtpKS3VboMo24iEN+PNeLPPP5ej5Cg56uWXRefSClvrYR7BI3jEgQNaLRy2DwKUSZmUGRCAwgG1AUYgD9CL9+K9eMuWDtcdrjtcj49nvsyX+Xp4iM5l1+0+D6SQQsrMmerUR2Sk6Fg1zS3NLc0trWtXpmd6pk9N1ez2/7b/X2NoDI159VVlqDJUGbppk+hYAA8DI5AHUFe7FBZamluaW5oPHkycOHENN4hSSCGFMfUP07p18gB5gDxg5kzRsWqKuktuhw7SJemSdCkhQbOFw+ZZepaenT8fhQNqI4xAKkmdEmnRgnzJl3wPHaIUSqGUvn1F57If+PYnXH/yJ//ZsxWDYlAMGl6u+ojuXLuKcYhxiDl6lFIplVK7dxedyx52lp1lZ99801hqLDWWLl8uOg/Ao3AQHaC2yc/Pz8/Pv3WrzZg2Y9qM2bFDWiOtkdZ4erIv2Zfsyz//WXS+ewNTPuUzRiVUQiVDhjhzZ+7Mr11Tz0PDI6mHZCvoUrwUL8UnJNAlukSXtLvoQRURoVxWLiuXFy0SnQTgcaCAPKIrCVcSriSUl7dp3aZ1m9a7djn80+GfDv+0dRzs3Fl0vnvYCome9KQfMqR9m/Zt2rcpKsrPzc/Nz619rXbVLUeaNqXn6Dl67sAB+p6+p+81PBIcS2Np7ObNyjZlm7JtxgzRcQCqAq6BPKbTC04vOL3g5k310fDh6lSRhhtE3b5GorbaXbNGDpAD5IDa80m4R1SPqB5RDRvy2Xw2n71rFyVREiV5e4vOZVckRVJkTIxLtku2S/b06epBzkXHAqgKGIFUEXVKqLzcucS5xLlk1y4+j8/j8/r0YSfYCXZCuz2yWQErYAV+fs6hzqHOoRZL/t78vfl7jx0TnetuBoPBYDA4ONzYd2PfjX3bt7MMlsEytL7J5OHDRa2LWhe1Hjfu+AfHPzj+QXm56EQAVQkX0avJndanTcqalDWJjWWD2WA2WPsNo9T7Ed5+2+Rh8jB5aGVTQcZks2yWzRs3MgMzMMO0aaIT2cMX8UV8UVpahbnCXGEeNOj3I1SAugUFpJp1jega0TWiUaMW+hb6FvroaJpFs2jWiBGicz0I/4J/wb9YtcrkanI1ub7+uqgc8jn5nHzugw/YJDaJTZo7V/TrYg9fypfypdnZlmGWYZZhPj625d+icwFUJ1wDqWa5IbkhuSG3bpXkleSV5I0bx2fymXym9htEsalsKpu6cKF8Wj4tn37vvZr++bJJNsmm0FDNF46VfCVf+d13FTkVORU5gwejcEB9ghFIDbNdBG6yqsmqJqt27lSPjh4tOtcDJVESJYWFKS2VlkrL6mu0pC7LDQlRO0eGh4s+7fu7dEm9sdTb29a4THQigJqEEUgNO/fiuRfPvVhWpv7hefFF2yod0bkeyI/8yG/BAvUP/OrVVf306rLcKVPUZcYffST6dO3qR/2o348/sqVsKVvq44PCAfUZVmEJoq7aslr7F/Yv7F/49dfXrl27du1a167qv/bsKTqfXYwYMU/P9lPbT20/tUWL/NT81PzUw4cf9enuNMjqQ32oz7ZtbCPbyDZqsC8HJ068oEDyl/wlfz8/Y4AxwBjw3XeiYwGIpL03aj0THR0dHR1tsbi4uLi4uEyeTJtpM23etk10rgdh8Syexc+ZI+fKuXLuJ5/cPvrQU6K6Ql2hrtDfX9op7ZR2bt9+p8+JxvAMnsEzrl+3NrY2tjYeOjQzODM4M1jDHSwBahCugWiM7X6HC/0v9L/Q/4sv1Fa2kyaJzvVA0RRN0Rs2KC6Ki+Jiu9Paar37P9MX64v1xZ6efC6fy+cePqxuX/7EE6Lj342f5Cf5yZISJjGJSc8/rzCFKezIEdG5ALQEBUSjbIXEbDabzWbbLq1TpojO9UCcOPHPPlOvDQQGqgetVneDu8Hd0Lu3tbW1tbV1SgqdpJN0smVL0XH/mO2GvzFj1O3w4+JEJwLQIs1NGYDq3Llz586d41y9VrJ3r3OKc4pzSqdOFEMxFOPmJjqfXYwYMVluP6/9vPbzOnZ0/sX5F+dfvvvOusS6xLokMZF9zj5nn7duLTrmPQIpkAKtVlbMilnxpElKvBKvxO/eLToWgJZhBFKrSJK6Cmr9evUPtW1vJQ273aKVjGQkY8OGouPc4/Z293wYH8aHBQaaRplGmUZt3Cg6FkBtgAJSKzEmvyO/I78TEcH2sX1sX3Cw6ES1FTMzMzMvXGi8ZrxmvPb++6LzANQmmMKqpfKP5B/JP3LokLOzs7Ozc6tW6tE+fUTnql1CQxWzYlbM774rOglAbYRlvLUa5+pF3r//nQ7SQTqo4RvwtGI8jafxn3yivm5LloiOA1CbYQqrjtGV6kp1pcuXkxd5kVft6fNR7W7fX6P0VHoqPW2r2e5dZgwADw8jkDpGaaw0VhovXsw38o18I6ZmeByP43Gxsc2eafZMs2emTlWPonAAVAUUkDrKJJtkk/zWW/QqvUqvaqWvRw0KozAKS0pyaufUzqndhAlH2VF2lFVUiI4FUJeggNRxSpASpAS9/ba6jHbpUtF5qh0nTjwjo6SgpKCkYPRotXCUloqOBVAXoYDUE+pWHO+8QyEUQiHiGkRVrzNnHGMdYx1jn39e3fX4xg3RiQDqMizjrWfy9+Tvyd/zzTdPlj5Z+mRpSQm1olbUSvutdu3yIi/yys2VHCVHydHX9+T0k9NPTr96VXQsgPoAq7DqOfmYfEw+Nn8+m8PmsDlhYaLzPCy+mq/mq/PyLAMtAy0Dvb3VToAXL4rOBVCfYAqrnjMNMA0wDVi9mubSXJo7b57oPA/Ul/pS36tX2Xw2n83390fhABAHIxD4HV2sLlYXGxREcRRHcZ98QgoppDx8n4/qVVSkfvX1VW8ENJlEJwKoz3ANBH4nf0f+jvwdRmN73p635/n5LIgFsaBhw8QWkuJi3p13592HDTPFm+JN8RkZol8nAMAIBB5AjpVj5dhXX2VX2BV25dNPaT2tp/U10HL29i6+TMd0TDd6tPE142vG1w4eFP16AMD/QwGBh6I7qDuoOzhtGl2my3R5w4bqKiQ8kkfySItFHfn89a9qY6roaNHnDwD3QgGBSpG7yl3lrlOn0nbaTts3bqyqXua2wiEtk5ZJy6ZONe437jfu//JL0ecLAPbhGghUSv6v+b/m/5qd3b5N+zbt22RksLFsLBvbvz+lURqlVb5FLU/lqTz122+l36TfpN8MBuMc4xzjnL17RZ8nADwYlvHCIzGtMK0wrYiPbza+2fhm47t3V48uWcJX8pV85Xff2fu+3//7kiXXP7v+2fXPevUy9jT2NPY8elT0eQHAw8MUFlQL1zDXMNewJ55o2L1h94bd27UryynLKcu5cuX0gtMLTi+4eVN0PgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAO/4PSBxbMqgmA24AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTctMTItMTVUMTU6NTc6MjcrMDg6MDCiEb4vAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE3LTEyLTE1VDE1OjU3OjI3KzA4OjAw00wGkwAAAE10RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fY2sxYnphMHpqOWpqZGN4ci9jaGVjay5zdmfbTpDYAAAAAElFTkSuQmCC');
+ background-size: contain;
+ content: ' ';
+ inset: 0;
+}
+
+.icon-close::before {
+ position: absolute;
+ z-index: 9999;
+ display: block;
+ width: 16px;
+ height: 16px;
+ margin: auto;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAYAAAD9yHLdAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAADwRJREFUeNrt3V1sU+cZwPHndTAjwZ0mbZPKR/hKm0GqtiJJGZ9CIvMCawJoUksvOpC2XjSi4kMECaa2SO0qFEEhgFCQSqWOVWqJEGJJuyYYWCG9QCIOhQvYlgGCIFmatrVSUhzixO8ujNM1gSZOfPye857/7wYlfPg5xj5/n/fExyIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABATizsWti1sCs/v6y0rLSsdMaMZ/Y8s+eZPZMnm54LQO6kn/fp/UB6v2B6LrdRpgcwZf7e+Xvn7505MxAIBAKBrVt1ja7RNdXVaqlaqpbOmTP0z+u9eq/ee/euFEqhFH7ySeCjwEeBj+rr299of6P9jb//3fT2AMhcWVlZWVnZ3Ln6uD6uj2/eLF3SJV1VVapW1ara6dOH/nn9hf5Cf3HzpupW3aq7qSl5LHkseay+/nLt5drLtbdvm96eXPNZQJQqn1Q+qXzS73+vN+gNesObb0q7tEv7xImZ/kv6kr6kL/X3q0PqkDpUXx/aFNoU2rRz53l1Xp1X/f2mtxTAcMv1cr1cT5jQfb37evf1ujrpkR7p2bxZ1agaVZOXl/E/WCM1UnP/vv5cf64/f+utjg87Puz4cPfu1G9qbXp7neaTgChVeqD0QOmBP/5RHVPH1LHf/CbrN1EplVLZ2iqt0iqtv/51NBqNRqP37pnecgDpI42CgtTz9OTJ1PO0sjLbt6PX6/V6/Z/+1LG5Y3PH5g0bHnzX2pBkXlyPKTtadrTs6Ouvq/fV++r9LVscu6EbckNuPPGEhCUs4UWLpsanxqfGT5yIxWKxWCyRMH0/AH40GI6whCXc3Cyn5bScDoeduj11RV1RV559dkrFlIopFX19sauxq7GrbW2m7wenBEwP4JT0OY7UV6+/nrMbjkhEIitWSIVUSEVLS0ljSWNJYyhk+v4A/GQwHHtkj+xpahp8XuaImqwmq8m7di2oXlC9oHr2bNP3h1OsDUhgfWB9YP2WLdIgDdLwgx/kfICzclbOLluW35Hfkd/x5z8PPqABOGbYEcd22S7bKypyPsiDc6v9df11/XWvvWb6fnGKtQHRj+nH9GOrV5ueY/CVz4MHNCEBsm9YOHJ8xPEo6og6oo64YD/k1PaZHiDbvruD/uYb0/MMUyEVUtHWFi+Pl8fLf/Wray9ee/Haiz09pscCvGjYUpWpI44RBE8FTwVPFRRcLLxYeLEwHjc9T7ZYdwSi2lSbavvxj03P8UgsbQHj5pqlqlFK9iZ7k70u3i+NkXUB6Tvcd7jv8H//a3qOEXGyHciY6ZPjYzXw0sBLAy95YL+UIeuWsNJK75feL71/545arBarxYWFpucZUVjCEj53LvWEqK7mfSTAt9x6jmNEi2WxLL59O3ooeih6aNYs0+Nkm3VHIIO6pEu6Pv3U9Bijxsl2YBjPhiOtUAql0EP7oQxZG5C8SXmT8ibt35++5IjpeUaNpS3As0tVabpBN+iGgQE5Lsfl+KFDpudxirUBuTT90vRL0//xj/S1qkzPkzFOtsOHvHZy/FFUsSpWxfv2pZai//Y30/M4xfpLmRR/VvxZ8Wd//Wvf7b7bfbd//vPBS454xU25KTdnz+YSKbCZ55eq0h5cE2/OB3M+mPPBb3977dq1a9eu2XstLGtPog+Vvp5/X1tfW19bU5N6V72r3v3FL0zPlTHeRwKLeOV9HCPaLbtl94UL8a/jX8e/fv55vzwvfROQNEICmEc47OC7gKQREiD3CIddfBuQNEICOI9w2Mn3AUkjJED2EQ67EZAhCAkwfoTDHwjIIxASIHOEw18IyAgICTAywuFPBGSUCAkwHOHwNwKSIUICEA6kEJAxIiTwI8KB/0dAxomQwA8IBx6GgGQJIYGNCAe+DwHJMkICGxAOjAYBcQghgRcRDmSCgDiMkMALCAfGgoDkCCGBGxEOjAcByTFCAjcgHMgGAmIIIYEJhAPZREAMIyTIBcIBJxAQlyAkcALhgJMIiMsQEmQD4UAuEBCXIiQYC8KBXCIgLkdIMBqEAyYQEI8gJHgYwgGTCIjHEBKIEA64AwHxKELiT4QDbkJAPI6Q+APhgBsREEsQEjsRDrgZAbEMIbED4YAXEBBLERJvIhzwEgJiOULiDYQDXkRAfIKQuBPhgJcREJ8hJO5AOGADAuJThMQMwgGbEBCfIyS5QThgIwICESEkTiEcsBkBwXcQkuwgHPADAoKHIiRjQzjgJwQE34uQjA7hgB8REIwKIXk4wgE/IyDICCFJIRwAAcEY+TUkhAP4FgHBuPglJIQDGI6AICtsDUl+XX5dfl0ySTiA4QgIsmrwlXpYwhJubpaIRCSyYoXpuTIWlrCEz50b/Nrr2xGRiESqq6PRaDQavXfP9FiwAwGBI6w5IvEqjjiQAwQEjiIkOUY4kEMEBDlBSBxGOGAAAUFOEZIsIxwwiIDACEIyToQDLkBAYBQhyRDhgIsQELgCIRkB4YALERC4CiEZgnDAxQgIXMn3ISEc8AACAlfzXUgIBzyEgMATrA8J4YAHERB4inUhIRzwsIDpAYBMJNYm1ibWKqUeV4+rx5X3XwCdkTNyxoLtgC/xwIUnWPN5HI/i8Ge2A04gIHA168MxFCGBhxAQuJLvwjEUIYEHEBC4iu/DMRQhgYsRELgC4RgBIYELERAYRTgyREjgIgQERhCOcSIkcAECgpwiHFlGSGAQAUFOEA6HERIYQEDgKMKRY4QEOURA4AjCYRghQQ7kmR4AdhkMR1jCEm5uliNyRI54MBxhCUv43DkpkiIpunVLbspNuTl7tumxRu2W3JJbM2cGC4IFwYKFC6fGp8anxk+ciMVisVgskTA9HuzAxRSRFcOOOCISkciKFabnylj66ril8dJ46Zo1wY3BjcGNVVV6m96mt505Y3q8jKX/HyqkQipaWkoaSxpLGkMh02PBDixhYVysWaoa4bLq1lxGnqUtZBEBwZj4JRxDERLgWwQEGfFrOIYiJAABwSgRjocjJPAzAoLvRThGh5DAjwgIHopwjA0hgZ8QEHwH4cgOQgI/ICAQEcLhFEICmxEQnyMcuUFIYCMC4lOEwwxCApsQEJ8hHO5ASGADAuIThMOdCAm8jIBYjnB4AyGBFxEQSxEObyIk8BICYhnCYQdCAi8gIJYgHHYiJHAzAuJxhMMfCAnciIB4FOHwJ0ICNyEgHkM4IEJI4A4ExCMIBx6GkMAkAuJyhAOjQUhgAgFxKcKBsSAkyCUC4jKEA9lASJALBMQlCAecQEjgJAJiGOFALhASOIGAGEI4YAIhQTYRkBwjHHADQoJsICA5QjjgRoQE4xEwPYDtbAtH4kriSuIKT1BbXCy8WHixMB6fuGzisonLVq/W2/Q2ve3MGdNzZeysnJWzy5blt+e357f/5S8ljSWNJY2hkOmxbMcRiENsDcfV7Ve3X93+zTemx4IzOCJBJghIlhEO2ICQYDQISJYQDtiIkOD7EJBxIhzwA0KChyEgY0Q44EeEBP+PgGSIcACEBCkEZJQIBzAcIfE3AjICwgGMjJD4EwF5BMIBZI6Q+AsBGYJwAONHSPyBgDxAOIDsIyR2831ACAfgPEJiJ98GhHAAuUdI7OK7gBAOwDxCYgffBIRwAO5DSLzN+oAs18v1cj1hQk95T3lP+aefpr77y1+anitje2SP7Dl7NhW+1auj0Wg0Gr13z/RYQDYMvsALS1jCzc0SkYhEVqwwPVfGKqVSKltbQ++E3gm9U1V1Xp1X51V/v+mxnGL9B0p1X+++3n29ri71FeEA3GjwcR2RiESqq1MhOXfO9FwZa5VWaa2s7DnYc7Dn4O7dpsdxmrUBKX+7/O3yt3/2M5krc2Xupk2m58lYeqkqmogmomvWEA74QfpxHtwY3BjcWFXl1U9I1Iv0Ir1o69b53fO753fPm2d6HqdYG5BkXjIvmbd1q3pOPaeemzDB9Dyjlj7i2Ck7ZeeqVZzjgB+lP2o3dU5kzRqvHZGoGlWjavLyAg2BhkDDa6+Znscp1gZEzVQz1cyqKtNzjBpLVcAwnl/aOi7H5biH9kMZsi4gCzoXdC7o/OEPZZ/sk33TppmeZ0QsVQEj8vbS1owZJY0ljSWNoZDpSbLNuoAMrBtYN7DuRz8yPceIWKoCMubVpa3Q/ND80HwP7JcyZF1ARIkS9e9/mx7jkTjiAMbNa0ckgUmBSYFJ//mP6Tmyzdr3gZTGS+Ol8Rs31FK1VC2dM8f0POkjjuCTwSeDT1ZXp19JmR4LsIFr30eyQ3bIjs7O6AvRF6IvFBebHifb7DsCeUA1qAbV0Nxseg7CATjPrSfb9VP6Kf2UC/ZDDrE2IMlkMplM7t8vNVIjNffv53yAIUtVhANwnluWtvRhfVgf7u1VL6uX1csHDpi+X5xibUAu116uvVx7+3bqqz/8IWc3nD7imBecF5y3ciUnx4HcM36yPSlJSb71VrQj2hHtuHPH9P3hlDzTAzgt1hRrijW1tU3ZMWXHlB1z5qgr6oq68uyzWb+h/bJf9re0BIuCRcGitWs54gDMi8VisVgskZganxqfGj9xInWtqvJyuSE35MYTT2T79vRJfVKfPHas4+mOpzuerq01vf1Osz4gabGWWEus5dSpaV9N+2raV4mE7JJdsmvJEmmXdmnP/J3q+pK+pC/190undErn3r1FkaJIUeR3vzv9yulXTr/S12d6ewF8Kx2S4gvFF4ovfPxxX29fb19vQYE+qo/qowsWqPfUe+q9QMYrMumlKlklq2TVm29+Nxxam95up1n7U1gjKSstKy0rnTFDr9Qr9cotW1SLalEtq1enfgy4qOjhf+vOHVkn62TdJ58M3B24O3C3vv7Lg18e/PJgZ6fp7QGQufQ18/QpfUqf2rw59d3nn0/9OmPGsL+wRJbIkn/+U7+qX9WvNjUFZgVmBWbV17cXtBe0F3R1md6eXPNtQB4l/fkEiTWJNYk1P/1p+n0lvF8D8I/BHwvWokX/5CehaCgaiv7rX6nLs/f2mp4PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtvsf2vlfs7i0WI4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTctMTItMTVUMTU6NTc6MjcrMDg6MDCiEb4vAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE3LTEyLTE1VDE1OjU3OjI3KzA4OjAw00wGkwAAAE10RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fY2sxYnphMHpqOWpqZGN4ci9jbG9zZS5zdmdHkn2WAAAAAElFTkSuQmCC');
+ background-size: contain;
+ content: ' ';
+ inset: 0;
+}
+
+.icon-right::before {
+ position: absolute;
+ z-index: 9999;
+ display: block;
+ width: 16px;
+ height: 16px;
+ margin: auto;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAYAAAD9yHLdAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAJ4pJREFUeNrt3XtcVXW6P/Dn2VwCBxUzNbnkkXRSGzXW2huQRLyMIqKRJF7Q1CkrDS+VGp3Gy9g5YzI6qVsNfTmlqGmipQiIiJqAcnOvhaKRHidshoatpKaBogL7OX+s6Mz8flO5CfzutXne/+zXWhR8QOXZ3+93Pd8vAHuAEKW10lpp7dix0mXpsnR5/34pX8qX8r/7TpZlWZaJGl//9f6+fY3/X+PnEf2dMMYY/yJqYcbbxtvG2/7+lEM5lLN7NyyCRbBowICmfj56m96mt/PzDZGGSEPkxImWNpY2ljYVFaK/T8ZY6+MiOoCzMn1t+tr09a9/TQfpIB0sLITlsByW9+r1Sz8v5mEe5vn7Q3toD+0nT/Y77Xfa73ROTuWNyhuVNyorRX/fjLHWg0cgzUybcmrThvIoj/JUFcMwDMOeeKLFvmA8xEN8TQ2sh/Ww/rnnFFVRFfXwYdE/B8aY8zOIDuBsqDf1pt6vvdbihaPRBtgAG7y8wAQmMKWlyflyvpw/aZLonwNjzPlxAWlWiOiN3ugdH//Av7QFLGBxd4dzcA7O7dgh75H3yHvmzBH9E2GMOS+ewmomplhTrCn2qads5bZyW3lJieg8jWgADaABf/yjul5dr65fvPj7uyQ6F2NM/3gE0kxsb9vetr3do4foHP8vLMACLPj977W1mS1bwimcwsnVVXQuxpj+cQFpLt/Ct/BtmzaiY/y0adNqltYsrVmakqIVEg8P0YkYY/rFj/E2E5+zPmd9znbpAggI+PzzovP8qItwES727n23w90OdzuEhfl86fOlz5f79lmtVqvVeveu6HiMMf3gEUgzqVfqlXqluFi7qqsTnefnYCImYmJ4OOVSLuWeONF/Zv+Z/Wf6+orOxRjTD15Eb2ZSlVQlVWVkYCRGYuSoUaLz3C86QSfoRHk5lVAJlURElISWhJaE/vWvonMxxhwXj0CaGT1Lz9KzS5eCDDLI+nnaCQfiQBwYEID1WI/1J05oi+6SJDoXY8xx8RpIM7tccbnickVlZdekrkldk4gwAzMwY8gQ0bnuF2ZhFmZ5eWkd7pMn+1T4VPhUKIq2RvLll6LzMcYcB09htShE6YJ0Qbqwdi3GYRzG6bCxbxbMgll372ojqilTlEAlUAncu1d0LMaYeDyF1aKI1CfUJ9Qn5s6FuTAX5r71lt6mtiAJkiDpoYeojuqo7uOP5VQ5VU6dOVN0LMaYeDwCecCkFClFSpk+HcbBOBi3eTOa0IQm/TX20RbaQlsSE9V+aj+131tvic7DGHvwuIAIIp+Xz8vno6OpJ/Wknrt2YRAGYZCnp+hcdpsAE2DC++8rbypvKm82TtHZbKJjMcZaHhcQwYxnjWeNZ8PDaTpNp+mpqdrd9u1F52qa/fu9LF4WL8ukSTmYgzl4547oRIyxlsNrIIJZ+lr6Wvrm5GBv7I29Bw6EN+ANeOMf/xCdq2mefbbGWGOsMR48GHQx6GLQxXbtRCdijLUcfozXQVSWVpZWllZV+df51/nX7dtH8RRP8aNGwQk4ASc6dhSdzz7du9NVukpXR4zoFNMpplPM/v1Xsq9kX8m+dUt0MsZY8+ERiIMpTitOK067dMm1zrXOtS4sTLurqqJz2e04HIfjsuw623W26+yCgsD8wPzAfMfbrZgx1nRcQBxUUVxRXFHclSu1CbUJtQnh4dpd/R1V+0OHuxGNaMzLazw3RXQuxtgvx4voOtEnpU9KnxR3d88yzzLPsu3bIQ3SIG38eNG57BYEQRB04wZVUzVVP/OMukPdoe7IyxMdizFmPx6B6ETZ+LLxZePv3Qv4PODzgM/j4mg37abdGzeKzmW3YiiGYm9vHIyDcXB2tlwil8gl48aJjsUYsx+PQHROTpaT5eSEBDCDGcwrVojOYy9KoiRKamgAK1jBOmuWGq1Gq9GbN4vOxRj7eVxAnISUKWVKma++ihVYgRXr1sEm2ASbDPoZYTZu8bIJNsGmd95RUEEF//AH0bEYYz9OP79g2E9SI9VINfL996mWaql23DjaQBtog44a+RRQQEEEIxjBuHSptgml2ax9UEeFkLFWhEcgTko7z2PIECqiIiravx+DMRiD9drYt3MnEBDQ9OmKqqiK6vgnPjLWGvA7OyelKIqiKJ99pj31NHQovAavwWtVVaJzNU1cHKyCVbAqM/Ppjk93fLpj27aiEzHGeATSahjTjenG9IAAOkyH6XBWFpyEk3BSf419tISW0JJTp2wdbB1sHaKiTg86Pej0oG++EZ2LsdaIC0grozXyPfpow7SGaQ3TMjNxKS7FpTps7CMgoPPntYuICG1q6+9/Fx2LsdaEC0gr1Z/6U3/y9nZNcE1wTThwAI7CUTjauHWK3litVEEVVDFypFqlVqlVpaWiEzHWGvAaSCt1Bs/gGbxx46bfTb+bfsOHUxqlUZpej6rt2hVX4kpcefy4sYOxg7HD00+LTsRYa8AjEAYAALGxsbGxsS4u5XK5XC4nJcEe2AN7XnpJdK6muX1bex0/XnuYICNDdCLGnBEXEPZvIMokk0xLlzb2ZYhOZK/GDne8htfw2iuvKJFKpBL5wQeiczHmTPg8EPZvWZdZl1mXHT/uY/Yx+5ivX4cn4Ul4MiLih4Y/B4cZmIEZBgPchJtwc8wY33Lfct/y2trKO5V3Ku+cPCk6H2POwOF/ETDHoDUmxsVpV1u3aq9ubqJzNY3ZrE1tvf66ds1nuDPWFFxAmF0C9wTuCdwzbBj6oi/67tuHc3AOztFfYx9Npak0dft2TMZkTH7xRe5wZ8x+XEBYk5i6m7qbuptMtlG2UbZRGRlQCIVQ2KmT6Fx2i4RIiExPh0zIhMwJE7SRSeMiPGPsp3ABYb+INrXVq5d2lZWlvT72mOhcdiMgoKIi7WL0aG1EcvWq6FiMOTLuA2G/iPaOvbEjPCQEBsEgGKTDRj4EBAwOhkWwCBbl5BhvG28bb/v7i47FmCPjEQhrVn379u3bt2+HDm55bnlueWlpOAyH4TAdNvaFQiiE/u1v2Bk7Y+eICMtiy2LL4gsXRMdizJHwY7ysWVVVVVVVVd2545Ptk+2T/fHH2t3GvbZ+/WvR+e5bBVRAhbc3zaJZNCsu7lG3R90edcvLu6xcVi4rX38tOh5jjoCnsFiLaFyMDggICAgIiI6mPbSH9uivkQ/n4Tyc9/DDBjSgAbOzA/MD8wPzR44UnYsxR8BTWOwBQpTmS/Ol+StW4HE8jsfffFN0IrsZwQjGe/dgGkyDadOnK6FKqBK6a5foWIyJwFNY7IGyFlgLrAVHjnTd3nV71+03buDj+Dg+PmKEXjrcoRIqodLFBaqgCqpiYnzAB3ygpsZqtVqt1oIC0fEYe5C4gDAhrNus26zbiop8yZd86dIlqIEaqBk9Wvuoi+P/vbSCFayNBW/EiK5ZXbO6Znl6WpOsSdako0dFx2PsQXD8d3ysVZCWS8ul5aNH4yf4CX6ye7d2t00b0bmaJjnZy+Jl8bLMmJGDOZiD9fWiEzHWEriAMIciS7IkS8HB2lV6utaf8cgjonPZi+IpnuIPHHAf7j7cffjEiYX+hf6F/rW1onMx1pz4KSzmULQO8KKihjUNaxrWhIdTPuVTfkWF6Fz2wg24ATc888y9gnsF9woyM7XC2L696FyMNScuIMwhnR50etDpQWVltI7W0bqwMMqjPMrTXyMfJmIiJoaHUy7lUu6JE/1n9p/Zf6avr+hcjDUHnsJiuhBSEVIRUvHww3Xn6s7VnUtP17YcGTBAdC57USIlUuKlS7YDtgO2AxERp82nzafNFy+KzsVYU/AIhOmCtoZw/bpWQIYPh9WwGlYfOiQ6l70wARMwoXt3wzjDOMO4vDxtM0pJEp2LsabgAsJ0pXRh6cLShbdu1V6uvVx7OTqaUimVUvXXyIev4+v4epcuEA/xEJ+To62RjBghOhdj9uApLOYEELVfwCtXak9tzZ8vOpHdvu9wJ5lkkp9/Xn1ZfVl9OSVFdCzGforjN2wxdh+0TvDDh31W+KzwWXHnDtRDPdQPG/avDX8OrLHDfQbMgBkxMT6jfUb7jK6qsn5s/dj6scUiOh5j/47j/8NirAm0tYVp0+gUnaJTf/kLmtCEJldX0bnsRVtoC21JTFT7qf3Ufm+9JToPY/+MCwhzavJ5+bx8PjqaelJP6rlrFwZhEAZ5eorOZbcJMAEmvP++8qbypvLmnDnaTZtNdCzWunEBYa2C8azxrPFseDhNp+k0PTVVu6vDxr4oiIKoffu8lnkt81oWF6dtlXLnjuhYrHXip7BYq2Dpa+lr6ZuTg72xN/YeOBDegDfgjX/8Q3Quu2VABmSMHVtjrDHWGA8eDLoYdDHoYrt2omOx1okX0VmrUllaWVpZWlXlX+df51+3b5+2Z9WoUXACTsCJjh1F57NP9+50la7S1REjOsV0iukUs3//lewr2Veyb90SnYy1DjwCYa1ScVpxWnHapUuuda51rnVhYdpdVRWdy27H4Tgcl2XX2a6zXWcXFGgnJvboIToWax24gLBWrSiuKK4o7sqV2oTahNqE8HDt7uHDonPZCwfiQBwYEIBGNKIxL88Ua4o1xTaeRc9Yy+BFdMb+SZ+UPil9UtzdPcs8yzzLtm+HNEiDtPHjReeyWxAEQdCNG1RN1VT9zDPqDnWHuiMvT3Qs5lx4BMLYPykbXza+bPy9ewGfB3we8HlcHO2m3bR740bRuexWDMVQ7O2Ng3EwDs7OlkvkErlk3DjRsZhz4REIY/dBTpaT5eSEBDCDGcwrVojOYy9KoiRKamjQOvNnzVKj1Wg1evNm0bmYvnEBYcwOUqaUKWW++ipWYAVWrFsHm2ATbDLoZyQvgwwykZb7nXcUVFDBP/xBdCymT/r5i8+YA1Aj1Ug18v33qZZqqXbcONpAG2iDjhr5FFBAQdQ2b1y6VLogXZAumM3aB3VUCJlD4BEIY7+AtufWkCFUREVUtH8/BmMwBuu1sW/nTiAgoOnTtaOF6+pEJ2KOjd9xMPYLKIqiKMpnn2lPPQ0dCq/Ba/BaVZXoXE0TFwerYBWsysx8uuPTHZ/u2Lat6ETMsfEIhLFmZEw3phvTAwLoMB2mw1lZcBJOwkn9NfbRElpCS06dsnWwdbB1iIrSzqj/5hvRuZhj4QLCWAvQGvkefbRhWsO0hmmZmbgUl+JSHTb2ERDQ+fPaRUSENrX197+LjsUcAxcQxlpQf+pP/cnb2zXBNcE14cABOApH4Wjj1il6Y7VSBVVQxciRapVapVaVlopOxMTiNRDGWtAZPINn8MaNm343/W76DR9OaZRGaXv3is7VNF274kpciSuPHzd2MHYwdnj6adGJmFg8AmHsAYqNjY2NjXVxKZfL5XI5KQn2wB7Y89JLonM1ze3b2uv48drDBBkZohOxB4sLCGPCIMokk0xLlzb2ZYhOZK/GDne8htfw2iuvKJFKpBL5wQeic7EHg88DYUwg6zLrMuuy48d9zD5mH/P16/AkPAlPRkT80PDn4DADMzDDYICbcBNujhnjW+5b7lteW1t5p/JO5Z2TJ0XnYy3L4f+CMtaaaI2JcXHa1dat2qubm+hcTWM2a1Nbr7+uXfMZ7s6GCwhjDihwT+CewD3DhqEv+qLvvn04B+fgHP019tFUmkpTt2/HZEzG5Bdf5A5358IFhDEHZupu6m7qbjLZRtlG2UZlZEAhFEJhp06ic9ktEiIhMj0dMiETMidM0EYmjYvwTK+4gDCmA9rUVq9e2lVWlvb62GOic9mNgICKigwHDAcMB6KiTvmd8jvld+2a6FisabgPhDEd0N6xN3aEh4TAIBgEg3TYyIeAgMHBtmJbsa04NzfoYtDFoIt+fqJjsabhEQhjOtS3b9++fft26OCW55bnlpeWhsNwGA7TYWNfKIRC6N/+hp2xM3aOiLAstiy2LL5wQXQsdn/4MV7GdKiqqqqqqurOHZ9sn2yf7I8/1u427rX161+LznffKqACKry9aRbNollxcY+6Per2qFte3mXlsnJZ+fpr0fHYT+MpLMZ0rHExOiAgICAgIDqa9tAe2qO/Rj6ch/Nw3sMPG9CABszODswPzA/MHzlSdC7203gKizGngyjNl+ZL81eswON4HI+/+aboRHYzghGM9+7hLbyFt6ZNs+yw7LDsaBxpMUfBU1iMOSFrgbXAWnDkSNftXbd33X7jBj6Oj+PjI0bopcMdKqESKl1coBt0g27PPecDPuADNTVWq9VqtRYUiI7HNFxAGHNi1m3WbdZtRUW+5Eu+dOkS1EAN1IwerX3UxfH//VvBCtbGgjdiRNesrlldszw9rUnWJGvS0aOi47V2jv9OhDHWbKTl0nJp+ejR+Al+gp/s3q3dbdNGdC67xUAMxGzd6vW219teb7/0Ug7mYA7W14uO1dpwAWGsFZIlWZKl4GDtKj1d68945BHRuexFGZRBGamp7nXude51kyYV+hf6F/rX1orO1VrwU1iMtULanlRFRQ1rGtY0rAkPp3zKp/yKCtG57IVRGIVR0dH3Cu4V3CvIzNQKY/v2onO1FlxAGGvFTg86Pej0oLIyWkfraF1YGOVRHuXpr5EPEzERE8PDKZdyKffEif4z+8/sP9PXV3QuZ8dTWIyxHzyV+1TuU7mdOhm+NXxr+DYjA9/Bd/Adk0l0LnvRCTpBJ8rLaRgNo2FhYSX5Jfkl+ZWVonM5Gx6BMMZ+oI1Ivvnmzt07d+/cHTpUu3v4sOhc9sKBOBAHBgQYFhsWGxbv3dsnpU9KnxR3d9G5nA2PQBhjP6rxF69HqEeoR+jWrRiN0Rg9aZLoXE3z6qta535SkugkzoILCGPsPhkM0gXpgnRhzRqMwziMmzNHdKL7thAWwsKvvlImKhOVid27i47jLLiAMMbsJifLyXJyQgKchJNw8t139dLhjs/is/hsr16862/z4DUQxpjdlGnKNGVaYiJFURRFvfIKJVESJTU0iM71s76Bb+Cb3/xGdAxnwQWEMdZkarQarUZv3ky9qTf1Hj8eXoFX4BWbTXSuH0PP0rP07K9+JTqHs+ACwhhrstjY2NjYWBcX3ISbcFNUFGyCTbDJ4Li/VxbCQljIW540F8f9g2aMOSztjPY2bb7c8OWGLzccOIC7cBfueuEF0bl+ViqkQuqNG6JjOAtX0QEYY/rReJQuHaWjdFRHR+nKIINMVLerblfdLotFdBxnwQWEMfazgi4GXQy66OfXcLbhbMPZrCwYBsNgWJ8+onPdL/oT/Yn+lJ9f6l3qXepdVSU6j7PgAsIY+1HaVFWvXg0TGyY2TMzK0u4+9pjoXPYypBhSDCl//KPoHM6G10AYY/8fU3dTd1N3kwlCIARCcnO1u/orHPQcPUfPbdpkednysuXlzEzReZwNj0AYYz+Q3pbelt6OiLBdt123Xf/kEyiEQijU32OvFE/xFH/gwHc139V8VzNvnug8zsrhO0cZYy1Pm6qKi9Outm7VXt3cROeyF31Kn9Kn27bhWByLY2fM0M49qasTnctZ8RQWY62Ysaexp7Hn7NlaA+D27dpd/RUOjdmsdlO7qd2mT+fC8WC4iA7AGHvwftjL6jSchtPvvaeXvaz+7xvQHssld3In94QE9Zh6TD22eLHoWK0Nj0AYawUaO8blcrlcLt+0CcxgBvOKFaJz2YtO0Sk6VV+P5/E8np8xQ/1U/VT9dOVK0blaK/2842CM2a2HuYe5h/mhh9pvbb+1/dbt2wEBAWNjRedqmtu3tU7y2FjFT/FT/A4eFJ2oteMRCGNOSDsIysurXVy7uHZxaWm6LRxzYS7M/fZbLMdyLB8xgguHY+ERCGNOJHhn8M7gnV261I2pG1M3JjMTB+NgHBwYKDqX3QbAABhQWQn5kA/5I0dqi+Jnz4qOxf4Vj0AYcwJBY4LGBI3p3r3erd6t3i0vT7eFIwzCIOyLL7TCMWAAFw7HxiMQxnTMOMU4xTjlN78hb/Im76wsKIACKPDxEZ3LXrSEltCSU6dwGS7DZaNGaYXj6lXRudhP4050xnRIJplkGjyYjGQk4/792t327UXnshfNp/k0/8gRzxc8X/B8ISbm5LWT105eq64WnYvdH57CYkxH5PPyefl8dDQVUREVNe7tpL/CAdEQDdEffYSrcBWuGjWKC4c+8RQWYzogpUgpUsr06TAOxsG4zZvRhCY0uep0BsFsVhRFUZTXX9euHfcIXPbTuIAw5sB+6BjXaeNfY8e4dtTtO+8oqKCCf/iD6Fiseej0HQxjzgxRKpPKpLJVq+B5eB6ef+MN0YnsRUmUREkNDWAFK1hnzVJRRRU3bxadizUvHoEw5gC0xj93d4+rHlc9riYn4wf4AX4wcaLoXHabBbNg1t27WIqlWDp5ssVsMVvMn3wiOhZrGVxAGBOo38p+K/ut/NWv3ILdgt2C9+6F1+F1eH3kSNG57BYEQRB04wZVUzVVP/OMukPdoe7IyxMdi7UsLiCMCRBSEVIRUvHww3Xn6s7VnUtPh0WwCBYNGCA6V9NYrbZSW6mtNDKypK6krqTuzBnRidiDwY/xMvYABa4KXBW4qlu3ex3vdbzXMT9fr4WDTtAJOlFerl2FhXHhaJ14EZ2xB+Cp3Kdyn8rt0weDMRiDDx3CUAzFUH9/0bnstgyWwTKLpX59/fr69VFRpUqpUqpUVYmOxcTgKSzGWpAsyZIsBQdrV+np2q64jzwiOpfdhsNwGH7smMuLLi+6vDh2bHHP4p7FPb/7TnQsJhZPYTHWAqTl0nJp+ejRWsE4dky3hSMKoiBq3z6vd73e9Xo3KooLB/tnPAJhrBlJnaXOUucpU9Af/dH/ww+1uzo8YzwVUiF1wwbt/I25c7Wb3DHO/hWfic5YM5COSEekI/PmYSAGYuDGjdoZ4/rbaoS20Bbakpio9lR7qj0bGxiJROdijkl3f8EZcxyIUqlUKpW++y7+Dn+Hv0tIEJ3IXo0d42hFK1pnz1b7qf3Ufhs3is7F9IGnsBizQ2xsbGxsrItL+ZflX5Z/uXGjtrYxY4boXHb7vmOcbGQj29Sp6svqy+rLKSmiYzF94QLC2H3oYe5h7mF+6KH2Ie1D2ofs3Kn9Ao6JEZ3LbvEQD/E1NRADMRATE6N4K96Kd3a26FhMn7iAMPYT+lN/6k/e3q5GV6OrMS1NuztwoOhc9qLVtJpWX7liWGRYZFgUGWnJteRacktKROdi+sZrIIz9G7Isy7LctSscgANwoPHgpv79ReeyFyVSIiVeumTba9tr2xsRoeaquWruxYuiczHnwCMQxv6JVjgefxwICCgrS1vjePxx0bnsRTmUQznnzjUsaFjQsGDkyDMbz2w8s/Ef/xCdizkXbiRkDAACQwNDA0ONRgiBEAgpKNBt4UigBErIycFBOAgHDRzIhYO1JB6BsFZNmi3NlmYPHQprYA2s2bdP26uqXTvRuexFGZRBGamp7nXude51kyYV+hf6F/rX1orOxZwbr4GwVklaK62V1o4dC8EQDME7d2qFw8NDdC67xUAMxGzd2rZL2y5tu7z0Ug7mYA7W14uOxVoHHoGwVkUaJA2SBsXH4xScglPMZu2sboPupnJ/6Bjvp/ZT+731lug8rHXiAsJaBTlZTpaTExLADGYwr1ghOo/93wDIIBNBOIRD+IIFymRlsjL5vfdEx2Ktm+7eeTF2Pxo7xqW/Sn+V/pqUpNvCYQQjGO/dw9t4G2/HxXHhYI6ERyDMqfzQMX69/fX217dtgzRIg7Tx40XnspsJTGC6dcs21TbVNnXcuJLQktCS0EOHRMdi7J/xCIQ5hT4pfVL6pHh5tYtrF9cuLi1Nr4WD1tJaWnv9uo1sZKPhw7lwMEfGIxCma8E7g3cG7+zSpf7P9X+u//PBg9pdSRKdy26hEAqhf/sbdsbO2DkiwrLYstiy+MIF0bEY+yn8GC/TpaAxQWOCxnTvXu9W71bvlpWl3e3ZU3Quu/0efg+/Lytz6evS16VvRIR24t/XX4uOxdj94ALCdMU4xTjFOOU3v2mIbIhsiDx0CFbACljh6ys6l90ICKioyBBkCDIERUUV+xX7FftduyY6FmP24CkspgvGs8azxrPh4TSdptP01FTtbvv2onPZbR2sg3VpaW7+bv5u/hMmcMc40zNeRGcOzfhfxv8y/tczz9j62PrY+jTuiqu/wkGf0qf06bZtMBtmw+znnuPCwZwBj0CYQ9J2xZ02jU7RKTr1l7+gCU1o0t8Z4xqzWVEURVFee0275jPGmXPgEQhzKD90jMsgg7xli+4Kx/cd49SNulG3N9/UCse8edoHuXAw58IjEOYAEOW18lp57Z/+BNtgG2xbsEB0IntpI6X6esNgw2DD4Fde0U78+/BD0bkYa0n6eWfHnIrW+Ofu7hHqEeoRunUrREM0RE+aJDpX09y+jZVYiZWxsVrhaOxHYcy58RQWe6D6rey3st/KX/3K447HHY87+/djNEajHgvHXJgLc7/9FsuxHMtHjFD8FD/FjwsHa11cRAdgrUNIRUhFSMXDD9Ntuk23MzNxFa7CVUOHis5ltwEwAAZUVsJe2At7f/tb5ZJySblksYiOxZgIvAbCWpR2VKyPj+Gu4a7hbuOeTn37is5ltzAIg7AvvoBcyIXckSMVVVEV9e9/Fx2LMZF4Cou1iMDqwOrA6t698TP8DD8rLNTu6q9w0BJaQktOndIKx6BBXDgY+z88AmHNytjT2NPYMyiI2lJbapuRAQgI+MgjonPZbSWshJVHj3rEesR6xI4de/LayWsnr1VXi47FmCPhEQhrFsZ0Y7ox/be/tSXbkm3JR47otnBEQzREf/QRLIAFsCAykgsHYz+ORyDsF5E6S52lzlOmoD/6o39j34Obm+hc9qKdtJN2rlunPqE+oT7R2DFus4nOxZgj4xEIaxJZkiVZmjsX/xv/G/87OVm7q6PC0XjGuAUsYFm2TCscc+dqH+TCwdj94BEIswOiTDLJtHSpdlb30qWiE9mLkiiJkhoawApWsM6apUar0Wr05s2iczGmR1xA2E+KjY2NjY11cSmXy+VyOSkJ9sAe2PPSS6Jz2W0WzIJZd+9iKZZi6eTJFrPFbDF/8onoWIzpGRcQ9m/1MPcw9zA/9FA7j3Ye7Tw++gg34Sbc9NxzonPZLQiCIOjGDaqmaqp+5hl1h7pD3ZGXJzoWY86A10DYv+hP/ak/eXu3/7r91+2/zs7Wa+GgAiqggsuXDVcNVw1XhwzhwsFY8+OtTBgAAJhiTbGm2EcfhTbQBtpkZ+OH+CF+GBwsOpe96ASdoBPl5aSSSurQocp8Zb4yv6xMdC7GnBEXkFZO698ICKAqqqKqY8dwG27DbX36iM5lt8EwGAYrSn1ZfVl92dChZyaemXhmYkWF6FiMOTPezr2VkiRJkiRZpm/pW/r24EE4CSfhZOfOonM1zWefucx0meky89lnlZ5KT6Xnd9+JTsRYa8BrIK2MdlTskCFQDMVQfOwYrIE1sEaHhSMKoiBq3z4vi5fFyzJqVHHP4p7FXDgYe6D4KaxWQlorrZXWjh0LwRAMwTt3YjzGY7yHh+hcdkuFVEjdsEE7f4Mb/xgTiUcgTk7KlDKlzFdfRU/0RM+9e/VaOGgLbaEtiYla4Zg9W7vLhYMxkXgNxEnJyXKynJyQAItgESxasUJ0Hns1doyjFa1onT1b7af2U/tt3Cg6F2Ps//BTWE6isWPc44DHAY8D77+PC3EhLnzrLdG57PZ9x7i21ciUKepkdbI6uXGvLcaYI+E1EJ3rk9InpU+Ku7tnmWeZZ9n27ZAGaZA2frzoXHaLh3iIr6mBGIiBmJgYxVvxVryzs0XHYoz9OC4gOqUVDi8vz0TPRM/Exj2dRowQnctetJpW0+orVwyLDIsMiyIjLbmWXEtuSYnoXIyxn8drIDoTvDN4Z/DOLl3qE+sT6xMPHtTuSpLoXPaiREqkxEuXbHtte217IyLUXDVXzb14UXQuxtj946ewdELbo+o//qPukbpH6h7JzdXu6rBw5FAO5Zw717C3YW/D3rCw0+bT5tNmLhyM6RFPYTk403rTetP6J5+0dbB1sHXIyoL34D14z9dXdC57UQIlUEJODq7AFbgiOlpRFVVRb94UnYsx1nRcQByUNFIaKY0MCdEWxdPTMQRDMKRjR9G57EUZlEEZqanude517nWTJhX6F/oX+tfWis7FGPvleA3EwQSWB5YHlo8ZA92gG3TbvRuDMAiDPD1F57JbDMRAzNatbbu07dK2y0sv5WAO5mB9vehYjLHmwyMQByEfk4/Jx6ZOpcE0mAZ/8AGa0IQmV90V+MaOca3xT4d9KIyx+8YFRDDpiHREOjJvHqZgCqasXg0KKKCgfv5cZJBBJoJwCIfwBQuUycpkZfJ774mOxRhrebp7h+scEOUb8g35RmIiDINhMGzhQtGJ7GYEIxjv3cNbeAtvTZtmmWyZbJn88ceiYzHGHhwuIA9IOIVTOLm6Vv+5+s/Vf960SSscL7wgOpfdTGAC061btqm2qbap48aVhJaEloQeOiQ6FmPsweM+kBamnb/Rpk31N9XfVH+Tmoq7cBfu0l/hoLW0ltZev24jG9lo+HAuHIwx/cy168zTHZ/u+HTHtm3v/O7O7+787vBh+Aw+g89CQkTnsttCWAgLv/rKMNAw0DAwIuKU3ym/U37/8z+iYzHGxOMC0iIQ5Xw5X85PTYU5MAfmjBkjOlHTnD1re8j2kO2hkSNL8kvyS/IrK0UnYow5Di4gzcw4xTjFOGXiRPqCvqAvdu0SncduBARUVGQ4YDhgOBAVpY04rl0THYsx5nh4DaSZUSfqRJ3+8z9F57DbOlgH69LS3FLdUt1ShwzhwsEY+zlcQJqJMd2YbkwPCIBcyIXcfv1E57lvH8FH8NGWLV4DvAZ4DYiJ4a1GGGP3ix/jbSbUg3pQj759Reewj9ms9FJ6Kb1ee+3774JEJ2KM6QePQJoJlVIplXboIDrHj/q+Y1w7Y/yNNxRFURRl3rzv03PhYIzZjQtIMyEjGcnoeGsGdIpO0an6ejyP5/H8jBmWSkulpXL1atG5GGP65yI6gLN4rPyx8sfK6+qomqqpuvGdvUDfd4wbrAarwRoTY1lvWW9Zv2eP6FiMMefBI5BmUpxWnFacdukSLIElsOTMGVE5qJAKqfDaNfqKvqKvfvtby8uWly0vZ2aK/vkwxpwPF5BmRlfoCl1ZvlzMV7dawRd8wXfoUPWQekg9VFgo+ufBGHNe3EjYIhCly9Jl6fK+fRiFURgVHd1iXyoMwiDsiy9wOS7H5RERljaWNpY2FRWifwKMMefHI5AWQeT5pOeTnk8+/zy8C+/Cu7m5zf4lvv+8hgWGBYYFYWFcOBhjDxovoreQitqK2orae/d8Pvf53Ofzjz4CBAR0c6NiKqZiWcbNuBk3u7nd7+fT/r/aWqzHeqxftQpWwkpY+cILloWWhZaFNTWiv1/GWOvDU1gPWGBoYGhgqI+Py1cuX7l8NWEC7aW9tHfIELpO1+m6v3/jf4cP48P4cEUFvUPv0DvHjtF39B19l5LCmxoyxhzF/wKeYeMy/zPC/wAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0xMi0xNVQxNTo1NzoyNyswODowMKIRvi8AAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMTItMTVUMTU6NTc6MjcrMDg6MDDTTAaTAAAATXRFWHRzdmc6YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9jazFiemEwemo5ampkY3hyL3JpZ2h0LnN2Z7O3J80AAAAASUVORK5CYII=');
+ background-size: contain;
+ content: ' ';
+ inset: 0;
+}
+
+.icon-refresh::before {
+ position: absolute;
+ z-index: 9999;
+ display: block;
+ width: 16px;
+ height: 16px;
+ margin: auto;
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADIEAYAAAD9yHLdAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAMQpJREFUeNrt3XlcVHX3B/Bz7rCISi6IC+ijkpZpIswMyBLgluVuKm4pqWmEuG/hUpr5uFYoiuaSFrklZvroo+jPFRURZgYVxZ1K3HIXUBSGe35/XC9PWpYL8J2B8/6H1wwGn3sb5sz93u/3fAEYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOM/QUUHYCx59F0ddPVTVdXq5YXkxeTF1O3Ll7H63jdzY3eoDfojTp1UIta1FatCm/D2/C2kxPchttwu0oVyIRMyKxShVpSS2pZuTIkQzIklyuHv+Av+IudHURBFERJkvJbKlQo+IWhEAqhsgz2YA/2d+8WPP/oMXWkjtTx4UMMwAAMuH4d2kE7aHf9OoVQCIX8/jvuxJ2489o1WkJLaMmlS+AHfuB37hwmYAImnDtnNBlNRlNGhvJDiUSfX/ZygiiIgqhMmayJWROzJgYF4Xbcjtv9/akX9aJerq7QE3pCTwcHiIEYiMnMxNpYG2ufOYNTcApOOXDAcNZw1nA2KUn0cTwrLiBMKO+z3me9z9asKa+V18prtVr5tHxaPv3mmzgaR+Nod3cYCANhYMOGyr9+9VXla9myonMXFoqmaIp+8ADDMRzDz56FTtAJOh07RgmUQAkGA17Da3jNYMjrldcrr1dKyrGxx8YeG3vvnujc7I8QdbG6WF3skCFUjapRtYkTcSSOxJHVqr3Yz0tNVb6OH280Go1G43//K/oIn3rkogOwkgzR09bT1tPW3V3jrHHWOLdoIRtkg2zw84PTcBpO+/jgGByDY2rWFJ3U0tEiWkSL8vNxDa7BNSdOkAM5kMOuXTgYB+PgnTvz1uStyVuzbx8XmOKh0+q0Oq2tLW2hLbRl9WrsgB2wQ7duhf17aAWtoBWzZpncTe4m94gI0cf9JC4g7KU0oSbUhCpW1FTTVNNUa98eTGACU9u2uAf34J6WLWEuzIW5VauKzlni6UEP+txcZYju0CGoDtWh+pYt+QH5AfkB69cfxaN4FH/9VXTMkkJ3UXdRd3HBAuWKMTy8yH8hAQENH64MeUZFiT5+FRcQ9kwaN27cuHHjSpVsbW1tbW2DgxEREbt2Vb7bvLny1dZWdE721+gz+ow+S06W+kn9pH7r1+fdyruVd2vdOi4sz0f7rvZd7bs+Pvgv/Bf+KyEBjGAEIxb5+yjNp/k0PytLE6mJ1ES+9lpybHJscuzVq6LPBxcQ9hjlJqCNTbY+W5+tb98eFsEiWNS3LxyDY3CsXTvlsb296JzsJT2aHEBdqAt12bULMzADM5YsgQ/hQ/hw0yblk25enuiYlka7XLtcu3zTJozGaIzu2LG4fz85kzM5jxxpijPFmeLmzhV9PriAlHKefp5+nn4uLtgQG2LDQYOwDJbBMh99BIfgEBxycRGdjxUvOkSH6NDVq7gEl+CSFSvMx83Hzcejo49+c/Sbo99cuiQ6nyjKPY8qVchABjJcuYJe6IVeNjbFHqQNtIE2W7YYpxmnGad16CD6vEiiA7DipfwhNG6sS9Wl6lJ/+EF6KD2UHv76K6ZgCqZMmcKFo3RDX/RF3+rVYQWsgBXjx9uQDdlQero2XZuuTV+2zOui10Wvi6+9JjpncaMP6AP6ICBAWOFQc0RQBEXUqSP6fKj4CqSEKxizvY7X8fqkSaADHejati2usVtWwqhDX+2pPbXfsIFqU22qPW1aSl5KXkre0aOi4xUV3QPdA92Df/8b/MEf/CdMEJvmwgVlem/t2qLPC1+BlDAe8R7xHvENGypXGuvWFdzsAwCAdu24cLCXshgWw2JJUqetSv2l/lL/lBT19abfot+i3+LmJjpmYaMbdINu1K0rOgf4gi/4irsCehIXECvX5OMmHzf52NVVO087TzsvJkZzSnNKcyo1FRAQMDiYCwYrUurr69HrTa4iV5GrnDihu6O7o7sze7Y6e090zJeFC3ABLnjRhYGFiICALGe2IxcQK+OT4ZPhk+HgoNPpdDrdp5/agA3YwOnTGIMxGNO3r/oJUXROVjopK+rLlIGW0BJajh1rF2gXaBd4+rRut263bndIyKN/ZX0faHbADthRrpzoGCCDDDIXEPacPL/0/NLzy8DAXKdcp1ynlBTl2alT1Z5OovMx9pcSIRESnZ1hLIyFsd9/rxunG6cbt2+fOtQqOt4zQ0BAC3jj9gIvEHgT/0lcQCyUcqVRubJypfHdd9IZ6Yx0Zu9epWnf66+LzsfYC9kFu2BXQIDmjOaM5ozJpCMd6WjKFLU1iOh4Fo+vQNjfUWdN5Z7OPZ172mBQnv3gA76XwUoUdUGqHvSgnzwZpsJUmJqQoP9C/4X+C/6A9DTkTu7kzlcg7JHg4ODg4GCNRv0kpvwhHTiAn+An+IkFzPpgrDhMhskwWa+nS3SJLhmNWq1Wq9V+9JHoWJYGwzAMwzQa5ZH4e53CA5RW+vv6+/r7tWqlD0oflD5o9271k9jjLxDGShl1nxZERFy8WHtVe1V7deNGtWmn6HiWol5Uvah6UeKHsizmUqi00J3SndKd6tRJNskm2bR8OY7H8Ti+cmXRuUobSqIkSsrJUVYW37sHw2E4DH+Gwn0QDsLBihV5SLF4YDtsh+06dbLxt/G38U9OVu6VdOmi9OpS980oerSX9tJeRGyGzbCZ6LMCUPZh2YdlH6pDWQ8fisrBfwDFRNtH20fbZ8IELItlsey0afwG9ILCIAzCHj6kntSTep49C6thNaw+fRpDMARDTp/GbtgNu506BTNgBsy4cEFpQXHrltnb7G32vnXLYaLDRIeJt24l1kqslVgrJ+d5f/3jzSadneVj8jH5WNWqmmhNtCbaxYXqUT2q5+xMy2k5La9ZU9mBsHFj/Ba/xW8bNVKuNF9/HQxgAIOdnejTaXW8wAu87t3DbMzG7IEDDSsNKw0r164t6l+rzdJmabNMJqWAeHqKPg1mg9lgNlSqpHRTvnNHVA5+Aysij88qWbhQmQY4cKDoXJaODtABOpCeDtfgGlw7cADfw/fwvf37lfN34ICbm5ubm9vZs7GxsbGxsfn5ovM+L/V1kT83f27+3Pr1bZbYLLFZ4u5OJ+kknQwMpP20n/a3aMGz7Z4RAQF99ZVyRTJunPKkLBf2r9Fu0W7RbklJwck4GSd7eIg+bOW4nZ2V475xQ1QMHsIqZMoWra+8kt8zv2d+z9hY5dnWrUXnshjhEA7h2dlwAS7Aha1boTW0htYbN5pjzDHmmPj4ow5HHY46PL3rq9IDSPRBvLiCNumBEAiBaWnKs+rXtWuhLJSFsn/oknwOz+G5Fi0wHuMxvkUL6A29oXeHDkpBrVJF9PEIh4CAo0frknRJuqS6dW1r2NawrdGnz4teYVoLZYtjSVI2cBOXgwtIIVH/4M3VzdXN1bdsUXo7iL/UFev+faX99O7d0AJaQIvY2JwbOTdybmzYkDYlbUralOxsmAJTYIronJYnJSElISXh8mXl0cqV0AAaQIOVK9VZe+nn08+nn/f1LWhZQ0BAvXuX2sISBmEQ1qVLHuVRHu3Z4z7HfY77nI4dlS1+r1172R+P+ZiP+Tzk/CQuIC9JWejXoIHyyXrnTmgGzaCZq6voXMVN3fEOHdERHRcsKN+8fPPyzdet24f7cB8+eADTYBpME53S+j0+dHfggPpVmZUzblyF7yt8X+H7Nm0gEiIhMjQUVsJKWPnOO6XmnhsCAjZtalvHto5tnQMHlL/PNm2UK9fz50XHKyz2SfZJ9knip/GW/BdUEfFM8EzwTKhXT1otrZZW79tXavbReLT3tjLdctMmnIpTceqSJYb2hvaG9jt3io7HHlfwOh0qDZWGDh2q3IT+8MPS0gKHIimSIn//HbpBN+jWurXpmuma6dqxY8/7c3QjdSN1I48ehXiIh3h3d9HHJblJbpJbjRqit7blAvKcvDt4d/DuULeueb15vXn9vn3oh37oV6uW6FxFpWC6axZmYdaCBTZbbbbabP3qq8O9D/c+3Pv330XnY89H2RDKySn/Qv6F/AsffYRDcAgOGTWqpA99USIlUuLNm+iDPujTurVyRWJ65rsHllZAZHvZXrZ3dX18qLP48RDWM1IX/pkTzAnmhF27SmrhoGRKpmSzGebDfJi/Zk2+lC/lS599drTi0YpHK/76q+h87OUk10yumVzz5k3l0YwZDdc1XNdw3fz5DjkOOQ454eFUn+pT/YkTcSgOxaGOjqLzFhalcDg5KY9271b+ntu2NZQ1lDWUVffL+RvxEA/xljMEKLvL7rI7IiRAAvxz+iIjfAzN0qn7bdBb9Ba9tWdPiWsxogMd6IigA3SADuvWaS5rLmsuN2pkGm4abhoeEqLMM+fCUVKldU/rntY9O9v4gfED4wezZtEYGkNjGjSAYAiG4KVLCz5QlCgVKtBaWktrt29X7pE0b/6P/0kgBEIgkejkKvvR9qPtR4svaMIDWCp1Ixw7WztbO9uEBOUSv0ED0bkKjT/4g/+5c8rK6o8+Ui7p9+wRHYtZFrXtunRdui5dX7oUp+N0nO7nJzpXYVGHaKVvpW+lb7t2NXxk+Mjw0bZtT/47pdCo904aNxadW5l1V7u2Mi38wgVRMfgK5AnqSmPb8bbjbcevW1dSCof6SZKaUTNqNnu27VjbsbZj3d25cLC/cyTwSOCRwLQ0U1dTV1PXgAByJmdyHjlS+e79+6LzvSz0Rm/0dnAgIxnJuHGjsrPne++p31c6SAQEwAgYASMsYEfCR+Tecm+5N1+BWBztae1p7emoKOyNvbH30KGi8xQOkwnLYTksN3CgId4Qb4hXN6Ri7MUon8hffVV5tHSp8vUZhoIsXMGQ3VbYCluPH7eYledPUFqZ1K0reoiZC8gjavtotQuo6DwvTL2nYQADGL7+uryxvLG8MSJCWY9R0saymWVA1LvoXfQuI0bIF+WL8sXZs5UmlZazb0VJo3HRuGhc3NySNidtTtr8yy+icpT6ISx9qj5VnxoUpBSOBQtE53lRdJgO0+HMTPkr+Sv5q27dlLHRMWO4cLCiR2S4bLhsuBwZSV/T1/R1y5Z0iA7RIXHrE1jxKLUFRNlfoE4dpVvr+vXKs+L767+Y1FTNVc1VzVUvrxTHFMcUxw0bRCdipVPKmJQxKWPi45V7bTodTaAJNOEZpsmy55IXlBeUF1T4TSOfV6krIGovIRu9jd5G/8MPVruAahksg2U//qg88PFR5vefOSM6FmMA/+vl9SD/Qf6D/ObN6Uf6kX785hvRuUoKzWDNYM1g8QWk1I1Rnrc/b3/e/pNPlGaHb70lOs9z2wSbYFN0tLGmsaax5rBhypPiX0iM/RVlnUlurvIoLEz3ve573fe//gpREAVRM2eKzmet8lvlt8pvJX47g1JzBaIP1AfqAz09ldlIkyeLzvO8aAWtoBWzZimFY8gQ5VkuHMy6FCxYnEbTaFp4OIRCKITy6/h52bjauNq4ij9vJb6AKF1K7e3pHt2je99/by07wdEiWkSL8vPhM/gMPgsLM7mb3E3uERGiczFWGExtTG1MbRYuhMWwGBb37as8m5cnOpe1eOj90PuhNxeQIlehZ4WeFXqql8oWsIL0n6ifyE7BKTj1wQfGTsZOxk48dsxKJmUh6+rVFEIhFNKjR8EHJ/a3bNfYrrFdwwWkyHh+6fml55eBgbARNsJG9V6B5aOVtJJWDhtmCjGFmEJWrRKdh7HioPRe+/lnyIRMyBw9WnQeS2e7yXaT7SYuIIVOnWUl1ZfqS/WjopRLZPEbr/wT8iRP8pwyxRRvijfFR0eLzsOYCKZWplamVvPm0WbaTJvV6fXsSXmd8jrldRJ/pWbxb6zPKz09PT09/aOPYCpMhalNmojO848ezaoyLTMtMy37/HPRcRizBPI5+Zx8bvBg8AEf8Ll+XXQeS1PmtzK/lfmNr0AKjU+GT4ZPRuXKysYxX3whOs8/WgSLYNGGDY9Px2WMAahNHK9fV3b6DA8XncfSZEVkRWRFcAEpNHmYh3k4derjG8dYHppBM2jGmTOaSppKmkr9+yvPin8hMGaJlJY8sbE8pPW4SmMqjak0hoewXpq+j76Pvs+bb5ILuZBLaKjoPE8VDuEQnp0tl5HLyGXeey+pflL9pPqZmaJjMWYNzKvMq8yrwsOVfTBu3BCdR7RsXbYuWye+gFj9SnQ6SSfp5FdfWXr3TzKTmcwffqjuryA6D2N/5BXsFewVXL268qh6dfm8fF4+b2+PU3EqTnV0pMk0mSaXL6+8gf9Fz7gFsAAW2NjQEBpCQ/6wFe7H8DF8fOcOfoPf4DfPsKMfAgLev4+f4+f4+cOHT36belJP6rluHfwIP8KPgweLPm/F7lG3beMS4xLjEvHrZqy2nbtOq9PqtE2bKi+4xETReZ6G3qF36J3ISNN003TT9FGjROdhpZPSPLRiRRudjc5GFxKi/N107qxcGXt5QTREQ3T58qJzsn+gBz3oc3ONi42LjYvt7UXHsdohLNpKW2nr+PGiczzVRJgIE9PSMqtnVs+sbsE5WYmm3abdpt02eLDmoOag5uD580rhmDdP+W7z5lw4rExTaApN1d5i4lnskM/TFNzz+Iw+o886dhSd508erSSXt8vb5e0DB55bfG7xucV/vhRnrCjpZutm62ZHR8MkmASTSuFQT0mlAQ1oxA9dqazuCoReo9fotYgIMIIRjOL3BP6TztAZOkdFpSxOWZyy+NAh0XFY6aIM7Q4bVmrvEZRwVJfqUl3LuQKxmgKi36Lfot/i5kaTaBJN6tFDdJ4/GQtjYeyvv+bszdmbs/fTT0XHYaWLUjgqVFCGOHhBaollYUNYVlNA5GPyMfnYuHEWO9tqNsyG2aGhyv4H2dmi47DShcpTeSrfpw8kQRIkVawoOg8rIjLIIPMQ1jPzPut91vvsK6/gT/gT/qS2fbY0O3YoC5527BCdhJVO2AybYbOWLUXnYEWMgIC4gDwzcw9zD3OPnj2VR2XLis5T4LGNcHiWFRNMBzrQubmJjsGKFgZgAAbwENYzwxbYAluoLT8syFW4ClfXrFH2MzCZRMdhpRu1ptbU+g8L+FgJxlcg/8gj3iPeI75hQ9gDe2CPj4/oPAXCIAzCHj7UJGuSNcl8s5xZBpyEk3DS7duic7Ai1hyaQ3O+AvlHmhRNiibFAq88FsEiWLR8edLmpM1Jm3/5RXQcxgAAoA/0gT7nzomOwYrYHtgDe65eFR1DZXEFRJmOaGurbLBkQTfNH93zkDZJm6RNc+eKjsPYH+FwHI7DeRJHiXcQDsJBy+mlZ3EFRF4vr5fXv/sujsSROLJaNdF5CiyGxbB427bkmsk1k2ueOSM6DmN/ZH/C/oT9idhYZT+cmzdF52FFQ+or9ZX6xsaKzlGQR3SAPwXqJnWTullgi5JdsAt2qT2EGLMsB28evHnwZlYWtISW0HLyZNF5WFHYuDE5Njk2OfbIEdFJVBZWQBBhNIyG0W3aiE6iomk0jaadOGGsaKxorLhzp+g8jP0dU7wp3hQfHU0hFEIhP/wgOg97Sf7gD/7nzklukpvkFhYmOs6TLKaA6AP1gfpADw/4Gr6Gr11dRedRKbNboqOVR8+wnwFjFsA03DTcNLxfP+XRzJm0iBbRIvEbELFn1BJaQsv9+8255lxzbrNmypWH5dw8V1lMAVFaMLRtKzrG4/LylJWfljPmyNizk2VlndL48VKUFCVFeXjQJtpEm9asocN0mA7zjpjCPdogSpmeq+5r9P77xtnG2cbZzZod/eboN0e/uXRJdMynsZhuttqftD9pfzp4EKfjdJzu5yc6D0RCJETGxRkDjYHGQMsZUmOsMDRc13Bdw3V2duViy8WWi23Y0DzLPMs8q1YtTT9NP00/Z2c5W86Ws//ccw5H42gcXb48mMEM5r/YmTAKoiCqXDlaQStohZ3d8+bCnbgTd5Ypo3S1dnB40eOjZbSMlt27Bz2hJ/QshHUTs2E2zM7MxLfxbXz7+a/kcASOwBFEspPsJDtdvy6Nk8ZJ465exbbYFtsmJSmTc6xv8oPwAuKT4ZPhk1G5cu6V3Cu5V65dwzAMwzCNRnQumANzYM4HHxhbGFsYW8TEiI7DGGOWRnhXW/N483jz+Nat8SSexJPiCwdFUzRFP3hg42TjZOO0caPoPIwxZqmE3wMhLWlJazmtSjAcwzE8Li6pflL9pPo8RswYY08jvIDAG/AGvOHtLTqGSpm2+3//JzoHY4xZOmEFJIiCKIhsbJQuoh4eok+Eit6it+itPXtE52CMMUsn7B7I/e73u9/v/uabmI7pmP7isy0KzQgYASOuXUtxTHFMcTx1SnQcxhizdMKuQPL75PfJ7+PlJfoEqCiLsihr9+5Hj3jBIGOM/QNx90BOwAk4odOJPgEFJ2KptFRaunev6ByMMWYthBUQvIk38aZeL/oEqEgiiaTkZNE5GGPMWggrIDSLZtGs+vVFn4DH9zbnex+MMfasir2AqCvPsSk2xaavvCL6BMBxOA7H09OVnkH374uOwxhj1qLYC4j5ffP75vdr1xZ94AUOwkE4ePy46BiMMWZtir2AyF3lrnLXOnVEH7iKfMmXfE+cEJ2DMcasTfHfA2kEjaDRv/4l+sALTsCv0q/Sr6dPi87BGGPWptgLCLqjO7pb0BXISlpJKy1voxbGGLN0xX8F0gbaQBvLKSDSIGmQNOj6ddE5GGPM2hR/ASEgoBo1RB+4StnA6sYN0TkYY8zaFHsBoZk0k2ZWqiT6wFXZKdkp2SnXronOwRhj1qb4r0DKQBkoU6GC6ANX3L2b1j2te1r3QtjykjHGSpniLyB2YAd2llJAeOEgY4y9qOKfhbUcl+Nye3vRBw6+4Au+3HWXMcZeVLEVkODg4ODgYI0GjGAEI6LoA+cCwhhjL6fYCkhKQEpASoCNsA2sGGOMFa5iKyB21e2q21XnT/yMMVZSFFsBUWY75eWBDnSgs4BCcggOwSELGEpjjDErVcw30YnAG7zBW/y0WepDfahP5cqiczDGmLUq/mm8RjCCMSdH9IFjOIZjeJky/k7+Tv5Ojo6i8zDGmLUp/gISBEEQdOeO6ANXPajzoM6DOlWris7BGGPWpvgLyByYA3Nu3RJ94CpyJmdydnYWnYMxxqxN8ffC2k7bafvNm6IPvOAE+Ev+kj9fgTDG2PMq/pXoC3EhLrSc5oWyXtbL+po1RedgjDFrU/xDWJWhMlS+cEH0gauwMTbGxg0bis7BGGPWpvgLyApYASsyMkQfuIrKUlkq26iR6ByMMWZtir+AAACABV2BfIqf4qdcQBhj7HkVewGR58vz5fmnT4s+8AKJkAiJzs4e8R7xHvE8G4sxxp5VsReQepH1IutF/vILRVM0RT94IPoEFJyIddI6aV3jxqJzMMaYtdAU9y9MS0tLS0sjcnF0cXRx7N4dfoPf4Ldq1USfCGm7tF3afvbsZfNl82Xz/v2i8zDGmKUTdA8EAKpAFahiMok+ASoaQSNoRIsWonMwxpi1EFZA6Cf6iX46dEj0CSjI05k6U2c/P58MnwyfDAcH0XkYY8zSCdvgCQEBwXIKiNpcMdc31zfX19dXeXb3btG5GGPMUgm7AjGajCaj6cQJ5dHdu6JPRIEBMAAGNG8uOgZjjFk6cfdAAABAlpWvhw+LPhEFMiADMt55R3QMxhizdIILCAAYwAAGCxrKmopTcaqXl8cwj2Eew+rXF52HMcYslfgCchfuwt2DB0XHeJLGXeOuce/RQ3QOxhizVMILyN2YuzF3Y+Lj6TAdpsOZmaLzFFgIC2Hh+++LjsEYY5aq2BcSPunWtlvbbm3Lz3eRXCQXydMTzsAZOGMBvakQELBKlZpv1Xyr5lubNl1Ou5x2Oe3qVdGxGGPMUgi/AinQGlpD640bRcd4krxUXiov7d1bdA7GGLM0llNAhsAQGLJ1K+hBD/rcXNFxVHScjtPxDz90n+M+x31OuXKi8zDGmKUQPoSlunLlypUrVx4+dIl0iXSJ9PeH7bAdtterJzoXxmEcxjk4SD2lnlLPS5eurLqy6sqq5GTRuRhjRcfrotdFr4uvvVa9SvUq1av4+ro2c23m2qxBg2oPqz2s9tDRMcAnwCfA5/ff1d5+ovOKImwl+tPgcByOwzduJIkkkt59V3SeglzZmI3ZI0Yoj775RvmqrmNhjFmj4ODg4OBgjSb9fPr59PP9+9NMmkkzx46VO8md5E6vvaZ0zAAgICAAkEACCQDS09PT09Pv3tVO107XTl+7Vr4qX5WvfvXVkagjUUeizp4VfVzFBUUHeJIyVFS1qu0523O25zIylHUidnaic6kohEIopEsX03DTcNPwn38WnYcx9vx0Wp1Wp61ShSIogiLWr8dZOAtnBQW93E/Ny4NBMAgGzZgBS2AJLJk2Tem4kZcn+niLisUVEJUuRZeiS1m7FgbCQBhoOesxaBftol0HD5oqmiqaKr71lug8jLFnpwxNOTnJHeWOcscDB5TZlg0aFPovagNtoM2WLXer3q16t2q3bueGnRt2btjDh6KPv7BZzk30J3mAB3ioQ0WWA1tiS2zp76+7qLuou9i2reg8jLF/pg5VyWlympy2YUORFQ7VNtgG29q3f6XtK21faTt3rujjLyoWW0CMaEQj7tsHARAAASdPis7zJEqlVEqdPVt9YYrOwxh7uvT26e3T248ZA+NhPIwPDCyu34uzcTbODg319PP08/TT60Wfh8Jm8W98NSrWqFijoq0t3sf7eN+Cbqrvxt24u2rVW7du3bp169IlZRaZ0Sg6F2Psf7wWeC3wWtCokTIpZ80a5Z6qTfFNHroCV+AKIprRjGZJUt4nNm8WfV4Ki8VegajyLuVdyrv0/feUREmUlJMjOs+fzIW5MHfqVH8nfyd/J0dH0XEYYwBBFERBZGMj15HryHW++w4WwSJYZG8vNlXJu2dq8QUkNTU1NTX19m2IhEiIXLNGdJ4n4UgciSOrVXtw6cGlB5ciIkTnYYwBZK/OXp29etgwmAyTYbL4oSNKpmRKrl1bdI7CZvEFRCVfkC/IF2bOVP5HmM2i8/yJP/iD/9ix+kB9oD7Q01N0HMZKoybUhJpQnTqwH/bD/qlTRecpkAzJkIwWO+v1RVlNAVEX6OAMnIEzVq4Uneev2dqSjnSk++67husarmu4znLWrzBW8iHa7LfZb7N/0SLlDdtyWg/halyNqy9eFJ2jsFlNASmwATbAhmnTlAcWuEAnHuIh3t29TL0y9crU+/RT0XEYKw309fX19fXDw2EkjISRljPZpkAf6AN9jh0THaOwWV0BMRqNRqPx/HnqRb2o1w8/iM7zVB7gAR4REV51vep61fXyEh2HsZJI30ffR9/nzTflU/Ip+dTs2aLzPA2GYiiG7tghOkdhs7oCorLZZ7PPZp/lXomgF3qhl41N/on8E/knfvjB+6z3We+zr7wiOhdjJYHaHZvSKI3SYmPRG73R28FBdK4/CYMwCHv40DzPPM88b8MG0XEKm9UWkKTNSZuTNv/yC8RCLMSuWCE6z9NgAAZgwOuvmx3NjmbHmJhHz5a4m2mMFSebXja9bHotXVrkK8pfEt2je3Rv3bojgUcCjwRevy46T2Gz2gKiyvsp76e8nz79FIbBMBh2+7boPE+D7bAdtuvUSZukTdIm8b0Rxl6EvpK+kr7SuHHYCTthp169ROd5GlpEi2hRfj4NoAE0YMYM0XmKSon5JKzT6XQ6XViY8mjhQtF5nioUQiFUlukG3aAbnTqZJpgmmCZs2SI6FmOWTDtBO0E74Z13oDN0hs7//S+GYRiGWXALIQICWrZM6cY7aJDoOEWlxBQQhSRpN2k3aTclJuJUnIpTLf3m9d27+Aa+gW+89ZZhpWGlYeXx46ITMWZJ1FYksqPsKDvu3w9REAVRlSqJzvU0lEiJlHjzJjbFpti0QQOlgNy4ITpXUbH6IazHyTJ8Dp/D52Fh6iWk6ER/r0IFeofeoXd27dJ/of9C/8Xrr4tOxJglaPJxk4+bfOzqKq+QV8grtm619MJRYCtsha3jx5f0wqGy3EvAF3TlkRquNVxruDo74xk8g2e8vUXneqpESITEcuWoP/Wn/u3aVS1btWzVsuvX/2743fC7IStLdDzGipNPhk+GT0blyuAADuCwZ4+yolz81tb/hCbQBJqQkGB6z/Se6b2hQx89W+K3ui1xBUTlkumS6ZKZkAB+4Ad+ISFwES7CRcttdog7cSfurFRJ6i/1l/q/+67LWZezLmfXrVPK4f37ovMxVpSUHQIrVJCvydfka9u2QQzEQIzltwRSm7xiCIZgSLt2yt9ryZtt9TQlbAjrf5RLyLt35SA5SA4KCVFvXovO9Y/+Df+GfzdsqExP3L7dI94j3iPe2Vl0LMaKglo4oAW0gBZxcbAH9sAeHx/RuZ7ZQTgIBz/7TFngfOqU6DjFrcQWEFVKcEpwSvCuXeRDPuQzZ47oPM9Hq5UeSA+kBwcOeHfw7uDdoW5d0YkYKwwFhQMAALZvt7bCoW5t/er8V+e/Oj8yUnQeUUrYLKynU/cHyI7LjsuOi4+HSTAJJvn6is71rOgQHaJDV69KraRWUqu2bQ3xhnhDfEqK6FyMPQ9lun2NGsojdfq6Vis61zPzBm/wvnPHvNC80LzQ0/MoHsWj+OuvomOJUuKvQFT7cB/uQ7MZ8zEf8/v0ocN0mA5nZorO9azQF33Rt3p16kf9qF98vO6O7o7uzttvi87F2LPwzPLM8sx64w3lnuShQ8qzVlQ4HsEszMKssLDSXjhUJfYm+tNcXn159eXVt2/XqFejXo16GRl4GA/j4S5dROd6ZsmQDMl2dpAGaZDWo0eNcjXK1Sh3+/aV3678duW35GTR8Rj7Ix3pSEfNmuFaXItrd+yA9bAe1levLjrXi1m0yLjduN24fdYs0UksRakZwnoa5ZJaXbmurmS3VqtX53yS80nOJ6Ghad3Tuqd1z84WnYiVTrpVulW6VaNGKV2zZ81Sm4uKzvW81Om5D/If5D/Ib95c+bvKzRWdy1KUmiGspylvKG8obxg2DN6Bd+Cd7dtF53k5vXs72DjYONgcPlwwZMBYMVA2UCtfXpeiS9GlrF0LX8PX8PVXX1lr4QBf8AXfy5dxOk7H6d26ceH4a6W+gKj3RjT9Nf01/bt3p320j/ZZcUuRR9OApVgpVopNStJqtVqt9qOPlG9yF2BWuLTvat/Vvuvj44AO6IAmEwyEgTCwRw/RuV6Uuq4DEiABErp0UabnXrkiOpel4jeUJ6gtFGwCbAJsAg4fVj5JubqKzlU4DhzAztgZOw8caPjU8Knh09OnRSdi1qVgNmNMdkx2zOjRSouRL75QvmtrKzrfC3u0TkzuJfeSewUHpzimOKY4lrz9OwobF5CnUHcSlCvLleXKe/cqz5YtKzrXyyr4hPVoAZQ6jz02NjY2NtbSe4cxUTxDPUM9Q319sQN2wA4LF+JknIyTPTxE5yoseAWv4JVRowyXDZcNl0vvuo7nxQXkH+hO6U7pTnXqBO/D+/B+bKzyrBV/0noC7aW9tDclheIojuLGjlUXXorOxcTyuuh10euik1N+bn5ufu6sWTgTZ+LMAQPACEYwlqCh0P7QH/rPmGEcYhxiHDJhgug41qbkvBCKmH6Yfph+WNeudJAO0sE1a5RnS04hedyOHVgOy2G5iAhesFg6KLMRy5ZVNmYbOpRepVfp1XHjcDgOx+GVK4vOV9ioA3WgDgsWmKaYppimqM0P2fPiAvKclNlNXbpIzaRmUrO1a5VnS2AhUXuHLYbFsHjtWnm+PF+eP3lyil+KX4rfuXOi47GXUy+qXlS9KHv7ivMrzq84f9Ag+YR8Qj4xcaK6YFV0vqJCsRRLsd9+a3IzuZnc1I2eSn7X3KJS6mdhPa/Hb6699x6EQRiEPXwoOlehWwyLYbH06PXRu7d0XDouHT99Wrtau1q7+v/+zzPdM90zvUMH5fslaEijhFJ7T2l3andqdw4fXsGpglMFp/Pn6RV6hV6ZP7+kFw6IhViIXbJEKRzqrEQuHC+L//BfknLp37kz6EEP+h9/BAMYwGBnJzpXcaHP6XP6/MgRuA/34f68eZlXM69mXl2z5tywc8PODSuBhdVKKAWjaVNl5feAARAMwRDcp4/yXeufDPLMtsE22DZ3rrGqsaqx6qhRypNcOAoLF5BCohSSdu0gHMIhfO1aiIZoiC5fXnSuYjcMhsGw27fpOl2n6z//jANxIA5cu9ZtkNsgt0G7d/Nsr8Klv6+/r79fq5ZskA2yoUcPvIE38Eb//gXbApQ2j4ZeqQE1oAaffGIKNAWaAr/8UnSskooLSCHzCvYK9gr28MgfnD84f/DmzTgGx+CYmjVF5xKNIimSIn//Hd3QDd3WrwdXcAXX9etzYnNic2ITEnil79/TVtVW1VZ1d1dWRnfsCCfhJJzs3BmyIAuytNoSNzvqhdy/L++V98p7+/bldRzFo5S/4IqOp5+nn6efi4s0QZogTdi0CSbDZJis14vOZZnu36fRNJpGJyRIA6QB0oC9e+EW3IJbe/aUcyjnUM4hKUntGCA6aWFTF+Zl2mXaZdo1aiStllZLqwMDyZ/8yT8wEHfhLtwVGAhzYS7MrVpVdF5Lo25zoHld87rm9Y4dk39J/iX5F24qWly4gBQxdXokhVIohcbE4GJcjIu7dhWdy2p4gRd43btHs2gWzTpxQlnwdeKE0uTu5EnpXeld6d3jx/MG5Q3KG3TypNJm+7fflP+4+Me63ee4z3GfU66c3VG7o3ZH69bNn5o/NX9q3bo4GAfj4FdfhVbQClo1boxrcA2u8fBQJmE0agSLYBEssrcXfbqtS2oqEBBQ+/bKDqQXLohOVNpwASlWiLoFugW6Bf/+NxyDY3AsIoKHHgoXJVMyJZvNYAYzmG/cgFzIhdz/fcUojMKoa9cgBEIg5M6dZ/65QECg0WAwBmOwkxO0hJbQ0slJ+blVqkAe5EFelSqQCImQyFsQF5l20A7a/fyzpq+mr6Zvv35J9ZPqJ9W3nn19Shp+4xKkYEOoltASWn7/vfKsulMbYwwAgKIpmqIfPIBsyIbsiAhTK1MrU6t580TnYgouIIJ5xHvEe8Q7O2t2aHZodixfrkw7bN9edC7GRKJpNI2mnTiBE3EiTuzVSxmiSk0VnYs9jguIRUFU2q8PGoSIiKg2dStF8/ZZ6aQDHeiIIAIiIGLpUltbW1tb2xEjEmsl1kqslZMjOh77a1xALJQ6bRPSIR3SV63CIAzCoDffFJ2LsULlB37g99tvShv1jz9WWuXExYmOxZ4NtzKxUKZrpmuma8eOYSAGYqBWq8xCGjGCDtNhOsw3DZk1y8tTvkZF5QTkBOQEvPkmFw7rxFcgVkZdX4I9sAf2mDkTT+AJPNGnD8/mYpZvz578yPzI/MghQ44EHgk8EpiWJjoRezn8hmPl9Kn6VH1qUBD1o37Ub/585dnGjUXnYqXcoz3FoQt0gS7jxxtbGFsYW8TEiI7FChcPYVk5Q2NDY0PjffuUhQo6nTrUBT7gAz7Xr4vOx0oHdUU4jIJRMGr0aDgEh+BQ/fpcOEo2vgIpodQV0TaeNp42ngMHKiu4J0zglhisUIyAETDi2jWQQQb566+VvdHnzzcajUaj8f590fFY8eACUkp4n/U+6332lVfMn5g/MX8SGoou6IIuI0YonxRdXETnYxZuFIyCUZcugR3Ygd2sWeW7le9WvtvSpUqPsgcPRMdjYvAQVimhtnwwbTBtMG2YMycnMCcwJ7BuXWXr2g8/LNjXgzEAUDok7N+PNbAG1ggJuXvz7s27N1991RhsDDYGz5/PhYMB8BUIe4JnqGeoZ6ivLzbEhtgwLAyaQlNoGhyM4RiO4WXKiM7HChfNo3k079YtfA1fw9diYmQH2UF2WLJEaYd+8qTofMyycQFhf6sJNaEmVLGiTZxNnE1c166URVmU1bcv3sE7eCcg4PGtb5lly8tT2ubv26c0m/zuO8e+jn0d+/70E19RsBfBBYS9EHUnPPov/Zf+27mzsg6lc2eaTtNpemAgeqEXetnYiM5ZOt29C8tgGSyLi4McyIGcTZtyQ3NDc0Pj4lJTU1NTU2/fFp2QlQxcQFih8snwyfDJqFw51y3XLdft7bexMTbGxq1awTgYB+NatYI5MAfm1KkjOqfVerRlK1SBKlDl1Ck6Rsfo2O7dOAJH4Ij//CdnR86OnB379vEOj6w4cAFhxUq/Rb9Fv8XNTR4gD5AH+PmhCU1o8vGBTtAJOvn6Kv9KXQhpays6b7FT95RHQsLERGgADaBBYiJshI2w8fBhjMM4jEtMVLrT3r0rOi4r3biAMIui0+q0Oq2tLV2ki3TxjTfgS/gSvmzcGDMxEzMbNYIgCIKgWrXgB/gBfqhdW5k95uqKq3AVrnJ1tZid/fSgB31urrID4W+/QTWoBtXOnwc3cAO38+dhH+yDfenpShfa8+el8lJ5qfzJk8k1k2sm1zx7Vvkhxb+jImPPgwsIK1G8gr2CvYKrVzdfMl8yX6pZU1ouLZeWu7pCb+gNve3sKIIiKKJcOZgJM2GmnZ2UJWVJWXZ2NIkm0aRy5ZQFcYjkS77kW768ci8nK0uZrXT7Ni7ABbggKwuGwlAYmpmpdJHNytL8R/MfzX+ysiAO4iDuxo26H9T9oO4Hly/HxsbGxsbm54s+L4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYKzb/D4DEm9oGCaFQAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTEyLTE1VDE1OjU3OjI3KzA4OjAwohG+LwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0xMi0xNVQxNTo1NzoyNyswODowMNNMBpMAAABPdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2NrMWJ6YTB6ajlqamRjeHIvcmVmcmVzaC5zdmejF0ikAAAAAElFTkSuQmCC');
+ background-size: contain;
+ content: ' ';
+ inset: 0;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Verifition/src/Verify/VerifyPictureWord.vue b/src/components/Verifition/src/Verify/VerifyPictureWord.vue
new file mode 100644
index 0000000..e1725f8
--- /dev/null
+++ b/src/components/Verifition/src/Verify/VerifyPictureWord.vue
@@ -0,0 +1,196 @@
+<template>
+ <div style="position: relative">
+ <div class="verify-img-out">
+ <div
+ :style="{
+ width: setSize.imgWidth,
+ height: setSize.imgHeight,
+ 'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+ 'margin-bottom': vSpace + 'px'
+ }"
+ class="verify-img-panel"
+ >
+ <div v-show="showRefresh" class="verify-refresh" style="z-index: 3" @click="refresh">
+ <i class="iconfont icon-refresh"></i>
+ </div>
+ <img
+ @click="refresh"
+ ref="canvas"
+ :src="'data:image/png;base64,' + verificationCodeImg"
+ alt=""
+ style="display: block; width: 100%; height: 100%"
+ />
+ </div>
+ </div>
+ <div
+ :style="{
+ width: setSize.imgWidth,
+ color: barAreaColor,
+ 'border-color': barAreaBorderColor
+ // 'line-height': barSize.height
+ }"
+ class="verify-bar-area"
+ >
+ <div class="verify-msg">{{ text }}</div>
+ <div
+ :style="{
+ 'line-height': barSize.height
+ }"
+ >
+ <input class="verify-input" type="text" v-model="userCode" />
+ </div>
+ <button type="button" class="verify-btn" @click="submit" :disabled="checking">{{
+ t('captcha.verify')
+ }}</button>
+ </div>
+ </div>
+</template>
+<script setup type="text/babel">
+/**
+ * VerifyPictureWord
+ * @description 杈撳叆鏂囧瓧
+ * */
+import { resetSize } from '../utils/util'
+import { aesEncrypt } from '../utils/ase'
+import { getCode, reqCheck } from '@/api/login'
+import { getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs } from 'vue'
+
+const props = defineProps({
+ // 寮瑰嚭寮� pop锛屽浐瀹� fixed
+ mode: {
+ type: String,
+ default: 'fixed'
+ },
+ captchaType: {
+ type: String
+ },
+ // 闂撮殧
+ vSpace: {
+ type: Number,
+ default: 5
+ },
+ imgSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '155px'
+ }
+ }
+ },
+ barSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '40px'
+ }
+ }
+ }
+})
+
+const { t } = useI18n()
+const { mode, captchaType } = toRefs(props)
+const { proxy } = getCurrentInstance()
+let secretKey = ref(''), // 鍚庣杩斿洖鐨刟se鍔犲瘑绉橀挜
+ userCode = ref(''), // 鐢ㄦ埛杈撳叆鐨勯獙璇佺爜 鏆傚瓨鑷硃ointJson锛屾棤闇�鍔犲瘑
+ verificationCodeImg = ref(''), // 鍚庣鑾峰彇鍒扮殑鑳屾櫙鍥剧墖
+ backToken = ref(''), // 鍚庣杩斿洖鐨則oken鍊�
+ setSize = reactive({
+ imgHeight: 0,
+ imgWidth: 0,
+ barHeight: 0,
+ barWidth: 0
+ }),
+ text = ref(''),
+ barAreaColor = ref('#000'),
+ barAreaBorderColor = ref('#ddd'),
+ showRefresh = ref(true),
+ // bindingClick = ref(true)
+ checking = ref(false)
+
+const init = () => {
+ // 鍔犺浇椤甸潰
+ getPicture()
+ nextTick(() => {
+ let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
+ setSize.imgHeight = imgHeight
+ setSize.imgWidth = imgWidth
+ setSize.barHeight = barHeight
+ setSize.barWidth = barWidth
+ proxy.$parent.$emit('ready', proxy)
+ })
+}
+onMounted(() => {
+ // 绂佹鎷栨嫿
+ init()
+ proxy.$el.onselectstart = function () {
+ return false
+ }
+})
+const canvas = ref(null)
+
+const submit = () => {
+ checking.value = true
+ // 鍙戦�佸悗绔姹�
+ const captchaVerification = secretKey.value
+ ? aesEncrypt(backToken.value + '---' + userCode.value, secretKey.value)
+ : backToken.value + '---' + userCode.value
+ let data = {
+ captchaType: captchaType.value,
+ pointJson: userCode.value,
+ token: backToken.value
+ }
+ reqCheck(data).then((res) => {
+ if (res.repCode === '0000') {
+ barAreaColor.value = '#4cae4c'
+ barAreaBorderColor.value = '#5cb85c'
+ text.value = t('captcha.success')
+ // bindingClick.value = false
+ if (mode.value === 'pop') {
+ setTimeout(() => {
+ proxy.$parent.clickShow = false
+ refresh()
+ }, 1500)
+ }
+ proxy.$parent.$emit('success', { captchaVerification })
+ } else {
+ proxy.$parent.$emit('error', proxy)
+ barAreaColor.value = '#d9534f'
+ barAreaBorderColor.value = '#d9534f'
+ text.value = t('captcha.fail')
+ setTimeout(() => {
+ refresh()
+ }, 700)
+ }
+ checking.value = false
+ })
+}
+
+const refresh = async function () {
+ barAreaColor.value = '#000'
+ barAreaBorderColor.value = '#ddd'
+ checking.value = false
+
+ userCode.value = ''
+
+ await getPicture()
+ showRefresh.value = true
+}
+
+// 璇锋眰鑳屾櫙鍥剧墖鍜岄獙璇佸浘鐗�
+const getPicture = async () => {
+ let data = {
+ captchaType: captchaType.value
+ }
+ const res = await getCode(data)
+ if (res.repCode === '0000') {
+ verificationCodeImg.value = res.repData.originalImageBase64
+ backToken.value = res.repData.token
+ secretKey.value = res.repData.secretKey
+ text.value = t('captcha.code')
+ } else {
+ text.value = res.repMsg
+ }
+}
+</script>
diff --git a/src/components/Verifition/src/Verify/VerifyPoints.vue b/src/components/Verifition/src/Verify/VerifyPoints.vue
new file mode 100644
index 0000000..9d04f29
--- /dev/null
+++ b/src/components/Verifition/src/Verify/VerifyPoints.vue
@@ -0,0 +1,250 @@
+<template>
+ <div style="position: relative">
+ <div class="verify-img-out">
+ <div
+ :style="{
+ width: setSize.imgWidth,
+ height: setSize.imgHeight,
+ 'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
+ 'margin-bottom': vSpace + 'px'
+ }"
+ class="verify-img-panel"
+ >
+ <div v-show="showRefresh" class="verify-refresh" style="z-index: 3" @click="refresh">
+ <i class="iconfont icon-refresh"></i>
+ </div>
+ <img
+ ref="canvas"
+ :src="'data:image/png;base64,' + pointBackImgBase"
+ alt=""
+ style="display: block; width: 100%; height: 100%"
+ @click="bindingClick ? canvasClick($event) : undefined"
+ />
+
+ <div
+ v-for="(tempPoint, index) in tempPoints"
+ :key="index"
+ :style="{
+ 'background-color': '#1abd6c',
+ color: '#fff',
+ 'z-index': 9999,
+ width: '20px',
+ height: '20px',
+ 'text-align': 'center',
+ 'line-height': '20px',
+ 'border-radius': '50%',
+ position: 'absolute',
+ top: parseInt(tempPoint.y - 10) + 'px',
+ left: parseInt(tempPoint.x - 10) + 'px'
+ }"
+ class="point-area"
+ >
+ {{ index + 1 }}
+ </div>
+ </div>
+ </div>
+ <!-- 'height': this.barSize.height, -->
+ <div
+ :style="{
+ width: setSize.imgWidth,
+ color: barAreaColor,
+ 'border-color': barAreaBorderColor,
+ 'line-height': barSize.height
+ }"
+ class="verify-bar-area"
+ >
+ <span class="verify-msg">{{ text }}</span>
+ </div>
+ </div>
+</template>
+<script setup type="text/babel">
+/**
+ * VerifyPoints
+ * @description 鐐归��
+ * */
+import { resetSize } from './../utils/util'
+import { aesEncrypt } from './../utils/ase'
+import { getCode, reqCheck } from '@/api/login'
+import { getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs } from 'vue'
+
+const props = defineProps({
+ //寮瑰嚭寮弍op锛屽浐瀹歠ixed
+ mode: {
+ type: String,
+ default: 'fixed'
+ },
+ captchaType: {
+ type: String
+ },
+ //闂撮殧
+ vSpace: {
+ type: Number,
+ default: 5
+ },
+ imgSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '155px'
+ }
+ }
+ },
+ barSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '40px'
+ }
+ }
+ }
+})
+
+const { t } = useI18n()
+const { mode, captchaType } = toRefs(props)
+const { proxy } = getCurrentInstance()
+let secretKey = ref(''), //鍚庣杩斿洖鐨刟se鍔犲瘑绉橀挜
+ checkNum = ref(3), //榛樿闇�瑕佺偣鍑荤殑瀛楁暟
+ fontPos = reactive([]), //閫変腑鐨勫潗鏍囦俊鎭�
+ checkPosArr = reactive([]), //鐢ㄦ埛鐐瑰嚮鐨勫潗鏍�
+ num = ref(1), //鐐瑰嚮鐨勮鏁�
+ pointBackImgBase = ref(''), //鍚庣鑾峰彇鍒扮殑鑳屾櫙鍥剧墖
+ poinTextList = reactive([]), //鍚庣杩斿洖鐨勭偣鍑诲瓧浣撻『搴�
+ backToken = ref(''), //鍚庣杩斿洖鐨則oken鍊�
+ setSize = reactive({
+ imgHeight: 0,
+ imgWidth: 0,
+ barHeight: 0,
+ barWidth: 0
+ }),
+ tempPoints = reactive([]),
+ text = ref(''),
+ barAreaColor = ref(undefined),
+ barAreaBorderColor = ref(undefined),
+ showRefresh = ref(true),
+ bindingClick = ref(true)
+
+const init = () => {
+ //鍔犺浇椤甸潰
+ fontPos.splice(0, fontPos.length)
+ checkPosArr.splice(0, checkPosArr.length)
+ num.value = 1
+ getPictrue()
+ nextTick(() => {
+ let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
+ setSize.imgHeight = imgHeight
+ setSize.imgWidth = imgWidth
+ setSize.barHeight = barHeight
+ setSize.barWidth = barWidth
+ proxy.$parent.$emit('ready', proxy)
+ })
+}
+onMounted(() => {
+ // 绂佹鎷栨嫿
+ init()
+ proxy.$el.onselectstart = function () {
+ return false
+ }
+})
+const canvas = ref(null)
+const canvasClick = (e) => {
+ checkPosArr.push(getMousePos(canvas, e))
+ if (num.value == checkNum.value) {
+ num.value = createPoint(getMousePos(canvas, e))
+ //鎸夋瘮渚嬭浆鎹㈠潗鏍囧��
+ let arr = pointTransfrom(checkPosArr, setSize)
+ checkPosArr.length = 0
+ checkPosArr.push(...arr)
+ //绛夊垱寤哄潗鏍囨墽琛屽畬
+ setTimeout(() => {
+ // var flag = this.comparePos(this.fontPos, this.checkPosArr);
+ //鍙戦�佸悗绔姹�
+ var captchaVerification = secretKey.value
+ ? aesEncrypt(backToken.value + '---' + JSON.stringify(checkPosArr), secretKey.value)
+ : backToken.value + '---' + JSON.stringify(checkPosArr)
+ let data = {
+ captchaType: captchaType.value,
+ pointJson: secretKey.value
+ ? aesEncrypt(JSON.stringify(checkPosArr), secretKey.value)
+ : JSON.stringify(checkPosArr),
+ token: backToken.value
+ }
+ reqCheck(data).then((res) => {
+ if (res.repCode == '0000') {
+ barAreaColor.value = '#4cae4c'
+ barAreaBorderColor.value = '#5cb85c'
+ text.value = t('captcha.success')
+ bindingClick.value = false
+ if (mode.value == 'pop') {
+ setTimeout(() => {
+ proxy.$parent.clickShow = false
+ refresh()
+ }, 1500)
+ }
+ proxy.$parent.$emit('success', { captchaVerification })
+ } else {
+ proxy.$parent.$emit('error', proxy)
+ barAreaColor.value = '#d9534f'
+ barAreaBorderColor.value = '#d9534f'
+ text.value = t('captcha.fail')
+ setTimeout(() => {
+ refresh()
+ }, 700)
+ }
+ })
+ }, 400)
+ }
+ if (num.value < checkNum.value) {
+ num.value = createPoint(getMousePos(canvas, e))
+ }
+}
+//鑾峰彇鍧愭爣
+const getMousePos = function (obj, e) {
+ var x = e.offsetX
+ var y = e.offsetY
+ return { x, y }
+}
+//鍒涘缓鍧愭爣鐐�
+const createPoint = function (pos) {
+ tempPoints.push(Object.assign({}, pos))
+ return num.value + 1
+}
+const refresh = async function () {
+ tempPoints.splice(0, tempPoints.length)
+ barAreaColor.value = '#000'
+ barAreaBorderColor.value = '#ddd'
+ bindingClick.value = true
+ fontPos.splice(0, fontPos.length)
+ checkPosArr.splice(0, checkPosArr.length)
+ num.value = 1
+ await getPictrue()
+ showRefresh.value = true
+}
+
+// 璇锋眰鑳屾櫙鍥剧墖鍜岄獙璇佸浘鐗�
+const getPictrue = async () => {
+ let data = {
+ captchaType: captchaType.value
+ }
+ const res = await getCode(data)
+ if (res.repCode == '0000') {
+ pointBackImgBase.value = res.repData.originalImageBase64
+ backToken.value = res.repData.token
+ secretKey.value = res.repData.secretKey
+ poinTextList.value = res.repData.wordList
+ text.value = t('captcha.point') + '銆�' + poinTextList.value.join(',') + '銆�'
+ } else {
+ text.value = res.repMsg
+ }
+}
+//鍧愭爣杞崲鍑芥暟
+const pointTransfrom = function (pointArr, imgSize) {
+ var newPointArr = pointArr.map((p) => {
+ let x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth))
+ let y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight))
+ return { x, y }
+ })
+ return newPointArr
+}
+</script>
diff --git a/src/components/Verifition/src/Verify/VerifySlide.vue b/src/components/Verifition/src/Verify/VerifySlide.vue
new file mode 100644
index 0000000..f3c7bfe
--- /dev/null
+++ b/src/components/Verifition/src/Verify/VerifySlide.vue
@@ -0,0 +1,376 @@
+<template>
+ <div style="position: relative">
+ <div
+ v-if="type === '2'"
+ :style="{ height: parseInt(setSize.imgHeight) + vSpace + 'px' }"
+ class="verify-img-out"
+ >
+ <div :style="{ width: setSize.imgWidth, height: setSize.imgHeight }" class="verify-img-panel">
+ <img
+ :src="'data:image/png;base64,' + backImgBase"
+ alt=""
+ style="display: block; width: 100%; height: 100%"
+ />
+ <div v-show="showRefresh" class="verify-refresh" @click="refresh">
+ <i class="iconfont icon-refresh"></i>
+ </div>
+ <transition name="tips">
+ <span v-if="tipWords" :class="passFlag ? 'suc-bg' : 'err-bg'" class="verify-tips">
+ {{ tipWords }}
+ </span>
+ </transition>
+ </div>
+ </div>
+ <!-- 鍏叡閮ㄥ垎 -->
+ <div
+ :style="{ width: setSize.imgWidth, height: barSize.height, 'line-height': barSize.height }"
+ class="verify-bar-area"
+ >
+ <span class="verify-msg" v-text="text"></span>
+ <div
+ :style="{
+ width: leftBarWidth !== undefined ? leftBarWidth : barSize.height,
+ height: barSize.height,
+ 'border-color': leftBarBorderColor,
+ transaction: transitionWidth
+ }"
+ class="verify-left-bar"
+ >
+ <span class="verify-msg" v-text="finishText"></span>
+ <div
+ :style="{
+ width: barSize.height,
+ height: barSize.height,
+ 'background-color': moveBlockBackgroundColor,
+ left: moveBlockLeft,
+ transition: transitionLeft
+ }"
+ class="verify-move-block"
+ @mousedown="start"
+ @touchstart="start"
+ >
+ <i :class="['verify-icon iconfont', iconClass]" :style="{ color: iconColor }"></i>
+ <div
+ v-if="type === '2'"
+ :style="{
+ width: Math.floor((parseInt(setSize.imgWidth) * 47) / 310) + 'px',
+ height: setSize.imgHeight,
+ top: '-' + (parseInt(setSize.imgHeight) + vSpace) + 'px',
+ 'background-size': setSize.imgWidth + ' ' + setSize.imgHeight
+ }"
+ class="verify-sub-block"
+ >
+ <img
+ :src="'data:image/png;base64,' + blockBackImgBase"
+ alt=""
+ style="display: block; width: 100%; height: 100%; -webkit-user-drag: none"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script setup type="text/babel">
+/**
+ * VerifySlide
+ * @description 婊戝潡
+ * */
+import { aesEncrypt } from './../utils/ase'
+import { resetSize } from './../utils/util'
+import { getCode, reqCheck } from '@/api/login'
+
+const props = defineProps({
+ captchaType: {
+ type: String
+ },
+ type: {
+ type: String,
+ default: '1'
+ },
+ //寮瑰嚭寮弍op锛屽浐瀹歠ixed
+ mode: {
+ type: String,
+ default: 'fixed'
+ },
+ vSpace: {
+ type: Number,
+ default: 5
+ },
+ explain: {
+ type: String,
+ default: ''
+ },
+ imgSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '155px'
+ }
+ }
+ },
+ blockSize: {
+ type: Object,
+ default() {
+ return {
+ width: '50px',
+ height: '50px'
+ }
+ }
+ },
+ barSize: {
+ type: Object,
+ default() {
+ return {
+ width: '310px',
+ height: '30px'
+ }
+ }
+ }
+})
+
+const { t } = useI18n()
+const { mode, captchaType, type, blockSize, explain } = toRefs(props)
+const { proxy } = getCurrentInstance()
+let secretKey = ref(''), //鍚庣杩斿洖鐨刟se鍔犲瘑绉橀挜
+ passFlag = ref(''), //鏄惁閫氳繃鐨勬爣璇�
+ backImgBase = ref(''), //楠岃瘉鐮佽儗鏅浘鐗�
+ blockBackImgBase = ref(''), //楠岃瘉婊戝潡鐨勮儗鏅浘鐗�
+ backToken = ref(''), //鍚庣杩斿洖鐨勫敮涓�token鍊�
+ startMoveTime = ref(''), //绉诲姩寮�濮嬬殑鏃堕棿
+ endMovetime = ref(''), //绉诲姩缁撴潫鐨勬椂闂�
+ tipWords = ref(''),
+ text = ref(''),
+ finishText = ref(''),
+ setSize = reactive({
+ imgHeight: 0,
+ imgWidth: 0,
+ barHeight: 0,
+ barWidth: 0
+ }),
+ moveBlockLeft = ref(undefined),
+ leftBarWidth = ref(undefined),
+ // 绉诲姩涓牱寮�
+ moveBlockBackgroundColor = ref(undefined),
+ leftBarBorderColor = ref('#ddd'),
+ iconColor = ref(undefined),
+ iconClass = ref('icon-right'),
+ status = ref(false), //榧犳爣鐘舵��
+ isEnd = ref(false), //鏄楠岃瘉瀹屾垚
+ showRefresh = ref(true),
+ transitionLeft = ref(''),
+ transitionWidth = ref(''),
+ startLeft = ref(0)
+
+const barArea = computed(() => {
+ return proxy.$el.querySelector('.verify-bar-area')
+})
+const init = () => {
+ if (explain.value === '') {
+ text.value = t('captcha.slide')
+ } else {
+ text.value = explain.value
+ }
+ getPictrue()
+ nextTick(() => {
+ let { imgHeight, imgWidth, barHeight, barWidth } = resetSize(proxy)
+ setSize.imgHeight = imgHeight
+ setSize.imgWidth = imgWidth
+ setSize.barHeight = barHeight
+ setSize.barWidth = barWidth
+ proxy.$parent.$emit('ready', proxy)
+ })
+
+ window.removeEventListener('touchmove', function (e) {
+ move(e)
+ })
+ window.removeEventListener('mousemove', function (e) {
+ move(e)
+ })
+
+ //榧犳爣鏉惧紑
+ window.removeEventListener('touchend', function () {
+ end()
+ })
+ window.removeEventListener('mouseup', function () {
+ end()
+ })
+
+ window.addEventListener('touchmove', function (e) {
+ move(e)
+ })
+ window.addEventListener('mousemove', function (e) {
+ move(e)
+ })
+
+ //榧犳爣鏉惧紑
+ window.addEventListener('touchend', function () {
+ end()
+ })
+ window.addEventListener('mouseup', function () {
+ end()
+ })
+}
+watch(type, () => {
+ init()
+})
+onMounted(() => {
+ // 绂佹鎷栨嫿
+ init()
+ proxy.$el.onselectstart = function () {
+ return false
+ }
+})
+//榧犳爣鎸変笅
+const start = (e) => {
+ e = e || window.event
+ if (!e.touches) {
+ //鍏煎PC绔�
+ var x = e.clientX
+ } else {
+ //鍏煎绉诲姩绔�
+ var x = e.touches[0].pageX
+ }
+ startLeft.value = Math.floor(x - barArea.value.getBoundingClientRect().left)
+ startMoveTime.value = +new Date() //寮�濮嬫粦鍔ㄧ殑鏃堕棿
+ if (isEnd.value == false) {
+ text.value = ''
+ moveBlockBackgroundColor.value = '#337ab7'
+ leftBarBorderColor.value = '#337AB7'
+ iconColor.value = '#fff'
+ e.stopPropagation()
+ status.value = true
+ }
+}
+//榧犳爣绉诲姩
+const move = (e) => {
+ e = e || window.event
+ if (status.value && isEnd.value == false) {
+ if (!e.touches) {
+ //鍏煎PC绔�
+ var x = e.clientX
+ } else {
+ //鍏煎绉诲姩绔�
+ var x = e.touches[0].pageX
+ }
+ var bar_area_left = barArea.value.getBoundingClientRect().left
+ var move_block_left = x - bar_area_left //灏忔柟鍧楃浉瀵逛簬鐖跺厓绱犵殑left鍊�
+ if (
+ move_block_left >=
+ barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
+ ) {
+ move_block_left =
+ barArea.value.offsetWidth - parseInt(parseInt(blockSize.value.width) / 2) - 2
+ }
+ if (move_block_left <= 0) {
+ move_block_left = parseInt(parseInt(blockSize.value.width) / 2)
+ }
+ //鎷栧姩鍚庡皬鏂瑰潡鐨刲eft鍊�
+ moveBlockLeft.value = move_block_left - startLeft.value + 'px'
+ leftBarWidth.value = move_block_left - startLeft.value + 'px'
+ }
+}
+
+//榧犳爣鏉惧紑
+const end = () => {
+ endMovetime.value = +new Date()
+ //鍒ゆ柇鏄惁閲嶅悎
+ if (status.value && isEnd.value == false) {
+ var moveLeftDistance = parseInt((moveBlockLeft.value || '0').replace('px', ''))
+ moveLeftDistance = (moveLeftDistance * 310) / parseInt(setSize.imgWidth)
+ let data = {
+ captchaType: captchaType.value,
+ pointJson: secretKey.value
+ ? aesEncrypt(JSON.stringify({ x: moveLeftDistance, y: 5.0 }), secretKey.value)
+ : JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+ token: backToken.value
+ }
+ reqCheck(data).then((res) => {
+ if (res.repCode == '0000') {
+ moveBlockBackgroundColor.value = '#5cb85c'
+ leftBarBorderColor.value = '#5cb85c'
+ iconColor.value = '#fff'
+ iconClass.value = 'icon-check'
+ showRefresh.value = false
+ isEnd.value = true
+ if (mode.value == 'pop') {
+ setTimeout(() => {
+ proxy.$parent.clickShow = false
+ refresh()
+ }, 1500)
+ }
+ passFlag.value = true
+ tipWords.value = `${((endMovetime.value - startMoveTime.value) / 1000).toFixed(2)}s
+ ${t('captcha.success')}`
+ var captchaVerification = secretKey.value
+ ? aesEncrypt(
+ backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 }),
+ secretKey.value
+ )
+ : backToken.value + '---' + JSON.stringify({ x: moveLeftDistance, y: 5.0 })
+ setTimeout(() => {
+ tipWords.value = ''
+ proxy.$parent.closeBox()
+ proxy.$parent.$emit('success', { captchaVerification })
+ }, 1000)
+ } else {
+ moveBlockBackgroundColor.value = '#d9534f'
+ leftBarBorderColor.value = '#d9534f'
+ iconColor.value = '#fff'
+ iconClass.value = 'icon-close'
+ passFlag.value = false
+ setTimeout(function () {
+ refresh()
+ }, 1000)
+ proxy.$parent.$emit('error', proxy)
+ tipWords.value = t('captcha.fail')
+ setTimeout(() => {
+ tipWords.value = ''
+ }, 1000)
+ }
+ })
+ status.value = false
+ }
+}
+
+const refresh = async () => {
+ showRefresh.value = true
+ finishText.value = ''
+
+ transitionLeft.value = 'left .3s'
+ moveBlockLeft.value = 0
+
+ leftBarWidth.value = undefined
+ transitionWidth.value = 'width .3s'
+
+ leftBarBorderColor.value = '#ddd'
+ moveBlockBackgroundColor.value = '#fff'
+ iconColor.value = '#000'
+ iconClass.value = 'icon-right'
+ isEnd.value = false
+
+ await getPictrue()
+ setTimeout(() => {
+ transitionWidth.value = ''
+ transitionLeft.value = ''
+ text.value = explain.value
+ }, 300)
+}
+
+// 璇锋眰鑳屾櫙鍥剧墖鍜岄獙璇佸浘鐗�
+const getPictrue = async () => {
+ let data = {
+ captchaType: captchaType.value
+ }
+ const res = await getCode(data)
+ if (res.repCode == '0000') {
+ backImgBase.value = res.repData.originalImageBase64
+ blockBackImgBase.value = res.repData.jigsawImageBase64
+ backToken.value = res.repData.token
+ secretKey.value = res.repData.secretKey
+ } else {
+ tipWords.value = res.repMsg
+ }
+}
+</script>
diff --git a/src/components/Verifition/src/Verify/index.ts b/src/components/Verifition/src/Verify/index.ts
new file mode 100644
index 0000000..e027ab3
--- /dev/null
+++ b/src/components/Verifition/src/Verify/index.ts
@@ -0,0 +1,5 @@
+import VerifySlide from './VerifySlide.vue'
+import VerifyPoints from './VerifyPoints.vue'
+import VerifyPictureWord from './VerifyPictureWord.vue'
+
+export { VerifySlide, VerifyPoints, VerifyPictureWord }
\ No newline at end of file
diff --git a/src/components/Verifition/src/utils/ase.ts b/src/components/Verifition/src/utils/ase.ts
new file mode 100644
index 0000000..d2e6b98
--- /dev/null
+++ b/src/components/Verifition/src/utils/ase.ts
@@ -0,0 +1,14 @@
+import CryptoJS from 'crypto-js'
+/**
+ * @word 瑕佸姞瀵嗙殑鍐呭
+ * @keyWord String 鏈嶅姟鍣ㄩ殢鏈鸿繑鍥炵殑鍏抽敭瀛�
+ * */
+export function aesEncrypt(word, keyWord = 'XwKsGlMcdPMEhR1B') {
+ const key = CryptoJS.enc.Utf8.parse(keyWord)
+ const srcs = CryptoJS.enc.Utf8.parse(word)
+ const encrypted = CryptoJS.AES.encrypt(srcs, key, {
+ mode: CryptoJS.mode.ECB,
+ padding: CryptoJS.pad.Pkcs7
+ })
+ return encrypted.toString()
+}
diff --git a/src/components/Verifition/src/utils/util.ts b/src/components/Verifition/src/utils/util.ts
new file mode 100644
index 0000000..15c1627
--- /dev/null
+++ b/src/components/Verifition/src/utils/util.ts
@@ -0,0 +1,97 @@
+export function resetSize(vm) {
+ let img_width, img_height, bar_width, bar_height //鍥剧墖鐨勫搴︺�侀珮搴︼紝绉诲姩鏉$殑瀹藉害銆侀珮搴�
+ const EmployeeWindow = window as any
+ const parentWidth = vm.$el.parentNode.offsetWidth || EmployeeWindow.offsetWidth
+ const parentHeight = vm.$el.parentNode.offsetHeight || EmployeeWindow.offsetHeight
+ if (vm.imgSize.width.indexOf('%') != -1) {
+ img_width = (parseInt(vm.imgSize.width) / 100) * parentWidth + 'px'
+ } else {
+ img_width = vm.imgSize.width
+ }
+
+ if (vm.imgSize.height.indexOf('%') != -1) {
+ img_height = (parseInt(vm.imgSize.height) / 100) * parentHeight + 'px'
+ } else {
+ img_height = vm.imgSize.height
+ }
+
+ if (vm.barSize.width.indexOf('%') != -1) {
+ bar_width = (parseInt(vm.barSize.width) / 100) * parentWidth + 'px'
+ } else {
+ bar_width = vm.barSize.width
+ }
+
+ if (vm.barSize.height.indexOf('%') != -1) {
+ bar_height = (parseInt(vm.barSize.height) / 100) * parentHeight + 'px'
+ } else {
+ bar_height = vm.barSize.height
+ }
+
+ return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
+}
+
+export const _code_chars = [
+ 1,
+ 2,
+ 3,
+ 4,
+ 5,
+ 6,
+ 7,
+ 8,
+ 9,
+ 'a',
+ 'b',
+ 'c',
+ 'd',
+ 'e',
+ 'f',
+ 'g',
+ 'h',
+ 'i',
+ 'j',
+ 'k',
+ 'l',
+ 'm',
+ 'n',
+ 'o',
+ 'p',
+ 'q',
+ 'r',
+ 's',
+ 't',
+ 'u',
+ 'v',
+ 'w',
+ 'x',
+ 'y',
+ 'z',
+ 'A',
+ 'B',
+ 'C',
+ 'D',
+ 'E',
+ 'F',
+ 'G',
+ 'H',
+ 'I',
+ 'J',
+ 'K',
+ 'L',
+ 'M',
+ 'N',
+ 'O',
+ 'P',
+ 'Q',
+ 'R',
+ 'S',
+ 'T',
+ 'U',
+ 'V',
+ 'W',
+ 'X',
+ 'Y',
+ 'Z'
+]
+export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
+export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']
diff --git a/src/components/VerticalButtonGroup/index.vue b/src/components/VerticalButtonGroup/index.vue
new file mode 100644
index 0000000..9c78ea2
--- /dev/null
+++ b/src/components/VerticalButtonGroup/index.vue
@@ -0,0 +1,44 @@
+<template>
+ <el-button-group v-bind="$attrs">
+ <slot></slot>
+ </el-button-group>
+</template>
+
+<script setup lang="ts">
+/**
+ * 鍨傜洿鎸夐挳缁�
+ * Element瀹樻柟鐨勬寜閽粍鍙敮鎸佹按骞虫樉绀猴紝閫氳繃閲嶅啓鏍峰紡瀹炵幇鍨傜洿甯冨眬
+ */
+defineOptions({ name: 'VerticalButtonGroup' })
+</script>
+
+<style scoped lang="scss">
+.el-button-group {
+ display: inline-flex;
+ flex-direction: column;
+}
+
+.el-button-group > :deep(.el-button:first-child) {
+ border-bottom-color: var(--el-button-divide-border-color);
+ border-top-right-radius: var(--el-border-radius-base);
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.el-button-group > :deep(.el-button:last-child) {
+ border-top-color: var(--el-button-divide-border-color);
+ border-top-right-radius: 0;
+ border-bottom-left-radius: var(--el-border-radius-base);
+ border-top-left-radius: 0;
+}
+
+.el-button-group :deep(.el-button--primary:not(:first-child, :last-child)) {
+ border-top-color: var(--el-button-divide-border-color);
+ border-bottom-color: var(--el-button-divide-border-color);
+}
+
+.el-button-group > :deep(.el-button:not(:last-child)) {
+ margin-right: 0;
+ margin-bottom: -1px;
+}
+</style>
diff --git a/src/components/XButton/index.ts b/src/components/XButton/index.ts
new file mode 100644
index 0000000..be0f0d4
--- /dev/null
+++ b/src/components/XButton/index.ts
@@ -0,0 +1,4 @@
+import XButton from './src/XButton.vue'
+import XTextButton from './src/XTextButton.vue'
+
+export { XButton, XTextButton }
diff --git a/src/components/XButton/src/XButton.vue b/src/components/XButton/src/XButton.vue
new file mode 100644
index 0000000..40cba1a
--- /dev/null
+++ b/src/components/XButton/src/XButton.vue
@@ -0,0 +1,50 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'XButton' })
+
+const props = defineProps({
+ modelValue: propTypes.bool.def(false),
+ loading: propTypes.bool.def(false),
+ preIcon: propTypes.string.def(''),
+ postIcon: propTypes.string.def(''),
+ title: propTypes.string.def(''),
+ type: propTypes.oneOf(['', 'primary', 'success', 'warning', 'danger', 'info']).def(''),
+ link: propTypes.bool.def(false),
+ circle: propTypes.bool.def(false),
+ round: propTypes.bool.def(false),
+ plain: propTypes.bool.def(false),
+ onClick: { type: Function as PropType<(...args) => any>, default: null }
+})
+const getBindValue = computed(() => {
+ const delArr: string[] = ['title', 'preIcon', 'postIcon', 'onClick']
+ const attrs = useAttrs()
+ const obj = { ...attrs, ...props }
+ for (const key in obj) {
+ if (delArr.indexOf(key) !== -1) {
+ delete obj[key]
+ }
+ }
+ return obj
+})
+</script>
+
+<template>
+ <el-button v-bind="getBindValue" @click="onClick">
+ <Icon v-if="preIcon" :icon="preIcon" class="mr-1px" />
+ {{ title ? title : '' }}
+ <Icon v-if="postIcon" :icon="postIcon" class="mr-1px" />
+ </el-button>
+</template>
+<style lang="scss" scoped>
+:deep(.el-button.is-text) {
+ padding: 8px 4px;
+ margin-left: 0;
+}
+
+:deep(.el-button.is-link) {
+ padding: 8px 4px;
+ margin-left: 0;
+}
+</style>
diff --git a/src/components/XButton/src/XTextButton.vue b/src/components/XButton/src/XTextButton.vue
new file mode 100644
index 0000000..b1a922b
--- /dev/null
+++ b/src/components/XButton/src/XTextButton.vue
@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { PropType } from 'vue'
+
+defineOptions({ name: 'XTextButton' })
+
+const props = defineProps({
+ modelValue: propTypes.bool.def(false),
+ loading: propTypes.bool.def(false),
+ preIcon: propTypes.string.def(''),
+ postIcon: propTypes.string.def(''),
+ title: propTypes.string.def(''),
+ type: propTypes.oneOf(['', 'primary', 'success', 'warning', 'danger', 'info']).def('primary'),
+ circle: propTypes.bool.def(false),
+ round: propTypes.bool.def(false),
+ plain: propTypes.bool.def(false),
+ onClick: { type: Function as PropType<(...args) => any>, default: null }
+})
+const getBindValue = computed(() => {
+ const delArr: string[] = ['title', 'preIcon', 'postIcon', 'onClick']
+ const attrs = useAttrs()
+ const obj = { ...attrs, ...props }
+ for (const key in obj) {
+ if (delArr.indexOf(key) !== -1) {
+ delete obj[key]
+ }
+ }
+ return obj
+})
+</script>
+
+<template>
+ <el-button link v-bind="getBindValue" @click="onClick">
+ <Icon v-if="preIcon" :icon="preIcon" class="mr-1px" />
+ {{ title ? title : '' }}
+ <Icon v-if="postIcon" :icon="postIcon" class="mr-1px" />
+ </el-button>
+</template>
+<style lang="scss" scoped>
+:deep(.el-button.is-text) {
+ padding: 8px 4px;
+ margin-left: 0;
+}
+
+:deep(.el-button.is-link) {
+ padding: 8px 4px;
+ margin-left: 0;
+}
+</style>
diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
new file mode 100644
index 0000000..00e887c
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue
@@ -0,0 +1,655 @@
+<template>
+ <div class="my-process-designer">
+ <div class="my-process-designer__header" style="z-index: 999; display: table-row-group">
+ <slot name="control-header"></slot>
+ <template v-if="!$slots['control-header']">
+ <ElButtonGroup key="file-control">
+ <XButton preIcon="ep:folder-opened" title="鎵撳紑鏂囦欢" @click="refFile.click()" />
+ <el-tooltip effect="light" placement="bottom">
+ <template #content>
+ <div style="color: #409eff">
+ <!-- <el-button link @click="downloadProcessAsXml()">涓嬭浇涓篨ML鏂囦欢</el-button> -->
+ <XTextButton title="涓嬭浇涓篨ML鏂囦欢" @click="downloadProcessAsXml()" />
+ <br />
+
+ <!-- <el-button link @click="downloadProcessAsSvg()">涓嬭浇涓篠VG鏂囦欢</el-button> -->
+ <XTextButton title="涓嬭浇涓篠VG鏂囦欢" @click="downloadProcessAsSvg()" />
+ <br />
+
+ <!-- <el-button link @click="downloadProcessAsBpmn()">涓嬭浇涓築PMN鏂囦欢</el-button> -->
+ <XTextButton title="涓嬭浇涓築PMN鏂囦欢" @click="downloadProcessAsBpmn()" />
+ </div>
+ </template>
+ <XButton title="涓嬭浇鏂囦欢" preIcon="ep:download" />
+ </el-tooltip>
+ <el-tooltip effect="light">
+ <XButton preIcon="ep:view" title="娴忚" />
+ <template #content>
+ <!-- <el-button link @click="previewProcessXML">棰勮XML</el-button> -->
+ <XTextButton title="棰勮XML" @click="previewProcessXML" />
+ <br />
+ <!-- <el-button link @click="previewProcessJson">棰勮JSON</el-button> -->
+ <XTextButton title="棰勮JSON" @click="previewProcessJson" />
+ </template>
+ </el-tooltip>
+ <el-tooltip
+ v-if="props.simulation"
+ effect="light"
+ :content="simulationStatus ? '閫�鍑烘ā鎷�' : '寮�鍚ā鎷�'"
+ >
+ <XButton preIcon="ep:cpu" title="妯℃嫙" @click="processSimulation" />
+ </el-tooltip>
+ </ElButtonGroup>
+ <ElButtonGroup key="align-control">
+ <el-tooltip effect="light" content="鍚戝乏瀵归綈">
+ <!-- <el-button
+ class="align align-left"
+ icon="el-icon-s-data"
+ @click="elementsAlign('left')"
+ /> -->
+ <XButton
+ preIcon="fa:align-left"
+ class="align align-bottom"
+ @click="elementsAlign('left')"
+ />
+ </el-tooltip>
+ <el-tooltip effect="light" content="鍚戝彸瀵归綈">
+ <!-- <el-button
+ class="align align-right"
+ icon="el-icon-s-data"
+ @click="elementsAlign('right')"
+ /> -->
+ <XButton
+ preIcon="fa:align-left"
+ class="align align-top"
+ @click="elementsAlign('right')"
+ />
+ </el-tooltip>
+ <el-tooltip effect="light" content="鍚戜笂瀵归綈">
+ <!-- <el-button
+ class="align align-top"
+ icon="el-icon-s-data"
+ @click="elementsAlign('top')"
+ /> -->
+ <XButton
+ preIcon="fa:align-left"
+ class="align align-left"
+ @click="elementsAlign('top')"
+ />
+ </el-tooltip>
+ <el-tooltip effect="light" content="鍚戜笅瀵归綈">
+ <!-- <el-button
+ class="align align-bottom"
+ icon="el-icon-s-data"
+ @click="elementsAlign('bottom')"
+ /> -->
+ <XButton
+ preIcon="fa:align-left"
+ class="align align-right"
+ @click="elementsAlign('bottom')"
+ />
+ </el-tooltip>
+ <el-tooltip effect="light" content="姘村钩灞呬腑">
+ <!-- <el-button
+ class="align align-center"
+ icon="el-icon-s-data"
+ @click="elementsAlign('center')"
+ /> -->
+ <!-- class="align align-center" -->
+ <XButton
+ preIcon="fa:align-left"
+ class="align align-center"
+ @click="elementsAlign('center')"
+ />
+ </el-tooltip>
+ <el-tooltip effect="light" content="鍨傜洿灞呬腑">
+ <!-- <el-button
+ class="align align-middle"
+ icon="el-icon-s-data"
+ @click="elementsAlign('middle')"
+ /> -->
+ <XButton
+ preIcon="fa:align-left"
+ class="align align-middle"
+ @click="elementsAlign('middle')"
+ />
+ </el-tooltip>
+ </ElButtonGroup>
+ <ElButtonGroup key="scale-control">
+ <el-tooltip effect="light" content="缂╁皬瑙嗗浘">
+ <!-- <el-button
+ :disabled="defaultZoom < 0.2"
+ icon="el-icon-zoom-out"
+ @click="processZoomOut()"
+ /> -->
+ <XButton
+ preIcon="ep:zoom-out"
+ @click="processZoomOut()"
+ :disabled="defaultZoom < 0.2"
+ />
+ </el-tooltip>
+ <el-button>{{ Math.floor(defaultZoom * 10 * 10) + '%' }}</el-button>
+ <el-tooltip effect="light" content="鏀惧ぇ瑙嗗浘">
+ <!-- <el-button
+ :disabled="defaultZoom > 4"
+ icon="el-icon-zoom-in"
+ @click="processZoomIn()"
+ /> -->
+ <XButton preIcon="ep:zoom-in" @click="processZoomIn()" :disabled="defaultZoom > 4" />
+ </el-tooltip>
+ <el-tooltip effect="light" content="閲嶇疆瑙嗗浘骞跺眳涓�">
+ <!-- <el-button icon="el-icon-c-scale-to-original" @click="processReZoom()" /> -->
+ <XButton preIcon="ep:scale-to-original" @click="processReZoom()" />
+ </el-tooltip>
+ </ElButtonGroup>
+ <ElButtonGroup key="stack-control">
+ <el-tooltip effect="light" content="鎾ら攢">
+ <!-- <el-button :disabled="!revocable" icon="el-icon-refresh-left" @click="processUndo()" /> -->
+ <XButton preIcon="ep:refresh-left" @click="processUndo()" :disabled="!revocable" />
+ </el-tooltip>
+ <el-tooltip effect="light" content="鎭㈠">
+ <!-- <el-button
+ :disabled="!recoverable"
+ icon="el-icon-refresh-right"
+ @click="processRedo()"
+ /> -->
+ <XButton preIcon="ep:refresh-right" @click="processRedo()" :disabled="!recoverable" />
+ </el-tooltip>
+ <el-tooltip effect="light" content="閲嶆柊缁樺埗">
+ <!-- <el-button icon="el-icon-refresh" @click="processRestart" /> -->
+ <XButton preIcon="ep:refresh" @click="processRestart()" />
+ </el-tooltip>
+ </ElButtonGroup>
+ </template>
+ <!-- 鐢ㄤ簬鎵撳紑鏈湴鏂囦欢-->
+ <input
+ type="file"
+ id="files"
+ ref="refFile"
+ style="display: none"
+ accept=".xml, .bpmn"
+ @change="importLocalFile"
+ />
+ </div>
+ <div class="my-process-designer__container">
+ <div
+ class="my-process-designer__canvas"
+ ref="bpmnCanvas"
+ id="bpmnCanvas"
+ style="width: 1680px; height: 800px"
+ ></div>
+ <!-- <div id="js-properties-panel" class="panel"></div> -->
+ <!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
+ </div>
+ <Dialog
+ title="棰勮"
+ v-model="previewModelVisible"
+ width="80%"
+ :scroll="true"
+ max-height="600px"
+ >
+ <div>
+ <pre><code v-dompurify-html="highlightedCode(previewResult)" class="hljs"></code></pre>
+ </div>
+ </Dialog>
+ </div>
+</template>
+
+<script lang="ts" setup>
+// import 'bpmn-js/dist/assets/diagram-js.css' // 宸﹁竟宸ュ叿鏍忎互鍙婄紪杈戣妭鐐圭殑鏍峰紡
+// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
+// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
+// import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
+// import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 鍙充晶妗嗘牱寮�
+import { ElMessage, ElMessageBox } from 'element-plus'
+import BpmnModeler from 'bpmn-js/lib/Modeler'
+import DefaultEmptyXML from './plugins/defaultEmpty'
+// 缈昏瘧鏂规硶
+import customTranslate from './plugins/translate/customTranslate'
+import translationsCN from './plugins/translate/zh'
+// 妯℃嫙娴佽浆娴佺▼
+import tokenSimulation from 'bpmn-js-token-simulation'
+// 鏍囩瑙f瀽鏋勫缓鍣�
+// import bpmnPropertiesProvider from "bpmn-js-properties-panel/lib/provider/bpmn";
+// import propertiesPanelModule from 'bpmn-js-properties-panel'
+// import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
+// 鏍囩瑙f瀽 Moddle
+import camundaModdleDescriptor from './plugins/descriptor/camundaDescriptor.json'
+import activitiModdleDescriptor from './plugins/descriptor/activitiDescriptor.json'
+import flowableModdleDescriptor from './plugins/descriptor/flowableDescriptor.json'
+// 鏍囩瑙f瀽 Extension
+import camundaModdleExtension from './plugins/extension-moddle/camunda'
+import activitiModdleExtension from './plugins/extension-moddle/activiti'
+import flowableModdleExtension from './plugins/extension-moddle/flowable'
+// 寮曞叆json杞崲涓庨珮浜�
+// import xml2js from 'xml-js'
+// import xml2js from 'fast-xml-parser'
+import { XmlNode, XmlNodeType, parseXmlString } from 'steady-xml'
+// 浠g爜楂樹寒鎻掍欢
+// import hljs from 'highlight.js/lib/highlight'
+// import 'highlight.js/styles/github-gist.css'
+// hljs.registerLanguage('xml', 'highlight.js/lib/languages/xml')
+// hljs.registerLanguage('json', 'highlight.js/lib/languages/json')
+// const eventName = reactive({
+// name: ''
+// })
+import hljs from 'highlight.js' // 瀵煎叆浠g爜楂樹寒鏂囦欢
+import 'highlight.js/styles/github.css' // 瀵煎叆浠g爜楂樹寒鏍峰紡
+
+defineOptions({ name: 'MyProcessDesigner' })
+
+const bpmnCanvas = ref()
+const refFile = ref()
+const emit = defineEmits([
+ 'destroy',
+ 'init-finished',
+ 'save',
+ 'commandStack-changed',
+ 'input',
+ 'change',
+ 'canvas-viewbox-changed',
+ // eventName.name
+ 'element-click'
+])
+
+const props = defineProps({
+ value: String, // xml 瀛楃涓�
+ // valueWatch: true, // xml 瀛楃涓茬殑 watch 鐘舵��
+ processId: String, // 娴佺▼ key 鏍囪瘑
+ processName: String, // 娴佺▼ name 鍚嶅瓧
+ formId: Number, // 娴佺▼ form 琛ㄥ崟缂栧彿
+ translations: {
+ // 鑷畾涔夌殑缈昏瘧鏂囦欢
+ type: Object,
+ default: () => {}
+ },
+ additionalModel: [Object, Array], // 鑷畾涔塵odel
+ moddleExtension: {
+ // 鑷畾涔塵oddle
+ type: Object,
+ default: () => {}
+ },
+ onlyCustomizeAddi: {
+ type: Boolean,
+ default: false
+ },
+ onlyCustomizeModdle: {
+ type: Boolean,
+ default: false
+ },
+ simulation: {
+ type: Boolean,
+ default: true
+ },
+ keyboard: {
+ type: Boolean,
+ default: true
+ },
+ prefix: {
+ type: String,
+ default: 'camunda'
+ },
+ events: {
+ type: Array,
+ default: () => ['element.click']
+ },
+ headerButtonSize: {
+ type: String,
+ default: 'small',
+ validator: (value: string) => ['default', 'medium', 'small', 'mini'].indexOf(value) !== -1
+ },
+ headerButtonType: {
+ type: String,
+ default: 'primary',
+ validator: (value: string) =>
+ ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1
+ }
+})
+
+/**
+ * 浠g爜楂樹寒
+ */
+const highlightedCode = (code: string) => {
+ // 楂樹寒
+ if (previewType.value === 'json') {
+ code = JSON.stringify(code, null, 2)
+ }
+ const result = hljs.highlight(code, { language: previewType.value, ignoreIllegals: true })
+ return result.value || ' '
+}
+
+provide('configGlobal', props)
+let bpmnModeler: any = null
+const defaultZoom = ref(1)
+const previewModelVisible = ref(false)
+const simulationStatus = ref(false)
+const previewResult = ref('')
+const previewType = ref('xml')
+const recoverable = ref(false)
+const revocable = ref(false)
+const additionalModules = computed(() => {
+ console.log(props.additionalModel, 'additionalModel')
+ const Modules: any[] = []
+ // 浠呬繚鐣欑敤鎴疯嚜瀹氫箟鎵╁睍妯″潡
+ if (props.onlyCustomizeAddi) {
+ if (Object.prototype.toString.call(props.additionalModel) == '[object Array]') {
+ return props.additionalModel || []
+ }
+ return [props.additionalModel]
+ }
+
+ // 鎻掑叆鐢ㄦ埛鑷畾涔夋墿灞曟ā鍧�
+ if (Object.prototype.toString.call(props.additionalModel) == '[object Array]') {
+ Modules.push(...(props.additionalModel as any[]))
+ } else {
+ props.additionalModel && Modules.push(props.additionalModel)
+ }
+
+ // 缈昏瘧妯″潡
+ const TranslateModule = {
+ translate: ['value', customTranslate(props.translations || translationsCN)]
+ }
+ Modules.push(TranslateModule)
+
+ // 妯℃嫙娴佽浆妯″潡
+ if (props.simulation) {
+ Modules.push(tokenSimulation)
+ }
+
+ // 鏍规嵁闇�瑕佺殑娴佺▼绫诲瀷璁剧疆鎵╁睍鍏冪礌鏋勫缓妯″潡
+ // if (this.prefix === "bpmn") {
+ // Modules.push(bpmnModdleExtension);
+ // }
+ console.log(props.prefix, 'props.prefix ')
+ if (props.prefix === 'camunda') {
+ Modules.push(camundaModdleExtension)
+ }
+ if (props.prefix === 'flowable') {
+ Modules.push(flowableModdleExtension)
+ }
+ if (props.prefix === 'activiti') {
+ Modules.push(activitiModdleExtension)
+ }
+
+ return Modules
+})
+const moddleExtensions = computed(() => {
+ console.log(props.onlyCustomizeModdle, 'props.onlyCustomizeModdle')
+ console.log(props.moddleExtension, 'props.moddleExtension')
+ console.log(props.prefix, 'props.prefix')
+ const Extensions: any = {}
+ // 浠呬娇鐢ㄧ敤鎴疯嚜瀹氫箟妯″潡
+ if (props.onlyCustomizeModdle) {
+ return props.moddleExtension || null
+ }
+
+ // 鎻掑叆鐢ㄦ埛鑷畾涔夋ā鍧�
+ if (props.moddleExtension) {
+ for (let key in props.moddleExtension) {
+ Extensions[key] = props.moddleExtension[key]
+ }
+ }
+
+ // 鏍规嵁闇�瑕佺殑 "娴佺▼绫诲瀷" 璁剧疆 瀵瑰簲鐨勮В鏋愭枃浠�
+ if (props.prefix === 'activiti') {
+ Extensions.activiti = activitiModdleDescriptor
+ }
+ if (props.prefix === 'flowable') {
+ Extensions.flowable = flowableModdleDescriptor
+ }
+ if (props.prefix === 'camunda') {
+ Extensions.camunda = camundaModdleDescriptor
+ }
+ return Extensions
+})
+console.log(additionalModules, 'additionalModules()')
+console.log(moddleExtensions, 'moddleExtensions()')
+const initBpmnModeler = () => {
+ if (bpmnModeler) return
+ let data = document.getElementById('bpmnCanvas')
+ console.log(data, 'data')
+ console.log(props.keyboard, 'props.keyboard')
+ console.log(additionalModules, 'additionalModules()')
+ console.log(moddleExtensions, 'moddleExtensions()')
+
+ bpmnModeler = new BpmnModeler({
+ // container: this.$refs['bpmn-canvas'],
+ // container: getCurrentInstance(),
+ // container: needClass,
+ // container: bpmnCanvas.value,
+ container: data,
+ // width: '100%',
+ // 娣诲姞鎺у埗鏉�
+ // propertiesPanel: {
+ // parent: '#js-properties-panel'
+ // },
+ keyboard: props.keyboard ? { bindTo: document } : null,
+ // additionalModules: additionalModules.value,
+ additionalModules: additionalModules.value,
+ moddleExtensions: moddleExtensions.value
+
+ // additionalModules: [
+ // additionalModules.value
+ // propertiesPanelModule,
+ // propertiesProviderModule
+ // propertiesProviderModule
+ // ],
+ // moddleExtensions: { camunda: moddleExtensions.value }
+ })
+
+ // bpmnModeler.createDiagram()
+
+ // console.log(bpmnModeler, 'bpmnModeler111111')
+ emit('init-finished', bpmnModeler)
+ initModelListeners()
+}
+
+const initModelListeners = () => {
+ const EventBus = bpmnModeler.get('eventBus')
+ console.log(EventBus, 'EventBus')
+ // 娉ㄥ唽闇�瑕佺殑鐩戝惉浜嬩欢, 灏�. 鏇挎崲涓� - , 閬垮厤瑙f瀽寮傚父
+ props.events.forEach((event: any) => {
+ EventBus.on(event, function (eventObj) {
+ let eventName = event.replace(/\./g, '-')
+ // eventName.name = eventName
+ let element = eventObj ? eventObj.element : null
+ console.log(eventName, 'eventName')
+ console.log(element, 'element')
+ emit('element-click', element, eventObj)
+ // emit(eventName, element, eventObj)
+ })
+ })
+ // 鐩戝惉鍥惧舰鏀瑰彉杩斿洖xml
+ EventBus.on('commandStack.changed', async (event) => {
+ try {
+ recoverable.value = bpmnModeler.get('commandStack').canRedo()
+ revocable.value = bpmnModeler.get('commandStack').canUndo()
+ let { xml } = await bpmnModeler.saveXML({ format: true })
+ emit('commandStack-changed', event)
+ emit('input', xml)
+ emit('change', xml)
+ emit('save', xml)
+ } catch (e: any) {
+ console.error(`[Process Designer Warn]: ${e.message || e}`)
+ }
+ })
+ // 鐩戝惉瑙嗗浘缂╂斁鍙樺寲
+ bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }) => {
+ emit('canvas-viewbox-changed', { viewbox })
+ const { scale } = viewbox
+ defaultZoom.value = Math.floor(scale * 100) / 100
+ })
+}
+/* 鍒涘缓鏂扮殑娴佺▼鍥� */
+const createNewDiagram = async (xml) => {
+ console.log(xml, 'xml')
+ // 灏嗗瓧绗︿覆杞崲鎴愬浘鏄剧ず鍑烘潵
+ let newId = props.processId || `Process_${new Date().getTime()}`
+ let newName = props.processName || `涓氬姟娴佺▼_${new Date().getTime()}`
+ let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
+ try {
+ // console.log(xmlString, 'xmlString')
+ // console.log(this.bpmnModeler.importXML);
+ let { warnings } = await bpmnModeler.importXML(xmlString)
+ console.log(warnings, 'warnings')
+ if (warnings && warnings.length) {
+ warnings.forEach((warn) => console.warn(warn))
+ }
+ } catch (e: any) {
+ console.error(`[Process Designer Warn]: ${e.message || e}`)
+ }
+}
+
+// 涓嬭浇娴佺▼鍥惧埌鏈湴
+const downloadProcess = async (type) => {
+ try {
+ // 鎸夐渶瑕佺被鍨嬪垱寤烘枃浠跺苟涓嬭浇
+ if (type === 'xml' || type === 'bpmn') {
+ const { err, xml } = await bpmnModeler.saveXML()
+ // 璇诲彇寮傚父鏃舵姏鍑哄紓甯�
+ if (err) {
+ console.error(`[Process Designer Warn ]: ${err.message || err}`)
+ }
+ let { href, filename } = setEncoded(type.toUpperCase(), xml)
+ downloadFunc(href, filename)
+ } else {
+ const { err, svg } = await bpmnModeler.saveSVG()
+ // 璇诲彇寮傚父鏃舵姏鍑哄紓甯�
+ if (err) {
+ return console.error(err)
+ }
+ let { href, filename } = setEncoded('SVG', svg)
+ downloadFunc(href, filename)
+ }
+ } catch (e: any) {
+ console.error(`[Process Designer Warn ]: ${e.message || e}`)
+ }
+ // 鏂囦欢涓嬭浇鏂规硶
+ function downloadFunc(href, filename) {
+ if (href && filename) {
+ let a = document.createElement('a')
+ a.download = filename //鎸囧畾涓嬭浇鐨勬枃浠跺悕
+ a.href = href // URL瀵硅薄
+ a.click() // 妯℃嫙鐐瑰嚮
+ URL.revokeObjectURL(a.href) // 閲婃斁URL 瀵硅薄
+ }
+ }
+}
+
+// 鏍规嵁鎵�闇�绫诲瀷杩涜杞爜骞惰繑鍥炰笅杞藉湴鍧�
+const setEncoded = (type, data) => {
+ const filename = 'diagram'
+ const encodedData = encodeURIComponent(data)
+ return {
+ filename: `${filename}.${type}`,
+ href: `data:application/${
+ type === 'svg' ? 'text/xml' : 'bpmn20-xml'
+ };charset=UTF-8,${encodedData}`,
+ data: data
+ }
+}
+
+// 鍔犺浇鏈湴鏂囦欢
+const importLocalFile = () => {
+ const file = refFile.value.files[0]
+ const reader = new FileReader()
+ reader.readAsText(file)
+ reader.onload = function () {
+ let xmlStr = this.result
+ createNewDiagram(xmlStr)
+ emit('save', xmlStr)
+ }
+}
+/* ------------------------------------------------ refs methods ------------------------------------------------------ */
+const downloadProcessAsXml = () => {
+ downloadProcess('xml')
+}
+const downloadProcessAsBpmn = () => {
+ downloadProcess('bpmn')
+}
+const downloadProcessAsSvg = () => {
+ downloadProcess('svg')
+}
+const processSimulation = () => {
+ simulationStatus.value = !simulationStatus.value
+ console.log(bpmnModeler.get('toggleMode', 'strict'), "bpmnModeler.get('toggleMode')")
+ props.simulation && bpmnModeler.get('toggleMode', 'strict').toggleMode()
+}
+const processRedo = () => {
+ bpmnModeler.get('commandStack').redo()
+}
+const processUndo = () => {
+ bpmnModeler.get('commandStack').undo()
+}
+const processZoomIn = (zoomStep = 0.1) => {
+ let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
+ if (newZoom > 4) {
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
+ }
+ defaultZoom.value = newZoom
+ bpmnModeler.get('canvas').zoom(defaultZoom.value)
+}
+const processZoomOut = (zoomStep = 0.1) => {
+ let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
+ if (newZoom < 0.2) {
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
+ }
+ defaultZoom.value = newZoom
+ bpmnModeler.get('canvas').zoom(defaultZoom.value)
+}
+const processReZoom = () => {
+ defaultZoom.value = 1
+ bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
+}
+const processRestart = () => {
+ recoverable.value = false
+ revocable.value = false
+ createNewDiagram(null)
+}
+const elementsAlign = (align) => {
+ const Align = bpmnModeler.get('alignElements')
+ const Selection = bpmnModeler.get('selection')
+ const SelectedElements = Selection.get()
+ if (!SelectedElements || SelectedElements.length <= 1) {
+ ElMessage.warning('璇锋寜浣� Shift 閿�夋嫨澶氫釜鍏冪礌瀵归綈')
+ // alert('璇锋寜浣� Ctrl 閿�夋嫨澶氫釜鍏冪礌瀵归綈
+ return
+ }
+ ElMessageBox.confirm('鑷姩瀵归綈鍙兘閫犳垚鍥惧舰鍙樺舰锛屾槸鍚︾户缁紵', '璀﹀憡', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ Align.trigger(SelectedElements, align)
+ })
+}
+/*----------------------------- 鏂规硶缁撴潫 ---------------------------------*/
+const previewProcessXML = () => {
+ console.log(bpmnModeler.saveXML, 'bpmnModeler')
+ bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
+ // console.log(xml, 'xml111111')
+ previewResult.value = xml
+ previewType.value = 'xml'
+ previewModelVisible.value = true
+ })
+}
+const previewProcessJson = () => {
+ bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
+ const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml))
+ previewResult.value = rootNodes.parent?.toJSON() as unknown as string
+ previewType.value = 'json'
+ previewModelVisible.value = true
+ })
+}
+
+/* ------------------------------------------------ 鑺嬮亾婧愮爜 methods ------------------------------------------------------ */
+onMounted(() => {
+ initBpmnModeler()
+ createNewDiagram(props.value)
+})
+onBeforeUnmount(() => {
+ if (bpmnModeler) bpmnModeler.destroy()
+ emit('destroy', bpmnModeler)
+ bpmnModeler = null
+})
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
new file mode 100644
index 0000000..34a54c8
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/ProcessViewer.vue
@@ -0,0 +1,379 @@
+<template>
+ <div class="process-viewer">
+ <div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div>
+ <!-- 鑷畾涔夌澶存牱寮忥紝鐢ㄤ簬宸插畬鎴愮姸鎬佷笅娴佺▼杩炵嚎绠ご -->
+ <defs ref="customDefs">
+ <marker
+ id="sequenceflow-end-white-success"
+ viewBox="0 0 20 20"
+ refX="11"
+ refY="10"
+ markerWidth="10"
+ markerHeight="10"
+ orient="auto"
+ >
+ <path
+ class="success-arrow"
+ d="M 1 5 L 11 10 L 1 15 Z"
+ style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
+ />
+ </marker>
+ <marker
+ id="conditional-flow-marker-white-success"
+ viewBox="0 0 20 20"
+ refX="-1"
+ refY="10"
+ markerWidth="10"
+ markerHeight="10"
+ orient="auto"
+ >
+ <path
+ class="success-conditional"
+ d="M 0 10 L 8 6 L 16 10 L 8 14 Z"
+ style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1"
+ />
+ </marker>
+ </defs>
+
+ <!-- 瀹℃壒璁板綍 -->
+ <el-dialog :title="dialogTitle || '瀹℃壒璁板綍'" v-model="dialogVisible" width="1000px">
+ <el-row>
+ <el-table
+ :data="selectTasks"
+ size="small"
+ border
+ header-cell-class-name="table-header-gray"
+ >
+ <el-table-column
+ label="搴忓彿"
+ header-align="center"
+ align="center"
+ type="index"
+ width="50"
+ />
+ <el-table-column
+ label="瀹℃壒浜�"
+ min-width="100"
+ align="center"
+ v-if="selectActivityType === 'bpmn:UserTask'"
+ >
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戣捣浜�"
+ prop="assigneeUser.nickname"
+ min-width="100"
+ align="center"
+ v-else
+ />
+ <el-table-column label="閮ㄩ棬" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�濮嬫椂闂�"
+ prop="createTime"
+ min-width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ min-width="140"
+ />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" min-width="90">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="瀹℃壒寤鸿"
+ prop="reason"
+ min-width="120"
+ v-if="selectActivityType === 'bpmn:UserTask'"
+ />
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="100">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-row>
+ </el-dialog>
+
+ <!-- Zoom锛氭斁澶с�佺缉灏� -->
+ <div style="position: absolute; top: 0; left: 0; width: 100%">
+ <el-row type="flex" justify="end">
+ <el-button-group key="scale-control" size="default">
+ <el-button
+ size="default"
+ :plain="true"
+ :disabled="defaultZoom <= 0.3"
+ :icon="ZoomOut"
+ @click="processZoomOut()"
+ />
+ <el-button size="default" style="width: 90px">
+ {{ Math.floor(defaultZoom * 10 * 10) + '%' }}
+ </el-button>
+ <el-button
+ size="default"
+ :plain="true"
+ :disabled="defaultZoom >= 3.9"
+ :icon="ZoomIn"
+ @click="processZoomIn()"
+ />
+ <el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" />
+ </el-button-group>
+ </el-row>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import '../theme/index.scss'
+import BpmnViewer from 'bpmn-js/lib/Viewer'
+import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { BpmProcessInstanceStatus } from '@/utils/constants'
+
+const props = defineProps({
+ xml: {
+ type: String,
+ required: true
+ },
+ view: {
+ type: Object,
+ require: true
+ }
+})
+
+const processCanvas = ref()
+const bpmnViewer = ref<BpmnViewer | null>(null)
+const customDefs = ref()
+const defaultZoom = ref(1) // 榛樿缂╂斁姣斾緥
+const isLoading = ref(false) // 鏄惁鍔犺浇涓�
+
+const processInstance = ref<any>({}) // 娴佺▼瀹炰緥
+const tasks = ref([]) // 娴佺▼浠诲姟
+
+const dialogVisible = ref(false) // 寮圭獥鍙鎬�
+const dialogTitle = ref<string | undefined>(undefined) // 寮圭獥鏍囬
+const selectActivityType = ref<string | undefined>(undefined) // 閫変腑 Task 鐨勬椿鍔ㄧ紪鍙�
+const selectTasks = ref<any[]>([]) // 閫変腑鐨勪换鍔℃暟缁�
+
+/** Zoom锛氭仮澶� */
+const processReZoom = () => {
+ defaultZoom.value = 1
+ bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto')
+}
+
+/** Zoom锛氭斁澶� */
+const processZoomIn = (zoomStep = 0.1) => {
+ let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
+ if (newZoom > 4) {
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
+ }
+ defaultZoom.value = newZoom
+ bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
+}
+
+/** Zoom锛氱缉灏� */
+const processZoomOut = (zoomStep = 0.1) => {
+ let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
+ if (newZoom < 0.2) {
+ throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
+ }
+ defaultZoom.value = newZoom
+ bpmnViewer.value?.get('canvas').zoom(defaultZoom.value)
+}
+
+/** 娴佺▼鍥鹃瑙堟竻绌� */
+const clearViewer = () => {
+ if (processCanvas.value) {
+ processCanvas.value.innerHTML = ''
+ }
+ if (bpmnViewer.value) {
+ bpmnViewer.value.destroy()
+ }
+ bpmnViewer.value = null
+}
+
+/** 娣诲姞鑷畾涔夌澶� */
+// TODO 鑺嬭壙锛氳嚜瀹氫箟绠ご涓嶇敓鏁堬紝鏈夌偣濂囨�紒锛侊紒锛佺浉鍏崇殑 marker-end銆乵arker-start 鏆傛椂涔熸敞閲婁簡锛侊紒锛�
+const addCustomDefs = () => {
+ if (!bpmnViewer.value) {
+ return
+ }
+ const canvas = bpmnViewer.value?.get('canvas')
+ const svg = canvas?._svg
+ svg.appendChild(customDefs.value)
+}
+
+/** 鑺傜偣閫変腑 */
+const onSelectElement = (element: any) => {
+ // 娓呯┖鍘熼�変腑
+ selectActivityType.value = undefined
+ dialogTitle.value = undefined
+ if (!element || !processInstance.value?.id) {
+ return
+ }
+
+ // UserTask 鐨勬儏鍐�
+ const activityType = element.type
+ selectActivityType.value = activityType
+ if (activityType === 'bpmn:UserTask') {
+ dialogTitle.value = element.businessObject ? element.businessObject.name : undefined
+ selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id)
+ dialogVisible.value = true
+ } else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') {
+ dialogTitle.value = '瀹℃壒淇℃伅'
+ selectTasks.value = [
+ {
+ assigneeUser: processInstance.value.startUser,
+ createTime: processInstance.value.startTime,
+ endTime: processInstance.value.endTime,
+ status: processInstance.value.status,
+ durationInMillis: processInstance.value.durationInMillis
+ }
+ ]
+ dialogVisible.value = true
+ }
+}
+
+/** 鍒濆鍖� BPMN 瑙嗗浘 */
+const importXML = async (xml: string) => {
+ // 娓呯┖娴佺▼鍥�
+ clearViewer()
+
+ // 鍒濆鍖栨祦绋嬪浘
+ if (xml != null && xml !== '') {
+ try {
+ bpmnViewer.value = new BpmnViewer({
+ additionalModules: [MoveCanvasModule],
+ container: processCanvas.value
+ })
+ // 澧炲姞鐐瑰嚮浜嬩欢
+ bpmnViewer.value.on('element.click', ({ element }) => {
+ onSelectElement(element)
+ })
+
+ // 鍒濆鍖� BPMN 瑙嗗浘
+ isLoading.value = true
+ await bpmnViewer.value.importXML(xml)
+ // 鑷畾涔夋垚鍔熺殑绠ご
+ addCustomDefs()
+ } catch (e) {
+ clearViewer()
+ } finally {
+ isLoading.value = false
+ // 楂樹寒娴佺▼
+ setProcessStatus(props.view)
+ }
+ }
+}
+
+/** 楂樹寒娴佺▼ */
+const setProcessStatus = (view: any) => {
+ // 璁剧疆鐩稿叧鍙橀噺
+ if (!view || !view.processInstance) {
+ return
+ }
+ processInstance.value = view.processInstance
+ tasks.value = view.tasks
+ if (isLoading.value || !bpmnViewer.value) {
+ return
+ }
+ const {
+ unfinishedTaskActivityIds,
+ finishedTaskActivityIds,
+ finishedSequenceFlowActivityIds,
+ rejectedTaskActivityIds
+ } = view
+ const canvas = bpmnViewer.value.get('canvas')
+ const elementRegistry = bpmnViewer.value.get('elementRegistry')
+
+ // 宸插畬鎴愯妭鐐�
+ if (Array.isArray(finishedSequenceFlowActivityIds)) {
+ finishedSequenceFlowActivityIds.forEach((item: any) => {
+ if (item != null) {
+ canvas.addMarker(item, 'success')
+ const element = elementRegistry.get(item)
+ const conditionExpression = element.businessObject.conditionExpression
+ if (conditionExpression) {
+ canvas.addMarker(item, 'condition-expression')
+ }
+ }
+ })
+ }
+ if (Array.isArray(finishedTaskActivityIds)) {
+ finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success'))
+ }
+
+ // 鏈畬鎴愯妭鐐�
+ if (Array.isArray(unfinishedTaskActivityIds)) {
+ unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary'))
+ }
+
+ // 琚嫆缁濊妭鐐�
+ if (Array.isArray(rejectedTaskActivityIds)) {
+ rejectedTaskActivityIds.forEach((item: any) => {
+ if (item != null) {
+ canvas.addMarker(item, 'danger')
+ }
+ })
+ }
+
+ // 鐗规畩锛氬鐞� end 鑺傜偣鐨勯珮浜�傚洜涓� end 鍦ㄦ嫆缁濄�佸彇娑堟椂锛岃鍚庣璁$畻鎴愪簡 finishedTaskActivityIds 閲�
+ if (
+ [BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes(
+ processInstance.value.status
+ )
+ ) {
+ const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent')
+ endNodes.forEach((item: any) => {
+ canvas.removeMarker(item.id, 'success')
+ if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) {
+ canvas.addMarker(item.id, 'cancel')
+ } else {
+ canvas.addMarker(item.id, 'danger')
+ }
+ })
+ }
+}
+
+watch(
+ () => props.xml,
+ (newXml) => {
+ importXML(newXml)
+ },
+ { immediate: true }
+)
+
+watch(
+ () => props.view,
+ (newView) => {
+ setProcessStatus(newView)
+ },
+ { immediate: true }
+)
+
+/** mounted锛氬垵濮嬪寲 */
+onMounted(() => {
+ importXML(props.xml)
+ setProcessStatus(props.view)
+})
+
+/** unmount锛氶攢姣� */
+onBeforeUnmount(() => {
+ clearViewer()
+})
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/designer/index.ts b/src/components/bpmnProcessDesigner/package/designer/index.ts
new file mode 100644
index 0000000..8522846
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/index.ts
@@ -0,0 +1,8 @@
+import MyProcessDesigner from './ProcessDesigner.vue'
+
+MyProcessDesigner.install = function (Vue) {
+ Vue.component(MyProcessDesigner.name, MyProcessDesigner)
+}
+
+// 娴佺▼鍥剧殑璁捐鍣紝鍙紪杈�
+export default MyProcessDesigner
diff --git a/src/components/bpmnProcessDesigner/package/designer/index2.ts b/src/components/bpmnProcessDesigner/package/designer/index2.ts
new file mode 100644
index 0000000..ebe8ca7
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/index2.ts
@@ -0,0 +1,8 @@
+import MyProcessViewer from './ProcessViewer.vue'
+
+MyProcessViewer.install = function (Vue) {
+ Vue.component(MyProcessViewer.name, MyProcessViewer)
+}
+
+// 娴佺▼鍥剧殑鏌ョ湅鍣紝涓嶅彲缂栬緫
+export default MyProcessViewer
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js b/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js
new file mode 100644
index 0000000..8783493
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/contentPadProvider.js
@@ -0,0 +1,423 @@
+import { assign, forEach, isArray } from 'min-dash'
+
+import { is } from 'bpmn-js/lib/util/ModelUtil'
+
+import { isExpanded, isEventSubProcess } from 'bpmn-js/lib/util/DiUtil'
+
+import { isAny } from 'bpmn-js/lib/features/modeling/util/ModelingUtil'
+
+import { getChildLanes } from 'bpmn-js/lib/features/modeling/util/LaneUtil'
+
+import { hasPrimaryModifier } from 'diagram-js/lib/util/Mouse'
+
+/**
+ * A provider for BPMN 2.0 elements context pad
+ */
+export default function ContextPadProvider(
+ config,
+ injector,
+ eventBus,
+ contextPad,
+ modeling,
+ elementFactory,
+ connect,
+ create,
+ popupMenu,
+ canvas,
+ rules,
+ translate
+) {
+ config = config || {}
+
+ contextPad.registerProvider(this)
+
+ this._contextPad = contextPad
+
+ this._modeling = modeling
+
+ this._elementFactory = elementFactory
+ this._connect = connect
+ this._create = create
+ this._popupMenu = popupMenu
+ this._canvas = canvas
+ this._rules = rules
+ this._translate = translate
+
+ if (config.autoPlace !== false) {
+ this._autoPlace = injector.get('autoPlace', false)
+ }
+
+ eventBus.on('create.end', 250, function (event) {
+ const context = event.context,
+ shape = context.shape
+
+ if (!hasPrimaryModifier(event) || !contextPad.isOpen(shape)) {
+ return
+ }
+
+ const entries = contextPad.getEntries(shape)
+
+ if (entries.replace) {
+ entries.replace.action.click(event, shape)
+ }
+ })
+}
+
+ContextPadProvider.$inject = [
+ 'config.contextPad',
+ 'injector',
+ 'eventBus',
+ 'contextPad',
+ 'modeling',
+ 'elementFactory',
+ 'connect',
+ 'create',
+ 'popupMenu',
+ 'canvas',
+ 'rules',
+ 'translate',
+ 'elementRegistry'
+]
+
+ContextPadProvider.prototype.getContextPadEntries = function (element) {
+ const contextPad = this._contextPad,
+ modeling = this._modeling,
+ elementFactory = this._elementFactory,
+ connect = this._connect,
+ create = this._create,
+ popupMenu = this._popupMenu,
+ canvas = this._canvas,
+ rules = this._rules,
+ autoPlace = this._autoPlace,
+ translate = this._translate
+
+ const actions = {}
+
+ if (element.type === 'label') {
+ return actions
+ }
+
+ const businessObject = element.businessObject
+
+ function startConnect(event, element) {
+ connect.start(event, element)
+ }
+
+ function removeElement() {
+ modeling.removeElements([element])
+ }
+
+ function getReplaceMenuPosition(element) {
+ const Y_OFFSET = 5
+
+ const diagramContainer = canvas.getContainer(),
+ pad = contextPad.getPad(element).html
+
+ const diagramRect = diagramContainer.getBoundingClientRect(),
+ padRect = pad.getBoundingClientRect()
+
+ const top = padRect.top - diagramRect.top
+ const left = padRect.left - diagramRect.left
+
+ const pos = {
+ x: left,
+ y: top + padRect.height + Y_OFFSET
+ }
+
+ return pos
+ }
+
+ /**
+ * Create an append action
+ *
+ * @param {string} type
+ * @param {string} className
+ * @param {string} [title]
+ * @param {Object} [options]
+ *
+ * @return {Object} descriptor
+ */
+ function appendAction(type, className, title, options) {
+ if (typeof title !== 'string') {
+ options = title
+ title = translate('Append {type}', { type: type.replace(/^bpmn:/, '') })
+ }
+
+ function appendStart(event, element) {
+ const shape = elementFactory.createShape(assign({ type: type }, options))
+ create.start(event, shape, {
+ source: element
+ })
+ }
+
+ const append = autoPlace
+ ? function (event, element) {
+ const shape = elementFactory.createShape(assign({ type: type }, options))
+
+ autoPlace.append(element, shape)
+ }
+ : appendStart
+
+ return {
+ group: 'model',
+ className: className,
+ title: title,
+ action: {
+ dragstart: appendStart,
+ click: append
+ }
+ }
+ }
+
+ function splitLaneHandler(count) {
+ return function (event, element) {
+ // actual split
+ modeling.splitLane(element, count)
+
+ // refresh context pad after split to
+ // get rid of split icons
+ contextPad.open(element, true)
+ }
+ }
+
+ if (isAny(businessObject, ['bpmn:Lane', 'bpmn:Participant']) && isExpanded(businessObject)) {
+ const childLanes = getChildLanes(element)
+
+ assign(actions, {
+ 'lane-insert-above': {
+ group: 'lane-insert-above',
+ className: 'bpmn-icon-lane-insert-above',
+ title: translate('Add Lane above'),
+ action: {
+ click: function (event, element) {
+ modeling.addLane(element, 'top')
+ }
+ }
+ }
+ })
+
+ if (childLanes.length < 2) {
+ if (element.height >= 120) {
+ assign(actions, {
+ 'lane-divide-two': {
+ group: 'lane-divide',
+ className: 'bpmn-icon-lane-divide-two',
+ title: translate('Divide into two Lanes'),
+ action: {
+ click: splitLaneHandler(2)
+ }
+ }
+ })
+ }
+
+ if (element.height >= 180) {
+ assign(actions, {
+ 'lane-divide-three': {
+ group: 'lane-divide',
+ className: 'bpmn-icon-lane-divide-three',
+ title: translate('Divide into three Lanes'),
+ action: {
+ click: splitLaneHandler(3)
+ }
+ }
+ })
+ }
+ }
+
+ assign(actions, {
+ 'lane-insert-below': {
+ group: 'lane-insert-below',
+ className: 'bpmn-icon-lane-insert-below',
+ title: translate('Add Lane below'),
+ action: {
+ click: function (event, element) {
+ modeling.addLane(element, 'bottom')
+ }
+ }
+ }
+ })
+ }
+
+ if (is(businessObject, 'bpmn:FlowNode')) {
+ if (is(businessObject, 'bpmn:EventBasedGateway')) {
+ assign(actions, {
+ 'append.receive-task': appendAction(
+ 'bpmn:ReceiveTask',
+ 'bpmn-icon-receive-task',
+ translate('Append ReceiveTask')
+ ),
+ 'append.message-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-message',
+ translate('Append MessageIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:MessageEventDefinition' }
+ ),
+ 'append.timer-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-timer',
+ translate('Append TimerIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:TimerEventDefinition' }
+ ),
+ 'append.condition-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-condition',
+ translate('Append ConditionIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:ConditionalEventDefinition' }
+ ),
+ 'append.signal-intermediate-event': appendAction(
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn-icon-intermediate-event-catch-signal',
+ translate('Append SignalIntermediateCatchEvent'),
+ { eventDefinitionType: 'bpmn:SignalEventDefinition' }
+ )
+ })
+ } else if (
+ isEventType(businessObject, 'bpmn:BoundaryEvent', 'bpmn:CompensateEventDefinition')
+ ) {
+ assign(actions, {
+ 'append.compensation-activity': appendAction(
+ 'bpmn:Task',
+ 'bpmn-icon-task',
+ translate('Append compensation activity'),
+ {
+ isForCompensation: true
+ }
+ )
+ })
+ } else if (
+ !is(businessObject, 'bpmn:EndEvent') &&
+ !businessObject.isForCompensation &&
+ !isEventType(businessObject, 'bpmn:IntermediateThrowEvent', 'bpmn:LinkEventDefinition') &&
+ !isEventSubProcess(businessObject)
+ ) {
+ assign(actions, {
+ 'append.end-event': appendAction(
+ 'bpmn:EndEvent',
+ 'bpmn-icon-end-event-none',
+ translate('Append EndEvent')
+ ),
+ 'append.gateway': appendAction(
+ 'bpmn:ExclusiveGateway',
+ 'bpmn-icon-gateway-none',
+ translate('Append Gateway')
+ ),
+ 'append.append-task': appendAction(
+ 'bpmn:UserTask',
+ 'bpmn-icon-user-task',
+ translate('Append Task')
+ ),
+ 'append.intermediate-event': appendAction(
+ 'bpmn:IntermediateThrowEvent',
+ 'bpmn-icon-intermediate-event-none',
+ translate('Append Intermediate/Boundary Event')
+ )
+ })
+ }
+ }
+
+ if (!popupMenu.isEmpty(element, 'bpmn-replace')) {
+ // Replace menu entry
+ assign(actions, {
+ replace: {
+ group: 'edit',
+ className: 'bpmn-icon-screw-wrench',
+ title: '淇敼绫诲瀷',
+ action: {
+ click: function (event, element) {
+ const position = assign(getReplaceMenuPosition(element), {
+ cursor: { x: event.x, y: event.y }
+ })
+
+ popupMenu.open(element, 'bpmn-replace', position)
+ }
+ }
+ }
+ })
+ }
+
+ if (
+ isAny(businessObject, [
+ 'bpmn:FlowNode',
+ 'bpmn:InteractionNode',
+ 'bpmn:DataObjectReference',
+ 'bpmn:DataStoreReference'
+ ])
+ ) {
+ assign(actions, {
+ 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation'),
+
+ connect: {
+ group: 'connect',
+ className: 'bpmn-icon-connection-multi',
+ title: translate(
+ 'Connect using ' +
+ (businessObject.isForCompensation ? '' : 'Sequence/MessageFlow or ') +
+ 'Association'
+ ),
+ action: {
+ click: startConnect,
+ dragstart: startConnect
+ }
+ }
+ })
+ }
+
+ if (isAny(businessObject, ['bpmn:DataObjectReference', 'bpmn:DataStoreReference'])) {
+ assign(actions, {
+ connect: {
+ group: 'connect',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Connect using DataInputAssociation'),
+ action: {
+ click: startConnect,
+ dragstart: startConnect
+ }
+ }
+ })
+ }
+
+ if (is(businessObject, 'bpmn:Group')) {
+ assign(actions, {
+ 'append.text-annotation': appendAction('bpmn:TextAnnotation', 'bpmn-icon-text-annotation')
+ })
+ }
+
+ // delete element entry, only show if allowed by rules
+ let deleteAllowed = rules.allowed('elements.delete', { elements: [element] })
+
+ if (isArray(deleteAllowed)) {
+ // was the element returned as a deletion candidate?
+ deleteAllowed = deleteAllowed[0] === element
+ }
+
+ if (deleteAllowed) {
+ assign(actions, {
+ delete: {
+ group: 'edit',
+ className: 'bpmn-icon-trash',
+ title: translate('Remove'),
+ action: {
+ click: removeElement
+ }
+ }
+ })
+ }
+
+ return actions
+}
+
+// helpers /////////
+
+function isEventType(eventBo, type, definition) {
+ const isType = eventBo.$instanceOf(type)
+ let isDefinition = false
+
+ const definitions = eventBo.eventDefinitions || []
+ forEach(definitions, function (def) {
+ if (def.$type === definition) {
+ isDefinition = true
+ }
+ })
+
+ return isType && isDefinition
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js b/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js
new file mode 100644
index 0000000..80009ef
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/content-pad/index.js
@@ -0,0 +1,6 @@
+import CustomContextPadProvider from './contentPadProvider'
+
+export default {
+ __init__: ['contextPadProvider'],
+ contextPadProvider: ['type', CustomContextPadProvider]
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js b/src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js
new file mode 100644
index 0000000..f3bc894
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/defaultEmpty.js
@@ -0,0 +1,24 @@
+export default (key, name, type) => {
+ if (!type) type = 'camunda'
+ const TYPE_TARGET = {
+ activiti: 'http://activiti.org/bpmn',
+ camunda: 'http://bpmn.io/schema/bpmn',
+ flowable: 'http://flowable.org/bpmn'
+ }
+ return `<?xml version="1.0" encoding="UTF-8"?>
+<bpmn2:definitions
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
+ id="diagram_${key}"
+ targetNamespace="${TYPE_TARGET[type]}">
+ <bpmn2:process id="${key}" name="${name}" isExecutable="true">
+ </bpmn2:process>
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1">
+ <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="${key}">
+ </bpmndi:BPMNPlane>
+ </bpmndi:BPMNDiagram>
+</bpmn2:definitions>`
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
new file mode 100644
index 0000000..94ba8f6
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/activitiDescriptor.json
@@ -0,0 +1,1004 @@
+{
+ "name": "Activiti",
+ "uri": "http://activiti.org/bpmn",
+ "prefix": "activiti",
+ "xml": {
+ "tagAlias": "lowerCase"
+ },
+ "associations": [],
+ "types": [
+ {
+ "name": "Definitions",
+ "isAbstract": true,
+ "extends": ["bpmn:Definitions"],
+ "properties": [
+ {
+ "name": "diagramRelationId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InOutBinding",
+ "superClass": ["Element"],
+ "isAbstract": true,
+ "properties": [
+ {
+ "name": "source",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "sourceExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "target",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "local",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "variables",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "In",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "Out",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "AsyncCapable",
+ "isAbstract": true,
+ "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncBefore",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncAfter",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "exclusive",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "JobPriorized",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "activiti:AsyncCapable"],
+ "properties": [
+ {
+ "name": "jobPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "SignalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:SignalEventDefinition"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ }
+ ]
+ },
+ {
+ "name": "ErrorEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ErrorEventDefinition"],
+ "properties": [
+ {
+ "name": "errorCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "errorMessageVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Error",
+ "isAbstract": true,
+ "extends": ["bpmn:Error"],
+ "properties": [
+ {
+ "name": "activiti:errorMessage",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "PotentialStarter",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "resourceAssignmentExpression",
+ "type": "bpmn:ResourceAssignmentExpression"
+ }
+ ]
+ },
+ {
+ "name": "FormSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "formHandlerClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formKey",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TemplateSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "bpmn:FlowElement"],
+ "properties": [
+ {
+ "name": "modelerTemplate",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Initiator",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent"],
+ "properties": [
+ {
+ "name": "initiator",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ScriptTask",
+ "isAbstract": true,
+ "extends": ["bpmn:ScriptTask"],
+ "properties": [
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Process",
+ "isAbstract": true,
+ "extends": ["bpmn:Process"],
+ "properties": [
+ {
+ "name": "candidateStarterGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStarterUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "versionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "historyTimeToLive",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "isStartableInTasklist",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ },
+ {
+ "name": "executionListener",
+ "isAbstract": true,
+ "type": "Expression"
+ }
+ ]
+ },
+ {
+ "name": "EscalationEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:EscalationEventDefinition"],
+ "properties": [
+ {
+ "name": "escalationCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FormalExpression",
+ "isAbstract": true,
+ "extends": ["bpmn:FormalExpression"],
+ "properties": [
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "multiinstance_type",
+ "superClass": ["Element"]
+ },
+ {
+ "name": "multiinstance_condition",
+ "superClass": ["Element"]
+ },
+ {
+ "name": "Assignable",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "assignee",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "dueDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "followUpDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "priority",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "multiinstance_condition",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStrategy",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateParam",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "CallActivity",
+ "extends": ["bpmn:CallActivity"],
+ "properties": [
+ {
+ "name": "calledElementBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "calledElementVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementVersionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "caseVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingDelegateExpression",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ServiceTaskLike",
+ "extends": [
+ "bpmn:ServiceTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:SendTask",
+ "bpmn:MessageEventDefinition"
+ ],
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "DmnCapable",
+ "extends": ["bpmn:BusinessRuleTask"],
+ "properties": [
+ {
+ "name": "decisionRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "decisionRefBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "decisionRefVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "mapDecisionResult",
+ "isAttr": true,
+ "type": "String",
+ "default": "resultList"
+ },
+ {
+ "name": "decisionRefTenantId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExternalCapable",
+ "extends": ["activiti:ServiceTaskLike"],
+ "properties": [
+ {
+ "name": "type",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "topic",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TaskPriorized",
+ "extends": ["bpmn:Process", "activiti:ExternalCapable"],
+ "properties": [
+ {
+ "name": "taskPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Properties",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "values",
+ "type": "Property",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Property",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Connector",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["activiti:ServiceTaskLike"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputOutput",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:FlowNode", "activiti:Connector"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ },
+ {
+ "name": "inputParameters",
+ "isMany": true,
+ "type": "InputParameter"
+ },
+ {
+ "name": "outputParameters",
+ "isMany": true,
+ "type": "OutputParameter"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameter",
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameterDefinition",
+ "isAbstract": true
+ },
+ {
+ "name": "List",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "items",
+ "isMany": true,
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Map",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "entries",
+ "isMany": true,
+ "type": "Entry"
+ }
+ ]
+ },
+ {
+ "name": "Entry",
+ "properties": [
+ {
+ "name": "key",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Value",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "id",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Script",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "scriptFormat",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Field",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "activiti:ServiceTaskLike",
+ "activiti:ExecutionListener",
+ "activiti:TaskListener"
+ ]
+ },
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "expression",
+ "type": "String"
+ },
+ {
+ "name": "stringValue",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "string",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "OutputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "Collectable",
+ "isAbstract": true,
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+ "superClass": ["activiti:AsyncCapable"],
+ "properties": [
+ {
+ "name": "collection",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "elementVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FailedJobRetryTimeCycle",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["activiti:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"]
+ },
+ "properties": [
+ {
+ "name": "body",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExecutionListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "bpmn:Task",
+ "bpmn:ServiceTask",
+ "bpmn:UserTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:ScriptTask",
+ "bpmn:ReceiveTask",
+ "bpmn:ManualTask",
+ "bpmn:ExclusiveGateway",
+ "bpmn:SequenceFlow",
+ "bpmn:ParallelGateway",
+ "bpmn:InclusiveGateway",
+ "bpmn:EventBasedGateway",
+ "bpmn:StartEvent",
+ "bpmn:IntermediateCatchEvent",
+ "bpmn:IntermediateThrowEvent",
+ "bpmn:EndEvent",
+ "bpmn:BoundaryEvent",
+ "bpmn:CallActivity",
+ "bpmn:SubProcess",
+ "bpmn:Process"
+ ]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "TaskListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "defaultValue",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "properties",
+ "type": "Properties"
+ },
+ {
+ "name": "validation",
+ "type": "Validation"
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Validation",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "constraints",
+ "type": "Constraint",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Constraint",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "config",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "ConditionalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ConditionalEventDefinition"],
+ "properties": [
+ {
+ "name": "variableName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableEvent",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ }
+ ],
+ "emumerations": []
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
new file mode 100644
index 0000000..8322561
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/camundaDescriptor.json
@@ -0,0 +1,1020 @@
+{
+ "name": "Camunda",
+ "uri": "http://camunda.org/schema/1.0/bpmn",
+ "prefix": "camunda",
+ "xml": {
+ "tagAlias": "lowerCase"
+ },
+ "associations": [],
+ "types": [
+ {
+ "name": "Definitions",
+ "isAbstract": true,
+ "extends": ["bpmn:Definitions"],
+ "properties": [
+ {
+ "name": "diagramRelationId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InOutBinding",
+ "superClass": ["Element"],
+ "isAbstract": true,
+ "properties": [
+ {
+ "name": "source",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "sourceExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "target",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "local",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "variables",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "In",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity", "bpmn:SignalEventDefinition"]
+ }
+ },
+ {
+ "name": "Out",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "AsyncCapable",
+ "isAbstract": true,
+ "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncBefore",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncAfter",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "exclusive",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "JobPriorized",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "camunda:AsyncCapable"],
+ "properties": [
+ {
+ "name": "jobPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "SignalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:SignalEventDefinition"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ }
+ ]
+ },
+ {
+ "name": "ErrorEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ErrorEventDefinition"],
+ "properties": [
+ {
+ "name": "errorCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "errorMessageVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Error",
+ "isAbstract": true,
+ "extends": ["bpmn:Error"],
+ "properties": [
+ {
+ "name": "camunda:errorMessage",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "PotentialStarter",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "resourceAssignmentExpression",
+ "type": "bpmn:ResourceAssignmentExpression"
+ }
+ ]
+ },
+ {
+ "name": "FormSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "formHandlerClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formKey",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TemplateSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "bpmn:FlowElement"],
+ "properties": [
+ {
+ "name": "modelerTemplate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "modelerTemplateVersion",
+ "isAttr": true,
+ "type": "Integer"
+ }
+ ]
+ },
+ {
+ "name": "Initiator",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent"],
+ "properties": [
+ {
+ "name": "initiator",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ScriptTask",
+ "isAbstract": true,
+ "extends": ["bpmn:ScriptTask"],
+ "properties": [
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Process",
+ "isAbstract": true,
+ "extends": ["bpmn:Process"],
+ "properties": [
+ {
+ "name": "candidateStarterGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStarterUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "versionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "historyTimeToLive",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "isStartableInTasklist",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "EscalationEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:EscalationEventDefinition"],
+ "properties": [
+ {
+ "name": "escalationCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FormalExpression",
+ "isAbstract": true,
+ "extends": ["bpmn:FormalExpression"],
+ "properties": [
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Assignable",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "assignee",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "dueDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "followUpDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "priority",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStrategy",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateParam",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "CallActivity",
+ "extends": ["bpmn:CallActivity"],
+ "properties": [
+ {
+ "name": "calledElementBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "calledElementVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementVersionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "caseVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingDelegateExpression",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ServiceTaskLike",
+ "extends": [
+ "bpmn:ServiceTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:SendTask",
+ "bpmn:MessageEventDefinition"
+ ],
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "DmnCapable",
+ "extends": ["bpmn:BusinessRuleTask"],
+ "properties": [
+ {
+ "name": "decisionRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "decisionRefBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "decisionRefVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "mapDecisionResult",
+ "isAttr": true,
+ "type": "String",
+ "default": "resultList"
+ },
+ {
+ "name": "decisionRefTenantId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExternalCapable",
+ "extends": ["camunda:ServiceTaskLike"],
+ "properties": [
+ {
+ "name": "type",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "topic",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TaskPriorized",
+ "extends": ["bpmn:Process", "camunda:ExternalCapable"],
+ "properties": [
+ {
+ "name": "taskPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Properties",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "values",
+ "type": "Property",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Property",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Connector",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["camunda:ServiceTaskLike"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputOutput",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:FlowNode", "camunda:Connector"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ },
+ {
+ "name": "inputParameters",
+ "isMany": true,
+ "type": "InputParameter"
+ },
+ {
+ "name": "outputParameters",
+ "isMany": true,
+ "type": "OutputParameter"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameter",
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameterDefinition",
+ "isAbstract": true
+ },
+ {
+ "name": "List",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "items",
+ "isMany": true,
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Map",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "entries",
+ "isMany": true,
+ "type": "Entry"
+ }
+ ]
+ },
+ {
+ "name": "Entry",
+ "properties": [
+ {
+ "name": "key",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Value",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "id",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Script",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "scriptFormat",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Field",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "camunda:ServiceTaskLike",
+ "camunda:ExecutionListener",
+ "camunda:TaskListener"
+ ]
+ },
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "expression",
+ "type": "String"
+ },
+ {
+ "name": "stringValue",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "string",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "OutputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "Collectable",
+ "isAbstract": true,
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+ "superClass": ["camunda:AsyncCapable"],
+ "properties": [
+ {
+ "name": "collection",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "elementVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FailedJobRetryTimeCycle",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["camunda:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"]
+ },
+ "properties": [
+ {
+ "name": "body",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExecutionListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "bpmn:Task",
+ "bpmn:ServiceTask",
+ "bpmn:UserTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:ScriptTask",
+ "bpmn:ReceiveTask",
+ "bpmn:ManualTask",
+ "bpmn:ExclusiveGateway",
+ "bpmn:SequenceFlow",
+ "bpmn:ParallelGateway",
+ "bpmn:InclusiveGateway",
+ "bpmn:EventBasedGateway",
+ "bpmn:StartEvent",
+ "bpmn:IntermediateCatchEvent",
+ "bpmn:IntermediateThrowEvent",
+ "bpmn:EndEvent",
+ "bpmn:BoundaryEvent",
+ "bpmn:CallActivity",
+ "bpmn:SubProcess",
+ "bpmn:Process"
+ ]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "TaskListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ },
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "eventDefinitions",
+ "type": "bpmn:TimerEventDefinition",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormData",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "fields",
+ "type": "FormField",
+ "isMany": true
+ },
+ {
+ "name": "businessKey",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "FormField",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "defaultValue",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "properties",
+ "type": "Properties"
+ },
+ {
+ "name": "validation",
+ "type": "Validation"
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Validation",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "constraints",
+ "type": "Constraint",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Constraint",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "config",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "ConditionalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ConditionalEventDefinition"],
+ "properties": [
+ {
+ "name": "variableName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableEvents",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ }
+ ],
+ "emumerations": []
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
new file mode 100644
index 0000000..c98c972
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/descriptor/flowableDescriptor.json
@@ -0,0 +1,1493 @@
+{
+ "name": "Flowable",
+ "uri": "http://flowable.org/bpmn",
+ "prefix": "flowable",
+ "xml": {
+ "tagAlias": "lowerCase"
+ },
+ "associations": [],
+ "types": [
+ {
+ "name": "InOutBinding",
+ "superClass": ["Element"],
+ "isAbstract": true,
+ "properties": [
+ {
+ "name": "source",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "sourceExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "target",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "local",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "variables",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "In",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "Out",
+ "superClass": ["InOutBinding"],
+ "meta": {
+ "allowedIn": ["bpmn:CallActivity"]
+ }
+ },
+ {
+ "name": "AsyncCapable",
+ "isAbstract": true,
+ "extends": ["bpmn:Activity", "bpmn:Gateway", "bpmn:Event"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncBefore",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "asyncAfter",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "exclusive",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "JobPriorized",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "flowable:AsyncCapable"],
+ "properties": [
+ {
+ "name": "jobPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "SignalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:SignalEventDefinition"],
+ "properties": [
+ {
+ "name": "async",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ }
+ ]
+ },
+ {
+ "name": "ErrorEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ErrorEventDefinition"],
+ "properties": [
+ {
+ "name": "errorCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "errorMessageVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Error",
+ "isAbstract": true,
+ "extends": ["bpmn:Error"],
+ "properties": [
+ {
+ "name": "flowable:errorMessage",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "PotentialStarter",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "resourceAssignmentExpression",
+ "type": "bpmn:ResourceAssignmentExpression"
+ }
+ ]
+ },
+ {
+ "name": "FormSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent", "bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "formHandlerClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formType",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "formReadOnly",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": false
+ },
+ {
+ "name": "formInit",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "TemplateSupported",
+ "isAbstract": true,
+ "extends": ["bpmn:Process", "bpmn:FlowElement"],
+ "properties": [
+ {
+ "name": "modelerTemplate",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Initiator",
+ "isAbstract": true,
+ "extends": ["bpmn:StartEvent"],
+ "properties": [
+ {
+ "name": "initiator",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ScriptTask",
+ "isAbstract": true,
+ "extends": ["bpmn:ScriptTask"],
+ "properties": [
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Process",
+ "isAbstract": true,
+ "extends": ["bpmn:Process"],
+ "properties": [
+ {
+ "name": "candidateStarterGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStarterUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "versionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "historyTimeToLive",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "isStartableInTasklist",
+ "isAttr": true,
+ "type": "Boolean",
+ "default": true
+ }
+ ]
+ },
+ {
+ "name": "EscalationEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:EscalationEventDefinition"],
+ "properties": [
+ {
+ "name": "escalationCodeVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FormalExpression",
+ "isAbstract": true,
+ "extends": ["bpmn:FormalExpression"],
+ "properties": [
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Assignable",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "assignee",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateUsers",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateGroups",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "dueDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "followUpDate",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "priority",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateStrategy",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "candidateParam",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Assignee",
+ "supperClass": "Element",
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "viewId",
+ "type": "Number",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "CallActivity",
+ "extends": ["bpmn:CallActivity"],
+ "properties": [
+ {
+ "name": "calledElementBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "calledElementVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementVersionTag",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "caseVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "caseTenantId",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingClass",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableMappingDelegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "calledElementType",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "processInstanceName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "inheritBusinessKey",
+ "isAttr": true,
+ "type": "Boolean"
+ },
+ {
+ "name": "businessKey",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "inheritVariables",
+ "isAttr": true,
+ "type": "Boolean"
+ }
+ ]
+ },
+ {
+ "name": "ServiceTaskLike",
+ "extends": [
+ "bpmn:ServiceTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:SendTask",
+ "bpmn:MessageEventDefinition"
+ ],
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resultVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "DmnCapable",
+ "extends": ["bpmn:BusinessRuleTask"],
+ "properties": [
+ {
+ "name": "decisionRef",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "decisionRefBinding",
+ "isAttr": true,
+ "type": "String",
+ "default": "latest"
+ },
+ {
+ "name": "decisionRefVersion",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "mapDecisionResult",
+ "isAttr": true,
+ "type": "String",
+ "default": "resultList"
+ },
+ {
+ "name": "decisionRefTenantId",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExternalCapable",
+ "extends": ["flowable:ServiceTaskLike"],
+ "properties": [
+ {
+ "name": "type",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "topic",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "TaskPriorized",
+ "extends": ["bpmn:Process", "flowable:ExternalCapable"],
+ "properties": [
+ {
+ "name": "taskPriority",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Properties",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["*"]
+ },
+ "properties": [
+ {
+ "name": "values",
+ "type": "Property",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Property",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Button",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "code",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "isHide",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "next",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "sort",
+ "type": "Integer",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Assignee",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "condition",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "operationType",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "sort",
+ "type": "Integer",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "Connector",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["flowable:ServiceTaskLike"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "InputOutput",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:FlowNode", "flowable:Connector"]
+ },
+ "properties": [
+ {
+ "name": "inputOutput",
+ "type": "InputOutput"
+ },
+ {
+ "name": "connectorId",
+ "type": "String"
+ },
+ {
+ "name": "inputParameters",
+ "isMany": true,
+ "type": "InputParameter"
+ },
+ {
+ "name": "outputParameters",
+ "isMany": true,
+ "type": "OutputParameter"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameter",
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "InputOutputParameterDefinition",
+ "isAbstract": true
+ },
+ {
+ "name": "List",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "items",
+ "isMany": true,
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Map",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "entries",
+ "isMany": true,
+ "type": "Entry"
+ }
+ ]
+ },
+ {
+ "name": "Entry",
+ "properties": [
+ {
+ "name": "key",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ },
+ {
+ "name": "definition",
+ "type": "InputOutputParameterDefinition"
+ }
+ ]
+ },
+ {
+ "name": "Value",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "id",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Script",
+ "superClass": ["InputOutputParameterDefinition"],
+ "properties": [
+ {
+ "name": "scriptFormat",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "resource",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "value",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Field",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "flowable:ServiceTaskLike",
+ "flowable:ExecutionListener",
+ "flowable:TaskListener"
+ ]
+ },
+ "properties": [
+ {
+ "name": "name",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "expression",
+ "type": "String"
+ },
+ {
+ "name": "stringValue",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "string",
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ChildField",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "InputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "OutputParameter",
+ "superClass": ["InputOutputParameter"]
+ },
+ {
+ "name": "Collectable",
+ "isAbstract": true,
+ "extends": ["bpmn:MultiInstanceLoopCharacteristics"],
+ "superClass": ["flowable:AsyncCapable"],
+ "properties": [
+ {
+ "name": "collection",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "elementVariable",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "FailedJobRetryTimeCycle",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["flowable:AsyncCapable", "bpmn:MultiInstanceLoopCharacteristics"]
+ },
+ "properties": [
+ {
+ "name": "body",
+ "isBody": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ExecutionListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": [
+ "bpmn:Task",
+ "bpmn:ServiceTask",
+ "bpmn:UserTask",
+ "bpmn:BusinessRuleTask",
+ "bpmn:ScriptTask",
+ "bpmn:ReceiveTask",
+ "bpmn:ManualTask",
+ "bpmn:ExclusiveGateway",
+ "bpmn:SequenceFlow",
+ "bpmn:ParallelGateway",
+ "bpmn:InclusiveGateway",
+ "bpmn:EventBasedGateway",
+ "bpmn:StartEvent",
+ "bpmn:IntermediateCatchEvent",
+ "bpmn:IntermediateThrowEvent",
+ "bpmn:EndEvent",
+ "bpmn:BoundaryEvent",
+ "bpmn:CallActivity",
+ "bpmn:SubProcess",
+ "bpmn:Process"
+ ]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "TaskListener",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "expression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "class",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "delegateExpression",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "event",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "script",
+ "type": "Script"
+ },
+ {
+ "name": "fields",
+ "type": "Field",
+ "isMany": true
+ },
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "eventDefinitions",
+ "type": "bpmn:TimerEventDefinition",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormProperty",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "required",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "readable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "writable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "variable",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "expression",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "default",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ },
+ {
+ "name": "children",
+ "type": "ChildField",
+ "isMany": true
+ },
+ {
+ "name": "extensionElements",
+ "type": "bpmn:ExtensionElements",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "FormData",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "fields",
+ "type": "FormField",
+ "isMany": true
+ },
+ {
+ "name": "businessKey",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "FormField",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "label",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "type",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "datePattern",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "defaultValue",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "properties",
+ "type": "Properties"
+ },
+ {
+ "name": "validation",
+ "type": "Validation"
+ },
+ {
+ "name": "values",
+ "type": "Value",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Validation",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "constraints",
+ "type": "Constraint",
+ "isMany": true
+ }
+ ]
+ },
+ {
+ "name": "Constraint",
+ "superClass": ["Element"],
+ "properties": [
+ {
+ "name": "name",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "config",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "ConditionalEventDefinition",
+ "isAbstract": true,
+ "extends": ["bpmn:ConditionalEventDefinition"],
+ "properties": [
+ {
+ "name": "variableName",
+ "isAttr": true,
+ "type": "String"
+ },
+ {
+ "name": "variableEvent",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "Condition",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:SequenceFlow"]
+ },
+ "properties": [
+ {
+ "name": "id",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "field",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "compare",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "value",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "logic",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "sort",
+ "type": "Integer",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "AssignStartUserHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:StartEvent", "bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "RejectHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "RejectReturnTaskId",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "String",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "AssignEmptyHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "AssignEmptyUserIds",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "String",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "ButtonsSetting",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "flowable:id",
+ "type": "Integer",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:enable",
+ "type": "Boolean",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:displayName",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "FieldsPermission",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "flowable:field",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:title",
+ "type": "String",
+ "isAttr": true
+ },
+ {
+ "name": "flowable:permission",
+ "type": "String",
+ "isAttr": true
+ }
+ ]
+ },
+ {
+ "name": "BoundaryEventType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:BoundaryEvent"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "TimeoutHandlerType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:BoundaryEvent"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "ApproveType",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "ApproveMethod",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "CandidateStrategy",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Integer",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "CandidateParam",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "String",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "SignEnable",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Boolean",
+ "isBody": true
+ }
+ ]
+ },
+ {
+ "name": "SkipExpression",
+ "extends": ["bpmn:UserTask"],
+ "properties": [
+ {
+ "name": "skipExpression",
+ "isAttr": true,
+ "type": "String"
+ }
+ ]
+ },
+ {
+ "name": "ReasonRequire",
+ "superClass": ["Element"],
+ "meta": {
+ "allowedIn": ["bpmn:UserTask"]
+ },
+ "properties": [
+ {
+ "name": "value",
+ "type": "Boolean",
+ "isBody": true
+ }
+ ]
+ }
+ ],
+ "emumerations": []
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js
new file mode 100644
index 0000000..56ef38a
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/activitiExtension.js
@@ -0,0 +1,83 @@
+'use strict'
+
+import { some } from 'min-dash'
+
+// const some = require('min-dash').some
+// const some = some
+
+const ALLOWED_TYPES = {
+ FailedJobRetryTimeCycle: [
+ 'bpmn:StartEvent',
+ 'bpmn:BoundaryEvent',
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn:Activity'
+ ],
+ Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+ Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent']
+}
+
+function is(element, type) {
+ return element && typeof element.$instanceOf === 'function' && element.$instanceOf(type)
+}
+
+function exists(element) {
+ return element && element.length
+}
+
+function includesType(collection, type) {
+ return (
+ exists(collection) &&
+ some(collection, function (element) {
+ return is(element, type)
+ })
+ )
+}
+
+function anyType(element, types) {
+ return some(types, function (type) {
+ return is(element, type)
+ })
+}
+
+function isAllowed(propName, propDescriptor, newElement) {
+ const name = propDescriptor.name,
+ types = ALLOWED_TYPES[name.replace(/activiti:/, '')]
+
+ return name === propName && anyType(newElement, types)
+}
+
+function ActivitiModdleExtension(eventBus) {
+ eventBus.on(
+ 'property.clone',
+ function (context) {
+ const newElement = context.newElement,
+ propDescriptor = context.propertyDescriptor
+
+ this.canCloneProperty(newElement, propDescriptor)
+ },
+ this
+ )
+}
+
+ActivitiModdleExtension.$inject = ['eventBus']
+
+ActivitiModdleExtension.prototype.canCloneProperty = function (newElement, propDescriptor) {
+ if (isAllowed('activiti:FailedJobRetryTimeCycle', propDescriptor, newElement)) {
+ return (
+ includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
+ includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
+ is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics')
+ )
+ }
+
+ if (isAllowed('activiti:Connector', propDescriptor, newElement)) {
+ return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+ }
+
+ if (isAllowed('activiti:Field', propDescriptor, newElement)) {
+ return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+ }
+}
+
+// module.exports = ActivitiModdleExtension;
+export default ActivitiModdleExtension
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js
new file mode 100644
index 0000000..c22ca34
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/activiti/index.js
@@ -0,0 +1,11 @@
+/*
+ * @author igdianov
+ * address https://github.com/igdianov/activiti-bpmn-moddle
+ * */
+
+import activitiExtension from './activitiExtension'
+
+export default {
+ __init__: ['ActivitiModdleExtension'],
+ ActivitiModdleExtension: ['type', activitiExtension]
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js
new file mode 100644
index 0000000..b8c37a5
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/extension.js
@@ -0,0 +1,151 @@
+'use strict'
+
+import { isFunction, isObject, some } from 'min-dash'
+
+// const isFunction = isFunction,
+// isObject = isObject,
+// some = some
+// const isFunction = require('min-dash').isFunction,
+// isObject = require('min-dash').isObject,
+// some = require('min-dash').some
+
+const WILDCARD = '*'
+
+function CamundaModdleExtension(eventBus) {
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
+ const self = this
+
+ eventBus.on('moddleCopy.canCopyProperty', function (context) {
+ const property = context.property,
+ parent = context.parent
+
+ return self.canCopyProperty(property, parent)
+ })
+}
+
+CamundaModdleExtension.$inject = ['eventBus']
+
+/**
+ * Check wether to disallow copying property.
+ */
+CamundaModdleExtension.prototype.canCopyProperty = function (property, parent) {
+ // (1) check wether property is allowed in parent
+ if (isObject(property) && !isAllowedInParent(property, parent)) {
+ return false
+ }
+
+ // (2) check more complex scenarios
+
+ if (is(property, 'camunda:InputOutput') && !this.canHostInputOutput(parent)) {
+ return false
+ }
+
+ if (isAny(property, ['camunda:Connector', 'camunda:Field']) && !this.canHostConnector(parent)) {
+ return false
+ }
+
+ if (is(property, 'camunda:In') && !this.canHostIn(parent)) {
+ return false
+ }
+}
+
+CamundaModdleExtension.prototype.canHostInputOutput = function (parent) {
+ // allowed in camunda:Connector
+ const connector = getParent(parent, 'camunda:Connector')
+
+ if (connector) {
+ return true
+ }
+
+ // special rules inside bpmn:FlowNode
+ const flowNode = getParent(parent, 'bpmn:FlowNode')
+
+ if (!flowNode) {
+ return false
+ }
+
+ if (isAny(flowNode, ['bpmn:StartEvent', 'bpmn:Gateway', 'bpmn:BoundaryEvent'])) {
+ return false
+ }
+
+ return !(is(flowNode, 'bpmn:SubProcess') && flowNode.get('triggeredByEvent'))
+}
+
+CamundaModdleExtension.prototype.canHostConnector = function (parent) {
+ const serviceTaskLike = getParent(parent, 'camunda:ServiceTaskLike')
+
+ if (is(serviceTaskLike, 'bpmn:MessageEventDefinition')) {
+ // only allow on throw and end events
+ return getParent(parent, 'bpmn:IntermediateThrowEvent') || getParent(parent, 'bpmn:EndEvent')
+ }
+
+ return true
+}
+
+CamundaModdleExtension.prototype.canHostIn = function (parent) {
+ const callActivity = getParent(parent, 'bpmn:CallActivity')
+
+ if (callActivity) {
+ return true
+ }
+
+ const signalEventDefinition = getParent(parent, 'bpmn:SignalEventDefinition')
+
+ if (signalEventDefinition) {
+ // only allow on throw and end events
+ return getParent(parent, 'bpmn:IntermediateThrowEvent') || getParent(parent, 'bpmn:EndEvent')
+ }
+
+ return true
+}
+
+// module.exports = CamundaModdleExtension;
+export default CamundaModdleExtension
+
+// helpers //////////
+
+function is(element, type) {
+ return element && isFunction(element.$instanceOf) && element.$instanceOf(type)
+}
+
+function isAny(element, types) {
+ return some(types, function (t) {
+ return is(element, t)
+ })
+}
+
+function getParent(element, type) {
+ if (!type) {
+ return element.$parent
+ }
+
+ if (is(element, type)) {
+ return element
+ }
+
+ if (!element.$parent) {
+ return
+ }
+
+ return getParent(element.$parent, type)
+}
+
+function isAllowedInParent(property, parent) {
+ // (1) find property descriptor
+ const descriptor = property.$type && property.$model.getTypeDescriptor(property.$type)
+
+ const allowedIn = descriptor && descriptor.meta && descriptor.meta.allowedIn
+
+ if (!allowedIn || isWildcard(allowedIn)) {
+ return true
+ }
+
+ // (2) check wether property has parent of allowed type
+ return some(allowedIn, function (type) {
+ return getParent(parent, type)
+ })
+}
+
+function isWildcard(allowedIn) {
+ return allowedIn.indexOf(WILDCARD) !== -1
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js
new file mode 100644
index 0000000..1da1bc7
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/camunda/index.js
@@ -0,0 +1,8 @@
+'use strict'
+
+import extension from './extension'
+
+export default {
+ __init__: ['camundaModdleExtension'],
+ camundaModdleExtension: ['type', extension]
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js
new file mode 100644
index 0000000..3dcea67
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/flowableExtension.js
@@ -0,0 +1,83 @@
+'use strict'
+
+import { some } from 'min-dash'
+
+// const some = some
+// const some = require('min-dash').some
+
+const ALLOWED_TYPES = {
+ FailedJobRetryTimeCycle: [
+ 'bpmn:StartEvent',
+ 'bpmn:BoundaryEvent',
+ 'bpmn:IntermediateCatchEvent',
+ 'bpmn:Activity'
+ ],
+ Connector: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent'],
+ Field: ['bpmn:EndEvent', 'bpmn:IntermediateThrowEvent']
+}
+
+function is(element, type) {
+ return element && typeof element.$instanceOf === 'function' && element.$instanceOf(type)
+}
+
+function exists(element) {
+ return element && element.length
+}
+
+function includesType(collection, type) {
+ return (
+ exists(collection) &&
+ some(collection, function (element) {
+ return is(element, type)
+ })
+ )
+}
+
+function anyType(element, types) {
+ return some(types, function (type) {
+ return is(element, type)
+ })
+}
+
+function isAllowed(propName, propDescriptor, newElement) {
+ const name = propDescriptor.name,
+ types = ALLOWED_TYPES[name.replace(/flowable:/, '')]
+
+ return name === propName && anyType(newElement, types)
+}
+
+function FlowableModdleExtension(eventBus) {
+ eventBus.on(
+ 'property.clone',
+ function (context) {
+ const newElement = context.newElement,
+ propDescriptor = context.propertyDescriptor
+
+ this.canCloneProperty(newElement, propDescriptor)
+ },
+ this
+ )
+}
+
+FlowableModdleExtension.$inject = ['eventBus']
+
+FlowableModdleExtension.prototype.canCloneProperty = function (newElement, propDescriptor) {
+ if (isAllowed('flowable:FailedJobRetryTimeCycle', propDescriptor, newElement)) {
+ return (
+ includesType(newElement.eventDefinitions, 'bpmn:TimerEventDefinition') ||
+ includesType(newElement.eventDefinitions, 'bpmn:SignalEventDefinition') ||
+ is(newElement.loopCharacteristics, 'bpmn:MultiInstanceLoopCharacteristics')
+ )
+ }
+
+ if (isAllowed('flowable:Connector', propDescriptor, newElement)) {
+ return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+ }
+
+ if (isAllowed('flowable:Field', propDescriptor, newElement)) {
+ return includesType(newElement.eventDefinitions, 'bpmn:MessageEventDefinition')
+ }
+}
+
+// module.exports = FlowableModdleExtension;
+export default FlowableModdleExtension
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js
new file mode 100644
index 0000000..6d59b67
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/extension-moddle/flowable/index.js
@@ -0,0 +1,10 @@
+/*
+ * @author igdianov
+ * address https://github.com/igdianov/activiti-bpmn-moddle
+ * */
+import flowableExtension from './flowableExtension'
+
+export default {
+ __init__: ['FlowableModdleExtension'],
+ FlowableModdleExtension: ['type', flowableExtension]
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js
new file mode 100644
index 0000000..788e4d1
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/CustomPalette.js
@@ -0,0 +1,233 @@
+import PaletteProvider from 'bpmn-js/lib/features/palette/PaletteProvider'
+import { assign } from 'min-dash'
+
+export default function CustomPalette(
+ palette,
+ create,
+ elementFactory,
+ spaceTool,
+ lassoTool,
+ handTool,
+ globalConnect,
+ translate
+) {
+ PaletteProvider.call(
+ this,
+ palette,
+ create,
+ elementFactory,
+ spaceTool,
+ lassoTool,
+ handTool,
+ globalConnect,
+ translate,
+ 2000
+ )
+}
+
+const F = function () {} // 鏍稿績锛屽埄鐢ㄧ┖瀵硅薄浣滀负涓粙锛�
+F.prototype = PaletteProvider.prototype // 鏍稿績锛屽皢鐖剁被鐨勫師鍨嬭祴鍊肩粰绌哄璞锛�
+
+// 鍒╃敤涓粙鍑芥暟閲嶅啓鍘熷瀷閾炬柟娉�
+F.prototype.getPaletteEntries = function () {
+ const actions = {},
+ create = this._create,
+ elementFactory = this._elementFactory,
+ spaceTool = this._spaceTool,
+ lassoTool = this._lassoTool,
+ handTool = this._handTool,
+ globalConnect = this._globalConnect,
+ translate = this._translate
+
+ function createAction(type, group, className, title, options) {
+ function createListener(event) {
+ const shape = elementFactory.createShape(assign({ type: type }, options))
+
+ if (options) {
+ shape.businessObject.di.isExpanded = options.isExpanded
+ }
+
+ create.start(event, shape)
+ }
+
+ const shortType = type.replace(/^bpmn:/, '')
+
+ return {
+ group: group,
+ className: className,
+ title: title || translate('Create {type}', { type: shortType }),
+ action: {
+ dragstart: createListener,
+ click: createListener
+ }
+ }
+ }
+
+ function createSubprocess(event) {
+ const subProcess = elementFactory.createShape({
+ type: 'bpmn:SubProcess',
+ x: 0,
+ y: 0,
+ isExpanded: true
+ })
+
+ const startEvent = elementFactory.createShape({
+ type: 'bpmn:StartEvent',
+ x: 40,
+ y: 82,
+ parent: subProcess
+ })
+
+ create.start(event, [subProcess, startEvent], {
+ hints: {
+ autoSelect: [startEvent]
+ }
+ })
+ }
+
+ function createParticipant(event) {
+ create.start(event, elementFactory.createParticipantShape())
+ }
+
+ assign(actions, {
+ 'hand-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-hand-tool',
+ title: '婵�娲绘姄鎵嬪伐鍏�',
+ // title: translate("Activate the hand tool"),
+ action: {
+ click: function (event) {
+ handTool.activateHand(event)
+ }
+ }
+ },
+ 'lasso-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-lasso-tool',
+ title: translate('Activate the lasso tool'),
+ action: {
+ click: function (event) {
+ lassoTool.activateSelection(event)
+ }
+ }
+ },
+ 'space-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-space-tool',
+ title: translate('Activate the create/remove space tool'),
+ action: {
+ click: function (event) {
+ spaceTool.activateSelection(event)
+ }
+ }
+ },
+ 'global-connect-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Activate the global connect tool'),
+ action: {
+ click: function (event) {
+ globalConnect.toggle(event)
+ }
+ }
+ },
+ 'tool-separator': {
+ group: 'tools',
+ separator: true
+ },
+ 'create.start-event': createAction(
+ 'bpmn:StartEvent',
+ 'event',
+ 'bpmn-icon-start-event-none',
+ translate('Create StartEvent')
+ ),
+ 'create.intermediate-event': createAction(
+ 'bpmn:IntermediateThrowEvent',
+ 'event',
+ 'bpmn-icon-intermediate-event-none',
+ translate('Create Intermediate/Boundary Event')
+ ),
+ 'create.end-event': createAction(
+ 'bpmn:EndEvent',
+ 'event',
+ 'bpmn-icon-end-event-none',
+ translate('Create EndEvent')
+ ),
+ 'create.exclusive-gateway': createAction(
+ 'bpmn:ExclusiveGateway',
+ 'gateway',
+ 'bpmn-icon-gateway-none',
+ translate('Create Gateway')
+ ),
+ 'create.user-task': createAction(
+ 'bpmn:UserTask',
+ 'activity',
+ 'bpmn-icon-user-task',
+ translate('Create User Task')
+ ),
+ 'create.call-activity': createAction(
+ 'bpmn:CallActivity',
+ 'activity',
+ 'bpmn-icon-call-activity',
+ translate('Create Call Activity')
+ ),
+ 'create.service-task': createAction(
+ 'bpmn:ServiceTask',
+ 'activity',
+ 'bpmn-icon-service',
+ translate('Create Service Task')
+ ),
+ 'create.data-object': createAction(
+ 'bpmn:DataObjectReference',
+ 'data-object',
+ 'bpmn-icon-data-object',
+ translate('Create DataObjectReference')
+ ),
+ 'create.data-store': createAction(
+ 'bpmn:DataStoreReference',
+ 'data-store',
+ 'bpmn-icon-data-store',
+ translate('Create DataStoreReference')
+ ),
+ 'create.subprocess-expanded': {
+ group: 'activity',
+ className: 'bpmn-icon-subprocess-expanded',
+ title: translate('Create expanded SubProcess'),
+ action: {
+ dragstart: createSubprocess,
+ click: createSubprocess
+ }
+ },
+ 'create.participant-expanded': {
+ group: 'collaboration',
+ className: 'bpmn-icon-participant',
+ title: translate('Create Pool/Participant'),
+ action: {
+ dragstart: createParticipant,
+ click: createParticipant
+ }
+ },
+ 'create.group': createAction(
+ 'bpmn:Group',
+ 'artifact',
+ 'bpmn-icon-group',
+ translate('Create Group')
+ )
+ })
+
+ return actions
+}
+
+CustomPalette.$inject = [
+ 'palette',
+ 'create',
+ 'elementFactory',
+ 'spaceTool',
+ 'lassoTool',
+ 'handTool',
+ 'globalConnect',
+ 'translate'
+]
+
+CustomPalette.prototype = new F() // 鏍稿績锛屽皢 F鐨勫疄渚嬭祴鍊肩粰瀛愮被锛�
+CustomPalette.prototype.constructor = CustomPalette // 淇瀛愮被CustomPalette鐨勬瀯閫犲櫒鎸囧悜锛岄槻姝㈠師鍨嬮摼鐨勬贩涔憋紱
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js
new file mode 100644
index 0000000..8e4f3ac
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/index.js
@@ -0,0 +1,22 @@
+// import PaletteModule from "diagram-js/lib/features/palette";
+// import CreateModule from "diagram-js/lib/features/create";
+// import SpaceToolModule from "diagram-js/lib/features/space-tool";
+// import LassoToolModule from "diagram-js/lib/features/lasso-tool";
+// import HandToolModule from "diagram-js/lib/features/hand-tool";
+// import GlobalConnectModule from "diagram-js/lib/features/global-connect";
+// import translate from "diagram-js/lib/i18n/translate";
+//
+// import PaletteProvider from "./paletteProvider";
+//
+// export default {
+// __depends__: [PaletteModule, CreateModule, SpaceToolModule, LassoToolModule, HandToolModule, GlobalConnectModule, translate],
+// __init__: ["paletteProvider"],
+// paletteProvider: ["type", PaletteProvider]
+// };
+
+import CustomPalette from './CustomPalette'
+
+export default {
+ __init__: ['paletteProvider'],
+ paletteProvider: ['type', CustomPalette]
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js
new file mode 100644
index 0000000..304875c
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/palette/paletteProvider.js
@@ -0,0 +1,219 @@
+import { assign } from 'min-dash'
+
+/**
+ * A palette provider for BPMN 2.0 elements.
+ */
+export default function PaletteProvider(
+ palette,
+ create,
+ elementFactory,
+ spaceTool,
+ lassoTool,
+ handTool,
+ globalConnect,
+ translate
+) {
+ this._palette = palette
+ this._create = create
+ this._elementFactory = elementFactory
+ this._spaceTool = spaceTool
+ this._lassoTool = lassoTool
+ this._handTool = handTool
+ this._globalConnect = globalConnect
+ this._translate = translate
+
+ palette.registerProvider(this)
+}
+
+PaletteProvider.$inject = [
+ 'palette',
+ 'create',
+ 'elementFactory',
+ 'spaceTool',
+ 'lassoTool',
+ 'handTool',
+ 'globalConnect',
+ 'translate'
+]
+
+PaletteProvider.prototype.getPaletteEntries = function () {
+ const actions = {},
+ create = this._create,
+ elementFactory = this._elementFactory,
+ spaceTool = this._spaceTool,
+ lassoTool = this._lassoTool,
+ handTool = this._handTool,
+ globalConnect = this._globalConnect,
+ translate = this._translate
+
+ function createAction(type, group, className, title, options) {
+ function createListener(event) {
+ const shape = elementFactory.createShape(assign({ type: type }, options))
+
+ if (options) {
+ shape.businessObject.di.isExpanded = options.isExpanded
+ }
+
+ create.start(event, shape)
+ }
+
+ const shortType = type.replace(/^bpmn:/, '')
+
+ return {
+ group: group,
+ className: className,
+ title: title || translate('Create {type}', { type: shortType }),
+ action: {
+ dragstart: createListener,
+ click: createListener
+ }
+ }
+ }
+
+ function createSubprocess(event) {
+ const subProcess = elementFactory.createShape({
+ type: 'bpmn:SubProcess',
+ x: 0,
+ y: 0,
+ isExpanded: true
+ })
+
+ const startEvent = elementFactory.createShape({
+ type: 'bpmn:StartEvent',
+ x: 40,
+ y: 82,
+ parent: subProcess
+ })
+
+ create.start(event, [subProcess, startEvent], {
+ hints: {
+ autoSelect: [startEvent]
+ }
+ })
+ }
+
+ function createParticipant(event) {
+ create.start(event, elementFactory.createParticipantShape())
+ }
+
+ assign(actions, {
+ 'hand-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-hand-tool',
+ title: translate('Activate the hand tool'),
+ action: {
+ click: function (event) {
+ handTool.activateHand(event)
+ }
+ }
+ },
+ 'lasso-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-lasso-tool',
+ title: translate('Activate the lasso tool'),
+ action: {
+ click: function (event) {
+ lassoTool.activateSelection(event)
+ }
+ }
+ },
+ 'space-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-space-tool',
+ title: translate('Activate the create/remove space tool'),
+ action: {
+ click: function (event) {
+ spaceTool.activateSelection(event)
+ }
+ }
+ },
+ 'global-connect-tool': {
+ group: 'tools',
+ className: 'bpmn-icon-connection-multi',
+ title: translate('Activate the global connect tool'),
+ action: {
+ click: function (event) {
+ globalConnect.toggle(event)
+ }
+ }
+ },
+ 'tool-separator': {
+ group: 'tools',
+ separator: true
+ },
+ 'create.start-event': createAction(
+ 'bpmn:StartEvent',
+ 'event',
+ 'bpmn-icon-start-event-none',
+ translate('Create StartEvent')
+ ),
+ 'create.intermediate-event': createAction(
+ 'bpmn:IntermediateThrowEvent',
+ 'event',
+ 'bpmn-icon-intermediate-event-none',
+ translate('Create Intermediate/Boundary Event')
+ ),
+ 'create.end-event': createAction(
+ 'bpmn:EndEvent',
+ 'event',
+ 'bpmn-icon-end-event-none',
+ translate('Create EndEvent')
+ ),
+ 'create.exclusive-gateway': createAction(
+ 'bpmn:ExclusiveGateway',
+ 'gateway',
+ 'bpmn-icon-gateway-none',
+ translate('Create Gateway')
+ ),
+ 'create.user-task': createAction(
+ 'bpmn:UserTask',
+ 'activity',
+ 'bpmn-icon-user-task',
+ translate('Create User Task')
+ ),
+ 'create.service-task': createAction(
+ 'bpmn:ServiceTask',
+ 'activity',
+ 'bpmn-icon-service',
+ translate('Create Service Task')
+ ),
+ 'create.data-object': createAction(
+ 'bpmn:DataObjectReference',
+ 'data-object',
+ 'bpmn-icon-data-object',
+ translate('Create DataObjectReference')
+ ),
+ 'create.data-store': createAction(
+ 'bpmn:DataStoreReference',
+ 'data-store',
+ 'bpmn-icon-data-store',
+ translate('Create DataStoreReference')
+ ),
+ 'create.subprocess-expanded': {
+ group: 'activity',
+ className: 'bpmn-icon-subprocess-expanded',
+ title: translate('Create expanded SubProcess'),
+ action: {
+ dragstart: createSubprocess,
+ click: createSubprocess
+ }
+ },
+ 'create.participant-expanded': {
+ group: 'collaboration',
+ className: 'bpmn-icon-participant',
+ title: translate('Create Pool/Participant'),
+ action: {
+ dragstart: createParticipant,
+ click: createParticipant
+ }
+ },
+ 'create.group': createAction(
+ 'bpmn:Group',
+ 'artifact',
+ 'bpmn-icon-group',
+ translate('Create Group')
+ )
+ })
+
+ return actions
+}
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js
new file mode 100644
index 0000000..d1796d3
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/customTranslate.js
@@ -0,0 +1,42 @@
+// import translations from "./zh";
+//
+// export default function customTranslate(template, replacements) {
+// replacements = replacements || {};
+//
+// // Translate
+// template = translations[template] || template;
+//
+// // Replace
+// return template.replace(/{([^}]+)}/g, function(_, key) {
+// let str = replacements[key];
+// if (
+// translations[replacements[key]] !== null &&
+// translations[replacements[key]] !== "undefined"
+// ) {
+// // eslint-disable-next-line no-mixed-spaces-and-tabs
+// str = translations[replacements[key]];
+// // eslint-disable-next-line no-mixed-spaces-and-tabs
+// }
+// return str || "{" + key + "}";
+// });
+// }
+
+export default function customTranslate(translations) {
+ return function (template, replacements) {
+ replacements = replacements || {};
+ // 灏嗘ā鏉垮拰缈昏瘧瀛楀吀鐨勯敭缁熶竴杞崲涓哄皬鍐欒繘琛屽尮閰�
+ const lowerTemplate = template.toLowerCase();
+ const translation = Object.keys(translations).find(key => key.toLowerCase() === lowerTemplate);
+
+ // 濡傛灉鎵惧埌鍖归厤鐨勭炕璇戯紝浣跨敤缈昏瘧鍚庣殑妯℃澘
+ if (translation) {
+ template = translations[translation];
+ }
+
+ // 鏇挎崲妯℃澘涓殑鍗犱綅绗�
+ return template.replace(/{([^}]+)}/g, function (_, key) {
+ // 濡傛灉鏇挎崲鍊煎瓨鍦紝杩斿洖鏇挎崲鍊硷紱鍚﹀垯杩斿洖鍘熷鍗犱綅绗�
+ return replacements[key] !== undefined ? replacements[key] : `{${key}}`;
+ });
+ };
+}
\ No newline at end of file
diff --git a/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js
new file mode 100644
index 0000000..4407e66
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/designer/plugins/translate/zh.js
@@ -0,0 +1,251 @@
+/**
+ * This is a sample file that should be replaced with the actual translation.
+ *
+ * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
+ * translations and labels to translate.
+ */
+export default {
+ // 娣诲姞閮ㄥ垎
+ 'Append EndEvent': '杩藉姞缁撴潫浜嬩欢',
+ 'Append Gateway': '杩藉姞缃戝叧',
+ 'Append Task': '杩藉姞浠诲姟',
+ 'Append Intermediate/Boundary Event': '杩藉姞涓棿鎶涘嚭浜嬩欢/杈圭晫浜嬩欢',
+ TextAnnotation: '鏂囨湰娉ㄩ噴',
+ 'Activate the global connect tool': '婵�娲诲叏灞�杩炴帴宸ュ叿',
+ 'Append {type}': '娣诲姞 {type}',
+ 'Add Lane above': '鍦ㄤ笂闈㈡坊鍔犻亾',
+ 'Divide into two Lanes': '鍒嗗壊鎴愪袱涓亾',
+ 'Divide into three Lanes': '鍒嗗壊鎴愪笁涓亾',
+ 'Add Lane below': '鍦ㄤ笅闈㈡坊鍔犻亾',
+ 'Append compensation activity': '杩藉姞琛ュ伩娲诲姩',
+ 'Change type': '淇敼绫诲瀷',
+ 'Connect using Association': '浣跨敤鍏宠仈杩炴帴',
+ 'Connect using Sequence/MessageFlow or Association': '浣跨敤椤哄簭/娑堟伅娴佹垨鑰呭叧鑱旇繛鎺�',
+ 'Connect using DataInputAssociation': '浣跨敤鏁版嵁杈撳叆鍏宠仈杩炴帴',
+ Remove: '绉婚櫎',
+ 'Activate the hand tool': '婵�娲绘姄鎵嬪伐鍏�',
+ 'Activate the lasso tool': '婵�娲诲绱㈠伐鍏�',
+ 'Activate the create/remove space tool': '婵�娲诲垱寤�/鍒犻櫎绌洪棿宸ュ叿',
+ 'Create expanded SubProcess': '鍒涘缓鎵╁睍瀛愯繃绋�',
+ 'Create IntermediateThrowEvent/BoundaryEvent': '鍒涘缓涓棿鎶涘嚭浜嬩欢/杈圭晫浜嬩欢',
+ 'Create Pool/Participant': '鍒涘缓姹�/鍙備笌鑰�',
+ 'Participant Multiplicity': '鍙備笌鑰呭閲嶆��',
+ 'Empty pool/participant (removes content)': '娓呯┖姹�/鍙備笌鑰咃紙绉婚櫎鍐呭锛�',
+ 'Empty pool/participant': '鏀剁缉姹�/鍙備笌鑰�',
+ 'Expanded pool/participant': '灞曞紑姹�/鍙備笌鑰�',
+ 'Parallel Multi-Instance': '骞惰澶氶噸浜嬩欢',
+ 'Sequential Multi-Instance': '鏃跺簭澶氶噸浜嬩欢',
+ DataObjectReference: '鏁版嵁瀵硅薄鍙傝��',
+ DataStoreReference: '鏁版嵁瀛樺偍鍙傝��',
+ 'Data object reference': '鏁版嵁瀵硅薄寮曠敤 ',
+ 'Data store reference': '鏁版嵁瀛樺偍寮曠敤 ',
+ Loop: '寰幆',
+ 'Ad-hoc': '鍗冲腑',
+ 'Create {type}': '鍒涘缓 {type}',
+ Task: '浠诲姟',
+ 'Send Task': '鍙戦�佷换鍔�',
+ 'Receive Task': '鎺ユ敹浠诲姟',
+ 'User Task': '鐢ㄦ埛浠诲姟',
+ 'Manual Task': '鎵嬪伐浠诲姟',
+ 'Business Rule Task': '涓氬姟瑙勫垯浠诲姟',
+ 'Service Task': '鏈嶅姟浠诲姟',
+ 'Script Task': '鑴氭湰浠诲姟',
+ 'Call Activity': '璋冪敤娲诲姩',
+ 'Sub-Process (collapsed)': '瀛愭祦绋嬶紙鎶樺彔鐨勶級',
+ 'Sub-Process (expanded)': '瀛愭祦绋嬶紙灞曞紑鐨勶級',
+ 'Ad-hoc sub-process': '鍗冲腑瀛愭祦绋�',
+ 'Ad-hoc sub-process (collapsed)': '鍗冲腑瀛愭祦绋嬶紙鎶樺彔鐨勶級',
+ 'Ad-hoc sub-process (expanded)': '鍗冲腑瀛愭祦绋嬶紙灞曞紑鐨勶級',
+ 'Start Event': '寮�濮嬩簨浠�',
+ StartEvent: '寮�濮嬩簨浠�',
+ 'Intermediate Throw Event': '涓棿浜嬩欢',
+ 'End Event': '缁撴潫浜嬩欢',
+ EndEvent: '缁撴潫浜嬩欢',
+ 'Create StartEvent': '鍒涘缓寮�濮嬩簨浠�',
+ 'Create EndEvent': '鍒涘缓缁撴潫浜嬩欢',
+ 'Create Task': '鍒涘缓浠诲姟',
+ 'Create User Task': '鍒涘缓鐢ㄦ埛浠诲姟',
+ 'Create Call Activity': '鍒涘缓璋冪敤娲诲姩',
+ 'Create Service Task': '鍒涘缓鏈嶅姟浠诲姟',
+ 'Create Gateway': '鍒涘缓缃戝叧',
+ 'Create DataObjectReference': '鍒涘缓鏁版嵁瀵硅薄',
+ 'Create DataStoreReference': '鍒涘缓鏁版嵁瀛樺偍',
+ 'Create Group': '鍒涘缓鍒嗙粍',
+ 'Create Intermediate/Boundary Event': '鍒涘缓涓棿/杈圭晫浜嬩欢',
+ 'Message Start Event': '娑堟伅寮�濮嬩簨浠�',
+ 'Timer Start Event': '瀹氭椂寮�濮嬩簨浠�',
+ 'Conditional Start Event': '鏉′欢寮�濮嬩簨浠�',
+ 'Signal Start Event': '淇″彿寮�濮嬩簨浠�',
+ 'Error Start Event': '閿欒寮�濮嬩簨浠�',
+ 'Escalation Start Event': '鍗囩骇寮�濮嬩簨浠�',
+ 'Compensation Start Event': '琛ュ伩寮�濮嬩簨浠�',
+ 'Message Start Event (non-interrupting)': '娑堟伅寮�濮嬩簨浠讹紙闈炰腑鏂級',
+ 'Timer Start Event (non-interrupting)': '瀹氭椂寮�濮嬩簨浠讹紙闈炰腑鏂級',
+ 'Conditional Start Event (non-interrupting)': '鏉′欢寮�濮嬩簨浠讹紙闈炰腑鏂級',
+ 'Signal Start Event (non-interrupting)': '淇″彿寮�濮嬩簨浠讹紙闈炰腑鏂級',
+ 'Escalation Start Event (non-interrupting)': '鍗囩骇寮�濮嬩簨浠讹紙闈炰腑鏂級',
+ 'Message Intermediate Catch Event': '娑堟伅涓棿鎹曡幏浜嬩欢',
+ 'Message Intermediate Throw Event': '娑堟伅涓棿鎶涘嚭浜嬩欢',
+ 'Timer Intermediate Catch Event': '瀹氭椂涓棿鎹曡幏浜嬩欢',
+ 'Escalation Intermediate Throw Event': '鍗囩骇涓棿鎶涘嚭浜嬩欢',
+ 'Conditional Intermediate Catch Event': '鏉′欢涓棿鎹曡幏浜嬩欢',
+ 'Link Intermediate Catch Event': '閾炬帴涓棿鎹曡幏浜嬩欢',
+ 'Link Intermediate Throw Event': '閾炬帴涓棿鎶涘嚭浜嬩欢',
+ 'Compensation Intermediate Throw Event': '琛ュ伩涓棿鎶涘嚭浜嬩欢',
+ 'Signal Intermediate Catch Event': '淇″彿涓棿鎹曡幏浜嬩欢',
+ 'Signal Intermediate Throw Event': '淇″彿涓棿鎶涘嚭浜嬩欢',
+ 'Message End Event': '娑堟伅缁撴潫浜嬩欢',
+ 'Escalation End Event': '瀹氭椂缁撴潫浜嬩欢',
+ 'Error End Event': '閿欒缁撴潫浜嬩欢',
+ 'Cancel End Event': '鍙栨秷缁撴潫浜嬩欢',
+ 'Compensation End Event': '琛ュ伩缁撴潫浜嬩欢',
+ 'Signal End Event': '淇″彿缁撴潫浜嬩欢',
+ 'Terminate End Event': '缁堟缁撴潫浜嬩欢',
+ 'Message Boundary Event': '娑堟伅杈圭晫浜嬩欢',
+ 'Message Boundary Event (non-interrupting)': '娑堟伅杈圭晫浜嬩欢锛堥潪涓柇锛�',
+ 'Timer Boundary Event': '瀹氭椂杈圭晫浜嬩欢',
+ 'Timer Boundary Event (non-interrupting)': '瀹氭椂杈圭晫浜嬩欢锛堥潪涓柇锛�',
+ 'Escalation Boundary Event': '鍗囩骇杈圭晫浜嬩欢',
+ 'Escalation Boundary Event (non-interrupting)': '鍗囩骇杈圭晫浜嬩欢锛堥潪涓柇锛�',
+ 'Conditional Boundary Event': '鏉′欢杈圭晫浜嬩欢',
+ 'Conditional Boundary Event (non-interrupting)': '鏉′欢杈圭晫浜嬩欢锛堥潪涓柇锛�',
+ 'Error Boundary Event': '閿欒杈圭晫浜嬩欢',
+ 'Cancel Boundary Event': '鍙栨秷杈圭晫浜嬩欢',
+ 'Signal Boundary Event': '淇″彿杈圭晫浜嬩欢',
+ 'Signal Boundary Event (non-interrupting)': '淇″彿杈圭晫浜嬩欢锛堥潪涓柇锛�',
+ 'Compensation Boundary Event': '琛ュ伩杈圭晫浜嬩欢',
+ 'Exclusive Gateway': '浜掓枼缃戝叧',
+ 'Parallel Gateway': '骞惰缃戝叧',
+ 'Inclusive Gateway': '鐩稿缃戝叧',
+ 'Complex Gateway': '澶嶆潅缃戝叧',
+ 'Event-based Gateway': '浜嬩欢缃戝叧',
+ Transaction: '杞繍',
+ 'sub-process': '瀛愭祦绋�',
+ 'Event sub-process': '浜嬩欢瀛愭祦绋�',
+ 'Collapsed Pool': '鎶樺彔姹�',
+ 'Expanded Pool': '灞曞紑姹�',
+
+ // Errors
+ 'no parent for {element} in {parent}': '鍦▄parent}閲岋紝{element}娌℃湁鐖剁被',
+ 'no shape type specified': '娌℃湁鎸囧畾鐨勫舰鐘剁被鍨�',
+ 'flow elements must be children of pools/participants': '娴佸厓绱犲繀椤绘槸姹�/鍙備笌鑰呯殑瀛愮被',
+ 'out of bounds release': 'out of bounds release',
+ 'more than {count} child lanes': '瀛愰亾澶т簬{count} ',
+ 'element required': '鍏冪礌涓嶈兘涓虹┖',
+ 'diagram not part of bpmn:Definitions': '娴佺▼鍥句笉绗﹀悎bpmn瑙勮寖',
+ 'no diagram to display': '娌℃湁鍙睍绀虹殑娴佺▼鍥�',
+ 'no process or collaboration to display': '娌℃湁鍙睍绀虹殑娴佺▼/鍗忎綔',
+ 'element {element} referenced by {referenced}#{property} not yet drawn':
+ '鐢眥referenced}#{property}寮曠敤鐨剓element}鍏冪礌浠嶆湭缁樺埗',
+ 'already rendered {element}': '{element} 宸茶娓叉煋',
+ 'failed to import {element}': '瀵煎叆{element}澶辫触',
+ //灞炴�ч潰鏉跨殑鍙傛暟
+ Id: '缂栧彿',
+ Name: '鍚嶇О',
+ General: '甯歌',
+ Details: '璇︽儏',
+ 'Message Name': '娑堟伅鍚嶇О',
+ Message: '娑堟伅',
+ Initiator: '鍒涘缓鑰�',
+ 'Asynchronous Continuations': '鎸佺画寮傛',
+ 'Asynchronous Before': '寮傛鍓�',
+ 'Asynchronous After': '寮傛鍚�',
+ 'Job Configuration': '宸ヤ綔閰嶇疆',
+ Exclusive: '鎺掗櫎',
+ 'Job Priority': '宸ヤ綔浼樺厛绾�',
+ 'Retry Time Cycle': '閲嶈瘯鏃堕棿鍛ㄦ湡',
+ Documentation: '鏂囨。',
+ 'Element Documentation': '鍏冪礌鏂囨。',
+ 'History Configuration': '鍘嗗彶閰嶇疆',
+ 'History Time To Live': '鍘嗗彶鐨勭敓瀛樻椂闂�',
+ Forms: '琛ㄥ崟',
+ 'Form Key': '琛ㄥ崟key',
+ 'Form Fields': '琛ㄥ崟瀛楁',
+ 'Business Key': '涓氬姟key',
+ 'Form Field': '琛ㄥ崟瀛楁',
+ ID: '缂栧彿',
+ Type: '绫诲瀷',
+ Label: '鍚嶇О',
+ 'Default Value': '榛樿鍊�',
+ 'Default Flow': '榛樿娴佽浆璺緞',
+ 'Conditional Flow': '鏉′欢娴佽浆璺緞',
+ 'Sequence Flow': '鏅�氭祦杞矾寰�',
+ Validation: '鏍¢獙',
+ 'Add Constraint': '娣诲姞绾︽潫',
+ Config: '閰嶇疆',
+ Properties: '灞炴��',
+ 'Add Property': '娣诲姞灞炴��',
+ Value: '鍊�',
+ Listeners: '鐩戝惉鍣�',
+ 'Execution Listener': '鎵ц鐩戝惉',
+ 'Event Type': '浜嬩欢绫诲瀷',
+ 'Listener Type': '鐩戝惉鍣ㄧ被鍨�',
+ 'Java Class': 'Java绫�',
+ Expression: '琛ㄨ揪寮�',
+ 'Must provide a value': '蹇呴』鎻愪緵涓�涓��',
+ 'Delegate Expression': '浠g悊琛ㄨ揪寮�',
+ Script: '鑴氭湰',
+ 'Script Format': '鑴氭湰鏍煎紡',
+ 'Script Type': '鑴氭湰绫诲瀷',
+ 'Inline Script': '鍐呰仈鑴氭湰',
+ 'External Script': '澶栭儴鑴氭湰',
+ Resource: '璧勬簮',
+ 'Field Injection': '瀛楁娉ㄥ叆',
+ Extensions: '鎵╁睍',
+ 'Input/Output': '杈撳叆/杈撳嚭',
+ 'Input Parameters': '杈撳叆鍙傛暟',
+ 'Output Parameters': '杈撳嚭鍙傛暟',
+ Parameters: '鍙傛暟',
+ 'Output Parameter': '杈撳嚭鍙傛暟',
+ 'Timer Definition Type': '瀹氭椂鍣ㄥ畾涔夌被鍨�',
+ 'Timer Definition': '瀹氭椂鍣ㄥ畾涔�',
+ Date: '鏃ユ湡',
+ Duration: '鎸佺画',
+ Cycle: '寰幆',
+ Signal: '淇″彿',
+ 'Signal Name': '淇″彿鍚嶇О',
+ Escalation: '鍗囩骇',
+ Error: '閿欒',
+ 'Link Name': '閾炬帴鍚嶇О',
+ Condition: '鏉′欢鍚嶇О',
+ 'Variable Name': '鍙橀噺鍚嶇О',
+ 'Variable Event': '鍙橀噺浜嬩欢',
+ 'Specify more than one variable change event as a comma separated list.':
+ '澶氫釜鍙橀噺浜嬩欢浠ラ�楀彿闅斿紑',
+ 'Wait for Completion': '绛夊緟瀹屾垚',
+ 'Activity Ref': '娲诲姩鍙傝��',
+ 'Version Tag': '鐗堟湰鏍囩',
+ Executable: '鍙墽琛屾枃浠�',
+ 'External Task Configuration': '鎵╁睍浠诲姟閰嶇疆',
+ 'Task Priority': '浠诲姟浼樺厛绾�',
+ External: '澶栭儴',
+ Connector: '杩炴帴鍣�',
+ 'Must configure Connector': '蹇呴』閰嶇疆杩炴帴鍣�',
+ 'Connector Id': '杩炴帴鍣ㄧ紪鍙�',
+ Implementation: '瀹炵幇鏂瑰紡',
+ 'Field Injections': '瀛楁娉ㄥ叆',
+ Fields: '瀛楁',
+ 'Result Variable': '缁撴灉鍙橀噺',
+ Topic: '涓婚',
+ 'Configure Connector': '閰嶇疆杩炴帴鍣�',
+ 'Input Parameter': '杈撳叆鍙傛暟',
+ Assignee: '浠g悊浜�',
+ 'Candidate Users': '鍊欓�夌敤鎴�',
+ 'Candidate Groups': '鍊欓�夌粍',
+ 'Due Date': '鍒版湡鏃堕棿',
+ 'Follow Up Date': '璺熻釜鏃ユ湡',
+ Priority: '浼樺厛绾�',
+ 'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
+ '璺熻釜鏃ユ湡蹇呴』绗﹀悎EL琛ㄨ揪寮忥紝濡傦細 ${someDate} ,鎴栬�呬竴涓狪SO鏍囧噯鏃ユ湡锛屽锛�2015-06-26T09:54:00',
+ 'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)':
+ '璺熻釜鏃ユ湡蹇呴』绗﹀悎EL琛ㄨ揪寮忥紝濡傦細 ${someDate} ,鎴栬�呬竴涓狪SO鏍囧噯鏃ユ湡锛屽锛�2015-06-26T09:54:00',
+ Variables: '鍙橀噺',
+ 'Candidate Starter Configuration': '鍊欓�変汉璧峰姩鍣ㄩ厤缃�',
+ 'Candidate Starter Groups': '鍊欓�変汉璧峰姩鍣ㄧ粍',
+ 'This maps to the process definition key.': '杩欐槧灏勫埌娴佺▼瀹氫箟閿��',
+ 'Candidate Starter Users': '鍊欓�変汉璧峰姩鍣ㄧ殑鐢ㄦ埛',
+ 'Specify more than one user as a comma separated list.': '鎸囧畾澶氫釜鐢ㄦ埛浣滀负閫楀彿鍒嗛殧鐨勫垪琛ㄣ��',
+ 'Tasklist Configuration': 'Tasklist閰嶇疆',
+ Startable: '鍚姩',
+ 'Specify more than one group as a comma separated list.': '鎸囧畾澶氫釜缁勪綔涓洪�楀彿鍒嗛殧鐨勫垪琛ㄣ��'
+}
diff --git a/src/components/bpmnProcessDesigner/package/index.ts b/src/components/bpmnProcessDesigner/package/index.ts
new file mode 100644
index 0000000..ce44a3c
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/index.ts
@@ -0,0 +1,11 @@
+import MyProcessDesigner from './designer'
+import MyProcessPenal from './penal'
+import MyProcessViewer from './designer/index2'
+
+import './theme/index.scss'
+import 'bpmn-js/dist/assets/diagram-js.css'
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
+import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
+
+export { MyProcessDesigner, MyProcessPenal, MyProcessViewer }
diff --git a/src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue b/src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue
new file mode 100644
index 0000000..ba97d96
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/palette/ProcessPalette.vue
@@ -0,0 +1,45 @@
+<template>
+ <div class="my-process-palette">
+ <div class="test-button" @click="addTask" @mousedown="addTask">娴嬭瘯浠诲姟</div>
+ <div class="test-container" id="palette-container">1</div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { assign } from 'min-dash'
+
+defineOptions({ name: 'MyProcessPalette' })
+
+const bpmnInstances = () => (window as any).bpmnInstances
+const addTask = (event, options: any = {}) => {
+ const ElementFactory = bpmnInstances().elementFactory
+ const create = bpmnInstances().modeler.get('create')
+
+ console.log(ElementFactory, create)
+
+ const shape = ElementFactory.createShape(assign({ type: 'bpmn:UserTask' }, options))
+
+ if (options) {
+ shape.businessObject.di.isExpanded = options.isExpanded
+ }
+
+ console.log(event, 'event')
+ console.log(shape, 'shape')
+ create.start(event, shape)
+}
+</script>
+
+<style scoped lang="scss">
+.my-process-palette {
+ padding: 80px 20px 20px;
+ box-sizing: border-box;
+
+ .test-button {
+ padding: 8px 16px;
+ cursor: pointer;
+ border: 1px solid rgb(24 144 255 / 80%);
+ border-radius: 4px;
+ box-sizing: border-box;
+ }
+}
+</style>
diff --git a/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
new file mode 100644
index 0000000..a87698f
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
@@ -0,0 +1,312 @@
+<template>
+ <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '600px' }">
+ <el-collapse v-model="activeTab" v-if="isReady">
+ <el-collapse-item name="base">
+ <!-- class="panel-tab__title" -->
+ <template #title>
+ <Icon icon="ep:info-filled" />
+ 甯歌</template
+ >
+ <ElementBaseInfo
+ :id-edit-disabled="idEditDisabled"
+ :business-object="elementBusinessObject"
+ :type="elementType"
+ :model="model"
+ />
+ </el-collapse-item>
+ <el-collapse-item name="condition" v-if="elementType === 'Process'" key="message">
+ <template #title><Icon icon="ep:comment" />娑堟伅涓庝俊鍙�</template>
+ <signal-and-massage />
+ </el-collapse-item>
+ <el-collapse-item name="condition" v-if="conditionFormVisible" key="condition">
+ <template #title><Icon icon="ep:promotion" />娴佽浆鏉′欢</template>
+ <flow-condition :business-object="elementBusinessObject" :type="elementType" />
+ </el-collapse-item>
+ <el-collapse-item name="condition" v-if="formVisible" key="form">
+ <template #title><Icon icon="ep:list" />琛ㄥ崟</template>
+ <element-form :id="elementId" :type="elementType" />
+ </el-collapse-item>
+ <el-collapse-item name="task" v-if="isTaskCollapseItemShow(elementType)" key="task">
+ <template #title
+ ><Icon icon="ep:checked" />{{ getTaskCollapseItemName(elementType) }}</template
+ >
+ <element-task :id="elementId" :type="elementType" />
+ </el-collapse-item>
+ <el-collapse-item
+ name="multiInstance"
+ v-if="elementType.indexOf('Task') !== -1"
+ key="multiInstance"
+ >
+ <template #title><Icon icon="ep:help-filled" />澶氫汉瀹℃壒鏂瑰紡</template>
+ <element-multi-instance
+ :id="elementId"
+ :business-object="elementBusinessObject"
+ :type="elementType"
+ />
+ </el-collapse-item>
+ <el-collapse-item name="listeners" key="listeners">
+ <template #title><Icon icon="ep:bell-filled" />鎵ц鐩戝惉鍣�</template>
+ <element-listeners :id="elementId" :type="elementType" />
+ </el-collapse-item>
+ <el-collapse-item name="taskListeners" v-if="elementType === 'UserTask'" key="taskListeners">
+ <template #title><Icon icon="ep:bell-filled" />浠诲姟鐩戝惉鍣�</template>
+ <user-task-listeners :id="elementId" :type="elementType" />
+ </el-collapse-item>
+ <el-collapse-item name="extensions" key="extensions">
+ <template #title><Icon icon="ep:circle-plus-filled" />鎵╁睍灞炴��</template>
+ <element-properties :id="elementId" :type="elementType" />
+ </el-collapse-item>
+ <el-collapse-item name="other" key="other">
+ <template #title><Icon icon="ep:promotion" />鍏朵粬</template>
+ <element-other-config :id="elementId" />
+ </el-collapse-item>
+ <el-collapse-item name="customConfig" key="customConfig">
+ <template #title><Icon icon="ep:tools" />鑷畾涔夐厤缃�</template>
+ <element-custom-config
+ :id="elementId"
+ :type="elementType"
+ :business-object="elementBusinessObject"
+ />
+ </el-collapse-item>
+ <!-- 鏂板鐨勬椂闂翠簨浠堕厤缃」 -->
+ <el-collapse-item v-if="elementType === 'IntermediateCatchEvent'" name="timeEvent">
+ <template #title><Icon icon="ep:timer" />鏃堕棿浜嬩欢</template>
+ <!-- 鐩稿叧 issue锛歨ttps://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICNRW2 -->
+ <TimeEventConfig :businessObject="elementBusinessObject" :key="elementId" />
+ </el-collapse-item>
+ </el-collapse>
+ </div>
+</template>
+<script lang="ts" setup>
+import ElementBaseInfo from './base/ElementBaseInfo.vue'
+import ElementOtherConfig from './other/ElementOtherConfig.vue'
+import ElementTask from './task/ElementTask.vue'
+import ElementMultiInstance from './multi-instance/ElementMultiInstance.vue'
+import FlowCondition from './flow-condition/FlowCondition.vue'
+import SignalAndMassage from './signal-message/SignalAndMessage.vue'
+import ElementListeners from './listeners/ElementListeners.vue'
+import ElementProperties from './properties/ElementProperties.vue'
+// import ElementForm from './form/ElementForm.vue'
+import UserTaskListeners from './listeners/UserTaskListeners.vue'
+import { getTaskCollapseItemName, isTaskCollapseItemShow } from './task/data'
+import TimeEventConfig from './time-event-config/TimeEventConfig.vue'
+import { ref, watch, onMounted } from 'vue'
+
+defineOptions({ name: 'MyPropertiesPanel' })
+
+/**
+ * 渚ц竟鏍�
+ * @Author MiyueFE
+ * @Home https://github.com/miyuesc
+ * @Date 2021骞�3鏈�31鏃�18:57:51
+ */
+const props = defineProps({
+ bpmnModeler: {
+ type: Object,
+ default: () => {}
+ },
+ prefix: {
+ type: String,
+ default: 'camunda'
+ },
+ width: {
+ type: Number,
+ default: 480
+ },
+ idEditDisabled: {
+ type: Boolean,
+ default: false
+ },
+ model: Object // 娴佺▼妯″瀷鐨勬暟鎹�
+})
+
+const activeTab = ref('base')
+const elementId = ref('')
+const elementType = ref('')
+const elementBusinessObject = ref<any>({}) // 鍏冪礌 businessObject 闀滃儚锛屾彁渚涚粰闇�瑕佸仛鍒ゆ柇鐨勭粍浠朵娇鐢�
+const conditionFormVisible = ref(false) // 娴佽浆鏉′欢璁剧疆
+const formVisible = ref(false) // 琛ㄥ崟閰嶇疆
+const bpmnElement = ref()
+const isReady = ref(false)
+
+const type = ref('time')
+const condition = ref('')
+provide('prefix', props.prefix)
+provide('width', props.width)
+
+// 鍒濆鍖� bpmnInstances
+const initBpmnInstances = () => {
+ if (!props.bpmnModeler) return false
+ try {
+ const instances = {
+ modeler: props.bpmnModeler,
+ modeling: props.bpmnModeler.get('modeling'),
+ moddle: props.bpmnModeler.get('moddle'),
+ eventBus: props.bpmnModeler.get('eventBus'),
+ bpmnFactory: props.bpmnModeler.get('bpmnFactory'),
+ elementFactory: props.bpmnModeler.get('elementFactory'),
+ elementRegistry: props.bpmnModeler.get('elementRegistry'),
+ replace: props.bpmnModeler.get('replace'),
+ selection: props.bpmnModeler.get('selection')
+ }
+
+ // 妫�鏌ユ墍鏈夊疄渚嬫槸鍚﹂兘瀛樺湪
+ const allInstancesExist = Object.values(instances).every((instance) => instance)
+ if (allInstancesExist) {
+ const w = window as any
+ w.bpmnInstances = instances
+ return true
+ }
+ return false
+ } catch (error) {
+ console.error('鍒濆鍖� bpmnInstances 澶辫触:', error)
+ return false
+ }
+}
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+// 鐩戝惉 props.bpmnModeler 鐒跺悗 initModels
+const unwatchBpmn = watch(
+ () => props.bpmnModeler,
+ async () => {
+ // 閬垮厤鍔犺浇鏃� 娴佺▼鍥� 骞舵湭鍔犺浇瀹屾垚
+ if (!props.bpmnModeler) {
+ console.log('缂哄皯props.bpmnModeler')
+ return
+ }
+
+ try {
+ // 绛夊緟 modeler 鍒濆鍖栧畬鎴�
+ await nextTick()
+ if (initBpmnInstances()) {
+ isReady.value = true
+ await nextTick()
+ getActiveElement()
+ } else {
+ console.error('modeler 瀹炰緥鏈畬鍏ㄥ垵濮嬪寲')
+ }
+ } catch (error) {
+ console.error('鍒濆鍖栧け璐�:', error)
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+const getActiveElement = () => {
+ if (!isReady.value || !props.bpmnModeler) return
+
+ // 鍒濆绗竴涓�変腑鍏冪礌 bpmn:Process
+ initFormOnChanged(null)
+ props.bpmnModeler.on('import.done', (e) => {
+ console.log(e, 'eeeee')
+ initFormOnChanged(null)
+ })
+ // 鐩戝惉閫夋嫨浜嬩欢锛屼慨鏀瑰綋鍓嶆縺娲荤殑鍏冪礌浠ュ強琛ㄥ崟
+ props.bpmnModeler.on('selection.changed', ({ newSelection }) => {
+ initFormOnChanged(newSelection[0] || null)
+ })
+ props.bpmnModeler.on('element.changed', ({ element }) => {
+ // 淇濊瘉 淇敼 "榛樿娴佽浆璺緞" 绫讳技闇�瑕佷慨鏀瑰涓厓绱犵殑浜嬩欢鍙戠敓鐨勬椂鍊欙紝鏇存柊琛ㄥ崟鐨勫厓绱犱笌鍘熼�変腑鍏冪礌涓嶄竴鑷淬��
+ if (element && element.id === elementId.value) {
+ initFormOnChanged(element)
+ }
+ })
+}
+
+// 鍒濆鍖栨暟鎹�
+const initFormOnChanged = (element) => {
+ if (!isReady.value || !bpmnInstances()) return
+
+ let activatedElement = element
+ if (!activatedElement) {
+ activatedElement =
+ bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Process') ??
+ bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration')
+ }
+ if (!activatedElement) return
+
+ try {
+ console.log(`
+ ----------
+ select element changed:
+ id: ${activatedElement.id}
+ type: ${activatedElement.businessObject.$type}
+ ----------
+ `)
+ console.log('businessObject: ', activatedElement.businessObject)
+ bpmnInstances().bpmnElement = activatedElement
+ bpmnElement.value = activatedElement
+ elementId.value = activatedElement.id
+ elementType.value = activatedElement.type.split(':')[1] || ''
+ elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject))
+ conditionFormVisible.value = !!(
+ elementType.value === 'SequenceFlow' &&
+ activatedElement.source &&
+ activatedElement.source.type.indexOf('StartEvent') === -1
+ )
+ formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
+ } catch (error) {
+ console.error('鍒濆鍖栬〃鍗曟暟鎹け璐�:', error)
+ }
+}
+
+onBeforeUnmount(() => {
+ const w = window as any
+ w.bpmnInstances = null
+ isReady.value = false
+})
+
+watch(
+ () => elementId.value,
+ () => {
+ activeTab.value = 'base'
+ }
+)
+
+function updateNode() {
+ const moddle = window.bpmnInstances?.moddle
+ const modeling = window.bpmnInstances?.modeling
+ const elementRegistry = window.bpmnInstances?.elementRegistry
+ if (!moddle || !modeling || !elementRegistry) return
+
+ const element = elementRegistry.get(props.businessObject.id)
+ if (!element) return
+
+ let timerDef = moddle.create('bpmn:TimerEventDefinition', {})
+ if (type.value === 'time') {
+ timerDef.timeDate = moddle.create('bpmn:FormalExpression', { body: condition.value })
+ } else if (type.value === 'duration') {
+ timerDef.timeDuration = moddle.create('bpmn:FormalExpression', { body: condition.value })
+ } else if (type.value === 'cycle') {
+ timerDef.timeCycle = moddle.create('bpmn:FormalExpression', { body: condition.value })
+ }
+
+ modeling.updateModdleProperties(element, element.businessObject, {
+ eventDefinitions: [timerDef]
+ })
+}
+
+// 鍒濆鍖栧拰鐩戝惉
+function syncFromBusinessObject() {
+ if (props.businessObject) {
+ const timerDef = (props.businessObject.eventDefinitions || [])[0]
+ if (timerDef) {
+ if (timerDef.timeDate) {
+ type.value = 'time'
+ condition.value = timerDef.timeDate.body
+ } else if (timerDef.timeDuration) {
+ type.value = 'duration'
+ condition.value = timerDef.timeDuration.body
+ } else if (timerDef.timeCycle) {
+ type.value = 'cycle'
+ condition.value = timerDef.timeCycle.body
+ }
+ }
+ }
+}
+onMounted(syncFromBusinessObject)
+watch(() => props.businessObject, syncFromBusinessObject, { deep: true })
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
new file mode 100644
index 0000000..3172338
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/base/ElementBaseInfo.vue
@@ -0,0 +1,183 @@
+<template>
+ <div class="panel-tab__content">
+ <el-form label-width="90px" :model="needProps" :rules="rules">
+ <div v-if="needProps.type == 'bpmn:Process'">
+ <!-- 濡傛灉鏄� Process 淇℃伅鐨勬椂鍊欙紝浣跨敤鑷畾涔夎〃鍗� -->
+ <el-form-item label="娴佺▼鏍囪瘑" prop="id">
+ <el-input
+ v-model="needProps.id"
+ placeholder="璇疯緭鍏ユ祦鏍囨爣璇�"
+ :disabled="needProps.id !== undefined && needProps.id.length > 0"
+ @change="handleKeyUpdate"
+ />
+ </el-form-item>
+ <el-form-item label="娴佺▼鍚嶇О" prop="name">
+ <el-input
+ v-model="needProps.name"
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ clearable
+ @change="handleNameUpdate"
+ />
+ </el-form-item>
+ </div>
+ <div v-else>
+ <el-form-item label="ID">
+ <el-input v-model="elementBaseInfo.id" clearable @change="updateBaseInfo('id')" />
+ </el-form-item>
+ <el-form-item label="鍚嶇О">
+ <el-input v-model="elementBaseInfo.name" clearable @change="updateBaseInfo('name')" />
+ </el-form-item>
+ </div>
+ </el-form>
+ </div>
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'ElementBaseInfo' })
+
+const props = defineProps({
+ businessObject: {
+ type: Object,
+ default: () => {}
+ },
+ model: {
+ type: Object,
+ default: () => {}
+ }
+})
+const needProps = ref<any>({})
+const bpmnElement = ref()
+const elementBaseInfo = ref<any>({})
+// 娴佺▼琛ㄥ崟鐨勪笅鎷夋鐨勬暟鎹�
+// const forms = ref([])
+// 娴佺▼妯″瀷鐨勬牎楠�
+const rules = reactive({
+ id: [{ required: true, message: '娴佺▼鏍囪瘑涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '娴佺▼鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetBaseInfo = () => {
+ console.log(window, 'window')
+ console.log(bpmnElement.value, 'bpmnElement')
+
+ bpmnElement.value = bpmnInstances()?.bpmnElement
+ // console.log(bpmnElement.value, 'resetBaseInfo11111111111')
+ elementBaseInfo.value = bpmnElement.value.businessObject
+ needProps.value['type'] = bpmnElement.value.businessObject.$type
+ // elementBaseInfo.value['typess'] = bpmnElement.value.businessObject.$type
+
+ // elementBaseInfo.value = JSON.parse(JSON.stringify(bpmnElement.value.businessObject))
+ // console.log(elementBaseInfo.value, 'elementBaseInfo22222222222')
+}
+const handleKeyUpdate = (value) => {
+ // 鏍¢獙 value 鐨勫�硷紝鍙湁 XML NCName 閫氳繃鐨勬儏鍐典笅锛屾墠杩涜璧嬪�笺�傚惁鍒欙紝浼氬鑷存祦绋嬪浘鎶ラ敊锛屾棤娉曠粯鍒剁殑闂
+ if (!value) {
+ return
+ }
+ if (!value.match(/[a-zA-Z_][\-_.0-9a-zA-Z$]*/)) {
+ console.log('key 涓嶆弧瓒� XML NCName 瑙勫垯锛屾墍浠ヤ笉杩涜璧嬪��')
+ return
+ }
+ console.log('key 婊¤冻 XML NCName 瑙勫垯锛屾墍浠ヨ繘琛岃祴鍊�')
+
+ // 鍦� BPMN 鐨� XML 涓紝娴佺▼鏍囪瘑 key锛屽叾瀹炲搴旂殑鏄� id 鑺傜偣
+ elementBaseInfo.value['id'] = value
+
+ setTimeout(() => {
+ updateBaseInfo('id')
+ }, 100)
+}
+const handleNameUpdate = (value) => {
+ console.log(elementBaseInfo, 'elementBaseInfo')
+ if (!value) {
+ return
+ }
+ elementBaseInfo.value['name'] = value
+
+ setTimeout(() => {
+ updateBaseInfo('name')
+ }, 100)
+}
+// const handleDescriptionUpdate=(value)=> {
+// TODO 鑺嬭壙锛歞ocumentation 鏆傛椂鏃犳硶淇敼锛屽悗缁湪鐪嬬湅
+// this.elementBaseInfo['documentation'] = value;
+// this.updateBaseInfo('documentation');
+// }
+const updateBaseInfo = (key) => {
+ console.log(key, 'key')
+ // 瑙﹀彂 elementBaseInfo 瀵瑰簲鐨勫瓧娈�
+ const attrObj = Object.create(null)
+ // console.log(attrObj, 'attrObj')
+ attrObj[key] = elementBaseInfo.value[key]
+ // console.log(attrObj, 'attrObj111')
+ // const attrObj = {
+ // id: elementBaseInfo.value[key]
+ // // di: { id: `${elementBaseInfo.value[key]}_di` }
+ // }
+ // console.log(elementBaseInfo, 'elementBaseInfo11111111111')
+ needProps.value = { ...elementBaseInfo.value, ...needProps.value }
+
+ if (key === 'id') {
+ // console.log('jinru')
+ console.log(window, 'window')
+ console.log(bpmnElement.value, 'bpmnElement')
+ console.log(toRaw(bpmnElement.value), 'bpmnElement')
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ id: elementBaseInfo.value[key],
+ di: { id: `${elementBaseInfo.value[key]}_di` }
+ })
+ } else {
+ console.log(attrObj, 'attrObj')
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), attrObj)
+ }
+}
+
+watch(
+ () => props.businessObject,
+ (val) => {
+ // console.log(val, 'val11111111111111111111')
+ if (val) {
+ // nextTick(() => {
+ resetBaseInfo()
+ // })
+ }
+ }
+)
+
+watch(
+ () => props.model?.key,
+ (val) => {
+ // 閽堝涓婁紶鐨� bpmn 娴佺▼鍥炬椂锛屼繚璇� key 鍜� name 鐨勬洿鏂�
+ if (val) {
+ handleKeyUpdate(props.model.key)
+ handleNameUpdate(props.model.name)
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+// watch(
+// () => ({ ...props }),
+// (oldVal, newVal) => {
+// console.log(oldVal, 'oldVal')
+// console.log(newVal, 'newVal')
+// if (newVal) {
+// needProps.value = newVal
+// }
+// },
+// {
+// immediate: true
+// }
+// )
+// 'model.key': {
+// immediate: false,
+// handler: function (val) {
+// this.handleKeyUpdate(val)
+// }
+// }
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+})
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue
new file mode 100644
index 0000000..f9cb9ac
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue
@@ -0,0 +1,39 @@
+<template>
+ <div class="panel-tab__content">
+ <component :is="customConfigComponent" v-bind="$props" />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { CustomConfigMap } from './data'
+
+defineOptions({ name: 'ElementCustomConfig' })
+
+const props = defineProps({
+ id: String,
+ type: String,
+ businessObject: {
+ type: Object,
+ default: () => {}
+ }
+})
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const customConfigComponent = ref<any>(null)
+
+watch(
+ () => props.businessObject,
+ () => {
+ if (props.type && props.businessObject) {
+ let val = props.type
+ if (props.businessObject.eventDefinitions) {
+ val += props.businessObject.eventDefinitions[0]?.$type.split(':')[1] || ''
+ }
+ customConfigComponent.value = CustomConfigMap[val]?.componet
+ }
+ },
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue
new file mode 100644
index 0000000..77948ce
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue
@@ -0,0 +1,263 @@
+<template>
+ <div>
+ <el-divider content-position="left">瀹℃壒浜鸿秴鏃舵湭澶勭悊鏃�</el-divider>
+ <el-form-item label="鍚敤寮�鍏�" prop="timeoutHandlerEnable">
+ <el-switch
+ v-model="timeoutHandlerEnable"
+ active-text="寮�鍚�"
+ inactive-text="鍏抽棴"
+ @change="timeoutHandlerChange"
+ />
+ </el-form-item>
+ <el-form-item label="鎵ц鍔ㄤ綔" prop="timeoutHandlerType" v-if="timeoutHandlerEnable">
+ <el-radio-group v-model="timeoutHandlerType.value" @change="onTimeoutHandlerTypeChanged">
+ <el-radio-button
+ v-for="item in TIMEOUT_HANDLER_TYPES"
+ :key="item.value"
+ :value="item.value"
+ :label="item.label"
+ />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="瓒呮椂鏃堕棿璁剧疆" v-if="timeoutHandlerEnable">
+ <span class="mr-2">褰撹秴杩�</span>
+ <el-form-item prop="timeDuration">
+ <el-input-number
+ class="mr-2"
+ :style="{ width: '100px' }"
+ v-model="timeDuration"
+ :min="1"
+ controls-position="right"
+ @change="
+ () => {
+ updateTimeModdle()
+ updateElementExtensions()
+ }
+ "
+ />
+ </el-form-item>
+ <el-select
+ v-model="timeUnit"
+ class="mr-2"
+ :style="{ width: '100px' }"
+ @change="onTimeUnitChange"
+ >
+ <el-option
+ v-for="item in TIME_UNIT_TYPES"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ 鏈鐞�
+ </el-form-item>
+ <el-form-item
+ label="鏈�澶ф彁閱掓鏁�"
+ prop="maxRemindCount"
+ v-if="timeoutHandlerEnable && timeoutHandlerType.value === 1"
+ >
+ <el-input-number
+ v-model="maxRemindCount"
+ :min="1"
+ :max="10"
+ @change="
+ () => {
+ updateTimeModdle()
+ updateElementExtensions()
+ }
+ "
+ />
+ </el-form-item>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import {
+ TimeUnitType,
+ TIME_UNIT_TYPES,
+ TIMEOUT_HANDLER_TYPES
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import { convertTimeUnit } from '@/components/SimpleProcessDesignerV2/src/utils'
+
+defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const timeoutHandlerEnable = ref(false)
+const boundaryEventType = ref()
+const timeoutHandlerType = ref({
+ value: undefined
+})
+const timeModdle = ref()
+const timeDuration = ref(6)
+const timeUnit = ref(TimeUnitType.HOUR)
+const maxRemindCount = ref(1)
+
+const elExtensionElements = ref()
+const otherExtensions = ref()
+const configExtensions = ref([])
+const eventDefinition = ref()
+
+const resetElement = () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ eventDefinition.value = bpmnElement.value.businessObject.eventDefinitions[0]
+
+ // 鑾峰彇鍏冪礌鎵╁睍灞炴�� 鎴栬�� 鍒涘缓鎵╁睍灞炴��
+ elExtensionElements.value =
+ bpmnElement.value.businessObject?.extensionElements ??
+ bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+
+ // 鏄惁寮�鍚嚜瀹氫箟鐢ㄦ埛浠诲姟瓒呮椂澶勭悊
+ boundaryEventType.value = elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:BoundaryEventType`
+ )?.[0]
+ if (boundaryEventType.value && boundaryEventType.value.value === 1) {
+ timeoutHandlerEnable.value = true
+ configExtensions.value.push(boundaryEventType.value)
+ }
+
+ // 鎵ц鍔ㄤ綔
+ timeoutHandlerType.value = elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:TimeoutHandlerType`
+ )?.[0]
+ if (timeoutHandlerType.value) {
+ configExtensions.value.push(timeoutHandlerType.value)
+ if (eventDefinition.value.timeCycle) {
+ const timeStr = eventDefinition.value.timeCycle.body
+ const maxRemindCountStr = timeStr.split('/')[0]
+ const timeDurationStr = timeStr.split('/')[1]
+ console.log(maxRemindCountStr)
+ maxRemindCount.value = parseInt(maxRemindCountStr.slice(1))
+ timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1))
+ timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1))
+ timeModdle.value = eventDefinition.value.timeCycle
+ }
+ if (eventDefinition.value.timeDuration) {
+ const timeDurationStr = eventDefinition.value.timeDuration.body
+ timeDuration.value = parseInt(timeDurationStr.slice(2, timeDurationStr.length - 1))
+ timeUnit.value = convertTimeUnit(timeDurationStr.slice(timeDurationStr.length - 1))
+ timeModdle.value = eventDefinition.value.timeDuration
+ }
+ }
+
+ // 淇濈暀鍓╀綑鎵╁睍鍏冪礌锛屼究浜庡悗闈㈡洿鏂拌鍏冪礌瀵瑰簲灞炴��
+ otherExtensions.value =
+ elExtensionElements.value.values?.filter(
+ (ex) =>
+ ex.$type !== `${prefix}:BoundaryEventType` && ex.$type !== `${prefix}:TimeoutHandlerType`
+ ) ?? []
+}
+
+const timeoutHandlerChange = (val) => {
+ timeoutHandlerEnable.value = val
+ if (val) {
+ // 鍚敤鑷畾涔夌敤鎴蜂换鍔¤秴鏃跺鐞�
+ // 杈圭晫浜嬩欢绫诲瀷 --- 瓒呮椂
+ boundaryEventType.value = bpmnInstances().moddle.create(`${prefix}:BoundaryEventType`, {
+ value: 1
+ })
+ configExtensions.value.push(boundaryEventType.value)
+ // 瓒呮椂澶勭悊绫诲瀷
+ timeoutHandlerType.value = bpmnInstances().moddle.create(`${prefix}:TimeoutHandlerType`, {
+ value: 1
+ })
+ configExtensions.value.push(timeoutHandlerType.value)
+ // 瓒呮椂鏃堕棿琛ㄨ揪寮�
+ timeDuration.value = 6
+ timeUnit.value = 2
+ maxRemindCount.value = 1
+ timeModdle.value = bpmnInstances().moddle.create(`bpmn:Expression`, {
+ body: 'PT6H'
+ })
+ eventDefinition.value.timeDuration = timeModdle.value
+ } else {
+ // 鍏抽棴鑷畾涔夌敤鎴蜂换鍔¤秴鏃跺鐞�
+ configExtensions.value = []
+ delete eventDefinition.value.timeDuration
+ delete eventDefinition.value.timeCycle
+ }
+ updateElementExtensions()
+}
+
+const onTimeoutHandlerTypeChanged = () => {
+ maxRemindCount.value = 1
+ updateElementExtensions()
+ updateTimeModdle()
+}
+
+const onTimeUnitChange = () => {
+ // 鍒嗛挓锛岄粯璁ゆ槸 60 鍒嗛挓
+ if (timeUnit.value === TimeUnitType.MINUTE) {
+ timeDuration.value = 60
+ }
+ // 灏忔椂锛岄粯璁ゆ槸 6 涓皬鏃�
+ if (timeUnit.value === TimeUnitType.HOUR) {
+ timeDuration.value = 6
+ }
+ // 澶╋紝 榛樿 1澶�
+ if (timeUnit.value === TimeUnitType.DAY) {
+ timeDuration.value = 1
+ }
+ updateTimeModdle()
+ updateElementExtensions()
+}
+
+const updateTimeModdle = () => {
+ if (maxRemindCount.value > 1) {
+ timeModdle.value.body = 'R' + maxRemindCount.value + '/' + isoTimeDuration()
+ if (!eventDefinition.value.timeCycle) {
+ delete eventDefinition.value.timeDuration
+ eventDefinition.value.timeCycle = timeModdle.value
+ }
+ } else {
+ timeModdle.value.body = isoTimeDuration()
+ if (!eventDefinition.value.timeDuration) {
+ delete eventDefinition.value.timeCycle
+ eventDefinition.value.timeDuration = timeModdle.value
+ }
+ }
+}
+
+const isoTimeDuration = () => {
+ let strTimeDuration = 'PT'
+ if (timeUnit.value === TimeUnitType.MINUTE) {
+ strTimeDuration += timeDuration.value + 'M'
+ }
+ if (timeUnit.value === TimeUnitType.HOUR) {
+ strTimeDuration += timeDuration.value + 'H'
+ }
+ if (timeUnit.value === TimeUnitType.DAY) {
+ strTimeDuration += timeDuration.value + 'D'
+ }
+ return strTimeDuration
+}
+
+const updateElementExtensions = () => {
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [...otherExtensions.value, ...configExtensions.value]
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: extensions
+ })
+}
+
+watch(
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ resetElement()
+ })
+ },
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
new file mode 100644
index 0000000..ac495ff
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTaskCustomConfig.vue
@@ -0,0 +1,688 @@
+<!-- UserTask 鑷畾涔夐厤缃細
+ 1. 瀹℃壒浜轰笌鎻愪氦浜轰负鍚屼竴浜烘椂
+ 2. 瀹℃壒浜烘嫆缁濇椂
+ 3. 瀹℃壒浜轰负绌烘椂
+ 4. 鎿嶄綔鎸夐挳
+ 5. 瀛楁鏉冮檺
+ 6. 瀹℃壒绫诲瀷
+ 7. 鏄惁闇�瑕佺鍚�
+-->
+<template>
+ <div>
+ <el-divider content-position="left">瀹℃壒绫诲瀷</el-divider>
+ <el-form-item prop="approveType">
+ <el-radio-group v-model="approveType.value">
+ <el-radio
+ v-for="(item, index) in APPROVE_TYPE"
+ :key="index"
+ :value="item.value"
+ :label="item.value"
+ >
+ {{ item.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+
+ <el-divider content-position="left">瀹℃壒浜烘嫆缁濇椂</el-divider>
+ <el-form-item prop="rejectHandlerType">
+ <el-radio-group
+ v-model="rejectHandlerType"
+ :disabled="returnTaskList.length === 0"
+ @change="updateRejectHandlerType"
+ >
+ <div class="flex-col">
+ <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </div>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+ label="椹冲洖鑺傜偣"
+ prop="returnNodeId"
+ >
+ <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
+ <el-option
+ v-for="item in returnTaskList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-divider content-position="left">瀹℃壒浜轰负绌烘椂</el-divider>
+ <el-form-item prop="assignEmptyHandlerType">
+ <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
+ <div class="flex-col">
+ <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </div>
+ </div>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+ label="鎸囧畾鐢ㄦ埛"
+ prop="assignEmptyHandlerUserIds"
+ span="24"
+ >
+ <el-select
+ v-model="assignEmptyUserIds"
+ clearable
+ multiple
+ style="width: 100%"
+ @change="updateAssignEmptyUserIds"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-divider content-position="left">瀹℃壒浜轰笌鎻愪氦浜轰负鍚屼竴浜烘椂</el-divider>
+ <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
+ <div class="flex-col">
+ <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+ <el-radio :key="item.value" :value="item.value" :label="item.label" />
+ </div>
+ </div>
+ </el-radio-group>
+
+ <el-divider content-position="left">鎿嶄綔鎸夐挳</el-divider>
+ <div class="button-setting-pane">
+ <div class="button-setting-title">
+ <div class="button-title-label">鎿嶄綔鎸夐挳</div>
+ <div class="pl-4 button-title-label">鏄剧ず鍚嶇О</div>
+ <div class="button-title-label">鍚敤</div>
+ </div>
+ <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index">
+ <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
+ <div class="button-setting-item-label">
+ <input
+ type="text"
+ class="editable-title-input"
+ @blur="btnDisplayNameBlurEvent(index)"
+ v-mountedFocus
+ v-model="item.displayName"
+ :placeholder="item.displayName"
+ v-if="btnDisplayNameEdit[index]"
+ />
+ <el-button v-else text @click="changeBtnDisplayName(index)"
+ >{{ item.displayName }} <Icon icon="ep:edit"
+ /></el-button>
+ </div>
+ <div class="button-setting-item-label">
+ <el-switch v-model="item.enable" @change="updateElementExtensions" />
+ </div>
+ </div>
+ </div>
+
+ <el-divider content-position="left">瀛楁鏉冮檺</el-divider>
+ <div class="field-setting-pane" v-if="formType === BpmModelFormType.NORMAL">
+ <div class="field-permit-title">
+ <div class="setting-title-label first-title"> 瀛楁鍚嶇О </div>
+ <div class="other-titles">
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')"
+ >鍙</span
+ >
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')"
+ >鍙紪杈�</span
+ >
+ <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')"
+ >闅愯棌</span
+ >
+ </div>
+ </div>
+ <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index">
+ <div class="field-setting-item-label"> {{ item.title }} </div>
+ <el-radio-group class="field-setting-item-group" v-model="item.permission">
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.READ"
+ size="large"
+ :label="FieldPermissionType.READ"
+ @change="updateElementExtensions"
+ >
+ <span></span>
+ </el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.WRITE"
+ size="large"
+ :label="FieldPermissionType.WRITE"
+ @change="updateElementExtensions"
+ >
+ <span></span>
+ </el-radio>
+ </div>
+ <div class="item-radio-wrap">
+ <el-radio
+ :value="FieldPermissionType.NONE"
+ size="large"
+ :label="FieldPermissionType.NONE"
+ @change="updateElementExtensions"
+ >
+ <span></span>
+ </el-radio>
+ </div>
+ </el-radio-group>
+ </div>
+ </div>
+
+ <el-divider content-position="left">鏄惁闇�瑕佺鍚�</el-divider>
+ <el-form-item prop="signEnable">
+ <el-switch
+ v-model="signEnable.value"
+ active-text="鏄�"
+ inactive-text="鍚�"
+ @change="updateElementExtensions"
+ />
+ </el-form-item>
+
+ <el-divider content-position="left">瀹℃壒鎰忚</el-divider>
+ <el-form-item prop="reasonRequire">
+ <el-switch
+ v-model="reasonRequire.value"
+ active-text="蹇呭~"
+ inactive-text="闈炲繀濉�"
+ @change="updateElementExtensions"
+ />
+ </el-form-item>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import {
+ ASSIGN_START_USER_HANDLER_TYPES,
+ RejectHandlerType,
+ REJECT_HANDLER_TYPES,
+ ASSIGN_EMPTY_HANDLER_TYPES,
+ AssignEmptyHandlerType,
+ OPERATION_BUTTON_NAME,
+ DEFAULT_BUTTON_SETTING,
+ FieldPermissionType,
+ APPROVE_TYPE,
+ ApproveType,
+ ButtonSetting
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import * as UserApi from '@/api/system/user'
+import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node'
+import { BpmModelFormType } from '@/utils/constants'
+
+defineOptions({ name: 'ElementCustomConfig4UserTask' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+
+// 瀹℃壒浜轰笌鎻愪氦浜轰负鍚屼竴浜烘椂
+const assignStartUserHandlerTypeEl = ref()
+const assignStartUserHandlerType = ref()
+
+// 瀹℃壒浜烘嫆缁濇椂
+const rejectHandlerTypeEl = ref()
+const rejectHandlerType = ref()
+const returnNodeIdEl = ref()
+const returnNodeId = ref()
+const returnTaskList = ref([])
+
+// 瀹℃壒浜轰负绌烘椂
+const assignEmptyHandlerTypeEl = ref()
+const assignEmptyHandlerType = ref()
+const assignEmptyUserIdsEl = ref()
+const assignEmptyUserIds = ref()
+
+// 鎿嶄綔鎸夐挳
+const buttonsSettingEl = ref()
+const { btnDisplayNameEdit, changeBtnDisplayName } = useButtonsSetting()
+const btnDisplayNameBlurEvent = (index: number) => {
+ btnDisplayNameEdit.value[index] = false
+ const buttonItem = buttonsSettingEl.value[index]
+ buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
+ updateElementExtensions()
+}
+
+// 瀛楁鏉冮檺
+const fieldsPermissionEl = ref([])
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+ FieldPermissionType.READ
+)
+
+// 瀹℃壒绫诲瀷
+const approveType = ref({ value: ApproveType.USER })
+
+// 鏄惁闇�瑕佺鍚�
+const signEnable = ref({ value: false })
+
+// 瀹℃壒鎰忚
+const reasonRequire = ref({ value: false })
+
+const elExtensionElements = ref()
+const otherExtensions = ref()
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetCustomConfigList = () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+
+ // 鑾峰彇鍙洖閫�鐨勫垪琛�
+ returnTaskList.value = findAllPredecessorsExcludingStart(
+ bpmnElement.value.id,
+ bpmnInstances().modeler
+ )
+ // 鑾峰彇鍏冪礌鎵╁睍灞炴�� 鎴栬�� 鍒涘缓鎵╁睍灞炴��
+ elExtensionElements.value =
+ bpmnElement.value.businessObject?.extensionElements ??
+ bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+
+ // 瀹℃壒绫诲瀷
+ approveType.value =
+ elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ApproveType`)?.[0] ||
+ bpmnInstances().moddle.create(`${prefix}:ApproveType`, { value: ApproveType.USER })
+
+ // 瀹℃壒浜轰笌鎻愪氦浜轰负鍚屼竴浜烘椂
+ assignStartUserHandlerTypeEl.value =
+ elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
+ assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
+
+ // 瀹℃壒浜烘嫆缁濇椂
+ rejectHandlerTypeEl.value =
+ elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:RejectHandlerType`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
+ rejectHandlerType.value = rejectHandlerTypeEl.value.value
+ returnNodeIdEl.value =
+ elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
+ returnNodeId.value = returnNodeIdEl.value.value
+
+ // 瀹℃壒浜轰负绌烘椂
+ assignEmptyHandlerTypeEl.value =
+ elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
+ assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
+ assignEmptyUserIdsEl.value =
+ elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
+ )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
+ assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value?.split(',').map((item) => {
+ // 濡傛灉鏁板瓧瓒呭嚭浜嗘渶澶у畨鍏ㄦ暣鏁拌寖鍥达紝鍒欏皢鍏朵綔涓哄瓧绗︿覆澶勭悊
+ let num = Number(item)
+ return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+ })
+
+ // 鎿嶄綔鎸夐挳
+ buttonsSettingEl.value = elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:ButtonsSetting`
+ )
+ if (buttonsSettingEl.value.length === 0) {
+ DEFAULT_BUTTON_SETTING.forEach((item) => {
+ buttonsSettingEl.value.push(
+ bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
+ 'flowable:id': item.id,
+ 'flowable:displayName': item.displayName,
+ 'flowable:enable': item.enable
+ })
+ )
+ })
+ }
+
+ // 瀛楁鏉冮檺
+ if (formType.value === BpmModelFormType.NORMAL) {
+ const fieldsPermissionList = elExtensionElements.value.values?.filter(
+ (ex) => ex.$type === `${prefix}:FieldsPermission`
+ )
+ fieldsPermissionEl.value = []
+ getNodeConfigFormFields()
+ fieldsPermissionConfig.value = fieldsPermissionConfig.value
+ fieldsPermissionConfig.value.forEach((element) => {
+ element.permission =
+ fieldsPermissionList?.find((obj) => obj.field === element.field)?.permission ?? '1'
+ fieldsPermissionEl.value.push(
+ bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, element)
+ )
+ })
+ }
+
+ // 鏄惁闇�瑕佺鍚�
+ signEnable.value =
+ elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:SignEnable`)?.[0] ||
+ bpmnInstances().moddle.create(`${prefix}:SignEnable`, { value: false })
+
+ // 瀹℃壒鎰忚
+ reasonRequire.value =
+ elExtensionElements.value.values?.filter((ex) => ex.$type === `${prefix}:ReasonRequire`)?.[0] ||
+ bpmnInstances().moddle.create(`${prefix}:ReasonRequire`, { value: false })
+
+ // 淇濈暀鍓╀綑鎵╁睍鍏冪礌锛屼究浜庡悗闈㈡洿鏂拌鍏冪礌瀵瑰簲灞炴��
+ otherExtensions.value =
+ elExtensionElements.value.values?.filter(
+ (ex) =>
+ ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
+ ex.$type !== `${prefix}:RejectHandlerType` &&
+ ex.$type !== `${prefix}:RejectReturnTaskId` &&
+ ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
+ ex.$type !== `${prefix}:AssignEmptyUserIds` &&
+ ex.$type !== `${prefix}:ButtonsSetting` &&
+ ex.$type !== `${prefix}:FieldsPermission` &&
+ ex.$type !== `${prefix}:ApproveType` &&
+ ex.$type !== `${prefix}:SignEnable` &&
+ ex.$type !== `${prefix}:ReasonRequire`
+ ) ?? []
+
+ // 鏇存柊鍏冪礌鎵╁睍灞炴�э紝閬垮厤鍚庣画鎶ラ敊
+ updateElementExtensions()
+}
+
+const updateAssignStartUserHandlerType = () => {
+ assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
+
+ updateElementExtensions()
+}
+
+const updateRejectHandlerType = () => {
+ rejectHandlerTypeEl.value.value = rejectHandlerType.value
+
+ returnNodeId.value = returnTaskList.value[0].id
+ returnNodeIdEl.value.value = returnNodeId.value
+
+ updateElementExtensions()
+}
+
+const updateReturnNodeId = () => {
+ returnNodeIdEl.value.value = returnNodeId.value
+
+ updateElementExtensions()
+}
+
+const updateAssignEmptyHandlerType = () => {
+ assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
+
+ updateElementExtensions()
+}
+
+const updateAssignEmptyUserIds = () => {
+ assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
+
+ updateElementExtensions()
+}
+
+const updateElementExtensions = () => {
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [
+ ...otherExtensions.value,
+ assignStartUserHandlerTypeEl.value,
+ rejectHandlerTypeEl.value,
+ returnNodeIdEl.value,
+ assignEmptyHandlerTypeEl.value,
+ assignEmptyUserIdsEl.value,
+ approveType.value,
+ ...buttonsSettingEl.value,
+ ...fieldsPermissionEl.value,
+ signEnable.value,
+ reasonRequire.value
+ ]
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: extensions
+ })
+}
+
+watch(
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ resetCustomConfigList()
+ })
+ },
+ { immediate: true }
+)
+
+function findAllPredecessorsExcludingStart(elementId, modeler) {
+ const elementRegistry = modeler.get('elementRegistry')
+ const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
+ const predecessors = new Set() // 浣跨敤 Set 鏉ラ伩鍏嶉噸澶嶈妭鐐�
+ const visited = new Set() // 鐢ㄤ簬璁板綍宸茶闂殑鑺傜偣
+
+ // 妫�鏌ユ槸鍚︽槸寮�濮嬩簨浠惰妭鐐�
+ function isStartEvent(element) {
+ return element.type === 'bpmn:StartEvent'
+ }
+
+ function findPredecessorsRecursively(element) {
+ // 濡傛灉璇ヨ妭鐐瑰凡缁忚闂繃锛岀洿鎺ヨ繑鍥烇紝閬垮厤寰幆
+ if (visited.has(element)) {
+ return
+ }
+
+ // 鏍囪褰撳墠鑺傜偣涓哄凡璁块棶
+ visited.add(element)
+
+ // 鑾峰彇涓庡綋鍓嶈妭鐐圭浉杩炵殑鎵�鏈夎繛鎺�
+ const incomingConnections = allConnections.filter((connection) => connection.target === element)
+
+ incomingConnections.forEach((connection) => {
+ const source = connection.source // 鑾峰彇鍓嶇疆鑺傜偣
+
+ // 鍙坊鍔犱笉鏄紑濮嬩簨浠剁殑鍓嶇疆鑺傜偣
+ if (!isStartEvent(source)) {
+ predecessors.add(source.businessObject)
+ // 閫掑綊鏌ユ壘鍓嶇疆鑺傜偣
+ findPredecessorsRecursively(source)
+ }
+ })
+ }
+
+ const targetElement = elementRegistry.get(elementId)
+ if (targetElement) {
+ findPredecessorsRecursively(targetElement)
+ }
+
+ return Array.from(predecessors) // 杩斿洖鍓嶇疆鑺傜偣鏁扮粍
+}
+
+function useButtonsSetting() {
+ const buttonsSetting = ref<ButtonSetting[]>()
+ // 鎿嶄綔鎸夐挳鏄剧ず鍚嶇О鍙紪杈�
+ const btnDisplayNameEdit = ref<boolean[]>([])
+ const changeBtnDisplayName = (index: number) => {
+ btnDisplayNameEdit.value[index] = true
+ }
+ return {
+ buttonsSetting,
+ btnDisplayNameEdit,
+ changeBtnDisplayName
+ }
+}
+
+/** 鎵归噺鏇存柊鏉冮檺 */
+// TODO @lesan锛氳繖涓〉闈紝鏈変竴浜� idea 绾㈣壊鎶ラ敊锛屽挶瑕佷笉瑕� fix 涓嬶紒
+const updatePermission = (type: string) => {
+ fieldsPermissionEl.value.forEach((field) => {
+ field.permission =
+ type === 'READ'
+ ? FieldPermissionType.READ
+ : type === 'WRITE'
+ ? FieldPermissionType.WRITE
+ : FieldPermissionType.NONE
+ })
+}
+
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+onMounted(async () => {
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
+
+<style lang="scss" scoped>
+.button-setting-pane {
+ display: flex;
+ margin-top: 8px;
+ font-size: 14px;
+ flex-direction: column;
+
+ .button-setting-desc {
+ padding-right: 8px;
+ margin-bottom: 16px;
+ font-size: 16px;
+ font-weight: 700;
+ }
+
+ .button-setting-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 45px;
+ padding-left: 12px;
+ background-color: #f8fafc0a;
+ border: 1px solid #1f38581a;
+
+ & > :first-child {
+ width: 100px !important;
+ text-align: left !important;
+ }
+
+ & > :last-child {
+ text-align: center !important;
+ }
+
+ .button-title-label {
+ width: 150px;
+ font-size: 13px;
+ font-weight: 700;
+ color: #000;
+ text-align: left;
+ }
+ }
+
+ .button-setting-item {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ height: 38px;
+ padding-left: 12px;
+ border: 1px solid #1f38581a;
+ border-top: 0;
+
+ & > :first-child {
+ width: 100px !important;
+ }
+
+ & > :last-child {
+ text-align: center !important;
+ }
+
+ .button-setting-item-label {
+ width: 150px;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .editable-title-input {
+ height: 24px;
+ max-width: 130px;
+ margin-left: 4px;
+ line-height: 24px;
+ border: 1px solid #d9d9d9;
+ border-radius: 4px;
+ transition: all 0.3s;
+
+ &:focus {
+ border-color: #40a9ff;
+ outline: 0;
+ box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+ }
+ }
+ }
+}
+
+.field-setting-pane {
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+
+ .field-setting-desc {
+ padding-right: 8px;
+ margin-bottom: 16px;
+ font-size: 16px;
+ font-weight: 700;
+ }
+
+ .field-permit-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 45px;
+ padding-left: 12px;
+ line-height: 45px;
+ background-color: #f8fafc0a;
+ border: 1px solid #1f38581a;
+
+ .first-title {
+ text-align: left !important;
+ }
+
+ .other-titles {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .setting-title-label {
+ display: inline-block;
+ width: 100px;
+ padding: 5px 0;
+ font-size: 13px;
+ font-weight: 700;
+ color: #000;
+ text-align: center;
+ }
+ }
+
+ .field-setting-item {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ height: 38px;
+ padding-left: 12px;
+ border: 1px solid #1f38581a;
+ border-top: 0;
+
+ .field-setting-item-label {
+ display: inline-block;
+ width: 100px;
+ min-height: 16px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ cursor: text;
+ }
+
+ .field-setting-item-group {
+ display: flex;
+ justify-content: space-between;
+
+ .item-radio-wrap {
+ display: inline-block;
+ width: 100px;
+ text-align: center;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts b/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts
new file mode 100644
index 0000000..a45355e
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts
@@ -0,0 +1,13 @@
+import UserTaskCustomConfig from './components/UserTaskCustomConfig.vue'
+import BoundaryEventTimer from './components/BoundaryEventTimer.vue'
+
+export const CustomConfigMap = {
+ UserTask: {
+ name: '鐢ㄦ埛浠诲姟',
+ componet: UserTaskCustomConfig
+ },
+ BoundaryEventTimerEventDefinition: {
+ name: '瀹氭椂杈圭晫浜嬩欢(闈炰腑鏂�)',
+ componet: BoundaryEventTimer
+ }
+}
diff --git a/src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue b/src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue
new file mode 100644
index 0000000..304630d
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/flow-condition/FlowCondition.vue
@@ -0,0 +1,191 @@
+<template>
+ <div class="panel-tab__content">
+ <el-form :model="flowConditionForm" label-width="90px" size="small">
+ <el-form-item label="娴佽浆绫诲瀷">
+ <el-select v-model="flowConditionForm.type" @change="updateFlowType">
+ <el-option label="鏅�氭祦杞矾寰�" value="normal" />
+ <el-option label="榛樿娴佽浆璺緞" value="default" />
+ <el-option label="鏉′欢娴佽浆璺緞" value="condition" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏉′欢鏍煎紡" v-if="flowConditionForm.type === 'condition'" key="condition">
+ <el-select v-model="flowConditionForm.conditionType">
+ <el-option label="琛ㄨ揪寮�" value="expression" />
+ <el-option label="鑴氭湰" value="script" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ label="琛ㄨ揪寮�"
+ v-if="flowConditionForm.conditionType && flowConditionForm.conditionType === 'expression'"
+ key="express"
+ >
+ <el-input
+ v-model="flowConditionForm.body"
+ style="width: 192px"
+ clearable
+ @change="updateFlowCondition"
+ />
+ </el-form-item>
+ <template
+ v-if="flowConditionForm.conditionType && flowConditionForm.conditionType === 'script'"
+ >
+ <el-form-item label="鑴氭湰璇█" key="language">
+ <el-input v-model="flowConditionForm.language" clearable @change="updateFlowCondition" />
+ </el-form-item>
+ <el-form-item label="鑴氭湰绫诲瀷" key="scriptType">
+ <el-select v-model="flowConditionForm.scriptType">
+ <el-option label="鍐呰仈鑴氭湰" value="inlineScript" />
+ <el-option label="澶栭儴鑴氭湰" value="externalScript" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ label="鑴氭湰"
+ v-if="flowConditionForm.scriptType === 'inlineScript'"
+ key="body"
+ >
+ <el-input
+ v-model="flowConditionForm.body"
+ type="textarea"
+ clearable
+ @change="updateFlowCondition"
+ />
+ </el-form-item>
+ <el-form-item
+ label="璧勬簮鍦板潃"
+ v-if="flowConditionForm.scriptType === 'externalScript'"
+ key="resource"
+ >
+ <el-input v-model="flowConditionForm.resource" clearable @change="updateFlowCondition" />
+ </el-form-item>
+ </template>
+ </el-form>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'FlowCondition' })
+
+const props = defineProps({
+ businessObject: Object,
+ type: String
+})
+const flowConditionForm = ref<any>({})
+const bpmnElement = ref()
+const bpmnElementSource = ref()
+const bpmnElementSourceRef = ref()
+const flowConditionRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetFlowCondition = () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ bpmnElementSource.value = bpmnElement.value.source
+ bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef
+ // 鍒濆鍖栭粯璁ype涓篸efault
+ flowConditionForm.value = { type: 'default' }
+ if (
+ bpmnElementSourceRef.value &&
+ bpmnElementSourceRef.value.default &&
+ bpmnElementSourceRef.value.default.id === bpmnElement.value.id
+ ) {
+ flowConditionForm.value = { type: 'default' }
+ } else if (!bpmnElement.value.businessObject.conditionExpression) {
+ // 鏅��
+ flowConditionForm.value = { type: 'normal' }
+ } else {
+ // 甯︽潯浠�
+ const conditionExpression = bpmnElement.value.businessObject.conditionExpression
+ flowConditionForm.value = { ...conditionExpression, type: 'condition' }
+ // resource 鍙洿鎺ユ爣璇� 鏄惁鏄閮ㄨ祫婧愯剼鏈�
+ if (flowConditionForm.value.resource) {
+ // this.$set(this.flowConditionForm, "conditionType", "script");
+ // this.$set(this.flowConditionForm, "scriptType", "externalScript");
+ flowConditionForm.value['conditionType'] = 'script'
+ flowConditionForm.value['scriptType'] = 'externalScript'
+ return
+ }
+ if (conditionExpression.language) {
+ // this.$set(this.flowConditionForm, "conditionType", "script");
+ // this.$set(this.flowConditionForm, "scriptType", "inlineScript");
+ flowConditionForm.value['conditionType'] = 'script'
+ flowConditionForm.value['scriptType'] = 'inlineScript'
+
+ return
+ }
+ // this.$set(this.flowConditionForm, "conditionType", "expression");
+ flowConditionForm.value['conditionType'] = 'expression'
+ }
+}
+const updateFlowType = (flowType) => {
+ // 姝e父鏉′欢绫�
+ if (flowType === 'condition') {
+ flowConditionRef.value = bpmnInstances().moddle.create('bpmn:FormalExpression')
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ conditionExpression: flowConditionRef.value
+ })
+ return
+ }
+ // 榛樿璺緞
+ if (flowType === 'default') {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ conditionExpression: null
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
+ default: toRaw(bpmnElement.value)
+ })
+ return
+ }
+ // 姝e父璺緞锛屽鏋滄潵婧愯妭鐐圭殑榛樿璺緞鏄綋鍓嶈繛绾挎椂锛屾竻闄ょ埗鍏冪礌鐨勯粯璁よ矾寰勯厤缃�
+ if (
+ bpmnElementSourceRef.value.default &&
+ bpmnElementSourceRef.value.default.id === bpmnElement.value.id
+ ) {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
+ default: null
+ })
+ }
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ conditionExpression: null
+ })
+}
+const updateFlowCondition = () => {
+ let { conditionType, scriptType, body, resource, language } = flowConditionForm.value
+ let condition
+ if (conditionType === 'expression') {
+ condition = bpmnInstances().moddle.create('bpmn:FormalExpression', { body })
+ } else {
+ if (scriptType === 'inlineScript') {
+ condition = bpmnInstances().moddle.create('bpmn:FormalExpression', { body, language })
+ // this.$set(this.flowConditionForm, "resource", "");
+ flowConditionForm.value['resource'] = ''
+ } else {
+ // this.$set(this.flowConditionForm, "body", "");
+ flowConditionForm.value['body'] = ''
+ condition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+ resource,
+ language
+ })
+ }
+ }
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ conditionExpression: condition
+ })
+}
+
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+ bpmnElementSource.value = null
+ bpmnElementSourceRef.value = null
+})
+
+watch(
+ () => props.businessObject,
+ (val) => {
+ console.log(val, 'val')
+ nextTick(() => {
+ resetFlowCondition()
+ })
+ },
+ {
+ immediate: true
+ }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
new file mode 100644
index 0000000..2359aff
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/form/ElementForm.vue
@@ -0,0 +1,479 @@
+<template>
+ <div class="panel-tab__content">
+ <el-form label-width="80px">
+ <el-form-item label="娴佺▼琛ㄥ崟">
+ <!-- <el-input v-model="formKey" clearable @change="updateElementFormKey" />-->
+ <el-select v-model="formKey" clearable @change="updateElementFormKey">
+ <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+ </el-select>
+ </el-form-item>
+ <!-- <el-form-item label="涓氬姟鏍囪瘑">-->
+ <!-- <el-select v-model="businessKey" @change="updateElementBusinessKey">-->
+ <!-- <el-option v-for="i in fieldList" :key="i.id" :value="i.id" :label="i.label" />-->
+ <!-- <el-option label="鏃�" value="" />-->
+ <!-- </el-select>-->
+ <!-- </el-form-item>-->
+ </el-form>
+
+ <!--瀛楁鍒楄〃-->
+ <!-- <div class="element-property list-property">-->
+ <!-- <el-divider><Icon icon="ep:coin" /> 琛ㄥ崟瀛楁</el-divider>-->
+ <!-- <el-table :data="fieldList" max-height="240" fit border>-->
+ <!-- <el-table-column label="搴忓彿" type="index" width="50px" />-->
+ <!-- <el-table-column label="瀛楁鍚嶇О" prop="label" min-width="80px" show-overflow-tooltip />-->
+ <!-- <el-table-column-->
+ <!-- label="瀛楁绫诲瀷"-->
+ <!-- prop="type"-->
+ <!-- min-width="80px"-->
+ <!-- :formatter="(row) => fieldType[row.type] || row.type"-->
+ <!-- show-overflow-tooltip-->
+ <!-- />-->
+ <!-- <el-table-column-->
+ <!-- label="榛樿鍊�"-->
+ <!-- prop="defaultValue"-->
+ <!-- min-width="80px"-->
+ <!-- show-overflow-tooltip-->
+ <!-- />-->
+ <!-- <el-table-column label="鎿嶄綔" width="90px">-->
+ <!-- <template #default="scope">-->
+ <!-- <el-button type="primary" link @click="openFieldForm(scope, scope.$index)"-->
+ <!-- >缂栬緫</el-button-->
+ <!-- >-->
+ <!-- <el-divider direction="vertical" />-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- style="color: #ff4d4f"-->
+ <!-- @click="removeField(scope, scope.$index)"-->
+ <!-- >绉婚櫎</el-button-->
+ <!-- >-->
+ <!-- </template>-->
+ <!-- </el-table-column>-->
+ <!-- </el-table>-->
+ <!-- </div>-->
+ <!-- <div class="element-drawer__button">-->
+ <!-- <XButton type="primary" proIcon="ep:plus" title="娣诲姞瀛楁" @click="openFieldForm(null, -1)" />-->
+ <!-- </div>-->
+
+ <!--瀛楁閰嶇疆渚ц竟鏍�-->
+ <!-- <el-drawer-->
+ <!-- v-model="fieldModelVisible"-->
+ <!-- title="瀛楁閰嶇疆"-->
+ <!-- :size="`${width}px`"-->
+ <!-- append-to-body-->
+ <!-- destroy-on-close-->
+ <!-- >-->
+ <!-- <el-form :model="formFieldForm" label-width="90px">-->
+ <!-- <el-form-item label="瀛楁ID">-->
+ <!-- <el-input v-model="formFieldForm.id" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="绫诲瀷">-->
+ <!-- <el-select-->
+ <!-- v-model="formFieldForm.typeType"-->
+ <!-- placeholder="璇烽�夋嫨瀛楁绫诲瀷"-->
+ <!-- clearable-->
+ <!-- @change="changeFieldTypeType"-->
+ <!-- >-->
+ <!-- <el-option v-for="(value, key) of fieldType" :label="value" :value="key" :key="key" />-->
+ <!-- </el-select>-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="绫诲瀷鍚嶇О" v-if="formFieldForm.typeType === 'custom'">-->
+ <!-- <el-input v-model="formFieldForm.type" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="鍚嶇О">-->
+ <!-- <el-input v-model="formFieldForm.label" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="鏃堕棿鏍煎紡" v-if="formFieldForm.typeType === 'date'">-->
+ <!-- <el-input v-model="formFieldForm.datePattern" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="榛樿鍊�">-->
+ <!-- <el-input v-model="formFieldForm.defaultValue" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- </el-form>-->
+
+ <!-- <!– 鏋氫妇鍊艰缃� –>-->
+ <!-- <template v-if="formFieldForm.type === 'enum'">-->
+ <!-- <el-divider key="enum-divider" />-->
+ <!-- <p class="listener-filed__title" key="enum-title">-->
+ <!-- <span><Icon icon="ep:menu" />鏋氫妇鍊煎垪琛細</span>-->
+ <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'enum')"-->
+ <!-- >娣诲姞鏋氫妇鍊�</el-button-->
+ <!-- >-->
+ <!-- </p>-->
+ <!-- <el-table :data="fieldEnumList" key="enum-table" max-height="240" fit border>-->
+ <!-- <el-table-column label="搴忓彿" width="50px" type="index" />-->
+ <!-- <el-table-column label="鏋氫妇鍊肩紪鍙�" prop="id" min-width="100px" show-overflow-tooltip />-->
+ <!-- <el-table-column label="鏋氫妇鍊煎悕绉�" prop="name" min-width="100px" show-overflow-tooltip />-->
+ <!-- <el-table-column label="鎿嶄綔" width="90px">-->
+ <!-- <template #default="scope">-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- @click="openFieldOptionForm(scope, scope.$index, 'enum')"-->
+ <!-- >缂栬緫</el-button-->
+ <!-- >-->
+ <!-- <el-divider direction="vertical" />-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- style="color: #ff4d4f"-->
+ <!-- @click="removeFieldOptionItem(scope, scope.$index, 'enum')"-->
+ <!-- >绉婚櫎</el-button-->
+ <!-- >-->
+ <!-- </template>-->
+ <!-- </el-table-column>-->
+ <!-- </el-table>-->
+ <!-- </template>-->
+
+ <!-- <!– 鏍¢獙瑙勫垯 –>-->
+ <!-- <el-divider key="validation-divider" />-->
+ <!-- <p class="listener-filed__title" key="validation-title">-->
+ <!-- <span><Icon icon="ep:menu" />绾︽潫鏉′欢鍒楄〃锛�</span>-->
+ <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'constraint')"-->
+ <!-- >娣诲姞绾︽潫</el-button-->
+ <!-- >-->
+ <!-- </p>-->
+ <!-- <el-table :data="fieldConstraintsList" key="validation-table" max-height="240" fit border>-->
+ <!-- <el-table-column label="搴忓彿" width="50px" type="index" />-->
+ <!-- <el-table-column label="绾︽潫鍚嶇О" prop="name" min-width="100px" show-overflow-tooltip />-->
+ <!-- <el-table-column label="绾︽潫閰嶇疆" prop="config" min-width="100px" show-overflow-tooltip />-->
+ <!-- <el-table-column label="鎿嶄綔" width="90px">-->
+ <!-- <template #default="scope">-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- @click="openFieldOptionForm(scope, scope.$index, 'constraint')"-->
+ <!-- >缂栬緫</el-button-->
+ <!-- >-->
+ <!-- <el-divider direction="vertical" />-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- style="color: #ff4d4f"-->
+ <!-- @click="removeFieldOptionItem(scope, scope.$index, 'constraint')"-->
+ <!-- >绉婚櫎</el-button-->
+ <!-- >-->
+ <!-- </template>-->
+ <!-- </el-table-column>-->
+ <!-- </el-table>-->
+
+ <!-- <!– 琛ㄥ崟灞炴�� –>-->
+ <!-- <el-divider key="property-divider" />-->
+ <!-- <p class="listener-filed__title" key="property-title">-->
+ <!-- <span><Icon icon="ep:menu" />瀛楁灞炴�у垪琛細</span>-->
+ <!-- <el-button type="primary" @click="openFieldOptionForm(null, -1, 'property')"-->
+ <!-- >娣诲姞灞炴��</el-button-->
+ <!-- >-->
+ <!-- </p>-->
+ <!-- <el-table :data="fieldPropertiesList" key="property-table" max-height="240" fit border>-->
+ <!-- <el-table-column label="搴忓彿" width="50px" type="index" />-->
+ <!-- <el-table-column label="灞炴�х紪鍙�" prop="id" min-width="100px" show-overflow-tooltip />-->
+ <!-- <el-table-column label="灞炴�у��" prop="value" min-width="100px" show-overflow-tooltip />-->
+ <!-- <el-table-column label="鎿嶄綔" width="90px">-->
+ <!-- <template #default="scope">-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- @click="openFieldOptionForm(scope, scope.$index, 'property')"-->
+ <!-- >缂栬緫</el-button-->
+ <!-- >-->
+ <!-- <el-divider direction="vertical" />-->
+ <!-- <el-button-->
+ <!-- type="primary"-->
+ <!-- link-->
+ <!-- style="color: #ff4d4f"-->
+ <!-- @click="removeFieldOptionItem(scope, scope.$index, 'property')"-->
+ <!-- >绉婚櫎</el-button-->
+ <!-- >-->
+ <!-- </template>-->
+ <!-- </el-table-column>-->
+ <!-- </el-table>-->
+
+ <!-- <!– 搴曢儴鎸夐挳 –>-->
+ <!-- <div class="element-drawer__button">-->
+ <!-- <el-button>鍙� 娑�</el-button>-->
+ <!-- <el-button type="primary" @click="saveField">淇� 瀛�</el-button>-->
+ <!-- </div>-->
+ <!-- </el-drawer>-->
+
+ <!-- <el-dialog-->
+ <!-- v-model="fieldOptionModelVisible"-->
+ <!-- :title="optionModelTitle"-->
+ <!-- width="600px"-->
+ <!-- append-to-body-->
+ <!-- destroy-on-close-->
+ <!-- >-->
+ <!-- <el-form :model="fieldOptionForm" label-width="96px">-->
+ <!-- <el-form-item label="缂栧彿/ID" v-if="fieldOptionType !== 'constraint'" key="option-id">-->
+ <!-- <el-input v-model="fieldOptionForm.id" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="鍚嶇О" v-if="fieldOptionType !== 'property'" key="option-name">-->
+ <!-- <el-input v-model="fieldOptionForm.name" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="閰嶇疆" v-if="fieldOptionType === 'constraint'" key="option-config">-->
+ <!-- <el-input v-model="fieldOptionForm.config" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- <el-form-item label="鍊�" v-if="fieldOptionType === 'property'" key="option-value">-->
+ <!-- <el-input v-model="fieldOptionForm.value" clearable />-->
+ <!-- </el-form-item>-->
+ <!-- </el-form>-->
+ <!-- <template #footer>-->
+ <!-- <el-button @click="fieldOptionModelVisible = false">鍙� 娑�</el-button>-->
+ <!-- <el-button type="primary" @click="saveFieldOption">纭� 瀹�</el-button>-->
+ <!-- </template>-->
+ <!-- </el-dialog>-->
+ </div>
+</template>
+
+<script lang="ts" setup>
+import * as FormApi from '@/api/bpm/form'
+
+defineOptions({ name: 'ElementForm' })
+
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+const width = inject('width')
+
+const formKey = ref(undefined)
+const businessKey = ref('')
+const optionModelTitle = ref('')
+const fieldList = ref<any[]>([])
+const formFieldForm = ref<any>({})
+const fieldType = ref({
+ long: '闀挎暣鍨�',
+ string: '瀛楃涓�',
+ boolean: '甯冨皵绫�',
+ date: '鏃ユ湡绫�',
+ enum: '鏋氫妇绫�',
+ custom: '鑷畾涔夌被鍨�'
+})
+const formFieldIndex = ref(-1) // 缂栬緫涓殑瀛楁锛� -1 涓烘柊澧�
+const formFieldOptionIndex = ref(-1) // 缂栬緫涓殑瀛楁閰嶇疆椤癸紝 -1 涓烘柊澧�
+const fieldModelVisible = ref(false)
+const fieldOptionModelVisible = ref(false)
+const fieldOptionForm = ref<any>({}) // 褰撳墠婵�娲荤殑瀛楁閰嶇疆椤规暟鎹�
+const fieldOptionType = ref('') // 褰撳墠婵�娲荤殑瀛楁閰嶇疆椤瑰脊绐� 绫诲瀷
+const fieldEnumList = ref<any[]>([]) // 鏋氫妇鍊煎垪琛�
+const fieldConstraintsList = ref<any[]>([]) // 绾︽潫鏉′欢鍒楄〃
+const fieldPropertiesList = ref<any[]>([]) // 缁戝畾灞炴�у垪琛�
+const bpmnELement = ref()
+const elExtensionElements = ref()
+const formData = ref()
+const otherExtensions = ref()
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const resetFormList = () => {
+ bpmnELement.value = bpmnInstances().bpmnElement
+ formKey.value = bpmnELement.value.businessObject.formKey
+ // if (formKey.value?.length > 0) {
+ // formKey.value = parseInt(formKey.value)
+ // }
+ // 鑾峰彇鍏冪礌鎵╁睍灞炴�� 鎴栬�� 鍒涘缓鎵╁睍灞炴��
+ elExtensionElements.value =
+ bpmnELement.value.businessObject.get('extensionElements') ||
+ bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+ // 鑾峰彇鍏冪礌琛ㄥ崟閰嶇疆 鎴栬�� 鍒涘缓鏂扮殑琛ㄥ崟閰嶇疆
+ formData.value =
+ elExtensionElements.value.values.filter((ex) => ex.$type === `${prefix}:FormData`)?.[0] ||
+ bpmnInstances().moddle.create(`${prefix}:FormData`, { fields: [] })
+
+ // 涓氬姟鏍囪瘑 businessKey锛� 缁戝畾鍦� formData 涓�
+ businessKey.value = formData.value.businessKey
+
+ // 淇濈暀鍓╀綑鎵╁睍鍏冪礌锛屼究浜庡悗闈㈡洿鏂拌鍏冪礌瀵瑰簲灞炴��
+ otherExtensions.value = elExtensionElements.value.values.filter(
+ (ex) => ex.$type !== `${prefix}:FormData`
+ )
+
+ // 澶嶅埗鍘熷鍊硷紝濉厖琛ㄦ牸
+ fieldList.value = JSON.parse(JSON.stringify(formData.value.fields || []))
+
+ // 鏇存柊鍏冪礌鎵╁睍灞炴�э紝閬垮厤鍚庣画鎶ラ敊
+ updateElementExtensions()
+}
+const updateElementFormKey = () => {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
+ formKey: formKey.value
+ })
+}
+const updateElementBusinessKey = () => {
+ bpmnInstances().modeling.updateModdleProperties(toRaw(bpmnELement.value), formData.value, {
+ businessKey: businessKey.value
+ })
+}
+// 鏍规嵁绫诲瀷璋冩暣瀛楁type
+const changeFieldTypeType = (type) => {
+ // this.$set(this.formFieldForm, "type", type === "custom" ? "" : type);
+ formFieldForm.value['type'] = type === 'custom' ? '' : type
+}
+
+// 鎵撳紑瀛楁璇︽儏渚ц竟鏍�
+const openFieldForm = (field, index) => {
+ formFieldIndex.value = index
+ if (index !== -1) {
+ const FieldObject = formData.value.fields[index]
+ formFieldForm.value = JSON.parse(JSON.stringify(field))
+ // 璁剧疆鑷畾涔夌被鍨�
+ // this.$set(this.formFieldForm, "typeType", !this.fieldType[field.type] ? "custom" : field.type);
+ formFieldForm.value['typeType'] = !fieldType.value[field.type] ? 'custom' : field.type
+ // 鍒濆鍖栨灇涓惧�煎垪琛�
+ field.type === 'enum' &&
+ (fieldEnumList.value = JSON.parse(JSON.stringify(FieldObject?.values || [])))
+ // 鍒濆鍖栫害鏉熸潯浠跺垪琛�
+ fieldConstraintsList.value = JSON.parse(
+ JSON.stringify(FieldObject?.validation?.constraints || [])
+ )
+ // 鍒濆鍖栬嚜瀹氫箟灞炴�у垪琛�
+ fieldPropertiesList.value = JSON.parse(JSON.stringify(FieldObject?.properties?.values || []))
+ } else {
+ formFieldForm.value = {}
+ // 鍒濆鍖栨灇涓惧�煎垪琛�
+ fieldEnumList.value = []
+ // 鍒濆鍖栫害鏉熸潯浠跺垪琛�
+ fieldConstraintsList.value = []
+ // 鍒濆鍖栬嚜瀹氫箟灞炴�у垪琛�
+ fieldPropertiesList.value = []
+ }
+ fieldModelVisible.value = true
+}
+// 鎵撳紑瀛楁 鏌愪釜 閰嶇疆椤� 寮圭獥
+const openFieldOptionForm = (option, index, type) => {
+ fieldOptionModelVisible.value = true
+ fieldOptionType.value = type
+ formFieldOptionIndex.value = index
+ if (type === 'property') {
+ fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
+ return (optionModelTitle.value = '灞炴�ч厤缃�')
+ }
+ if (type === 'enum') {
+ fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
+ return (optionModelTitle.value = '鏋氫妇鍊奸厤缃�')
+ }
+ fieldOptionForm.value = option ? JSON.parse(JSON.stringify(option)) : {}
+ return (optionModelTitle.value = '绾︽潫鏉′欢閰嶇疆')
+}
+
+// 淇濆瓨瀛楁 鏌愪釜 閰嶇疆椤�
+const saveFieldOption = () => {
+ if (formFieldOptionIndex.value === -1) {
+ if (fieldOptionType.value === 'property') {
+ fieldPropertiesList.value.push(fieldOptionForm.value)
+ }
+ if (fieldOptionType.value === 'constraint') {
+ fieldConstraintsList.value.push(fieldOptionForm.value)
+ }
+ if (fieldOptionType.value === 'enum') {
+ fieldEnumList.value.push(fieldOptionForm.value)
+ }
+ } else {
+ fieldOptionType.value === 'property' &&
+ fieldPropertiesList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
+ fieldOptionType.value === 'constraint' &&
+ fieldConstraintsList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
+ fieldOptionType.value === 'enum' &&
+ fieldEnumList.value.splice(formFieldOptionIndex.value, 1, fieldOptionForm.value)
+ }
+ fieldOptionModelVisible.value = false
+ fieldOptionForm.value = {}
+}
+// 淇濆瓨瀛楁閰嶇疆
+const saveField = () => {
+ const { id, type, label, defaultValue, datePattern } = formFieldForm.value
+ const Field = bpmnInstances().moddle.create(`${prefix}:FormField`, { id, type, label })
+ defaultValue && (Field.defaultValue = defaultValue)
+ datePattern && (Field.datePattern = datePattern)
+ // 鏋勫缓灞炴��
+ if (fieldPropertiesList.value && fieldPropertiesList.value.length) {
+ const fieldPropertyList = fieldPropertiesList.value.map((fp) => {
+ return bpmnInstances().moddle.create(`${prefix}:Property`, {
+ id: fp.id,
+ value: fp.value
+ })
+ })
+ Field.properties = bpmnInstances().moddle.create(`${prefix}:Properties`, {
+ values: fieldPropertyList
+ })
+ }
+ // 鏋勫缓鏍¢獙瑙勫垯
+ if (fieldConstraintsList.value && fieldConstraintsList.value.length) {
+ const fieldConstraintList = fieldConstraintsList.value.map((fc) => {
+ return bpmnInstances().moddle.create(`${prefix}:Constraint`, {
+ name: fc.name,
+ config: fc.config
+ })
+ })
+ Field.validation = bpmnInstances().moddle.create(`${prefix}:Validation`, {
+ constraints: fieldConstraintList
+ })
+ }
+ // 鏋勫缓鏋氫妇鍊�
+ if (fieldEnumList.value && fieldEnumList.value.length) {
+ Field.values = fieldEnumList.value.map((fe) => {
+ return bpmnInstances().moddle.create(`${prefix}:Value`, { name: fe.name, id: fe.id })
+ })
+ }
+ // 鏇存柊鏁扮粍 涓� 琛ㄥ崟閰嶇疆瀹炰緥
+ if (formFieldIndex.value === -1) {
+ fieldList.value.push(formFieldForm.value)
+ formData.value.fields.push(Field)
+ } else {
+ fieldList.value.splice(formFieldIndex.value, 1, formFieldForm.value)
+ formData.value.fields.splice(formFieldIndex.value, 1, Field)
+ }
+ updateElementExtensions()
+ fieldModelVisible.value = false
+}
+
+// 绉婚櫎鏌愪釜 瀛楁鐨� 閰嶇疆椤�
+const removeFieldOptionItem = (option, index, type) => {
+ // console.log(option, 'option')
+ if (type === 'property') {
+ fieldPropertiesList.value.splice(index, 1)
+ return
+ }
+ if (type === 'enum') {
+ fieldEnumList.value.splice(index, 1)
+ return
+ }
+ fieldConstraintsList.value.splice(index, 1)
+}
+// 绉婚櫎 瀛楁
+const removeField = (field, index) => {
+ console.log(field, 'field')
+ fieldList.value.splice(index, 1)
+ formData.value.fields.splice(index, 1)
+ updateElementExtensions()
+}
+
+const updateElementExtensions = () => {
+ // 鏇存柊鍥炴墿灞曞厓绱�
+ const newElExtensionElements = bpmnInstances().moddle.create(`bpmn:ExtensionElements`, {
+ values: otherExtensions.value.concat(formData.value)
+ })
+ // 鏇存柊鍒板厓绱犱笂
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnELement.value), {
+ extensionElements: newElExtensionElements
+ })
+}
+
+const formList = ref([]) // 娴佺▼琛ㄥ崟鐨勪笅鎷夋鐨勬暟鎹�
+onMounted(async () => {
+ formList.value = await FormApi.getFormSimpleList()
+ formKey.value = parseInt(formKey.value)
+})
+
+watch(
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ resetFormList()
+ })
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/index.js b/src/components/bpmnProcessDesigner/package/penal/index.js
new file mode 100644
index 0000000..7fa5617
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/index.js
@@ -0,0 +1,7 @@
+import MyPropertiesPanel from './PropertiesPanel.vue'
+
+MyPropertiesPanel.install = function (Vue) {
+ Vue.component(MyPropertiesPanel.name, MyPropertiesPanel)
+}
+
+export default MyPropertiesPanel
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
new file mode 100644
index 0000000..c8726ab
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ElementListeners.vue
@@ -0,0 +1,468 @@
+<template>
+ <div class="panel-tab__content">
+ <el-table :data="elementListenersList" size="small" border>
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column label="浜嬩欢绫诲瀷" min-width="100px" prop="event" />
+ <el-table-column
+ label="鐩戝惉鍣ㄧ被鍨�"
+ min-width="100px"
+ show-overflow-tooltip
+ :formatter="(row) => listenerTypeObject[row.listenerType]"
+ />
+ <el-table-column label="鎿嶄綔" width="100px">
+ <template #default="scope">
+ <el-button size="small" link @click="openListenerForm(scope.row, scope.$index)"
+ >缂栬緫</el-button
+ >
+ <el-divider direction="vertical" />
+ <el-button size="small" link style="color: #ff4d4f" @click="removeListener(scope.$index)"
+ >绉婚櫎</el-button
+ >
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="element-drawer__button">
+ <XButton
+ type="primary"
+ preIcon="ep:plus"
+ title="娣诲姞鐩戝惉鍣�"
+ size="small"
+ @click="openListenerForm(null)"
+ />
+ <XButton
+ type="success"
+ preIcon="ep:select"
+ title="閫夋嫨鐩戝惉鍣�"
+ size="small"
+ @click="openProcessListenerDialog"
+ />
+ </div>
+
+ <!-- 鐩戝惉鍣� 缂栬緫/鍒涘缓 閮ㄥ垎 -->
+ <el-drawer
+ v-model="listenerFormModelVisible"
+ title="鎵ц鐩戝惉鍣�"
+ :size="`${width}px`"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form :model="listenerForm" label-width="96px" ref="listenerFormRef">
+ <el-form-item
+ label="浜嬩欢绫诲瀷"
+ prop="event"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-select v-model="listenerForm.event">
+ <el-option label="start" value="start" />
+ <el-option label="end" value="end" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ label="鐩戝惉鍣ㄧ被鍨�"
+ prop="listenerType"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-select v-model="listenerForm.listenerType">
+ <el-option
+ v-for="i in Object.keys(listenerTypeObject)"
+ :key="i"
+ :label="listenerTypeObject[i]"
+ :value="i"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'classListener'"
+ label="Java绫�"
+ prop="class"
+ key="listener-class"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.class" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'expressionListener'"
+ label="琛ㄨ揪寮�"
+ prop="expression"
+ key="listener-expression"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.expression" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'delegateExpressionListener'"
+ label="浠g悊琛ㄨ揪寮�"
+ prop="delegateExpression"
+ key="listener-delegate"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.delegateExpression" clearable />
+ </el-form-item>
+ <template v-if="listenerForm.listenerType === 'scriptListener'">
+ <el-form-item
+ label="鑴氭湰鏍煎紡"
+ prop="scriptFormat"
+ key="listener-script-format"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒剼鏈牸寮�' }"
+ >
+ <el-input v-model="listenerForm.scriptFormat" clearable />
+ </el-form-item>
+ <el-form-item
+ label="鑴氭湰绫诲瀷"
+ prop="scriptType"
+ key="listener-script-type"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇烽�夋嫨鑴氭湰绫诲瀷' }"
+ >
+ <el-select v-model="listenerForm.scriptType">
+ <el-option label="鍐呰仈鑴氭湰" value="inlineScript" />
+ <el-option label="澶栭儴鑴氭湰" value="externalScript" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.scriptType === 'inlineScript'"
+ label="鑴氭湰鍐呭"
+ prop="value"
+ key="listener-script"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒剼鏈唴瀹�' }"
+ >
+ <el-input v-model="listenerForm.value" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.scriptType === 'externalScript'"
+ label="璧勬簮鍦板潃"
+ prop="resource"
+ key="listener-resource"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒祫婧愬湴鍧�' }"
+ >
+ <el-input v-model="listenerForm.resource" clearable />
+ </el-form-item>
+ </template>
+ </el-form>
+ <el-divider />
+ <p class="listener-filed__title">
+ <span><Icon icon="ep:menu" />娉ㄥ叆瀛楁锛�</span>
+ <XButton type="primary" @click="openListenerFieldForm(null)" title="娣诲姞瀛楁" />
+ </p>
+ <el-table
+ :data="fieldsListOfListener"
+ size="small"
+ max-height="240"
+ fit
+ border
+ style="flex: none"
+ >
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column label="瀛楁鍚嶇О" min-width="100px" prop="name" />
+ <el-table-column
+ label="瀛楁绫诲瀷"
+ min-width="80px"
+ show-overflow-tooltip
+ :formatter="(row) => fieldTypeObject[row.fieldType]"
+ />
+ <el-table-column
+ label="瀛楁鍊�/琛ㄨ揪寮�"
+ min-width="100px"
+ show-overflow-tooltip
+ :formatter="(row) => row.string || row.expression"
+ />
+ <el-table-column label="鎿嶄綔" width="130px">
+ <template #default="scope">
+ <el-button size="small" link @click="openListenerFieldForm(scope.row, scope.$index)"
+ >缂栬緫</el-button
+ >
+ <el-divider direction="vertical" />
+ <el-button
+ size="small"
+ link
+ style="color: #ff4d4f"
+ @click="removeListenerField(scope.$index)"
+ >绉婚櫎</el-button
+ >
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="element-drawer__button">
+ <el-button @click="listenerFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="saveListenerConfig">淇� 瀛�</el-button>
+ </div>
+ </el-drawer>
+
+ <!-- 娉ㄥ叆瑗挎 缂栬緫/鍒涘缓 閮ㄥ垎 -->
+ <el-dialog
+ title="瀛楁閰嶇疆"
+ v-model="listenerFieldFormModelVisible"
+ width="600px"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form
+ :model="listenerFieldForm"
+ label-width="96spx"
+ ref="listenerFieldFormRef"
+ style="height: 136px"
+ >
+ <el-form-item
+ label="瀛楁鍚嶇О锛�"
+ prop="name"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.name" clearable />
+ </el-form-item>
+ <el-form-item
+ label="瀛楁绫诲瀷锛�"
+ prop="fieldType"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-select v-model="listenerFieldForm.fieldType">
+ <el-option
+ v-for="i in Object.keys(fieldTypeObject)"
+ :key="i"
+ :label="fieldTypeObject[i]"
+ :value="i"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerFieldForm.fieldType === 'string'"
+ label="瀛楁鍊硷細"
+ prop="string"
+ key="field-string"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.string" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerFieldForm.fieldType === 'expression'"
+ label="琛ㄨ揪寮忥細"
+ prop="expression"
+ key="field-expression"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.expression" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button size="small" @click="listenerFieldFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button size="small" type="primary" @click="saveListenerFiled">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+
+ <!-- 閫夋嫨寮圭獥 -->
+ <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
+</template>
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+import { createListenerObject, updateElementExtensions } from '../../utils'
+import {
+ initListenerType,
+ initListenerForm,
+ listenerType,
+ fieldType,
+ initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from './ProcessListenerDialog.vue'
+
+defineOptions({ name: 'ElementListeners' })
+
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+const width = inject('width')
+const elementListenersList = ref<any[]>([]) // 鐩戝惉鍣ㄥ垪琛�
+const listenerForm = ref<any>({}) // 鐩戝惉鍣ㄨ鎯呰〃鍗�
+const listenerFormModelVisible = ref(false) // 鐩戝惉鍣� 缂栬緫 渚ц竟鏍忔樉绀虹姸鎬�
+const fieldsListOfListener = ref<any[]>([])
+const listenerFieldForm = ref<any>({}) // 鐩戝惉鍣� 娉ㄥ叆瀛楁 璇︽儏琛ㄥ崟
+const listenerFieldFormModelVisible = ref(false) // 鐩戝惉鍣� 娉ㄥ叆瀛楁琛ㄥ崟寮圭獥 鏄剧ず鐘舵��
+const editingListenerIndex = ref(-1) // 鐩戝惉鍣ㄦ墍鍦ㄤ笅鏍囷紝-1 涓烘柊澧�
+const editingListenerFieldIndex = ref(-1) // 瀛楁鎵�鍦ㄤ笅鏍囷紝-1 涓烘柊澧�
+const listenerTypeObject = ref(listenerType)
+const fieldTypeObject = ref(fieldType)
+const otherExtensionList = ref()
+const bpmnElementListeners = ref()
+const listenerFormRef = ref()
+const listenerFieldFormRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetListenersList = () => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ // 鐩存帴浣跨敤鍘熷BPMN鍏冪礌锛岄伩鍏峍ue鍝嶅簲寮忎唬鐞嗛棶棰�
+ const bpmnElement = instances.bpmnElement
+ const businessObject = bpmnElement.businessObject
+
+ otherExtensionList.value =
+ businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type !== `${prefix}:ExecutionListener`
+ ) ?? [] // 淇濈暀闈炵洃鍚櫒绫诲瀷鐨勬墿灞曞睘鎬э紝閬垮厤绉婚櫎鐩戝惉鍣ㄦ椂娓呯┖鍏朵粬閰嶇疆锛堝瀹℃壒浜虹瓑锛夈�傜浉鍏虫渚嬶細https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
+ bpmnElementListeners.value =
+ businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type === `${prefix}:ExecutionListener`
+ ) ?? []
+ elementListenersList.value = bpmnElementListeners.value.map((listener) =>
+ initListenerType(listener)
+ )
+}
+// 鎵撳紑 鐩戝惉鍣ㄨ鎯� 渚ц竟鏍�
+const openListenerForm = (listener, index?) => {
+ // debugger
+ if (listener) {
+ listenerForm.value = initListenerForm(listener)
+ editingListenerIndex.value = index
+ } else {
+ listenerForm.value = {}
+ editingListenerIndex.value = -1 // 鏍囪涓烘柊澧�
+ }
+ if (listener && listener.fields) {
+ fieldsListOfListener.value = listener.fields.map((field) => ({
+ ...field,
+ fieldType: field.string ? 'string' : 'expression'
+ }))
+ } else {
+ fieldsListOfListener.value = []
+ listenerForm.value['fields'] = []
+ }
+ // 鎵撳紑渚ц竟鏍忓苟娓呮楠岃瘉鐘舵��
+ listenerFormModelVisible.value = true
+ nextTick(() => {
+ if (listenerFormRef.value) {
+ listenerFormRef.value.clearValidate()
+ }
+ })
+}
+// 鎵撳紑鐩戝惉鍣ㄥ瓧娈电紪杈戝脊绐�
+const openListenerFieldForm = (field, index?) => {
+ listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {}
+ editingListenerFieldIndex.value = field ? index : -1
+ listenerFieldFormModelVisible.value = true
+ nextTick(() => {
+ if (listenerFieldFormRef.value) {
+ listenerFieldFormRef.value.clearValidate()
+ }
+ })
+}
+// 淇濆瓨鐩戝惉鍣ㄦ敞鍏ュ瓧娈�
+const saveListenerFiled = async () => {
+ // debugger
+ let validateStatus = await listenerFieldFormRef.value.validate()
+ if (!validateStatus) return // 楠岃瘉涓嶉�氳繃鐩存帴杩斿洖
+ if (editingListenerFieldIndex.value === -1) {
+ fieldsListOfListener.value.push(listenerFieldForm.value)
+ listenerForm.value.fields.push(listenerFieldForm.value)
+ } else {
+ fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
+ listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
+ }
+ listenerFieldFormModelVisible.value = false
+ nextTick(() => {
+ listenerFieldForm.value = {}
+ })
+}
+// 绉婚櫎鐩戝惉鍣ㄥ瓧娈�
+const removeListenerField = (index) => {
+ // debugger
+ ElMessageBox.confirm('纭绉婚櫎璇ュ瓧娈靛悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭� 璁�',
+ cancelButtonText: '鍙� 娑�'
+ })
+ .then(() => {
+ fieldsListOfListener.value.splice(index, 1)
+ listenerForm.value.fields.splice(index, 1)
+ })
+ .catch(() => console.info('鎿嶄綔鍙栨秷'))
+}
+// 绉婚櫎鐩戝惉鍣�
+const removeListener = (index) => {
+ ElMessageBox.confirm('纭绉婚櫎璇ョ洃鍚櫒鍚楋紵', '鎻愮ず', {
+ confirmButtonText: '纭� 璁�',
+ cancelButtonText: '鍙� 娑�'
+ })
+ .then(() => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ bpmnElementListeners.value.splice(index, 1)
+ elementListenersList.value.splice(index, 1)
+ updateElementExtensions(
+ instances.bpmnElement,
+ otherExtensionList.value.concat(bpmnElementListeners.value)
+ )
+ })
+ .catch(() => console.info('鎿嶄綔鍙栨秷'))
+}
+// 淇濆瓨鐩戝惉鍣ㄩ厤缃�
+const saveListenerConfig = async () => {
+ // debugger
+ let validateStatus = await listenerFormRef.value.validate()
+ if (!validateStatus) return // 楠岃瘉涓嶉�氳繃鐩存帴杩斿洖
+
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ const bpmnElement = instances.bpmnElement
+ const listenerObject = createListenerObject(listenerForm.value, false, prefix)
+
+ if (editingListenerIndex.value === -1) {
+ bpmnElementListeners.value.push(listenerObject)
+ elementListenersList.value.push(listenerForm.value)
+ } else {
+ bpmnElementListeners.value.splice(editingListenerIndex.value, 1, listenerObject)
+ elementListenersList.value.splice(editingListenerIndex.value, 1, listenerForm.value)
+ }
+ // 淇濆瓨鍏朵粬閰嶇疆
+ otherExtensionList.value =
+ bpmnElement.businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type !== `${prefix}:ExecutionListener`
+ ) ?? []
+ updateElementExtensions(
+ bpmnElement,
+ otherExtensionList.value.concat(bpmnElementListeners.value)
+ )
+ // 4. 闅愯棌渚ц竟鏍�
+ listenerFormModelVisible.value = false
+ listenerForm.value = {}
+}
+
+// 鎵撳紑鐩戝惉鍣ㄥ脊绐�
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+ processListenerDialogRef.value.open('execution')
+}
+const selectProcessListener = (listener) => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ const bpmnElement = instances.bpmnElement
+ const listenerForm = initListenerForm2(listener)
+ const listenerObject = createListenerObject(listenerForm, false, prefix)
+ bpmnElementListeners.value.push(listenerObject)
+ elementListenersList.value.push(listenerForm)
+
+ // 淇濆瓨鍏朵粬閰嶇疆
+ otherExtensionList.value =
+ bpmnElement.businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type !== `${prefix}:ExecutionListener`
+ ) ?? []
+ updateElementExtensions(
+ bpmnElement,
+ otherExtensionList.value.concat(bpmnElementListeners.value)
+ )
+}
+
+watch(
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ resetListenersList()
+ })
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
new file mode 100644
index 0000000..21088ab
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue
@@ -0,0 +1,85 @@
+<!-- 鎵ц鍣ㄩ�夋嫨 -->
+<template>
+ <Dialog title="璇烽�夋嫨鐩戝惉鍣�" v-model="dialogVisible" width="1024px">
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜嬩欢" align="center" prop="event" />
+ <el-table-column label="鍊肩被鍨�" align="center" prop="valueType">
+ <template #default="scope">
+ <dict-tag
+ :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
+ :value="scope.row.valueType"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍊�" align="center" prop="value" />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="select(scope.row)"> 閫夋嫨 </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { DICT_TYPE } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 娴佺▼ 琛ㄥ崟 */
+defineOptions({ name: 'ProcessListenerDialog' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProcessListenerVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: '',
+ status: CommonStatusEnum.ENABLE
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string) => {
+ queryParams.pageNo = 1
+ queryParams.type = type
+ getList()
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const select = async (row) => {
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('select', row)
+}
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
new file mode 100644
index 0000000..cea9f1b
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/UserTaskListeners.vue
@@ -0,0 +1,510 @@
+<template>
+ <div class="panel-tab__content">
+ <el-table :data="elementListenersList" size="small" border>
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column
+ label="浜嬩欢绫诲瀷"
+ min-width="80px"
+ show-overflow-tooltip
+ :formatter="(row) => listenerEventTypeObject[row.event]"
+ />
+ <el-table-column label="浜嬩欢id" min-width="80px" prop="id" show-overflow-tooltip />
+ <el-table-column
+ label="鐩戝惉鍣ㄧ被鍨�"
+ min-width="80px"
+ show-overflow-tooltip
+ :formatter="(row) => listenerTypeObject[row.listenerType]"
+ />
+ <el-table-column label="鎿嶄綔" width="90px">
+ <template #default="scope">
+ <el-button size="small" link @click="openListenerForm(scope.row, scope.$index)"
+ >缂栬緫</el-button
+ >
+ <el-divider direction="vertical" />
+ <el-button
+ size="small"
+ link
+ style="color: #ff4d4f"
+ @click="removeListener(scope.row, scope.$index)"
+ >绉婚櫎</el-button
+ >
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="element-drawer__button">
+ <XButton
+ size="small"
+ type="primary"
+ preIcon="ep:plus"
+ title="娣诲姞鐩戝惉鍣�"
+ @click="openListenerForm(null)"
+ />
+ <XButton
+ type="success"
+ preIcon="ep:select"
+ title="閫夋嫨鐩戝惉鍣�"
+ size="small"
+ @click="openProcessListenerDialog"
+ />
+ </div>
+
+ <!-- 鐩戝惉鍣� 缂栬緫/鍒涘缓 閮ㄥ垎 -->
+ <el-drawer
+ v-model="listenerFormModelVisible"
+ title="浠诲姟鐩戝惉鍣�"
+ :size="`${width}px`"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form size="small" :model="listenerForm" label-width="96px" ref="listenerFormRef">
+ <el-form-item
+ label="浜嬩欢绫诲瀷"
+ prop="event"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-select v-model="listenerForm.event">
+ <el-option
+ v-for="i in Object.keys(listenerEventTypeObject)"
+ :key="i"
+ :label="listenerEventTypeObject[i]"
+ :value="i"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ label="鐩戝惉鍣↖D"
+ prop="id"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.id" clearable />
+ </el-form-item>
+ <el-form-item
+ label="鐩戝惉鍣ㄧ被鍨�"
+ prop="listenerType"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-select v-model="listenerForm.listenerType">
+ <el-option
+ v-for="i in Object.keys(listenerTypeObject)"
+ :key="i"
+ :label="listenerTypeObject[i]"
+ :value="i"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'classListener'"
+ label="Java绫�"
+ prop="class"
+ key="listener-class"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.class" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'expressionListener'"
+ label="琛ㄨ揪寮�"
+ prop="expression"
+ key="listener-expression"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.expression" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'delegateExpressionListener'"
+ label="浠g悊琛ㄨ揪寮�"
+ prop="delegateExpression"
+ key="listener-delegate"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.delegateExpression" clearable />
+ </el-form-item>
+ <template v-if="listenerForm.listenerType === 'scriptListener'">
+ <el-form-item
+ label="鑴氭湰鏍煎紡"
+ prop="scriptFormat"
+ key="listener-script-format"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒剼鏈牸寮�' }"
+ >
+ <el-input v-model="listenerForm.scriptFormat" clearable />
+ </el-form-item>
+ <el-form-item
+ label="鑴氭湰绫诲瀷"
+ prop="scriptType"
+ key="listener-script-type"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇烽�夋嫨鑴氭湰绫诲瀷' }"
+ >
+ <el-select v-model="listenerForm.scriptType">
+ <el-option label="鍐呰仈鑴氭湰" value="inlineScript" />
+ <el-option label="澶栭儴鑴氭湰" value="externalScript" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.scriptType === 'inlineScript'"
+ label="鑴氭湰鍐呭"
+ prop="value"
+ key="listener-script"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒剼鏈唴瀹�' }"
+ >
+ <el-input v-model="listenerForm.value" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.scriptType === 'externalScript'"
+ label="璧勬簮鍦板潃"
+ prop="resource"
+ key="listener-resource"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒祫婧愬湴鍧�' }"
+ >
+ <el-input v-model="listenerForm.resource" clearable />
+ </el-form-item>
+ </template>
+
+ <template v-if="listenerForm.event === 'timeout'">
+ <el-form-item label="瀹氭椂鍣ㄧ被鍨�" prop="eventDefinitionType" key="eventDefinitionType">
+ <el-select v-model="listenerForm.eventDefinitionType">
+ <el-option label="鏃ユ湡" value="date" />
+ <el-option label="鎸佺画鏃堕暱" value="duration" />
+ <el-option label="寰幆" value="cycle" />
+ <el-option label="鏃�" value="null" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="!!listenerForm.eventDefinitionType && listenerForm.eventDefinitionType !== 'null'"
+ label="瀹氭椂鍣�"
+ prop="eventTimeDefinitions"
+ key="eventTimeDefinitions"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欏畾鏃跺櫒閰嶇疆' }"
+ >
+ <el-input v-model="listenerForm.eventTimeDefinitions" clearable />
+ </el-form-item>
+ </template>
+ </el-form>
+
+ <el-divider />
+ <p class="listener-filed__title">
+ <span><Icon icon="ep:menu" />娉ㄥ叆瀛楁锛�</span>
+ <el-button size="small" type="primary" @click="openListenerFieldForm(null)"
+ >娣诲姞瀛楁</el-button
+ >
+ </p>
+ <el-table
+ :data="fieldsListOfListener"
+ size="small"
+ max-height="240"
+ fit
+ border
+ style="flex: none"
+ >
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column label="瀛楁鍚嶇О" min-width="100px" prop="name" />
+ <el-table-column
+ label="瀛楁绫诲瀷"
+ min-width="80px"
+ show-overflow-tooltip
+ :formatter="(row) => fieldTypeObject[row.fieldType]"
+ />
+ <el-table-column
+ label="瀛楁鍊�/琛ㄨ揪寮�"
+ min-width="100px"
+ show-overflow-tooltip
+ :formatter="(row) => row.string || row.expression"
+ />
+ <el-table-column label="鎿嶄綔" width="100px">
+ <template #default="scope">
+ <el-button size="small" link @click="openListenerFieldForm(scope.row, scope.$index)"
+ >缂栬緫</el-button
+ >
+ <el-divider direction="vertical" />
+ <el-button
+ size="small"
+ link
+ style="color: #ff4d4f"
+ @click="removeListenerField(scope.row, scope.$index)"
+ >绉婚櫎</el-button
+ >
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="element-drawer__button">
+ <el-button size="small" @click="listenerFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button size="small" type="primary" @click="saveListenerConfig">淇� 瀛�</el-button>
+ </div>
+ </el-drawer>
+
+ <!-- 娉ㄥ叆瑗挎 缂栬緫/鍒涘缓 閮ㄥ垎 -->
+ <el-dialog
+ title="瀛楁閰嶇疆"
+ v-model="listenerFieldFormModelVisible"
+ width="600px"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form
+ :model="listenerFieldForm"
+ size="small"
+ label-width="96px"
+ ref="listenerFieldFormRef"
+ style="height: 136px"
+ >
+ <el-form-item
+ label="瀛楁鍚嶇О锛�"
+ prop="name"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.name" clearable />
+ </el-form-item>
+ <el-form-item
+ label="瀛楁绫诲瀷锛�"
+ prop="fieldType"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-select v-model="listenerFieldForm.fieldType">
+ <el-option
+ v-for="i in Object.keys(fieldTypeObject)"
+ :key="i"
+ :label="fieldTypeObject[i]"
+ :value="i"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerFieldForm.fieldType === 'string'"
+ label="瀛楁鍊硷細"
+ prop="string"
+ key="field-string"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.string" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerFieldForm.fieldType === 'expression'"
+ label="琛ㄨ揪寮忥細"
+ prop="expression"
+ key="field-expression"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.expression" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button size="small" @click="listenerFieldFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button size="small" type="primary" @click="saveListenerFiled">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+
+ <!-- 閫夋嫨寮圭獥 -->
+ <ProcessListenerDialog ref="processListenerDialogRef" @select="selectProcessListener" />
+</template>
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+import { createListenerObject, updateElementExtensions } from '../../utils'
+import {
+ initListenerForm,
+ initListenerType,
+ eventType,
+ listenerType,
+ fieldType,
+ initListenerForm2
+} from './utilSelf'
+import ProcessListenerDialog from '@/components/bpmnProcessDesigner/package/penal/listeners/ProcessListenerDialog.vue'
+
+defineOptions({ name: 'UserTaskListeners' })
+
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+const width = inject('width')
+const elementListenersList = ref<any[]>([])
+const listenerEventTypeObject = ref(eventType)
+const listenerTypeObject = ref(listenerType)
+const listenerFormModelVisible = ref(false)
+const listenerForm = ref<any>({})
+const fieldTypeObject = ref(fieldType)
+const fieldsListOfListener = ref<any[]>([])
+const listenerFieldFormModelVisible = ref(false) // 鐩戝惉鍣� 娉ㄥ叆瀛楁琛ㄥ崟寮圭獥 鏄剧ず鐘舵��
+const editingListenerIndex = ref(-1) // 鐩戝惉鍣ㄦ墍鍦ㄤ笅鏍囷紝-1 涓烘柊澧�
+const editingListenerFieldIndex = ref(-1) // 瀛楁鎵�鍦ㄤ笅鏍囷紝-1 涓烘柊澧�
+const listenerFieldForm = ref<any>({}) // 鐩戝惉鍣� 娉ㄥ叆瀛楁 璇︽儏琛ㄥ崟
+const bpmnElementListeners = ref()
+const otherExtensionList = ref()
+const listenerFormRef = ref()
+const listenerFieldFormRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetListenersList = () => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ // 鐩存帴浣跨敤鍘熷BPMN鍏冪礌锛岄伩鍏峍ue鍝嶅簲寮忎唬鐞嗛棶棰�
+ const bpmnElement = instances.bpmnElement
+ const businessObject = bpmnElement.businessObject
+
+ console.log(bpmnElement, 'bpmnElement - resetListenersList')
+
+ otherExtensionList.value =
+ businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type !== `${prefix}:TaskListener`
+ ) ?? [] // 淇濈暀闈炵洃鍚櫒绫诲瀷鐨勬墿灞曞睘鎬э紝閬垮厤绉婚櫎鐩戝惉鍣ㄦ椂娓呯┖鍏朵粬閰嶇疆锛堝瀹℃壒浜虹瓑锛夈�傜浉鍏虫渚嬶細https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ICMSYC
+ bpmnElementListeners.value =
+ businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type === `${prefix}:TaskListener`
+ ) ?? []
+ elementListenersList.value = bpmnElementListeners.value.map((listener) =>
+ initListenerType(listener)
+ )
+}
+const openListenerForm = (listener, index?) => {
+ if (listener) {
+ listenerForm.value = initListenerForm(listener)
+ editingListenerIndex.value = index
+ } else {
+ listenerForm.value = {}
+ editingListenerIndex.value = -1 // 鏍囪涓烘柊澧�
+ }
+ if (listener && listener.fields) {
+ fieldsListOfListener.value = listener.fields.map((field) => ({
+ ...field,
+ fieldType: field.string ? 'string' : 'expression'
+ }))
+ } else {
+ fieldsListOfListener.value = []
+ listenerForm.value['fields'] = []
+ }
+ // 鎵撳紑渚ц竟鏍忓苟娓呮楠岃瘉鐘舵��
+ listenerFormModelVisible.value = true
+ nextTick(() => {
+ if (listenerFormRef.value) listenerFormRef.value.clearValidate()
+ })
+}
+// 绉婚櫎鐩戝惉鍣�
+const removeListener = (listener, index?) => {
+ console.log(listener, 'listener')
+ ElMessageBox.confirm('纭绉婚櫎璇ョ洃鍚櫒鍚楋紵', '鎻愮ず', {
+ confirmButtonText: '纭� 璁�',
+ cancelButtonText: '鍙� 娑�'
+ })
+ .then(() => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ bpmnElementListeners.value.splice(index, 1)
+ elementListenersList.value.splice(index, 1)
+ updateElementExtensions(
+ instances.bpmnElement,
+ otherExtensionList.value.concat(bpmnElementListeners.value)
+ )
+ })
+ .catch(() => console.info('鎿嶄綔鍙栨秷'))
+}
+// 淇濆瓨鐩戝惉鍣�
+const saveListenerConfig = async () => {
+ let validateStatus = await listenerFormRef.value.validate()
+ if (!validateStatus) return // 楠岃瘉涓嶉�氳繃鐩存帴杩斿洖
+
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ const bpmnElement = instances.bpmnElement
+ const listenerObject = createListenerObject(listenerForm.value, true, prefix)
+
+ if (editingListenerIndex.value === -1) {
+ bpmnElementListeners.value.push(listenerObject)
+ elementListenersList.value.push(listenerForm.value)
+ } else {
+ bpmnElementListeners.value.splice(editingListenerIndex.value, 1, listenerObject)
+ elementListenersList.value.splice(editingListenerIndex.value, 1, listenerForm.value)
+ }
+ // 淇濆瓨鍏朵粬閰嶇疆
+ otherExtensionList.value =
+ bpmnElement.businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type !== `${prefix}:TaskListener`
+ ) ?? []
+ updateElementExtensions(
+ bpmnElement,
+ otherExtensionList.value.concat(bpmnElementListeners.value)
+ )
+ // 4. 闅愯棌渚ц竟鏍�
+ listenerFormModelVisible.value = false
+ listenerForm.value = {}
+}
+// 鎵撳紑鐩戝惉鍣ㄥ瓧娈电紪杈戝脊绐�
+const openListenerFieldForm = (field, index?) => {
+ listenerFieldForm.value = field ? JSON.parse(JSON.stringify(field)) : {}
+ editingListenerFieldIndex.value = field ? index : -1
+ listenerFieldFormModelVisible.value = true
+ nextTick(() => {
+ if (listenerFieldFormRef.value) listenerFieldFormRef.value.clearValidate()
+ })
+}
+// 淇濆瓨鐩戝惉鍣ㄦ敞鍏ュ瓧娈�
+const saveListenerFiled = async () => {
+ let validateStatus = await listenerFieldFormRef.value.validate()
+ if (!validateStatus) return // 楠岃瘉涓嶉�氳繃鐩存帴杩斿洖
+ if (editingListenerFieldIndex.value === -1) {
+ fieldsListOfListener.value.push(listenerFieldForm.value)
+ listenerForm.value.fields.push(listenerFieldForm.value)
+ } else {
+ fieldsListOfListener.value.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
+ listenerForm.value.fields.splice(editingListenerFieldIndex.value, 1, listenerFieldForm.value)
+ }
+ listenerFieldFormModelVisible.value = false
+ nextTick(() => {
+ listenerFieldForm.value = {}
+ })
+}
+// 绉婚櫎鐩戝惉鍣ㄥ瓧娈�
+const removeListenerField = (field, index) => {
+ console.log(field, 'field')
+ ElMessageBox.confirm('纭绉婚櫎璇ュ瓧娈靛悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭� 璁�',
+ cancelButtonText: '鍙� 娑�'
+ })
+ .then(() => {
+ fieldsListOfListener.value.splice(index, 1)
+ listenerForm.value.fields.splice(index, 1)
+ })
+ .catch(() => console.info('鎿嶄綔鍙栨秷'))
+}
+
+// 鎵撳紑鐩戝惉鍣ㄥ脊绐�
+const processListenerDialogRef = ref()
+const openProcessListenerDialog = async () => {
+ processListenerDialogRef.value.open('task')
+}
+const selectProcessListener = (listener) => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ const bpmnElement = instances.bpmnElement
+ const listenerForm = initListenerForm2(listener)
+ const listenerObject = createListenerObject(listenerForm, true, prefix)
+ bpmnElementListeners.value.push(listenerObject)
+ elementListenersList.value.push(listenerForm)
+
+ // 淇濆瓨鍏朵粬閰嶇疆
+ otherExtensionList.value =
+ bpmnElement.businessObject?.extensionElements?.values?.filter(
+ (ex) => ex.$type !== `${prefix}:TaskListener`
+ ) ?? []
+ updateElementExtensions(
+ bpmnElement,
+ otherExtensionList.value.concat(bpmnElementListeners.value)
+ )
+}
+
+watch(
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ resetListenersList()
+ })
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/template.js b/src/components/bpmnProcessDesigner/package/penal/listeners/template.js
new file mode 100644
index 0000000..430dc64
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/template.js
@@ -0,0 +1,178 @@
+export const template = (isTaskListener) => {
+ return `
+ <div class="panel-tab__content">
+ <el-table :data="elementListenersList" size="small" border>
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column label="浜嬩欢绫诲瀷" min-width="100px" prop="event" />
+ <el-table-column label="鐩戝惉鍣ㄧ被鍨�" min-width="100px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" />
+ <el-table-column label="鎿嶄綔" width="90px">
+ <template #default="scope">
+ <el-button size="small" type="primary" link @click="openListenerForm(scope, scope.$index)">缂栬緫</el-button>
+ <el-divider direction="vertical" />
+ <el-button size="small" type="primary" link style="color: #ff4d4f" @click="removeListener(scope, scope.$index)">绉婚櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="element-drawer__button">
+ <el-button size="small" type="primary" icon="el-icon-plus" @click="openListenerForm(null)">娣诲姞鐩戝惉鍣�</el-button>
+ </div>
+
+ <!-- 鐩戝惉鍣� 缂栬緫/鍒涘缓 閮ㄥ垎 -->
+ <el-drawer :visible.sync="listenerFormModelVisible" title="鎵ц鐩戝惉鍣�" :size="width + 'px'" append-to-body destroy-on-close>
+ <el-form size="small" :model="listenerForm" label-width="96px" ref="listenerFormRef" @submit.native.prevent>
+ <el-form-item label="浜嬩欢绫诲瀷" prop="event" :rules="{ required: true, trigger: ['blur', 'change'] }">
+ <el-select v-model="listenerForm.event">
+ <el-option label="start" value="start" />
+ <el-option label="end" value="end" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐩戝惉鍣ㄧ被鍨�" prop="listenerType" :rules="{ required: true, trigger: ['blur', 'change'] }">
+ <el-select v-model="listenerForm.listenerType">
+ <el-option v-for="i in Object.keys(listenerTypeObject)" :key="i" :label="listenerTypeObject[i]" :value="i" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'classListener'"
+ label="Java绫�"
+ prop="class"
+ key="listener-class"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.class" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'expressionListener'"
+ label="琛ㄨ揪寮�"
+ prop="expression"
+ key="listener-expression"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.expression" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.listenerType === 'delegateExpressionListener'"
+ label="浠g悊琛ㄨ揪寮�"
+ prop="delegateExpression"
+ key="listener-delegate"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerForm.delegateExpression" clearable />
+ </el-form-item>
+ <template v-if="listenerForm.listenerType === 'scriptListener'">
+ <el-form-item
+ label="鑴氭湰鏍煎紡"
+ prop="scriptFormat"
+ key="listener-script-format"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒剼鏈牸寮�' }"
+ >
+ <el-input v-model="listenerForm.scriptFormat" clearable />
+ </el-form-item>
+ <el-form-item
+ label="鑴氭湰绫诲瀷"
+ prop="scriptType"
+ key="listener-script-type"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇烽�夋嫨鑴氭湰绫诲瀷' }"
+ >
+ <el-select v-model="listenerForm.scriptType">
+ <el-option label="鍐呰仈鑴氭湰" value="inlineScript" />
+ <el-option label="澶栭儴鑴氭湰" value="externalScript" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.scriptType === 'inlineScript'"
+ label="鑴氭湰鍐呭"
+ prop="value"
+ key="listener-script"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒剼鏈唴瀹�' }"
+ >
+ <el-input v-model="listenerForm.value" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerForm.scriptType === 'externalScript'"
+ label="璧勬簮鍦板潃"
+ prop="resource"
+ key="listener-resource"
+ :rules="{ required: true, trigger: ['blur', 'change'], message: '璇峰~鍐欒祫婧愬湴鍧�' }"
+ >
+ <el-input v-model="listenerForm.resource" clearable />
+ </el-form-item>
+ </template>
+ ${
+ isTaskListener
+ ? "<el-form-item label='瀹氭椂鍣ㄧ被鍨�' prop='eventDefinitionType' key='eventDefinitionType'>" +
+ "<el-select v-model='listenerForm.eventDefinitionType'>" +
+ "<el-option label='鏃ユ湡' value='date' />" +
+ "<el-option label='鎸佺画鏃堕暱' value='duration' />" +
+ "<el-option label='寰幆' value='cycle' />" +
+ "<el-option label='鏃�' value='' />" +
+ '</el-select>' +
+ '</el-form-item>' +
+ "<el-form-item v-if='!!listenerForm.eventDefinitionType' label='瀹氭椂鍣�' prop='eventDefinitions' key='eventDefinitions'>" +
+ "<el-input v-model='listenerForm.eventDefinitions' clearable />" +
+ '</el-form-item>'
+ : ''
+ }
+ </el-form>
+ <el-divider />
+ <p class="listener-filed__title">
+ <span><i class="el-icon-menu"></i>娉ㄥ叆瀛楁锛�</span>
+ <el-button size="small" type="primary" @click="openListenerFieldForm(null)">娣诲姞瀛楁</el-button>
+ </p>
+ <el-table :data="fieldsListOfListener" size="small" max-height="240" border fit style="flex: none">
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column label="瀛楁鍚嶇О" min-width="100px" prop="name" />
+ <el-table-column label="瀛楁绫诲瀷" min-width="80px" show-overflow-tooltip :formatter="row => fieldTypeObject[row.fieldType]" />
+ <el-table-column label="瀛楁鍊�/琛ㄨ揪寮�" min-width="100px" show-overflow-tooltip :formatter="row => row.string || row.expression" />
+ <el-table-column label="鎿嶄綔" width="100px">
+ <template #default="scope">
+ <el-button size="small" type="primary" link @click="openListenerFieldForm(scope, scope.$index)">缂栬緫</el-button>
+ <el-divider direction="vertical" />
+ <el-button size="small" type="primary" link style="color: #ff4d4f" @click="removeListenerField(scope, scope.$index)">绉婚櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="element-drawer__button">
+ <el-button size="small" @click="listenerFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button size="small" type="primary" @click="saveListenerConfig">淇� 瀛�</el-button>
+ </div>
+ </el-drawer>
+
+ <!-- 娉ㄥ叆瑗挎 缂栬緫/鍒涘缓 閮ㄥ垎 -->
+ <el-dialog title="瀛楁閰嶇疆" :visible.sync="listenerFieldFormModelVisible" width="600px" append-to-body destroy-on-close>
+ <el-form :model="listenerFieldForm" size="small" label-width="96px" ref="listenerFieldFormRef" style="height: 136px" @submit.native.prevent>
+ <el-form-item label="瀛楁鍚嶇О锛�" prop="name" :rules="{ required: true, trigger: ['blur', 'change'] }">
+ <el-input v-model="listenerFieldForm.name" clearable />
+ </el-form-item>
+ <el-form-item label="瀛楁绫诲瀷锛�" prop="fieldType" :rules="{ required: true, trigger: ['blur', 'change'] }">
+ <el-select v-model="listenerFieldForm.fieldType">
+ <el-option v-for="i in Object.keys(fieldTypeObject)" :key="i" :label="fieldTypeObject[i]" :value="i" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="listenerFieldForm.fieldType === 'string'"
+ label="瀛楁鍊硷細"
+ prop="string"
+ key="field-string"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.string" clearable />
+ </el-form-item>
+ <el-form-item
+ v-if="listenerFieldForm.fieldType === 'expression'"
+ label="琛ㄨ揪寮忥細"
+ prop="expression"
+ key="field-expression"
+ :rules="{ required: true, trigger: ['blur', 'change'] }"
+ >
+ <el-input v-model="listenerFieldForm.expression" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button size="small" @click="listenerFieldFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button size="small" type="primary" @click="saveListenerFiled">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+ `
+}
diff --git a/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
new file mode 100644
index 0000000..b4eb1d2
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/listeners/utilSelf.ts
@@ -0,0 +1,89 @@
+// 鍒濆鍖栬〃鍗曟暟鎹�
+export function initListenerForm(listener) {
+ let self = {
+ ...listener
+ }
+ if (listener.script) {
+ self = {
+ ...listener,
+ ...listener.script,
+ scriptType: listener.script.resource ? 'externalScript' : 'inlineScript'
+ }
+ }
+ if (listener.event === 'timeout' && listener.eventDefinitions) {
+ if (listener.eventDefinitions.length) {
+ let k = ''
+ for (const key in listener.eventDefinitions[0]) {
+ console.log(listener.eventDefinitions, key)
+ if (key.indexOf('time') !== -1) {
+ k = key
+ self.eventDefinitionType = key.replace('time', '').toLowerCase()
+ }
+ }
+ console.log(k)
+ self.eventTimeDefinitions = listener.eventDefinitions[0][k].body
+ }
+ }
+ return self
+}
+
+export function initListenerType(listener) {
+ let listenerType
+ if (listener.class) listenerType = 'classListener'
+ if (listener.expression) listenerType = 'expressionListener'
+ if (listener.delegateExpression) listenerType = 'delegateExpressionListener'
+ if (listener.script) listenerType = 'scriptListener'
+ return {
+ ...JSON.parse(JSON.stringify(listener)),
+ ...(listener.script ?? {}),
+ listenerType: listenerType
+ }
+}
+
+/** 灏� ProcessListenerDO 杞崲鎴� initListenerForm 鎯冲悓鐨� Form 瀵硅薄 */
+export function initListenerForm2(processListener) {
+ if (processListener.valueType === 'class') {
+ return {
+ listenerType: 'classListener',
+ class: processListener.value,
+ event: processListener.event,
+ fields: []
+ }
+ } else if (processListener.valueType === 'expression') {
+ return {
+ listenerType: 'expressionListener',
+ expression: processListener.value,
+ event: processListener.event,
+ fields: []
+ }
+ } else if (processListener.valueType === 'delegateExpression') {
+ return {
+ listenerType: 'delegateExpressionListener',
+ delegateExpression: processListener.value,
+ event: processListener.event,
+ fields: []
+ }
+ }
+ throw new Error('鏈煡鐨勭洃鍚櫒绫诲瀷')
+}
+
+export const listenerType = {
+ classListener: 'Java 绫�',
+ expressionListener: '琛ㄨ揪寮�',
+ delegateExpressionListener: '浠g悊琛ㄨ揪寮�',
+ scriptListener: '鑴氭湰'
+}
+
+export const eventType = {
+ create: '鍒涘缓',
+ assignment: '鎸囨淳',
+ complete: '瀹屾垚',
+ delete: '鍒犻櫎',
+ update: '鏇存柊',
+ timeout: '瓒呮椂'
+}
+
+export const fieldType = {
+ string: '瀛楃涓�',
+ expression: '琛ㄨ揪寮�'
+}
diff --git a/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
new file mode 100644
index 0000000..99ee6f8
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/multi-instance/ElementMultiInstance.vue
@@ -0,0 +1,421 @@
+<template>
+ <div class="panel-tab__content">
+ <el-radio-group
+ v-if="type === 'UserTask'"
+ v-model="approveMethod"
+ @change="onApproveMethodChange"
+ >
+ <div class="flex-col">
+ <div v-for="(item, index) in APPROVE_METHODS" :key="index">
+ <el-radio :value="item.value" :label="item.value">
+ {{ item.label }}
+ </el-radio>
+ <el-form-item prop="approveRatio">
+ <el-input-number
+ v-model="approveRatio"
+ :min="10"
+ :max="100"
+ :step="10"
+ size="small"
+ v-if="
+ item.value === ApproveMethodType.APPROVE_BY_RATIO &&
+ approveMethod === ApproveMethodType.APPROVE_BY_RATIO
+ "
+ @change="onApproveRatioChange"
+ />
+ </el-form-item>
+ </div>
+ </div>
+ </el-radio-group>
+ <div v-else>
+ 闄や簡UserTask浠ュ鑺傜偣鐨勫瀹炰緥寰呭疄鐜�
+ </div>
+ <!-- 涓嶴imple璁捐鍣ㄩ厤缃悎骞讹紝淇濈暀浠ュ墠鐨勪唬鐮� -->
+ <el-form label-width="90px" style="display: none">
+ <el-form-item label="蹇嵎閰嶇疆">
+ <el-button size="small" @click="changeConfig('渚濇瀹℃壒')">渚濇瀹℃壒</el-button>
+ <el-button size="small" @click="changeConfig('浼氱')">浼氱</el-button>
+ <el-button size="small" @click="changeConfig('鎴栫')">鎴栫</el-button>
+ </el-form-item>
+ <el-form-item label="浼氱绫诲瀷">
+ <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
+ <el-option label="骞惰澶氶噸浜嬩欢" value="ParallelMultiInstance" />
+ <el-option label="鏃跺簭澶氶噸浜嬩欢" value="SequentialMultiInstance" />
+ <el-option label="鏃�" value="Null" />
+ </el-select>
+ </el-form-item>
+ <template
+ v-if="
+ loopCharacteristics === 'ParallelMultiInstance' ||
+ loopCharacteristics === 'SequentialMultiInstance'
+ "
+ >
+ <el-form-item label="寰幆鏁伴噺" key="loopCardinality">
+ <el-input
+ v-model="loopInstanceForm.loopCardinality"
+ clearable
+ @change="updateLoopCardinality"
+ />
+ </el-form-item>
+ <el-form-item label="闆嗗悎" key="collection" v-show="false">
+ <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
+ </el-form-item>
+ <!-- add by 鑺嬭壙锛氱敱浜庛�屽厓绱犲彉閲忋�嶆殏鏃剁敤涓嶅埌锛屾墍浠ヨ繖閲� display 涓� none -->
+ <el-form-item label="鍏冪礌鍙橀噺" key="elementVariable" style="display: none">
+ <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
+ </el-form-item>
+ <el-form-item label="瀹屾垚鏉′欢" key="completionCondition">
+ <el-input
+ v-model="loopInstanceForm.completionCondition"
+ clearable
+ @change="updateLoopCondition"
+ />
+ </el-form-item>
+ <!-- add by 鑺嬭壙锛氱敱浜庛�屽紓姝ョ姸鎬併�嶆殏鏃剁敤涓嶅埌锛屾墍浠ヨ繖閲� display 涓� none -->
+ <el-form-item label="寮傛鐘舵��" key="async" style="display: none">
+ <el-checkbox
+ v-model="loopInstanceForm.asyncBefore"
+ label="寮傛鍓�"
+ value="寮傛鍓�"
+ @change="updateLoopAsync('asyncBefore')"
+ />
+ <el-checkbox
+ v-model="loopInstanceForm.asyncAfter"
+ label="寮傛鍚�"
+ value="寮傛鍚�"
+ @change="updateLoopAsync('asyncAfter')"
+ />
+ <el-checkbox
+ v-model="loopInstanceForm.exclusive"
+ v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
+ label="鎺掗櫎"
+ value="鎺掗櫎"
+ @change="updateLoopAsync('exclusive')"
+ />
+ </el-form-item>
+ <el-form-item
+ label="閲嶈瘯鍛ㄦ湡"
+ prop="timeCycle"
+ v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
+ key="timeCycle"
+ >
+ <el-input v-model="loopInstanceForm.timeCycle" clearable @change="updateLoopTimeCycle" />
+ </el-form-item>
+ </template>
+ </el-form>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { ApproveMethodType, APPROVE_METHODS } from '@/components/SimpleProcessDesignerV2/src/consts'
+
+defineOptions({ name: 'ElementMultiInstance' })
+
+const props = defineProps({
+ businessObject: Object,
+ type: String,
+ id: String
+})
+const prefix = inject('prefix')
+const loopCharacteristics = ref('')
+//榛樿閰嶇疆锛岀敤鏉ヨ鐩栧師濮嬩笉瀛樺湪鐨勯�夐」锛岄伩鍏嶆姤閿�
+const defaultLoopInstanceForm = ref({
+ completionCondition: '',
+ loopCardinality: '',
+ extensionElements: [],
+ asyncAfter: false,
+ asyncBefore: false,
+ exclusive: false
+})
+const loopInstanceForm = ref<any>({})
+const bpmnElement = ref(null)
+const multiLoopInstance = ref(null)
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const getElementLoop = (businessObject) => {
+ if (!businessObject.loopCharacteristics) {
+ loopCharacteristics.value = 'Null'
+ loopInstanceForm.value = {}
+ return
+ }
+ if (businessObject.loopCharacteristics.$type === 'bpmn:StandardLoopCharacteristics') {
+ loopCharacteristics.value = 'StandardLoop'
+ loopInstanceForm.value = {}
+ return
+ }
+ if (businessObject.loopCharacteristics.isSequential) {
+ loopCharacteristics.value = 'SequentialMultiInstance'
+ } else {
+ loopCharacteristics.value = 'ParallelMultiInstance'
+ }
+ // 鍚堝苟閰嶇疆
+ loopInstanceForm.value = {
+ ...defaultLoopInstanceForm.value,
+ ...businessObject.loopCharacteristics,
+ completionCondition: businessObject.loopCharacteristics?.completionCondition?.body ?? '',
+ loopCardinality: businessObject.loopCharacteristics?.loopCardinality?.body ?? ''
+ }
+ // 淇濈暀褰撳墠鍏冪礌 businessObject 涓婄殑 loopCharacteristics 瀹炰緥
+ multiLoopInstance.value = bpmnInstances().bpmnElement.businessObject.loopCharacteristics
+ // 鏇存柊琛ㄥ崟
+ if (
+ businessObject.loopCharacteristics.extensionElements &&
+ businessObject.loopCharacteristics.extensionElements.values &&
+ businessObject.loopCharacteristics.extensionElements.values.length
+ ) {
+ loopInstanceForm.value['timeCycle'] =
+ businessObject.loopCharacteristics.extensionElements.values[0].body
+ }
+}
+
+const changeLoopCharacteristicsType = (type) => {
+ // this.loopInstanceForm = { ...this.defaultLoopInstanceForm }; // 鍒囨崲绫诲瀷鍙栨秷鍘熻〃鍗曢厤缃�
+ // 鍙栨秷澶氬疄渚嬮厤缃�
+ if (type === 'Null') {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ loopCharacteristics: null
+ })
+ return
+ }
+ // 閰嶇疆寰幆
+ if (type === 'StandardLoop') {
+ const loopCharacteristicsObject = bpmnInstances().moddle.create(
+ 'bpmn:StandardLoopCharacteristics'
+ )
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ loopCharacteristics: loopCharacteristicsObject
+ })
+ multiLoopInstance.value = null
+ return
+ }
+ // 鏃跺簭
+ if (type === 'SequentialMultiInstance') {
+ multiLoopInstance.value = bpmnInstances().moddle.create(
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ { isSequential: true }
+ )
+ } else {
+ multiLoopInstance.value = bpmnInstances().moddle.create(
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ { collection: '${coll_userList}' }
+ )
+ }
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ loopCharacteristics: toRaw(multiLoopInstance.value)
+ })
+}
+
+// 寰幆鍩烘暟
+const updateLoopCardinality = (cardinality) => {
+ let loopCardinality = null
+ if (cardinality && cardinality.length) {
+ loopCardinality = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+ body: cardinality
+ })
+ }
+ bpmnInstances().modeling.updateModdleProperties(
+ toRaw(bpmnElement.value),
+ multiLoopInstance.value,
+ {
+ loopCardinality
+ }
+ )
+}
+
+// 瀹屾垚鏉′欢
+const updateLoopCondition = (condition) => {
+ let completionCondition = null
+ if (condition && condition.length) {
+ completionCondition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+ body: condition
+ })
+ }
+ bpmnInstances().modeling.updateModdleProperties(
+ toRaw(bpmnElement.value),
+ multiLoopInstance.value,
+ {
+ completionCondition
+ }
+ )
+}
+
+// 閲嶈瘯鍛ㄦ湡
+const updateLoopTimeCycle = (timeCycle) => {
+ const extensionElements = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [
+ bpmnInstances().moddle.create(`${prefix}:FailedJobRetryTimeCycle`, {
+ body: timeCycle
+ })
+ ]
+ })
+ bpmnInstances().modeling.updateModdleProperties(
+ toRaw(bpmnElement.value),
+ multiLoopInstance.value,
+ {
+ extensionElements
+ }
+ )
+}
+
+// 鐩存帴鏇存柊鐨勫熀纭�淇℃伅
+const updateLoopBase = () => {
+ bpmnInstances().modeling.updateModdleProperties(
+ toRaw(bpmnElement.value),
+ multiLoopInstance.value,
+ {
+ collection: loopInstanceForm.value.collection || null,
+ elementVariable: loopInstanceForm.value.elementVariable || null
+ }
+ )
+}
+
+// 鍚勫紓姝ョ姸鎬�
+const updateLoopAsync = (key) => {
+ const { asyncBefore, asyncAfter } = loopInstanceForm.value
+ let asyncAttr = Object.create(null)
+ if (!asyncBefore && !asyncAfter) {
+ // this.$set(this.loopInstanceForm, "exclusive", false);
+ loopInstanceForm.value['exclusive'] = false
+ asyncAttr = { asyncBefore: false, asyncAfter: false, exclusive: false, extensionElements: null }
+ } else {
+ asyncAttr[key] = loopInstanceForm.value[key]
+ }
+ bpmnInstances().modeling.updateModdleProperties(
+ toRaw(bpmnElement.value),
+ multiLoopInstance.value,
+ asyncAttr
+ )
+}
+
+const changeConfig = (config) => {
+ if (config === '渚濇瀹℃壒') {
+ changeLoopCharacteristicsType('SequentialMultiInstance')
+ updateLoopCardinality('1')
+ updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+ } else if (config === '浼氱') {
+ changeLoopCharacteristicsType('ParallelMultiInstance')
+ updateLoopCondition('${ nrOfCompletedInstances >= nrOfInstances }')
+ } else if (config === '鎴栫') {
+ changeLoopCharacteristicsType('ParallelMultiInstance')
+ updateLoopCondition('${ nrOfCompletedInstances > 0 }')
+ }
+}
+
+/**
+ * -----鏂扮増鏈瀹炰緥-----
+ */
+const approveMethod = ref()
+const approveRatio = ref(100)
+const otherExtensions = ref()
+const getElementLoopNew = () => {
+ if (props.type === 'UserTask') {
+ const extensionElements =
+ bpmnElement.value.businessObject?.extensionElements ??
+ bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+ approveMethod.value = extensionElements.values.filter(
+ (ex) => ex.$type === `${prefix}:ApproveMethod`
+ )?.[0]?.value
+
+ otherExtensions.value =
+ extensionElements.values.filter((ex) => ex.$type !== `${prefix}:ApproveMethod`) ?? []
+
+ if (!approveMethod.value) {
+ approveMethod.value = ApproveMethodType.SEQUENTIAL_APPROVE
+ updateLoopCharacteristics()
+ }
+ }
+}
+const onApproveMethodChange = () => {
+ approveRatio.value = 100
+ updateLoopCharacteristics()
+}
+const onApproveRatioChange = () => {
+ updateLoopCharacteristics()
+}
+const updateLoopCharacteristics = () => {
+ // 鏍规嵁ApproveMethod鐢熸垚multiInstanceLoopCharacteristics鑺傜偣
+ if (approveMethod.value === ApproveMethodType.RANDOM_SELECT_ONE_APPROVE) {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ loopCharacteristics: null
+ })
+ } else {
+ if (approveMethod.value === ApproveMethodType.APPROVE_BY_RATIO) {
+ multiLoopInstance.value = bpmnInstances().moddle.create(
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ { isSequential: false, collection: '${coll_userList}' }
+ )
+ multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
+ 'bpmn:FormalExpression',
+ {
+ body: '${ nrOfCompletedInstances/nrOfInstances >= ' + approveRatio.value / 100 + '}'
+ }
+ )
+ }
+ if (approveMethod.value === ApproveMethodType.ANY_APPROVE) {
+ multiLoopInstance.value = bpmnInstances().moddle.create(
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ { isSequential: false, collection: '${coll_userList}' }
+ )
+ multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
+ 'bpmn:FormalExpression',
+ {
+ body: '${ nrOfCompletedInstances > 0 }'
+ }
+ )
+ }
+ if (approveMethod.value === ApproveMethodType.SEQUENTIAL_APPROVE) {
+ multiLoopInstance.value = bpmnInstances().moddle.create(
+ 'bpmn:MultiInstanceLoopCharacteristics',
+ { isSequential: true, collection: '${coll_userList}' }
+ )
+ multiLoopInstance.value.loopCardinality = bpmnInstances().moddle.create(
+ 'bpmn:FormalExpression',
+ {
+ body: '1'
+ }
+ )
+ multiLoopInstance.value.completionCondition = bpmnInstances().moddle.create(
+ 'bpmn:FormalExpression',
+ {
+ body: '${ nrOfCompletedInstances >= nrOfInstances }'
+ }
+ )
+ }
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ loopCharacteristics: toRaw(multiLoopInstance.value)
+ })
+ }
+
+ // 娣诲姞ApproveMethod鍒癊xtensionElements
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [
+ ...otherExtensions.value,
+ bpmnInstances().moddle.create(`${prefix}:ApproveMethod`, {
+ value: approveMethod.value
+ })
+ ]
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: extensions
+ })
+}
+
+onBeforeUnmount(() => {
+ multiLoopInstance.value = null
+ bpmnElement.value = null
+})
+
+watch(
+ () => props.id,
+ (val) => {
+ if (val) {
+ nextTick(() => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ // getElementLoop(val)
+ getElementLoopNew()
+ })
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue b/src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue
new file mode 100644
index 0000000..05532c6
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/other/ElementOtherConfig.vue
@@ -0,0 +1,55 @@
+<template>
+ <div class="panel-tab__content">
+ <div class="element-property input-property">
+ <div class="element-property__label">鍏冪礌鏂囨。锛�</div>
+ <div class="element-property__value">
+ <el-input
+ type="textarea"
+ v-model="documentation"
+ resize="vertical"
+ :autosize="{ minRows: 2, maxRows: 4 }"
+ @input="updateDocumentation"
+ @blur="updateDocumentation"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'ElementOtherConfig' })
+const props = defineProps({
+ id: String
+})
+const documentation = ref('')
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any).bpmnInstances
+const updateDocumentation = () => {
+ ;(bpmnElement.value && bpmnElement.value.id === props.id) ||
+ (bpmnElement.value = bpmnInstances().elementRegistry.get(props.id))
+ const documentations = bpmnInstances().bpmnFactory.create('bpmn:Documentation', {
+ text: documentation.value
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ documentation: [documentations]
+ })
+}
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+})
+
+watch(
+ () => props.id,
+ (id) => {
+ if (id && id.length) {
+ nextTick(() => {
+ const documentations = bpmnInstances().bpmnElement.businessObject?.documentation
+ documentation.value = documentations && documentations.length ? documentations[0].text : ''
+ })
+ } else {
+ documentation.value = ''
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue b/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue
new file mode 100644
index 0000000..49c35aa
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/properties/ElementProperties.vue
@@ -0,0 +1,181 @@
+<template>
+ <div class="panel-tab__content">
+ <el-table :data="elementPropertyList" max-height="240" fit border>
+ <el-table-column label="搴忓彿" width="50px" type="index" />
+ <el-table-column label="灞炴�у悕" prop="name" min-width="100px" show-overflow-tooltip />
+ <el-table-column label="灞炴�у��" prop="value" min-width="100px" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="110px">
+ <template #default="scope">
+ <el-button link @click="openAttributesForm(scope.row, scope.$index)" size="small">
+ 缂栬緫
+ </el-button>
+ <el-divider direction="vertical" />
+ <el-button
+ link
+ size="small"
+ style="color: #ff4d4f"
+ @click="removeAttributes(scope.row, scope.$index)"
+ >
+ 绉婚櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="element-drawer__button">
+ <XButton
+ type="primary"
+ preIcon="ep:plus"
+ title="娣诲姞灞炴��"
+ @click="openAttributesForm(null, -1)"
+ />
+ </div>
+
+ <el-dialog
+ v-model="propertyFormModelVisible"
+ title="灞炴�ч厤缃�"
+ width="600px"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form :model="propertyForm" label-width="80px" ref="attributeFormRef">
+ <el-form-item label="灞炴�у悕锛�" prop="name">
+ <el-input v-model="propertyForm.name" clearable />
+ </el-form-item>
+ <el-form-item label="灞炴�у�硷細" prop="value">
+ <el-input v-model="propertyForm.value" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="propertyFormModelVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="saveAttribute">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+defineOptions({ name: 'ElementProperties' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+// const width = inject('width')
+
+const elementPropertyList = ref<any[]>([])
+const propertyForm = ref<any>({})
+const editingPropertyIndex = ref(-1)
+const propertyFormModelVisible = ref(false)
+const otherExtensionList = ref()
+const bpmnElementProperties = ref()
+const bpmnElementPropertyList = ref()
+const attributeFormRef = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetAttributesList = () => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ // 鐩存帴浣跨敤鍘熷BPMN鍏冪礌锛岄伩鍏峍ue鍝嶅簲寮忎唬鐞嗛棶棰�
+ const bpmnElement = instances.bpmnElement
+ const businessObject = bpmnElement.businessObject
+
+ otherExtensionList.value = [] // 鍏朵粬鎵╁睍閰嶇疆
+ bpmnElementProperties.value =
+ businessObject?.extensionElements?.values?.filter((ex) => {
+ if (ex.$type !== `${prefix}:Properties`) {
+ otherExtensionList.value.push(ex)
+ }
+ return ex.$type === `${prefix}:Properties`
+ }) ?? []
+
+ // 淇濆瓨鎵�鏈夌殑 鎵╁睍灞炴�у瓧娈�
+ bpmnElementPropertyList.value = bpmnElementProperties.value.reduce(
+ (pre, current) => pre.concat(current.values),
+ []
+ )
+ // 澶嶅埗 鏄剧ず
+ elementPropertyList.value = JSON.parse(JSON.stringify(bpmnElementPropertyList.value ?? []))
+}
+const openAttributesForm = (attr, index) => {
+ editingPropertyIndex.value = index
+ propertyForm.value = index === -1 ? {} : JSON.parse(JSON.stringify(attr))
+ propertyFormModelVisible.value = true
+ nextTick(() => {
+ if (attributeFormRef.value) attributeFormRef.value.clearValidate()
+ })
+}
+const removeAttributes = (attr, index) => {
+ console.log(attr, 'attr')
+ ElMessageBox.confirm('纭绉婚櫎璇ュ睘鎬у悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭� 璁�',
+ cancelButtonText: '鍙� 娑�'
+ })
+ .then(() => {
+ elementPropertyList.value.splice(index, 1)
+ bpmnElementPropertyList.value.splice(index, 1)
+ // 鏂板缓涓�涓睘鎬у瓧娈电殑淇濆瓨鍒楄〃
+ const propertiesObject = bpmnInstances().moddle.create(`${prefix}:Properties`, {
+ values: bpmnElementPropertyList.value
+ })
+ updateElementExtensions(propertiesObject)
+ resetAttributesList()
+ })
+ .catch(() => console.info('鎿嶄綔鍙栨秷'))
+}
+const saveAttribute = () => {
+ console.log(propertyForm.value, 'propertyForm.value')
+ const { name, value } = propertyForm.value
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ const bpmnElement = instances.bpmnElement
+
+ if (editingPropertyIndex.value !== -1) {
+ instances.modeling.updateModdleProperties(
+ bpmnElement,
+ bpmnElementPropertyList.value[editingPropertyIndex.value],
+ {
+ name,
+ value
+ }
+ )
+ } else {
+ // 鏂板缓灞炴�у瓧娈�
+ const newPropertyObject = instances.moddle.create(`${prefix}:Property`, {
+ name,
+ value
+ })
+ // 鏂板缓涓�涓睘鎬у瓧娈电殑淇濆瓨鍒楄〃
+ const propertiesObject = instances.moddle.create(`${prefix}:Properties`, {
+ values: bpmnElementPropertyList.value.concat([newPropertyObject])
+ })
+ updateElementExtensions(propertiesObject)
+ }
+ propertyFormModelVisible.value = false
+ resetAttributesList()
+}
+const updateElementExtensions = (properties) => {
+ const instances = bpmnInstances()
+ if (!instances || !instances.bpmnElement) return
+
+ const bpmnElement = instances.bpmnElement
+ const extensions = instances.moddle.create('bpmn:ExtensionElements', {
+ values: otherExtensionList.value.concat([properties])
+ })
+ instances.modeling.updateProperties(bpmnElement, {
+ extensionElements: extensions
+ })
+}
+
+watch(
+ () => props.id,
+ (val) => {
+ if (val) {
+ val && val.length && resetAttributesList()
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue b/src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue
new file mode 100644
index 0000000..7dad24a
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/signal-message/SignalAndMessage.vue
@@ -0,0 +1,264 @@
+<template>
+ <div class="panel-tab__content">
+ <div class="panel-tab__content--title">
+ <span><Icon icon="ep:menu" style="margin-right: 8px; color: #555" />娑堟伅鍒楄〃</span>
+ <XButton type="primary" title="鍒涘缓鏂版秷鎭�" preIcon="ep:plus" @click="openModel('message')" />
+ </div>
+ <el-table :data="messageList" border>
+ <el-table-column type="index" label="搴忓彿" width="60px" />
+ <el-table-column label="娑堟伅ID" prop="id" min-width="120px" show-overflow-tooltip />
+ <el-table-column label="娑堟伅鍚嶇О" prop="name" min-width="120px" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="110px">
+ <!-- 琛ュ厖鈥滅紪杈戔�濄�佲�滅Щ闄も�濆姛鑳姐�傜浉鍏� issue锛歨ttps://github.com/YunaiV/yudao-cloud/issues/270 -->
+ <template #default="scope">
+ <el-button link @click="openEditModel('message', scope.row, scope.$index)" size="small">
+ 缂栬緫
+ </el-button>
+ <el-divider direction="vertical" />
+ <el-button
+ link
+ size="small"
+ style="color: #ff4d4f"
+ @click="removeObject('message', scope.row)"
+ >
+ 绉婚櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div
+ class="panel-tab__content--title"
+ style="padding-top: 8px; margin-top: 8px; border-top: 1px solid #eee"
+ >
+ <span><Icon icon="ep:menu" style="margin-right: 8px; color: #555" />淇″彿鍒楄〃</span>
+ <XButton type="primary" title="鍒涘缓鏂颁俊鍙�" preIcon="ep:plus" @click="openModel('signal')" />
+ </div>
+ <el-table :data="signalList" border>
+ <el-table-column type="index" label="搴忓彿" width="60px" />
+ <el-table-column label="淇″彿ID" prop="id" min-width="120px" show-overflow-tooltip />
+ <el-table-column label="淇″彿鍚嶇О" prop="name" min-width="120px" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="110px">
+ <template #default="scope">
+ <el-button link @click="openEditModel('signal', scope.row, scope.$index)" size="small">
+ 缂栬緫
+ </el-button>
+ <el-divider direction="vertical" />
+ <el-button
+ link
+ size="small"
+ style="color: #ff4d4f"
+ @click="removeObject('signal', scope.row)"
+ >
+ 绉婚櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <el-dialog
+ v-model="dialogVisible"
+ :title="modelConfig.title"
+ :close-on-click-modal="false"
+ width="400px"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form :model="modelObjectForm" label-width="90px">
+ <el-form-item :label="modelConfig.idLabel">
+ <el-input v-model="modelObjectForm.id" clearable />
+ </el-form-item>
+ <el-form-item :label="modelConfig.nameLabel">
+ <el-input v-model="modelObjectForm.name" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="addNewObject">淇� 瀛�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+defineOptions({ name: 'SignalAndMassage' })
+
+const message = useMessage()
+const signalList = ref<any[]>([])
+const messageList = ref<any[]>([])
+const dialogVisible = ref(false)
+const modelType = ref('')
+const modelObjectForm = ref<any>({})
+const rootElements = ref()
+const messageIdMap = ref()
+const signalIdMap = ref()
+const editingIndex = ref(-1) // 姝e湪缂栬緫鐨勭储寮曪紝-1 琛ㄧず鏂板缓
+const modelConfig = computed(() => {
+ const isEdit = editingIndex.value !== -1
+ if (modelType.value === 'message') {
+ return {
+ title: isEdit ? '缂栬緫娑堟伅' : '鍒涘缓娑堟伅',
+ idLabel: '娑堟伅ID',
+ nameLabel: '娑堟伅鍚嶇О'
+ }
+ } else {
+ return {
+ title: isEdit ? '缂栬緫淇″彿' : '鍒涘缓淇″彿',
+ idLabel: '淇″彿ID',
+ nameLabel: '淇″彿鍚嶇О'
+ }
+ }
+})
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+// 鐢熸垚瑙勮寖鍖栫殑ID
+const generateStandardId = (type: string): string => {
+ const prefix = type === 'message' ? 'Message_' : 'Signal_'
+ const timestamp = Date.now()
+ const random = Math.random().toString(36).substring(2, 6).toUpperCase()
+ return `${prefix}${timestamp}_${random}`
+}
+
+const initDataList = () => {
+ console.log(window, 'window')
+ rootElements.value = bpmnInstances().modeler.getDefinitions().rootElements
+ messageIdMap.value = {}
+ signalIdMap.value = {}
+ messageList.value = []
+ signalList.value = []
+ rootElements.value.forEach((el) => {
+ if (el.$type === 'bpmn:Message') {
+ messageIdMap.value[el.id] = true
+ messageList.value.push({ ...el })
+ }
+ if (el.$type === 'bpmn:Signal') {
+ signalIdMap.value[el.id] = true
+ signalList.value.push({ ...el })
+ }
+ })
+}
+const openModel = (type) => {
+ modelType.value = type
+ editingIndex.value = -1
+ modelObjectForm.value = {
+ id: generateStandardId(type),
+ name: ''
+ }
+ dialogVisible.value = true
+}
+
+const openEditModel = (type, row, index) => {
+ modelType.value = type
+ editingIndex.value = index
+ modelObjectForm.value = { ...row }
+ dialogVisible.value = true
+}
+const addNewObject = () => {
+ if (modelType.value === 'message') {
+ // 缂栬緫妯″紡
+ if (editingIndex.value !== -1) {
+ const targetMessage = messageList.value[editingIndex.value]
+ // 鏌ユ壘 rootElements 涓殑鍘熷瀵硅薄
+ const rootMessage = rootElements.value.find(
+ (el) => el.$type === 'bpmn:Message' && el.id === targetMessage.id
+ )
+ if (rootMessage) {
+ rootMessage.id = modelObjectForm.value.id
+ rootMessage.name = modelObjectForm.value.name
+ }
+ } else {
+ // 鏂板缓妯″紡
+ if (messageIdMap.value[modelObjectForm.value.id]) {
+ message.error('璇ユ秷鎭凡瀛樺湪锛岃淇敼id鍚庨噸鏂颁繚瀛�')
+ return
+ }
+ const messageRef = bpmnInstances().moddle.create('bpmn:Message', modelObjectForm.value)
+ rootElements.value.push(messageRef)
+ }
+ } else {
+ // 缂栬緫妯″紡
+ if (editingIndex.value !== -1) {
+ const targetSignal = signalList.value[editingIndex.value]
+ // 鏌ユ壘 rootElements 涓殑鍘熷瀵硅薄
+ const rootSignal = rootElements.value.find(
+ (el) => el.$type === 'bpmn:Signal' && el.id === targetSignal.id
+ )
+ if (rootSignal) {
+ rootSignal.id = modelObjectForm.value.id
+ rootSignal.name = modelObjectForm.value.name
+ }
+ } else {
+ // 鏂板缓妯″紡
+ if (signalIdMap.value[modelObjectForm.value.id]) {
+ message.error('璇ヤ俊鍙峰凡瀛樺湪锛岃淇敼id鍚庨噸鏂颁繚瀛�')
+ return
+ }
+ const signalRef = bpmnInstances().moddle.create('bpmn:Signal', modelObjectForm.value)
+ rootElements.value.push(signalRef)
+ }
+ }
+ dialogVisible.value = false
+ // 瑙﹀彂寤烘ā鍣ㄦ洿鏂颁互淇濆瓨鏇存敼
+ saveChanges()
+ initDataList()
+}
+
+const removeObject = (type, row) => {
+ ElMessageBox.confirm(`纭绉婚櫎璇�${type === 'message' ? '娑堟伅' : '淇″彿'}鍚楋紵`, '鎻愮ず', {
+ confirmButtonText: '纭� 璁�',
+ cancelButtonText: '鍙� 娑�'
+ })
+ .then(() => {
+ // 浠� rootElements 涓Щ闄�
+ const targetType = type === 'message' ? 'bpmn:Message' : 'bpmn:Signal'
+ const elementIndex = rootElements.value.findIndex(
+ (el) => el.$type === targetType && el.id === row.id
+ )
+ if (elementIndex !== -1) {
+ rootElements.value.splice(elementIndex, 1)
+ }
+ // 瑙﹀彂寤烘ā鍣ㄦ洿鏂颁互淇濆瓨鏇存敼
+ saveChanges()
+ // 鍒锋柊鍒楄〃
+ initDataList()
+ message.success('绉婚櫎鎴愬姛')
+ })
+ .catch(() => console.info('鎿嶄綔鍙栨秷'))
+}
+
+// 瑙﹀彂寤烘ā鍣ㄦ洿鏂颁互淇濆瓨鏇存敼
+const saveChanges = () => {
+ const modeler = bpmnInstances().modeler
+ if (!modeler) return
+
+ try {
+ // 鑾峰彇 canvas锛岄�氳繃瀹冩潵瑙﹀彂鍥捐〃鐨勯噸鏂版覆鏌�
+ const canvas = modeler.get('canvas')
+
+ // 鑾峰彇鏍瑰厓绱狅紙Process锛�
+ const rootElement = canvas.getRootElement()
+
+ // 瑙﹀彂 changed 浜嬩欢锛岄�氱煡寤烘ā鍣ㄦ暟鎹凡鏇存敼
+ const eventBus = modeler.get('eventBus')
+ if (eventBus) {
+ eventBus.fire('root.added', { element: rootElement })
+ eventBus.fire('elements.changed', { elements: [rootElement] })
+ }
+
+ // 鏍囪寤烘ā鍣ㄤ负宸蹭慨鏀圭姸鎬�
+ const commandStack = modeler.get('commandStack')
+ if (commandStack && commandStack._stack) {
+ // 娣诲姞涓�涓┖鍛戒护浠ユ爣璁颁负宸蹭慨鏀�
+ commandStack.execute('element.updateProperties', {
+ element: rootElement,
+ properties: {}
+ })
+ }
+ } catch (error) {
+ console.warn('淇濆瓨鏇存敼鏃跺嚭閿�:', error)
+ }
+}
+
+onMounted(() => {
+ initDataList()
+})
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
new file mode 100644
index 0000000..3a71b4c
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/ElementTask.vue
@@ -0,0 +1,78 @@
+<template>
+ <div class="panel-tab__content">
+ <el-form size="small" label-width="90px">
+ <!-- add by 鑺嬭壙锛氱敱浜庛�屽紓姝ュ欢缁�嶆殏鏃剁敤涓嶅埌锛屾墍浠ヨ繖閲� display 涓� none -->
+ <el-form-item label="寮傛寤剁画" style="display: none">
+ <el-checkbox
+ v-model="taskConfigForm.asyncBefore"
+ label="寮傛鍓�"
+ value="寮傛鍓�"
+ @change="changeTaskAsync"
+ />
+ <el-checkbox
+ v-model="taskConfigForm.asyncAfter"
+ label="寮傛鍚�"
+ value="寮傛鍚�"
+ @change="changeTaskAsync"
+ />
+ <el-checkbox
+ v-model="taskConfigForm.exclusive"
+ v-if="taskConfigForm.asyncAfter || taskConfigForm.asyncBefore"
+ label="鎺掗櫎"
+ value="鎺掗櫎"
+ @change="changeTaskAsync"
+ />
+ </el-form-item>
+ <component :is="witchTaskComponent" v-bind="$props" />
+ </el-form>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { installedComponent } from './data'
+
+defineOptions({ name: 'ElementTaskConfig' })
+
+const props = defineProps({
+ id: String,
+ type: String
+})
+const taskConfigForm = ref({
+ asyncAfter: false,
+ asyncBefore: false,
+ exclusive: false
+})
+const witchTaskComponent = ref()
+
+const bpmnElement = ref()
+
+const bpmnInstances = () => (window as any).bpmnInstances
+const changeTaskAsync = () => {
+ if (!taskConfigForm.value.asyncBefore && !taskConfigForm.value.asyncAfter) {
+ taskConfigForm.value.exclusive = false
+ }
+ bpmnInstances().modeling.updateProperties(bpmnInstances().bpmnElement, {
+ ...taskConfigForm.value
+ })
+}
+
+watch(
+ () => props.id,
+ () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ taskConfigForm.value.asyncBefore = bpmnElement.value?.businessObject?.asyncBefore
+ taskConfigForm.value.asyncAfter = bpmnElement.value?.businessObject?.asyncAfter
+ taskConfigForm.value.exclusive = bpmnElement.value?.businessObject?.exclusive
+ },
+ { immediate: true }
+)
+watch(
+ () => props.type,
+ () => {
+ if (props.type) {
+ witchTaskComponent.value = installedComponent[props.type].component
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/data.ts b/src/components/bpmnProcessDesigner/package/penal/task/data.ts
new file mode 100644
index 0000000..805c9ac
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/data.ts
@@ -0,0 +1,36 @@
+import UserTask from './task-components/UserTask.vue'
+import ServiceTask from './task-components/ServiceTask.vue'
+import ScriptTask from './task-components/ScriptTask.vue'
+import ReceiveTask from './task-components/ReceiveTask.vue'
+import CallActivity from './task-components/CallActivity.vue'
+
+export const installedComponent = {
+ UserTask: {
+ name: '鐢ㄦ埛浠诲姟',
+ component: UserTask
+ },
+ ServiceTask: {
+ name: '鏈嶅姟浠诲姟',
+ component: ServiceTask
+ },
+ ScriptTask: {
+ name: '鑴氭湰浠诲姟',
+ component: ScriptTask
+ },
+ ReceiveTask: {
+ name: '鎺ユ敹浠诲姟',
+ component: ReceiveTask
+ },
+ CallActivity: {
+ name: '璋冪敤娲诲姩',
+ component: CallActivity
+ }
+}
+
+export const getTaskCollapseItemName = (elementType) => {
+ return installedComponent[elementType].name
+}
+
+export const isTaskCollapseItemShow = (elementType) => {
+ return installedComponent[elementType]
+}
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue
new file mode 100644
index 0000000..6d8268b
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/CallActivity.vue
@@ -0,0 +1,280 @@
+<template>
+ <div>
+ <el-form label-width="100px">
+ <el-form-item label="瀹炰緥鍚嶇О" prop="processInstanceName">
+ <el-input
+ v-model="formData.processInstanceName"
+ clearable
+ placeholder="璇疯緭鍏ュ疄渚嬪悕绉�"
+ @change="updateCallActivityAttr('processInstanceName')"
+ />
+ </el-form-item>
+
+ <!-- TODO 闇�瑕佸彲閫夋嫨宸插瓨鍦ㄧ殑娴佺▼ -->
+ <el-form-item label="琚皟鐢ㄦ祦绋�" prop="calledElement">
+ <el-input
+ v-model="formData.calledElement"
+ clearable
+ placeholder="璇疯緭鍏ヨ璋冪敤娴佺▼"
+ @change="updateCallActivityAttr('calledElement')"
+ />
+ </el-form-item>
+
+ <el-form-item label="缁ф壙鍙橀噺" prop="inheritVariables">
+ <el-switch
+ v-model="formData.inheritVariables"
+ @change="updateCallActivityAttr('inheritVariables')"
+ />
+ </el-form-item>
+
+ <el-form-item label="缁ф壙涓氬姟閿�" prop="inheritBusinessKey">
+ <el-switch
+ v-model="formData.inheritBusinessKey"
+ @change="updateCallActivityAttr('inheritBusinessKey')"
+ />
+ </el-form-item>
+
+ <el-form-item v-if="!formData.inheritBusinessKey" label="涓氬姟閿〃杈惧紡" prop="businessKey">
+ <el-input
+ v-model="formData.businessKey"
+ clearable
+ placeholder="璇疯緭鍏ヤ笟鍔¢敭琛ㄨ揪寮�"
+ @change="updateCallActivityAttr('businessKey')"
+ />
+ </el-form-item>
+
+ <el-divider />
+ <div>
+ <div class="flex mb-10px">
+ <el-text>杈撳叆鍙傛暟</el-text>
+ <XButton
+ class="ml-auto"
+ type="primary"
+ preIcon="ep:plus"
+ title="娣诲姞鍙傛暟"
+ size="small"
+ @click="openVariableForm('in', null, -1)"
+ />
+ </div>
+ <el-table :data="inVariableList" max-height="240" fit border>
+ <el-table-column label="婧�" prop="source" min-width="100px" show-overflow-tooltip />
+ <el-table-column label="鐩爣" prop="target" min-width="100px" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="110px">
+ <template #default="scope">
+ <el-button link @click="openVariableForm('in', scope.row, scope.$index)" size="small">
+ 缂栬緫
+ </el-button>
+ <el-divider direction="vertical" />
+ <el-button
+ link
+ size="small"
+ style="color: #ff4d4f"
+ @click="removeVariable('in', scope.$index)"
+ >
+ 绉婚櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <el-divider />
+ <div>
+ <div class="flex mb-10px">
+ <el-text>杈撳嚭鍙傛暟</el-text>
+ <XButton
+ class="ml-auto"
+ type="primary"
+ preIcon="ep:plus"
+ title="娣诲姞鍙傛暟"
+ size="small"
+ @click="openVariableForm('out', null, -1)"
+ />
+ </div>
+ <el-table :data="outVariableList" max-height="240" fit border>
+ <el-table-column label="婧�" prop="source" min-width="100px" show-overflow-tooltip />
+ <el-table-column label="鐩爣" prop="target" min-width="100px" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="110px">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openVariableForm('out', scope.row, scope.$index)"
+ size="small"
+ >
+ 缂栬緫
+ </el-button>
+ <el-divider direction="vertical" />
+ <el-button
+ link
+ size="small"
+ style="color: #ff4d4f"
+ @click="removeVariable('out', scope.$index)"
+ >
+ 绉婚櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-form>
+
+ <!-- 娣诲姞鎴栦慨鏀瑰弬鏁� -->
+ <el-dialog
+ v-model="variableDialogVisible"
+ title="鍙傛暟閰嶇疆"
+ width="600px"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form :model="varialbeFormData" label-width="80px" ref="varialbeFormRef">
+ <el-form-item label="婧愶細" prop="source">
+ <el-input v-model="varialbeFormData.source" clearable />
+ </el-form-item>
+ <el-form-item label="鐩爣锛�" prop="target">
+ <el-input v-model="varialbeFormData.target" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="variableDialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="saveVariable">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'CallActivity' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+const message = useMessage()
+
+const formData = ref({
+ processInstanceName: '',
+ calledElement: '',
+ inheritVariables: false,
+ businessKey: '',
+ inheritBusinessKey: false,
+ calledElementType: 'key'
+})
+const inVariableList = ref()
+const outVariableList = ref()
+const variableType = ref() // 鍙傛暟绫诲瀷
+const editingVariableIndex = ref(-1) // 缂栬緫鍙傛暟涓嬫爣
+const variableDialogVisible = ref(false)
+const varialbeFormRef = ref()
+const varialbeFormData = ref({
+ source: '',
+ target: ''
+})
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+const bpmnElement = ref()
+const otherExtensionList = ref()
+
+const initCallActivity = () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ console.log(bpmnElement.value.businessObject, 'callActivity')
+
+ // 鍒濆鍖栨墍鏈夐厤缃」
+ Object.keys(formData.value).forEach((key) => {
+ formData.value[key] = bpmnElement.value.businessObject[key] ?? formData.value[key]
+ })
+
+ otherExtensionList.value = [] // 鍏朵粬鎵╁睍閰嶇疆
+ inVariableList.value = []
+ outVariableList.value = []
+ // 鍒濆鍖栬緭鍏ュ弬鏁�
+ bpmnElement.value.businessObject?.extensionElements?.values?.forEach((ex) => {
+ if (ex.$type === `${prefix}:In`) {
+ inVariableList.value.push(ex)
+ } else if (ex.$type === `${prefix}:Out`) {
+ outVariableList.value.push(ex)
+ } else {
+ otherExtensionList.value.push(ex)
+ }
+ })
+
+ // 榛樿娣诲姞
+ // bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ // calledElementType: 'key'
+ // })
+}
+
+const updateCallActivityAttr = (attr) => {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ [attr]: formData.value[attr]
+ })
+}
+
+const openVariableForm = (type, data, index) => {
+ editingVariableIndex.value = index
+ variableType.value = type
+ varialbeFormData.value = index === -1 ? {} : { ...data }
+ variableDialogVisible.value = true
+}
+
+const removeVariable = async (type, index) => {
+ try {
+ await message.delConfirm()
+ if (type === 'in') {
+ inVariableList.value.splice(index, 1)
+ }
+ if (type === 'out') {
+ outVariableList.value.splice(index, 1)
+ }
+ updateElementExtensions()
+ } catch {}
+}
+
+const saveVariable = () => {
+ if (editingVariableIndex.value === -1) {
+ if (variableType.value === 'in') {
+ inVariableList.value.push(
+ bpmnInstances().moddle.create(`${prefix}:In`, { ...varialbeFormData.value })
+ )
+ }
+ if (variableType.value === 'out') {
+ outVariableList.value.push(
+ bpmnInstances().moddle.create(`${prefix}:Out`, { ...varialbeFormData.value })
+ )
+ }
+ updateElementExtensions()
+ } else {
+ if (variableType.value === 'in') {
+ inVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source
+ inVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target
+ }
+ if (variableType.value === 'out') {
+ outVariableList.value[editingVariableIndex.value].source = varialbeFormData.value.source
+ outVariableList.value[editingVariableIndex.value].target = varialbeFormData.value.target
+ }
+ }
+ variableDialogVisible.value = false
+}
+
+const updateElementExtensions = () => {
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [...inVariableList.value, ...outVariableList.value, ...otherExtensionList.value]
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: extensions
+ })
+}
+
+watch(
+ () => props.id,
+ (val) => {
+ val &&
+ val.length &&
+ nextTick(() => {
+ initCallActivity()
+ })
+ },
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/HttpHeaderEditor.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/HttpHeaderEditor.vue
new file mode 100644
index 0000000..5d0a133
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/HttpHeaderEditor.vue
@@ -0,0 +1,178 @@
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ title="缂栬緫璇锋眰澶�"
+ width="600px"
+ :close-on-click-modal="false"
+ @close="handleClose"
+ >
+ <div class="header-editor">
+ <div class="header-list">
+ <div v-for="(item, index) in headerList" :key="index" class="header-item">
+ <el-input v-model="item.key" placeholder="璇疯緭鍏ュ弬鏁板悕" class="header-key" clearable />
+ <span class="separator">:</span>
+ <el-input
+ v-model="item.value"
+ placeholder="璇疯緭鍏ュ弬鏁板�� (鏀寔琛ㄨ揪寮� ${鍙橀噺鍚峿)"
+ class="header-value"
+ clearable
+ />
+ <el-button
+ type="danger"
+ :icon="Delete"
+ circle
+ size="small"
+ @click="removeHeader(index)"
+ />
+ </div>
+ </div>
+ <el-button type="primary" :icon="Plus" class="add-btn" @click="addHeader">
+ 娣诲姞璇锋眰澶�
+ </el-button>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="handleClose">鍙栨秷</el-button>
+ <el-button type="primary" @click="handleSave">淇濆瓨</el-button>
+ </span>
+ </template>
+ </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { Delete, Plus } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'HttpHeaderEditor' })
+
+const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false
+ },
+ headers: {
+ type: String,
+ default: ''
+ }
+})
+
+const emit = defineEmits(['update:modelValue', 'save'])
+
+interface HeaderItem {
+ key: string
+ value: string
+}
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const headerList = ref<HeaderItem[]>([])
+
+// 瑙f瀽璇锋眰澶村瓧绗︿覆涓哄垪琛�
+const parseHeaders = (headersStr: string): HeaderItem[] => {
+ if (!headersStr || !headersStr.trim()) {
+ return [{ key: '', value: '' }]
+ }
+
+ const lines = headersStr.split('\n').filter((line) => line.trim())
+ const parsed = lines.map((line) => {
+ const colonIndex = line.indexOf(':')
+ if (colonIndex > 0) {
+ return {
+ key: line.substring(0, colonIndex).trim(),
+ value: line.substring(colonIndex + 1).trim()
+ }
+ }
+ return { key: line.trim(), value: '' }
+ })
+
+ return parsed.length > 0 ? parsed : [{ key: '', value: '' }]
+}
+
+// 灏嗗垪琛ㄨ浆鎹负璇锋眰澶村瓧绗︿覆
+const stringifyHeaders = (headers: HeaderItem[]): string => {
+ return headers
+ .filter((item) => item.key.trim())
+ .map((item) => `${item.key}: ${item.value}`)
+ .join('\n')
+}
+
+// 娣诲姞璇锋眰澶�
+const addHeader = () => {
+ headerList.value.push({ key: '', value: '' })
+}
+
+// 绉婚櫎璇锋眰澶�
+const removeHeader = (index: number) => {
+ if (headerList.value.length === 1) {
+ // 鑷冲皯淇濈暀涓�琛�
+ headerList.value = [{ key: '', value: '' }]
+ } else {
+ headerList.value.splice(index, 1)
+ }
+}
+
+// 淇濆瓨
+const handleSave = () => {
+ const headersStr = stringifyHeaders(headerList.value)
+ emit('save', headersStr)
+ dialogVisible.value = false
+}
+
+// 鍏抽棴
+const handleClose = () => {
+ dialogVisible.value = false
+}
+
+// 鐩戝惉瀵硅瘽妗嗘墦寮�锛屽垵濮嬪寲鏁版嵁
+watch(
+ () => props.modelValue,
+ (val) => {
+ if (val) {
+ headerList.value = parseHeaders(props.headers)
+ }
+ },
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+.header-editor {
+ .header-list {
+ max-height: 400px;
+ overflow-y: auto;
+ margin-bottom: 16px;
+ }
+
+ .header-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 12px;
+
+ .header-key {
+ flex: 0 0 180px;
+ }
+
+ .separator {
+ color: #606266;
+ font-weight: 500;
+ }
+
+ .header-value {
+ flex: 1;
+ }
+ }
+
+ .add-btn {
+ width: 100%;
+ }
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+</style>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
new file mode 100644
index 0000000..a038e69
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ProcessExpressionDialog.vue
@@ -0,0 +1,70 @@
+<!-- 琛ㄨ揪寮忛�夋嫨 -->
+<template>
+ <Dialog title="璇烽�夋嫨琛ㄨ揪寮�" v-model="dialogVisible" width="1024px">
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="琛ㄨ揪寮�" align="center" prop="expression" />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="select(scope.row)"> 閫夋嫨 </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { CommonStatusEnum } from '@/utils/constants'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+
+/** BPM 娴佺▼ 琛ㄥ崟 */
+defineOptions({ name: 'ProcessExpressionDialog' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProcessExpressionVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: '',
+ status: CommonStatusEnum.ENABLE
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = (type: string) => {
+ queryParams.pageNo = 1
+ queryParams.type = type
+ getList()
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const select = async (row) => {
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('select', row)
+}
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue
new file mode 100644
index 0000000..83ed24e
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ReceiveTask.vue
@@ -0,0 +1,125 @@
+<template>
+ <div style="margin-top: 16px">
+ <el-form-item label="娑堟伅瀹炰緥">
+ <div
+ style="
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ "
+ >
+ <el-select v-model="bindMessageId" @change="updateTaskMessage">
+ <el-option
+ v-for="key in Object.keys(messageMap)"
+ :value="key"
+ :label="messageMap[key]"
+ :key="key"
+ />
+ </el-select>
+ <XButton
+ type="primary"
+ preIcon="ep:plus"
+ style="margin-left: 8px"
+ @click="openMessageModel"
+ />
+ </div>
+ </el-form-item>
+ <el-dialog
+ v-model="messageModelVisible"
+ :close-on-click-modal="false"
+ title="鍒涘缓鏂版秷鎭�"
+ width="400px"
+ append-to-body
+ destroy-on-close
+ >
+ <el-form :model="newMessageForm" size="small" label-width="90px">
+ <el-form-item label="娑堟伅ID">
+ <el-input v-model="newMessageForm.id" clearable />
+ </el-form-item>
+ <el-form-item label="娑堟伅鍚嶇О">
+ <el-input v-model="newMessageForm.name" clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button size="small" type="primary" @click="createNewMessage">纭� 璁�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'ReceiveTask' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+
+const message = useMessage()
+
+const bindMessageId = ref('')
+const newMessageForm = ref<any>({})
+const messageMap = ref<any>({})
+const messageModelVisible = ref(false)
+const bpmnElement = ref<any>()
+const bpmnMessageRefsMap = ref<any>()
+const bpmnRootElements = ref<any>()
+
+const bpmnInstances = () => (window as any).bpmnInstances
+const getBindMessage = () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ bindMessageId.value = bpmnElement.value.businessObject?.messageRef?.id || '-1'
+}
+const openMessageModel = () => {
+ messageModelVisible.value = true
+ newMessageForm.value = {}
+}
+const createNewMessage = () => {
+ if (messageMap.value[newMessageForm.value.id]) {
+ message.error('璇ユ秷鎭凡瀛樺湪锛岃淇敼id鍚庨噸鏂颁繚瀛�')
+ return
+ }
+ const newMessage = bpmnInstances().moddle.create('bpmn:Message', newMessageForm.value)
+ bpmnRootElements.value.push(newMessage)
+ messageMap.value[newMessageForm.value.id] = newMessageForm.value.name
+ bpmnMessageRefsMap.value[newMessageForm.value.id] = newMessage
+ messageModelVisible.value = false
+}
+const updateTaskMessage = (messageId) => {
+ if (messageId === '-1') {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ messageRef: null
+ })
+ } else {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ messageRef: bpmnMessageRefsMap.value[messageId]
+ })
+ }
+}
+
+onMounted(() => {
+ bpmnMessageRefsMap.value = Object.create(null)
+ bpmnRootElements.value = bpmnInstances().modeler.getDefinitions().rootElements
+ bpmnRootElements.value
+ .filter((el) => el.$type === 'bpmn:Message')
+ .forEach((m) => {
+ bpmnMessageRefsMap.value[m.id] = m
+ messageMap.value[m.id] = m.name
+ })
+ messageMap.value['-1'] = '鏃�'
+})
+
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+})
+watch(
+ () => props.id,
+ () => {
+ // bpmnElement.value = bpmnInstances().bpmnElement
+ nextTick(() => {
+ getBindMessage()
+ })
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue
new file mode 100644
index 0000000..683fef3
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ScriptTask.vue
@@ -0,0 +1,99 @@
+<template>
+ <div style="margin-top: 16px">
+ <el-form-item label="鑴氭湰鏍煎紡">
+ <el-input
+ v-model="scriptTaskForm.scriptFormat"
+ clearable
+ @input="updateElementTask()"
+ @change="updateElementTask()"
+ />
+ </el-form-item>
+ <el-form-item label="鑴氭湰绫诲瀷">
+ <el-select v-model="scriptTaskForm.scriptType">
+ <el-option label="鍐呰仈鑴氭湰" value="inline" />
+ <el-option label="澶栭儴璧勬簮" value="external" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑴氭湰" v-show="scriptTaskForm.scriptType === 'inline'">
+ <el-input
+ v-model="scriptTaskForm.script"
+ type="textarea"
+ resize="vertical"
+ :autosize="{ minRows: 2, maxRows: 4 }"
+ clearable
+ @input="updateElementTask()"
+ @change="updateElementTask()"
+ />
+ </el-form-item>
+ <el-form-item label="璧勬簮鍦板潃" v-show="scriptTaskForm.scriptType === 'external'">
+ <el-input
+ v-model="scriptTaskForm.resource"
+ clearable
+ @input="updateElementTask()"
+ @change="updateElementTask()"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴灉鍙橀噺">
+ <el-input
+ v-model="scriptTaskForm.resultVariable"
+ clearable
+ @input="updateElementTask()"
+ @change="updateElementTask()"
+ />
+ </el-form-item>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'ScriptTask' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+const defaultTaskForm = ref({
+ scriptFormat: '',
+ script: '',
+ resource: '',
+ resultVariable: ''
+})
+const scriptTaskForm = ref<any>({})
+const bpmnElement = ref()
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetTaskForm = () => {
+ for (let key in defaultTaskForm.value) {
+ let value = bpmnElement.value?.businessObject[key] || defaultTaskForm.value[key]
+ scriptTaskForm.value[key] = value
+ }
+ scriptTaskForm.value.scriptType = scriptTaskForm.value.script ? 'inline' : 'external'
+}
+const updateElementTask = () => {
+ let taskAttr = Object.create(null)
+ taskAttr.scriptFormat = scriptTaskForm.value.scriptFormat || null
+ taskAttr.resultVariable = scriptTaskForm.value.resultVariable || null
+ if (scriptTaskForm.value.scriptType === 'inline') {
+ taskAttr.script = scriptTaskForm.value.script || null
+ taskAttr.resource = null
+ } else {
+ taskAttr.resource = scriptTaskForm.value.resource || null
+ taskAttr.script = null
+ }
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
+}
+
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+})
+
+watch(
+ () => props.id,
+ () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ nextTick(() => {
+ resetTaskForm()
+ })
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue
new file mode 100644
index 0000000..c07e80b
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/ServiceTask.vue
@@ -0,0 +1,396 @@
+<template>
+ <div>
+ <el-form-item label="鎵ц绫诲瀷" key="executeType">
+ <el-select v-model="serviceTaskForm.executeType" @change="handleExecuteTypeChange">
+ <el-option label="Java绫�" value="class" />
+ <el-option label="琛ㄨ揪寮�" value="expression" />
+ <el-option label="浠g悊琛ㄨ揪寮�" value="delegateExpression" />
+ <el-option label="HTTP 璋冪敤" value="http" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="serviceTaskForm.executeType === 'class'"
+ label="Java绫�"
+ prop="class"
+ key="execute-class"
+ >
+ <el-input v-model="serviceTaskForm.class" clearable @change="updateElementTask" />
+ </el-form-item>
+ <el-form-item
+ v-if="serviceTaskForm.executeType === 'expression'"
+ label="琛ㄨ揪寮�"
+ prop="expression"
+ key="execute-expression"
+ >
+ <el-input v-model="serviceTaskForm.expression" clearable @change="updateElementTask" />
+ </el-form-item>
+ <el-form-item
+ v-if="serviceTaskForm.executeType === 'delegateExpression'"
+ label="浠g悊琛ㄨ揪寮�"
+ prop="delegateExpression"
+ key="execute-delegate"
+ >
+ <el-input
+ v-model="serviceTaskForm.delegateExpression"
+ clearable
+ @change="updateElementTask"
+ />
+ </el-form-item>
+ <template v-if="serviceTaskForm.executeType === 'http'">
+ <el-form-item label="璇锋眰鏂规硶" key="http-method">
+ <el-radio-group v-model="httpTaskForm.requestMethod">
+ <el-radio-button label="GET" value="GET" />
+ <el-radio-button label="POST" value="POST" />
+ <el-radio-button label="PUT" value="PUT" />
+ <el-radio-button label="DELETE" value="DELETE" />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="璇锋眰鍦板潃" key="http-url" prop="requestUrl">
+ <el-input v-model="httpTaskForm.requestUrl" clearable />
+ </el-form-item>
+ <el-form-item label="璇锋眰澶�" key="http-headers">
+ <div style="display: flex; gap: 8px; align-items: flex-start; width: 100%">
+ <el-input
+ v-model="httpTaskForm.requestHeaders"
+ type="textarea"
+ resize="vertical"
+ :autosize="{ minRows: 4, maxRows: 8 }"
+ readonly
+ placeholder="鐐瑰嚮鍙充晶缂栬緫鎸夐挳娣诲姞璇锋眰澶�"
+ style="flex: 1; min-width: 0"
+ />
+ <el-button
+ type="primary"
+ :icon="Edit"
+ @click="showHeaderEditor = true"
+ style="flex-shrink: 0"
+ >
+ 缂栬緫
+ </el-button>
+ </div>
+ </el-form-item>
+ <el-form-item label="绂佹閲嶅畾鍚�" key="http-disallow-redirects">
+ <el-switch v-model="httpTaskForm.disallowRedirects" />
+ </el-form-item>
+ <el-form-item label="蹇界暐寮傚父" key="http-ignore-exception">
+ <el-switch v-model="httpTaskForm.ignoreException" />
+ </el-form-item>
+ <el-form-item label="淇濆瓨杩斿洖鍙橀噺" key="http-save-response">
+ <el-switch v-model="httpTaskForm.saveResponseParameters" />
+ </el-form-item>
+ <el-form-item label="鏄惁鐬棿鍙橀噺" key="http-save-transient">
+ <el-switch v-model="httpTaskForm.saveResponseParametersTransient" />
+ </el-form-item>
+ <el-form-item label="杩斿洖鍙橀噺鍓嶇紑" key="http-result-variable-prefix">
+ <el-input v-model="httpTaskForm.resultVariablePrefix" />
+ </el-form-item>
+ <el-form-item label="鏍煎紡鍖栬繑鍥炰负JSON" key="http-save-json">
+ <el-switch v-model="httpTaskForm.saveResponseVariableAsJson" />
+ </el-form-item>
+ </template>
+
+ <!-- 璇锋眰澶寸紪杈戝櫒 -->
+ <HttpHeaderEditor
+ v-model="showHeaderEditor"
+ :headers="httpTaskForm.requestHeaders"
+ @save="handleHeadersSave"
+ />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { Edit } from '@element-plus/icons-vue'
+import { updateElementExtensions } from '@/components/bpmnProcessDesigner/package/utils'
+import HttpHeaderEditor from './HttpHeaderEditor.vue'
+
+defineOptions({ name: 'ServiceTask' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+
+const prefix = (inject('prefix', 'flowable') || 'flowable') as string
+const flowableTypeKey = `${prefix}:type`
+const flowableFieldType = `${prefix}:Field`
+
+const HTTP_FIELD_NAMES = [
+ 'requestMethod',
+ 'requestUrl',
+ 'requestHeaders',
+ 'disallowRedirects',
+ 'ignoreException',
+ 'saveResponseParameters',
+ 'resultVariablePrefix',
+ 'saveResponseParametersTransient',
+ 'saveResponseVariableAsJson'
+]
+const HTTP_BOOLEAN_FIELDS = new Set([
+ 'disallowRedirects',
+ 'ignoreException',
+ 'saveResponseParameters',
+ 'saveResponseParametersTransient',
+ 'saveResponseVariableAsJson'
+])
+
+const DEFAULT_TASK_FORM = {
+ executeType: '',
+ class: '',
+ expression: '',
+ delegateExpression: ''
+}
+
+const DEFAULT_HTTP_FORM = {
+ requestMethod: 'GET',
+ requestUrl: '',
+ requestHeaders: 'Content-Type: application/json',
+ resultVariablePrefix: '',
+ disallowRedirects: false,
+ ignoreException: false,
+ saveResponseParameters: false,
+ saveResponseParametersTransient: false,
+ saveResponseVariableAsJson: false
+}
+
+const serviceTaskForm = ref({ ...DEFAULT_TASK_FORM })
+const httpTaskForm = ref({ ...DEFAULT_HTTP_FORM })
+const bpmnElement = ref()
+const httpInitializing = ref(false)
+const showHeaderEditor = ref(false)
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+// 鍒ゆ柇瀛楃涓叉槸鍚﹀寘鍚〃杈惧紡
+const isExpression = (value: string): boolean => {
+ if (!value) return false
+ // 妫�娴� ${...} 鎴� #{...} 鏍煎紡鐨勮〃杈惧紡
+ return /\${[^}]+}/.test(value) || /#{[^}]+}/.test(value)
+}
+
+const collectHttpExtensionInfo = () => {
+ const businessObject = bpmnElement.value?.businessObject
+ const extensionElements = businessObject?.extensionElements
+ const httpFields = new Map<string, string>()
+ const httpFieldTypes = new Map<string, 'string' | 'expression'>()
+ const otherExtensions: any[] = []
+
+ extensionElements?.values?.forEach((item: any) => {
+ if (item?.$type === flowableFieldType && HTTP_FIELD_NAMES.includes(item.name)) {
+ const value = item.string ?? item.stringValue ?? item.expression ?? ''
+ const fieldType = item.expression ? 'expression' : 'string'
+ httpFields.set(item.name, value)
+ httpFieldTypes.set(item.name, fieldType)
+ } else {
+ otherExtensions.push(item)
+ }
+ })
+
+ return { httpFields, httpFieldTypes, otherExtensions }
+}
+
+const resetHttpDefaults = () => {
+ httpInitializing.value = true
+ httpTaskForm.value = { ...DEFAULT_HTTP_FORM }
+ nextTick(() => {
+ httpInitializing.value = false
+ })
+}
+
+const resetHttpForm = () => {
+ httpInitializing.value = true
+ const { httpFields } = collectHttpExtensionInfo()
+ const nextForm = { ...DEFAULT_HTTP_FORM }
+
+ HTTP_FIELD_NAMES.forEach((name) => {
+ const stored = httpFields.get(name)
+ if (stored !== undefined) {
+ nextForm[name] = HTTP_BOOLEAN_FIELDS.has(name) ? stored === 'true' : stored
+ }
+ })
+
+ httpTaskForm.value = nextForm
+ nextTick(() => {
+ httpInitializing.value = false
+ updateHttpExtensions(true)
+ })
+}
+
+const resetServiceTaskForm = () => {
+ const businessObject = bpmnElement.value?.businessObject
+ const nextForm = { ...DEFAULT_TASK_FORM }
+
+ if (businessObject) {
+ if (businessObject.class) {
+ nextForm.class = businessObject.class
+ nextForm.executeType = 'class'
+ }
+ if (businessObject.expression) {
+ nextForm.expression = businessObject.expression
+ nextForm.executeType = 'expression'
+ }
+ if (businessObject.delegateExpression) {
+ nextForm.delegateExpression = businessObject.delegateExpression
+ nextForm.executeType = 'delegateExpression'
+ }
+ if (businessObject.$attrs?.[flowableTypeKey] === 'http') {
+ nextForm.executeType = 'http'
+ } else {
+ // 鍏滃簳锛氬缂哄皯 flowable:type=http锛屼絾鎵╁睍閲屽凡鏈� HTTP 鐨勫瓧娈碉紝涔熻涓烘槸 HTTP
+ const { httpFields } = collectHttpExtensionInfo()
+ if (httpFields.size > 0) {
+ nextForm.executeType = 'http'
+ }
+ }
+ }
+
+ serviceTaskForm.value = nextForm
+
+ if (nextForm.executeType === 'http') {
+ resetHttpForm()
+ } else {
+ resetHttpDefaults()
+ }
+}
+
+const shouldPersistField = (name: string, value: any) => {
+ if (HTTP_BOOLEAN_FIELDS.has(name)) return true
+ if (name === 'requestMethod') return true
+ if (name === 'requestUrl') return !!value
+ return value !== undefined && value !== ''
+}
+
+const updateHttpExtensions = (force = false) => {
+ if (!bpmnElement.value) return
+ if (!force && (httpInitializing.value || serviceTaskForm.value.executeType !== 'http')) {
+ return
+ }
+
+ const {
+ httpFields: existingFields,
+ httpFieldTypes: existingTypes,
+ otherExtensions
+ } = collectHttpExtensionInfo()
+
+ const desiredEntries: [string, string][] = []
+ HTTP_FIELD_NAMES.forEach((name) => {
+ const rawValue = httpTaskForm.value[name]
+ if (!shouldPersistField(name, rawValue)) {
+ return
+ }
+
+ const persisted = HTTP_BOOLEAN_FIELDS.has(name)
+ ? String(!!rawValue)
+ : rawValue === undefined
+ ? ''
+ : String(rawValue)
+
+ desiredEntries.push([name, persisted])
+ })
+
+ // 妫�鏌ユ槸鍚︽湁鍙樺寲锛氫笉浠呮瘮杈冨�硷紝杩樿姣旇緝瀛楁绫诲瀷锛坰tring vs expression锛�
+ if (!force && desiredEntries.length === existingFields.size) {
+ let noChange = true
+ for (const [name, value] of desiredEntries) {
+ const existingValue = existingFields.get(name)
+ const existingType = existingTypes.get(name)
+ const currentType = isExpression(value) ? 'expression' : 'string'
+ if (existingValue !== value || existingType !== currentType) {
+ noChange = false
+ break
+ }
+ }
+ if (noChange) {
+ return
+ }
+ }
+
+ const moddle = bpmnInstances().moddle
+ const httpFieldElements = desiredEntries.map(([name, value]) => {
+ // 鏍规嵁鍊兼槸鍚﹀寘鍚〃杈惧紡鏉ュ喅瀹氫娇鐢� string 杩樻槸 expression 灞炴��
+ const isExpr = isExpression(value)
+ return moddle.create(flowableFieldType, {
+ name,
+ ...(isExpr ? { expression: value } : { string: value })
+ })
+ })
+
+ updateElementExtensions(bpmnElement.value, [...otherExtensions, ...httpFieldElements])
+}
+
+const removeHttpExtensions = () => {
+ if (!bpmnElement.value) return
+ const { httpFields, otherExtensions } = collectHttpExtensionInfo()
+ if (!httpFields.size) {
+ return
+ }
+
+ if (!otherExtensions.length) {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: null
+ })
+ return
+ }
+
+ updateElementExtensions(bpmnElement.value, otherExtensions)
+}
+
+const updateElementTask = () => {
+ if (!bpmnElement.value) return
+
+ const taskAttr: Record<string, any> = {
+ class: null,
+ expression: null,
+ delegateExpression: null,
+ [flowableTypeKey]: null
+ }
+
+ const type = serviceTaskForm.value.executeType
+ if (type === 'class' || type === 'expression' || type === 'delegateExpression') {
+ taskAttr[type] = serviceTaskForm.value[type] || null
+ } else if (type === 'http') {
+ taskAttr[flowableTypeKey] = 'http'
+ }
+
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), taskAttr)
+
+ if (type === 'http') {
+ updateHttpExtensions(true)
+ } else {
+ removeHttpExtensions()
+ }
+}
+
+const handleExecuteTypeChange = (value: string) => {
+ serviceTaskForm.value.executeType = value
+ if (value === 'http') {
+ resetHttpForm()
+ }
+ updateElementTask()
+}
+
+const handleHeadersSave = (headersStr: string) => {
+ httpTaskForm.value.requestHeaders = headersStr
+}
+
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+})
+
+watch(
+ () => props.id,
+ () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ nextTick(() => {
+ resetServiceTaskForm()
+ })
+ },
+ { immediate: true }
+)
+
+watch(
+ () => httpTaskForm.value,
+ () => {
+ updateHttpExtensions()
+ },
+ { deep: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
new file mode 100644
index 0000000..e4091b9
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/task/task-components/UserTask.vue
@@ -0,0 +1,470 @@
+<template>
+ <el-form label-width="120px">
+ <el-form-item label="瑙勫垯绫诲瀷" prop="candidateStrategy">
+ <el-select
+ v-model="userTaskForm.candidateStrategy"
+ clearable
+ style="width: 100%"
+ @change="changeCandidateStrategy"
+ >
+ <el-option
+ v-for="(dict, index) in CANDIDATE_STRATEGY"
+ :key="index"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy == CandidateStrategy.ROLE"
+ label="鎸囧畾瑙掕壊"
+ prop="candidateParam"
+ >
+ <el-select
+ v-model="userTaskForm.candidateParam"
+ clearable
+ multiple
+ style="width: 100%"
+ @change="updateElementTask"
+ >
+ <el-option v-for="item in roleOptions" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="
+ userTaskForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+ userTaskForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+ userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+ "
+ label="鎸囧畾閮ㄩ棬"
+ prop="candidateParam"
+ span="24"
+ >
+ <el-tree-select
+ ref="treeRef"
+ v-model="userTaskForm.candidateParam"
+ :data="deptTreeOptions"
+ :props="defaultProps"
+ empty-text="鍔犺浇涓紝璇风◢鍚�"
+ multiple
+ node-key="id"
+ show-checkbox
+ @change="updateElementTask"
+ />
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy == CandidateStrategy.POST"
+ label="鎸囧畾宀椾綅"
+ prop="candidateParam"
+ span="24"
+ >
+ <el-select
+ v-model="userTaskForm.candidateParam"
+ clearable
+ multiple
+ style="width: 100%"
+ @change="updateElementTask"
+ >
+ <el-option v-for="item in postOptions" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy == CandidateStrategy.USER"
+ label="鎸囧畾鐢ㄦ埛"
+ prop="candidateParam"
+ span="24"
+ >
+ <el-select
+ v-model="userTaskForm.candidateParam"
+ clearable
+ multiple
+ style="width: 100%"
+ @change="updateElementTask"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+ label="鎸囧畾鐢ㄦ埛缁�"
+ prop="candidateParam"
+ >
+ <el-select
+ v-model="userTaskForm.candidateParam"
+ clearable
+ multiple
+ style="width: 100%"
+ @change="updateElementTask"
+ >
+ <el-option
+ v-for="item in userGroupOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_USER"
+ label="琛ㄥ崟鍐呯敤鎴峰瓧娈�"
+ prop="formUser"
+ >
+ <el-select
+ v-model="userTaskForm.candidateParam"
+ clearable
+ style="width: 100%"
+ @change="handleFormUserChange"
+ >
+ <el-option
+ v-for="(item, idx) in userFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled="!item.required"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+ label="琛ㄥ崟鍐呴儴闂ㄥ瓧娈�"
+ prop="formDept"
+ >
+ <el-select
+ v-model="userTaskForm.candidateParam"
+ clearable
+ style="width: 100%"
+ @change="updateElementTask"
+ >
+ <el-option
+ v-for="(item, idx) in deptFieldOnFormOptions"
+ :key="idx"
+ :label="item.title"
+ :value="item.field"
+ :disabled="!item.required"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="
+ userTaskForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+ userTaskForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ userTaskForm.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+ userTaskForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+ "
+ :label="deptLevelLabel!"
+ prop="deptLevel"
+ span="24"
+ >
+ <el-select v-model="deptLevel" clearable @change="updateElementTask">
+ <el-option
+ v-for="(item, index) in MULTI_LEVEL_DEPT"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="userTaskForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+ label="娴佺▼琛ㄨ揪寮�"
+ prop="candidateParam"
+ >
+ <el-input
+ type="textarea"
+ v-model="userTaskForm.candidateParam[0]"
+ clearable
+ style="width: 100%"
+ @change="updateElementTask"
+ />
+ <XButton
+ class="!w-1/1 mt-5px"
+ type="success"
+ preIcon="ep:select"
+ title="閫夋嫨琛ㄨ揪寮�"
+ size="small"
+ @click="openProcessExpressionDialog"
+ />
+ <!-- 閫夋嫨寮圭獥 -->
+ <ProcessExpressionDialog ref="processExpressionDialogRef" @select="selectProcessExpression" />
+ </el-form-item>
+
+ <el-form-item label="璺宠繃琛ㄨ揪寮�" prop="skipExpression">
+ <el-input
+ type="textarea"
+ v-model="userTaskForm.skipExpression"
+ clearable
+ style="width: 100%"
+ @change="updateSkipExpression"
+ />
+ </el-form-item>
+ </el-form>
+</template>
+
+<script lang="ts" setup>
+import {
+ CANDIDATE_STRATEGY,
+ CandidateStrategy,
+ FieldPermissionType,
+ MULTI_LEVEL_DEPT
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PostApi from '@/api/system/post'
+import * as UserApi from '@/api/system/user'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
+import { ProcessExpressionVO } from '@/api/bpm/processExpression'
+import { useFormFieldsPermission } from '@/components/SimpleProcessDesignerV2/src/node'
+
+defineOptions({ name: 'UserTask' })
+const props = defineProps({
+ id: String,
+ type: String
+})
+const prefix = inject('prefix')
+const userTaskForm = ref({
+ candidateStrategy: undefined, // 鍒嗛厤瑙勫垯
+ candidateParam: [], // 鍒嗛厤閫夐」
+ skipExpression: '' // 璺宠繃琛ㄨ揪寮�
+})
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const roleOptions = ref<RoleApi.RoleVO[]>([]) // 瑙掕壊鍒楄〃
+const deptTreeOptions = ref() // 閮ㄩ棬鏍�
+const postOptions = ref<PostApi.PostVO[]>([]) // 宀椾綅鍒楄〃
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const userGroupOptions = ref<UserGroupApi.UserGroupVO[]>([]) // 鐢ㄦ埛缁勫垪琛�
+
+const { formFieldOptions } = useFormFieldsPermission(FieldPermissionType.READ)
+// 琛ㄥ崟鍐呯敤鎴峰瓧娈甸�夐」, 蹇呴』鏄繀濉拰鐢ㄦ埛閫夋嫨鍣�
+const userFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 琛ㄥ崟鍐呴儴闂ㄥ瓧娈甸�夐」, 蹇呴』鏄繀濉拰閮ㄩ棬閫夋嫨鍣�
+const deptFieldOnFormOptions = computed(() => {
+ return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
+
+const deptLevel = ref(1)
+const deptLevelLabel = computed(() => {
+ let label = '閮ㄩ棬璐熻矗浜烘潵婧�'
+ if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+ label = label + '(鎸囧畾閮ㄩ棬鍚戜笂)'
+ } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+ label = label + '(琛ㄥ崟鍐呴儴闂ㄥ悜涓�)'
+ } else {
+ label = label + '(鍙戣捣浜洪儴闂ㄥ悜涓�)'
+ }
+ return label
+})
+
+const otherExtensions = ref()
+
+const resetTaskForm = () => {
+ const businessObject = bpmnElement.value.businessObject
+ if (!businessObject) {
+ return
+ }
+
+ const extensionElements =
+ businessObject?.extensionElements ??
+ bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+ userTaskForm.value.candidateStrategy = extensionElements.values?.filter(
+ (ex) => ex.$type === `${prefix}:CandidateStrategy`
+ )?.[0]?.value
+ const candidateParamStr = extensionElements.values?.filter(
+ (ex) => ex.$type === `${prefix}:CandidateParam`
+ )?.[0]?.value
+ if (candidateParamStr && candidateParamStr.length > 0) {
+ if (userTaskForm.value.candidateStrategy === CandidateStrategy.EXPRESSION) {
+ // 鐗规畩锛氭祦绋嬭〃杈惧紡锛屽彧鏈変竴涓� input 杈撳叆妗�
+ userTaskForm.value.candidateParam = [candidateParamStr]
+ } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+ // 鐗规畩锛氬绾т笉閮ㄩ棬璐熻矗浜猴紝闇�瑕侀�氳繃'|'鍒嗗壊
+ userTaskForm.value.candidateParam = candidateParamStr
+ .split('|')[0]
+ .split(',')
+ .map((item) => {
+ // 濡傛灉鏁板瓧瓒呭嚭浜嗘渶澶у畨鍏ㄦ暣鏁拌寖鍥达紝鍒欏皢鍏朵綔涓哄瓧绗︿覆澶勭悊
+ let num = Number(item)
+ return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+ })
+ deptLevel.value = +candidateParamStr.split('|')[1]
+ } else if (
+ userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+ ) {
+ userTaskForm.value.candidateParam = +candidateParamStr
+ deptLevel.value = +candidateParamStr
+ } else if (userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+ userTaskForm.value.candidateParam = candidateParamStr.split('|')[0]
+ deptLevel.value = +candidateParamStr.split('|')[1]
+ } else {
+ userTaskForm.value.candidateParam = candidateParamStr.split(',').map((item) => {
+ // 濡傛灉鏁板瓧瓒呭嚭浜嗘渶澶у畨鍏ㄦ暣鏁拌寖鍥达紝鍒欏皢鍏朵綔涓哄瓧绗︿覆澶勭悊
+ let num = Number(item)
+ return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+ })
+ }
+ } else {
+ userTaskForm.value.candidateParam = []
+ }
+
+ otherExtensions.value =
+ extensionElements.values?.filter(
+ (ex) => ex.$type !== `${prefix}:CandidateStrategy` && ex.$type !== `${prefix}:CandidateParam`
+ ) ?? []
+
+ // 璺宠繃琛ㄨ揪寮�
+ if (businessObject.skipExpression != undefined) {
+ userTaskForm.value.skipExpression = businessObject.skipExpression
+ } else {
+ userTaskForm.value.skipExpression = ''
+ }
+
+ // 鏀圭敤閫氳繃extensionElements鏉ュ瓨鍌ㄦ暟鎹�
+ return
+ if (businessObject.candidateStrategy != undefined) {
+ userTaskForm.value.candidateStrategy = parseInt(businessObject.candidateStrategy) as any
+ } else {
+ userTaskForm.value.candidateStrategy = undefined
+ }
+ if (businessObject.candidateParam && businessObject.candidateParam.length > 0) {
+ if (userTaskForm.value.candidateStrategy === 60) {
+ // 鐗规畩锛氭祦绋嬭〃杈惧紡锛屽彧鏈変竴涓� input 杈撳叆妗�
+ userTaskForm.value.candidateParam = [businessObject.candidateParam]
+ } else {
+ userTaskForm.value.candidateParam = businessObject.candidateParam
+ .split(',')
+ .map((item) => item)
+ }
+ } else {
+ userTaskForm.value.candidateParam = []
+ }
+}
+
+/** 鏇存柊 candidateStrategy 瀛楁鏃讹紝闇�瑕佹竻绌� candidateParam锛屽苟瑙﹀彂 bpmn 鍥炬洿鏂� */
+const changeCandidateStrategy = () => {
+ userTaskForm.value.candidateParam = []
+ deptLevel.value = 1
+ // 娉ㄩ噴 by 鑺嬭壙锛氳繖涓氦浜掑緢澶氱敤鎴峰弽棣堣垂瑙o紝https://t.zsxq.com/xNmas 鎵�浠ユ殏鏃跺睆钄�
+ // if (userTaskForm.value.candidateStrategy === CandidateStrategy.FORM_USER) {
+ // // 鐗规畩澶勭悊琛ㄥ崟鍐呯敤鎴峰瓧娈碉紝褰撳彧鏈夊彂璧蜂汉閫夐」鏃跺簲閫変腑鍙戣捣浜�
+ // if (!userFieldOnFormOptions.value || userFieldOnFormOptions.value.length <= 1) {
+ // userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
+ // }
+ // }
+ updateElementTask()
+}
+
+/** 閫変腑鏌愪釜 options 鏃跺�欙紝鏇存柊 bpmn 鍥� */
+const updateElementTask = () => {
+ let candidateParam =
+ userTaskForm.value.candidateParam instanceof Array
+ ? userTaskForm.value.candidateParam.join(',')
+ : userTaskForm.value.candidateParam
+
+ // 鐗规畩澶勭悊澶氱骇閮ㄩ棬鎯呭喌
+ if (
+ userTaskForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+ userTaskForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+ ) {
+ candidateParam += '|' + deptLevel.value
+ }
+ // 鐗规畩澶勭悊鍙戣捣浜洪儴闂ㄨ礋璐d汉銆佸彂璧蜂汉杩炵画閮ㄩ棬璐熻矗浜�
+ if (
+ userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+ userTaskForm.value.candidateStrategy == CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER
+ ) {
+ candidateParam = deptLevel.value + ''
+ }
+
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: [
+ ...otherExtensions.value,
+ bpmnInstances().moddle.create(`${prefix}:CandidateStrategy`, {
+ value: userTaskForm.value.candidateStrategy
+ }),
+ bpmnInstances().moddle.create(`${prefix}:CandidateParam`, {
+ value: candidateParam
+ })
+ ]
+ })
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ extensionElements: extensions
+ })
+
+ // 鏀圭敤閫氳繃extensionElements鏉ュ瓨鍌ㄦ暟鎹�
+ return
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ candidateStrategy: userTaskForm.value.candidateStrategy,
+ candidateParam: userTaskForm.value.candidateParam.join(',')
+ })
+}
+
+const updateSkipExpression = () => {
+ if (userTaskForm.value.skipExpression && userTaskForm.value.skipExpression !== '') {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ skipExpression: userTaskForm.value.skipExpression
+ })
+ } else {
+ bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+ skipExpression: null
+ })
+ }
+}
+
+// 鎵撳紑鐩戝惉鍣ㄥ脊绐�
+const processExpressionDialogRef = ref()
+const openProcessExpressionDialog = async () => {
+ processExpressionDialogRef.value.open()
+}
+const selectProcessExpression = (expression: ProcessExpressionVO) => {
+ userTaskForm.value.candidateParam = [expression.expression]
+ updateElementTask()
+}
+
+const handleFormUserChange = (e) => {
+ if (e === 'PROCESS_START_USER_ID') {
+ userTaskForm.value.candidateParam = []
+ userTaskForm.value.candidateStrategy = CandidateStrategy.START_USER
+ }
+ updateElementTask()
+}
+
+watch(
+ () => props.id,
+ () => {
+ bpmnElement.value = bpmnInstances().bpmnElement
+ nextTick(() => {
+ resetTaskForm()
+ })
+ },
+ { immediate: true }
+)
+
+onMounted(async () => {
+ // 鑾峰緱瑙掕壊鍒楄〃
+ roleOptions.value = await RoleApi.getSimpleRoleList()
+ // 鑾峰緱閮ㄩ棬鍒楄〃
+ const deptOptions = await DeptApi.getSimpleDeptList()
+ deptTreeOptions.value = handleTree(deptOptions, 'id')
+ // 鑾峰緱宀椾綅鍒楄〃
+ postOptions.value = await PostApi.getSimplePostList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 鑾峰緱鐢ㄦ埛缁勫垪琛�
+ userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
+})
+
+onBeforeUnmount(() => {
+ bpmnElement.value = null
+})
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue b/src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue
new file mode 100644
index 0000000..302fe73
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/time-event-config/CycleConfig.vue
@@ -0,0 +1,285 @@
+<template>
+ <el-tabs v-model="tab">
+ <el-tab-pane label="CRON琛ㄨ揪寮�" name="cron">
+ <div style="margin-bottom: 10px">
+ <el-input
+ v-model="cronStr"
+ readonly
+ style="width: 400px; font-weight: bold"
+ :key="'cronStr'"
+ />
+ </div>
+ <div style="display: flex; gap: 8px; margin-bottom: 8px">
+ <el-input v-model="fields.second" placeholder="绉�" style="width: 80px" :key="'second'" />
+ <el-input v-model="fields.minute" placeholder="鍒�" style="width: 80px" :key="'minute'" />
+ <el-input v-model="fields.hour" placeholder="鏃�" style="width: 80px" :key="'hour'" />
+ <el-input v-model="fields.day" placeholder="澶�" style="width: 80px" :key="'day'" />
+ <el-input v-model="fields.month" placeholder="鏈�" style="width: 80px" :key="'month'" />
+ <el-input v-model="fields.week" placeholder="鍛�" style="width: 80px" :key="'week'" />
+ <el-input v-model="fields.year" placeholder="骞�" style="width: 80px" :key="'year'" />
+ </div>
+ <el-tabs v-model="activeField" type="card" style="margin-bottom: 8px">
+ <el-tab-pane v-for="f in cronFieldList" :label="f.label" :name="f.key" :key="f.key">
+ <div style="margin-bottom: 8px">
+ <el-radio-group v-model="cronMode[f.key]" :key="'radio-' + f.key">
+ <el-radio label="every" :key="'every-' + f.key">姣弡{ f.label }}</el-radio>
+ <el-radio label="range" :key="'range-' + f.key"
+ >浠�
+ <el-input-number
+ v-model="cronRange[f.key][0]"
+ :min="f.min"
+ :max="f.max"
+ size="small"
+ style="width: 60px"
+ :key="'range0-' + f.key"
+ />
+ 鍒�
+ <el-input-number
+ v-model="cronRange[f.key][1]"
+ :min="f.min"
+ :max="f.max"
+ size="small"
+ style="width: 60px"
+ :key="'range1-' + f.key"
+ />
+ 涔嬮棿姣弡{ f.label }}</el-radio
+ >
+ <el-radio label="step" :key="'step-' + f.key"
+ >浠庣
+ <el-input-number
+ v-model="cronStep[f.key][0]"
+ :min="f.min"
+ :max="f.max"
+ size="small"
+ style="width: 60px"
+ :key="'step0-' + f.key"
+ />
+ 寮�濮嬫瘡
+ <el-input-number
+ v-model="cronStep[f.key][1]"
+ :min="1"
+ :max="f.max"
+ size="small"
+ style="width: 60px"
+ :key="'step1-' + f.key"
+ />
+ {{ f.label }}</el-radio
+ >
+ <el-radio label="appoint" :key="'appoint-' + f.key">鎸囧畾</el-radio>
+ </el-radio-group>
+ </div>
+ <div v-if="cronMode[f.key] === 'appoint'">
+ <el-checkbox-group v-model="cronAppoint[f.key]" :key="'group-' + f.key">
+ <el-checkbox
+ v-for="n in f.max + 1"
+ :label="pad(n - 1)"
+ :key="'cb-' + f.key + '-' + (n - 1)"
+ >{{ pad(n - 1) }}</el-checkbox
+ >
+ </el-checkbox-group>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ </el-tab-pane>
+ <el-tab-pane label="鏍囧噯鏍煎紡" name="iso" :key="'iso-tab'">
+ <div style="margin-bottom: 10px">
+ <el-input
+ v-model="isoStr"
+ placeholder="濡俁1/2025-05-21T21:59:54/P3DT30M30S"
+ style="width: 400px; font-weight: bold"
+ :key="'isoStr'"
+ />
+ </div>
+ <div style="margin-bottom: 10px"
+ >寰幆娆℃暟锛�<el-input-number v-model="repeat" :min="1" style="width: 100px" :key="'repeat'"
+ /></div>
+ <div style="margin-bottom: 10px"
+ >鏃ユ湡鏃堕棿锛�<el-date-picker
+ v-model="isoDate"
+ type="datetime"
+ placeholder="閫夋嫨鏃ユ湡鏃堕棿"
+ style="width: 200px"
+ :key="'isoDate'"
+ /></div>
+ <div style="margin-bottom: 10px"
+ >褰撳墠鏃堕暱锛�<el-input
+ v-model="isoDuration"
+ placeholder="濡侾3DT30M30S"
+ style="width: 200px"
+ :key="'isoDuration'"
+ /></div>
+ <div>
+ <div
+ >绉掞細<el-button
+ v-for="s in [5, 10, 30, 50]"
+ @click="setDuration('S', s)"
+ :key="'sec-' + s"
+ >{{ s }}</el-button
+ >鑷畾涔�</div
+ >
+ <div
+ >鍒嗭細<el-button
+ v-for="m in [5, 10, 30, 50]"
+ @click="setDuration('M', m)"
+ :key="'min-' + m"
+ >{{ m }}</el-button
+ >鑷畾涔�</div
+ >
+ <div
+ >灏忔椂锛�<el-button
+ v-for="h in [4, 8, 12, 24]"
+ @click="setDuration('H', h)"
+ :key="'hour-' + h"
+ >{{ h }}</el-button
+ >鑷畾涔�</div
+ >
+ <div
+ >澶╋細<el-button
+ v-for="d in [1, 2, 3, 4]"
+ @click="setDuration('D', d)"
+ :key="'day-' + d"
+ >{{ d }}</el-button
+ >鑷畾涔�</div
+ >
+ <div
+ >鏈堬細<el-button
+ v-for="mo in [1, 2, 3, 4]"
+ @click="setDuration('M', mo)"
+ :key="'mon-' + mo"
+ >{{ mo }}</el-button
+ >鑷畾涔�</div
+ >
+ <div
+ >骞达細<el-button
+ v-for="y in [1, 2, 3, 4]"
+ @click="setDuration('Y', y)"
+ :key="'year-' + y"
+ >{{ y }}</el-button
+ >鑷畾涔�</div
+ >
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+</template>
+<script setup>
+import { ref, watch, computed } from 'vue'
+const props = defineProps({ value: String })
+const emit = defineEmits(['change'])
+
+const tab = ref('cron')
+const cronStr = ref(props.value || '* * * * * ?')
+const fields = ref({
+ second: '*',
+ minute: '*',
+ hour: '*',
+ day: '*',
+ month: '*',
+ week: '?',
+ year: ''
+})
+const cronFieldList = [
+ { key: 'second', label: '绉�', min: 0, max: 59 },
+ { key: 'minute', label: '鍒�', min: 0, max: 59 },
+ { key: 'hour', label: '鏃�', min: 0, max: 23 },
+ { key: 'day', label: '澶�', min: 1, max: 31 },
+ { key: 'month', label: '鏈�', min: 1, max: 12 },
+ { key: 'week', label: '鍛�', min: 1, max: 7 },
+ { key: 'year', label: '骞�', min: 1970, max: 2099 }
+]
+const activeField = ref('second')
+const cronMode = ref({
+ second: 'appoint',
+ minute: 'every',
+ hour: 'every',
+ day: 'every',
+ month: 'every',
+ week: 'every',
+ year: 'every'
+})
+const cronAppoint = ref({
+ second: ['00', '01'],
+ minute: [],
+ hour: [],
+ day: [],
+ month: [],
+ week: [],
+ year: []
+})
+const cronRange = ref({
+ second: [0, 1],
+ minute: [0, 1],
+ hour: [0, 1],
+ day: [1, 2],
+ month: [1, 2],
+ week: [1, 2],
+ year: [1970, 1971]
+})
+const cronStep = ref({
+ second: [1, 1],
+ minute: [1, 1],
+ hour: [1, 1],
+ day: [1, 1],
+ month: [1, 1],
+ week: [1, 1],
+ year: [1970, 1]
+})
+
+function pad(n) {
+ return n < 10 ? '0' + n : '' + n
+}
+
+watch(
+ [fields, cronMode, cronAppoint, cronRange, cronStep],
+ () => {
+ // 缁勮cron琛ㄨ揪寮�
+ let arr = cronFieldList.map((f) => {
+ if (cronMode.value[f.key] === 'every') return '*'
+ if (cronMode.value[f.key] === 'appoint') return cronAppoint.value[f.key].join(',') || '*'
+ if (cronMode.value[f.key] === 'range')
+ return `${cronRange.value[f.key][0]}-${cronRange.value[f.key][1]}`
+ if (cronMode.value[f.key] === 'step')
+ return `${cronStep.value[f.key][0]}/${cronStep.value[f.key][1]}`
+ return fields.value[f.key] || '*'
+ })
+ // week鍜寉ear鐗规畩澶勭悊
+ arr[5] = arr[5] || '?'
+ cronStr.value = arr.join(' ')
+ if (tab.value === 'cron') emit('change', cronStr.value)
+ },
+ { deep: true }
+)
+
+// 鏍囧噯鏍煎紡
+const isoStr = ref('')
+const repeat = ref(1)
+const isoDate = ref('')
+const isoDuration = ref('')
+function setDuration(type, val) {
+ // 缁勮ISO 8601瀛楃涓�
+ let d = isoDuration.value
+ if (!d.includes(type)) d += val + type
+ else d = d.replace(new RegExp(`\\d+${type}`), val + type)
+ isoDuration.value = d
+ updateIsoStr()
+}
+function updateIsoStr() {
+ let str = `R${repeat.value}`
+ if (isoDate.value)
+ str +=
+ '/' +
+ (typeof isoDate.value === 'string' ? isoDate.value : new Date(isoDate.value).toISOString())
+ if (isoDuration.value) str += '/' + isoDuration.value
+ isoStr.value = str
+ if (tab.value === 'iso') emit('change', isoStr.value)
+}
+watch([repeat, isoDate, isoDuration], updateIsoStr)
+watch(
+ () => props.value,
+ (val) => {
+ if (!val) return
+ if (tab.value === 'cron') cronStr.value = val
+ if (tab.value === 'iso') isoStr.value = val
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue b/src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue
new file mode 100644
index 0000000..1aa6a0b
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/time-event-config/DurationConfig.vue
@@ -0,0 +1,86 @@
+<template>
+ <div>
+ <div style="margin-bottom: 10px"
+ >褰撳墠閫夋嫨锛�<el-input v-model="isoString" readonly style="width: 300px"
+ /></div>
+ <div v-for="unit in units" :key="unit.key" style="margin-bottom: 8px">
+ <span>{{ unit.label }}锛�</span>
+ <el-button-group>
+ <el-button
+ v-for="val in unit.presets"
+ :key="val"
+ size="mini"
+ @click="setUnit(unit.key, val)"
+ >{{ val }}</el-button
+ >
+ <el-input
+ v-model.number="custom[unit.key]"
+ size="mini"
+ style="width: 60px; margin-left: 8px"
+ placeholder="鑷畾涔�"
+ @change="setUnit(unit.key, custom[unit.key])"
+ />
+ </el-button-group>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, watch, computed } from 'vue'
+const props = defineProps({ value: String })
+const emit = defineEmits(['change'])
+
+const units = [
+ { key: 'Y', label: '骞�', presets: [1, 2, 3, 4] },
+ { key: 'M', label: '鏈�', presets: [1, 2, 3, 4] },
+ { key: 'D', label: '澶�', presets: [1, 2, 3, 4] },
+ { key: 'H', label: '鏃�', presets: [4, 8, 12, 24] },
+ { key: 'm', label: '鍒�', presets: [5, 10, 30, 50] },
+ { key: 'S', label: '绉�', presets: [5, 10, 30, 50] }
+]
+const custom = ref({ Y: '', M: '', D: '', H: '', m: '', S: '' })
+const isoString = ref('')
+
+function setUnit(key, val) {
+ if (!val || isNaN(val)) {
+ custom.value[key] = ''
+ return
+ }
+ custom.value[key] = val
+ updateIsoString()
+}
+
+function updateIsoString() {
+ let str = 'P'
+ if (custom.value.Y) str += custom.value.Y + 'Y'
+ if (custom.value.M) str += custom.value.M + 'M'
+ if (custom.value.D) str += custom.value.D + 'D'
+ if (custom.value.H || custom.value.m || custom.value.S) str += 'T'
+ if (custom.value.H) str += custom.value.H + 'H'
+ if (custom.value.m) str += custom.value.m + 'M'
+ if (custom.value.S) str += custom.value.S + 'S'
+ isoString.value = str === 'P' ? '' : str
+ emit('change', isoString.value)
+}
+
+watch(
+ () => props.value,
+ (val) => {
+ if (!val) return
+ // 瑙f瀽ISO 8601瀛楃涓插埌custom
+ const match = val.match(
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/
+ )
+ if (match) {
+ custom.value.Y = match[1] || ''
+ custom.value.M = match[2] || ''
+ custom.value.D = match[3] || ''
+ custom.value.H = match[4] || ''
+ custom.value.m = match[5] || ''
+ custom.value.S = match[6] || ''
+ updateIsoString()
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue b/src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue
new file mode 100644
index 0000000..3ec31f9
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/penal/time-event-config/TimeEventConfig.vue
@@ -0,0 +1,312 @@
+<template>
+ <div class="panel-tab__content">
+ <div style="margin-top: 10px">
+ <span>绫诲瀷锛�</span>
+ <el-button-group>
+ <el-button size="mini" :type="type === 'time' ? 'primary' : ''" @click="setType('time')"
+ >鏃堕棿</el-button
+ >
+ <el-button
+ size="mini"
+ :type="type === 'duration' ? 'primary' : ''"
+ @click="setType('duration')"
+ >鎸佺画</el-button
+ >
+ <el-button size="mini" :type="type === 'cycle' ? 'primary' : ''" @click="setType('cycle')"
+ >寰幆</el-button
+ >
+ </el-button-group>
+ <el-icon v-if="valid" color="green" style="margin-left: 8px"><CircleCheckFilled /></el-icon>
+ </div>
+ <div style="margin-top: 10px; display: flex; align-items: center">
+ <span>鏉′欢锛�</span>
+ <el-input
+ v-model="condition"
+ :placeholder="placeholder"
+ style="width: calc(100% - 100px)"
+ :readonly="type !== 'duration' && type !== 'cycle'"
+ @focus="handleInputFocus"
+ @blur="updateNode"
+ >
+ <template #suffix>
+ <el-tooltip v-if="!valid" content="鏍煎紡閿欒" placement="top">
+ <el-icon color="orange"><WarningFilled /></el-icon>
+ </el-tooltip>
+ <el-tooltip :content="helpText" placement="top">
+ <el-icon color="#409EFF" style="cursor: pointer" @click="showHelp = true"
+ ><QuestionFilled
+ /></el-icon>
+ </el-tooltip>
+ <el-button
+ v-if="type === 'time'"
+ @click="showDatePicker = true"
+ style="margin-left: 4px"
+ circle
+ size="small"
+ >
+ <Icon icon="ep:calendar" />
+ </el-button>
+ <el-button
+ v-if="type === 'duration'"
+ @click="showDurationDialog = true"
+ style="margin-left: 4px"
+ circle
+ size="small"
+ >
+ <Icon icon="ep:timer" />
+ </el-button>
+ <el-button
+ v-if="type === 'cycle'"
+ @click="showCycleDialog = true"
+ style="margin-left: 4px"
+ circle
+ size="small"
+ >
+ <Icon icon="ep:setting" />
+ </el-button>
+ </template>
+ </el-input>
+ </div>
+ <!-- 鏃堕棿閫夋嫨鍣� -->
+ <el-dialog
+ v-model="showDatePicker"
+ title="閫夋嫨鏃堕棿"
+ width="400px"
+ @close="showDatePicker = false"
+ >
+ <el-date-picker
+ v-model="dateValue"
+ type="datetime"
+ placeholder="閫夋嫨鏃ユ湡鏃堕棿"
+ style="width: 100%"
+ @change="onDateChange"
+ />
+ <template #footer>
+ <el-button @click="showDatePicker = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="onDateConfirm">纭畾</el-button>
+ </template>
+ </el-dialog>
+ <!-- 鎸佺画鏃堕暱閫夋嫨鍣� -->
+ <el-dialog
+ v-model="showDurationDialog"
+ title="鏃堕棿閰嶇疆"
+ width="600px"
+ @close="showDurationDialog = false"
+ >
+ <DurationConfig :value="condition" @change="onDurationChange" />
+ <template #footer>
+ <el-button @click="showDurationDialog = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="onDurationConfirm">纭畾</el-button>
+ </template>
+ </el-dialog>
+ <!-- 寰幆閰嶇疆鍣� -->
+ <el-dialog
+ v-model="showCycleDialog"
+ title="鏃堕棿閰嶇疆"
+ width="800px"
+ @close="showCycleDialog = false"
+ >
+ <CycleConfig :value="condition" @change="onCycleChange" />
+ <template #footer>
+ <el-button @click="showCycleDialog = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="onCycleConfirm">纭畾</el-button>
+ </template>
+ </el-dialog>
+ <!-- 甯姪璇存槑 -->
+ <el-dialog v-model="showHelp" title="鏍煎紡璇存槑" width="600px" @close="showHelp = false">
+ <div v-html="helpHtml"></div>
+ <template #footer>
+ <el-button @click="showHelp = false">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, computed, watch, onMounted } from 'vue'
+import { CircleCheckFilled, WarningFilled, QuestionFilled } from '@element-plus/icons-vue'
+import DurationConfig from './DurationConfig.vue'
+import CycleConfig from './CycleConfig.vue'
+import { createListenerObject, updateElementExtensions } from '../../utils'
+const bpmnInstances = () => (window as any).bpmnInstances
+const props = defineProps({ businessObject: Object })
+const type = ref('time')
+const condition = ref('')
+const valid = ref(true)
+const showDatePicker = ref(false)
+const showDurationDialog = ref(false)
+const showCycleDialog = ref(false)
+const showHelp = ref(false)
+const dateValue = ref(null)
+const bpmnElement = ref(null)
+
+const placeholder = computed(() => {
+ if (type.value === 'time') return '璇疯緭鍏ユ椂闂�'
+ if (type.value === 'duration') return '璇疯緭鍏ユ寔缁椂闀�'
+ if (type.value === 'cycle') return '璇疯緭鍏ュ惊鐜〃杈惧紡'
+ return ''
+})
+const helpText = computed(() => {
+ if (type.value === 'time') return '閫夋嫨鍏蜂綋鏃堕棿'
+ if (type.value === 'duration') return 'ISO 8601鏍煎紡锛屽PT1H'
+ if (type.value === 'cycle') return 'CRON琛ㄨ揪寮忔垨ISO 8601鍛ㄦ湡'
+ return ''
+})
+const helpHtml = computed(() => {
+ if (type.value === 'duration') {
+ return `鎸囧畾瀹氭椂鍣ㄤ箣鍓嶈绛夊緟澶氶暱鏃堕棿銆係琛ㄧず绉掞紝M琛ㄧず鍒嗭紝D琛ㄧず澶╋紱P琛ㄧず鏃堕棿娈碉紝T琛ㄧず绮剧‘鍒版椂闂寸殑鏃堕棿娈点��<br>
+ 鏃堕棿鏍煎紡渚濈劧涓篒SO 8601鏍煎紡锛屼竴骞翠袱涓湀涓夊ぉ鍥涘皬鏃朵簲鍒嗗叚绉掑唴锛屽彲浠ュ啓鎴怭1Y2M3DT4H5M6S銆�<br>
+ P鏄紑濮嬫爣璁帮紝T鏄椂闂村拰鏃ユ湡鍒嗗壊鏍囪锛屾病鏈夋棩鏈熷彧鏈夋椂闂碩鏄笉鑳界渷鍘荤殑锛屾瘮濡�1灏忔椂鎵ц涓�娆″簲鍐欐垚PT1H銆俙
+ }
+ if (type.value === 'cycle') {
+ return `鏀寔CRON琛ㄨ揪寮忥紙濡�0 0/30 * * * ?锛夋垨ISO 8601鍛ㄦ湡锛堝R3/PT10M锛夈�俙
+ }
+ return ''
+})
+
+// 鍒濆鍖栧拰鐩戝惉
+function syncFromBusinessObject() {
+ if (props.businessObject) {
+ const timerDef = (props.businessObject.eventDefinitions || [])[0]
+ if (timerDef) {
+ if (timerDef.timeDate) {
+ type.value = 'time'
+ condition.value = timerDef.timeDate.body
+ } else if (timerDef.timeDuration) {
+ type.value = 'duration'
+ condition.value = timerDef.timeDuration.body
+ } else if (timerDef.timeCycle) {
+ type.value = 'cycle'
+ condition.value = timerDef.timeCycle.body
+ }
+ }
+ }
+}
+onMounted(syncFromBusinessObject)
+
+// 鍒囨崲绫诲瀷
+function setType(t) {
+ type.value = t
+ condition.value = ''
+ updateNode()
+}
+
+// 杈撳叆鏍¢獙
+watch([type, condition], () => {
+ valid.value = validate()
+ // updateNode() // 鍙互娉ㄩ噴鎺夛紝閬垮厤棰戠箒瑙﹀彂
+})
+
+function validate() {
+ if (type.value === 'time') {
+ return !!condition.value && !isNaN(Date.parse(condition.value))
+ }
+ if (type.value === 'duration') {
+ return /^P.*$/.test(condition.value)
+ }
+ if (type.value === 'cycle') {
+ return /^([0-9*\/?, ]+|R\d*\/P.*)$/.test(condition.value)
+ }
+ return true
+}
+
+// 閫夋嫨鏃堕棿
+function onDateChange(val) {
+ dateValue.value = val
+}
+function onDateConfirm() {
+ if (dateValue.value) {
+ condition.value = new Date(dateValue.value).toISOString()
+ showDatePicker.value = false
+ updateNode()
+ }
+}
+
+// 鎸佺画鏃堕暱
+function onDurationChange(val) {
+ condition.value = val
+}
+function onDurationConfirm() {
+ showDurationDialog.value = false
+ updateNode()
+}
+
+// 寰幆
+function onCycleChange(val) {
+ condition.value = val
+}
+function onCycleConfirm() {
+ showCycleDialog.value = false
+ updateNode()
+}
+
+// 杈撳叆妗嗚仛鐒︽椂寮圭獥锛堝彲閫夛級
+function handleInputFocus() {
+ if (type.value === 'time') showDatePicker.value = true
+ if (type.value === 'duration') showDurationDialog.value = true
+ if (type.value === 'cycle') showCycleDialog.value = true
+}
+
+// 鍚屾鍒拌妭鐐�
+function updateNode() {
+ const moddle = window.bpmnInstances?.moddle
+ const modeling = window.bpmnInstances?.modeling
+ const elementRegistry = window.bpmnInstances?.elementRegistry
+ if (!moddle || !modeling || !elementRegistry) return
+
+ // 鑾峰彇鍏冪礌
+ if (!props.businessObject || !props.businessObject.id) return
+ const element = elementRegistry.get(props.businessObject.id)
+ if (!element) return
+
+ // 1. 澶嶇敤鍘熸湁 timerDef锛屾垨鏂板缓
+ let timerDef =
+ element.businessObject.eventDefinitions && element.businessObject.eventDefinitions[0]
+ if (!timerDef) {
+ timerDef = bpmnInstances().bpmnFactory.create('bpmn:TimerEventDefinition', {})
+ modeling.updateProperties(element, {
+ eventDefinitions: [timerDef]
+ })
+ }
+
+ // 2. 娓呯┖鍘熸湁
+ delete timerDef.timeDate
+ delete timerDef.timeDuration
+ delete timerDef.timeCycle
+
+ // 3. 璁剧疆鏂扮殑
+ if (type.value === 'time' && condition.value) {
+ timerDef.timeDate = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+ body: condition.value
+ })
+ } else if (type.value === 'duration' && condition.value) {
+ timerDef.timeDuration = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+ body: condition.value
+ })
+ } else if (type.value === 'cycle' && condition.value) {
+ timerDef.timeCycle = bpmnInstances().bpmnFactory.create('bpmn:FormalExpression', {
+ body: condition.value
+ })
+ }
+
+ bpmnInstances().modeling.updateProperties(toRaw(element), {
+ eventDefinitions: [timerDef]
+ })
+}
+
+watch(
+ () => props.businessObject,
+ (val) => {
+ if (val) {
+ nextTick(() => {
+ syncFromBusinessObject()
+ })
+ }
+ },
+ { immediate: true }
+)
+</script>
+
+<style scoped>
+/* 鐩稿叧鏍峰紡 */
+</style>
diff --git a/src/components/bpmnProcessDesigner/package/theme/element-variables.scss b/src/components/bpmnProcessDesigner/package/theme/element-variables.scss
new file mode 100644
index 0000000..0646f8e
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/theme/element-variables.scss
@@ -0,0 +1,70 @@
+/* 鏀瑰彉涓婚鑹插彉閲� */
+$--color-primary: #1890ff;
+$--color-danger: #ff4d4f;
+
+/* 鏀瑰彉 icon 瀛椾綋璺緞鍙橀噺锛屽繀闇� */
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
+
+@use '~element-ui/packages/theme-chalk/src/index';
+
+.el-table td,
+.el-table th {
+ color: #333;
+}
+.el-drawer__header {
+ padding: 16px 16px 8px 16px;
+ margin: 0;
+ line-height: 24px;
+ font-size: 18px;
+ color: #303133;
+ box-sizing: border-box;
+ border-bottom: 1px solid #e8e8e8;
+}
+div[class^='el-drawer']:focus,
+span:focus {
+ outline: none;
+}
+.el-drawer__body {
+ box-sizing: border-box;
+ padding: 16px;
+ width: 100%;
+ overflow-y: auto;
+}
+
+.el-dialog {
+ margin-top: 50vh !important;
+ transform: translateY(-50%);
+ overflow: hidden;
+}
+.el-dialog__wrapper {
+ overflow: hidden;
+ max-height: 100vh;
+}
+.el-dialog__header {
+ padding: 16px 16px 8px 16px;
+ box-sizing: border-box;
+ border-bottom: 1px solid #e8e8e8;
+}
+.el-dialog__body {
+ padding: 16px;
+ max-height: 80vh;
+ box-sizing: border-box;
+ overflow-y: auto;
+}
+.el-dialog__footer {
+ padding: 16px;
+ box-sizing: border-box;
+ border-top: 1px solid #e8e8e8;
+}
+.el-dialog__close {
+ font-weight: 600;
+}
+.el-select {
+ width: 100%;
+}
+.el-divider:not(.el-divider--horizontal) {
+ margin: 0 8px;
+}
+.el-divider.el-divider--horizontal {
+ margin: 16px 0;
+}
diff --git a/src/components/bpmnProcessDesigner/package/theme/index.scss b/src/components/bpmnProcessDesigner/package/theme/index.scss
new file mode 100644
index 0000000..2404760
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/theme/index.scss
@@ -0,0 +1,117 @@
+@use './process-designer.scss';
+@use './process-panel.scss';
+
+$success-color: #4eb819;
+$primary-color: #409EFF;
+$danger-color: #F56C6C;
+$cancel-color: #909399;
+
+.process-viewer {
+ position: relative;
+ border: 1px solid #EFEFEF;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+') repeat!important;
+
+ .success-arrow {
+ fill: $success-color;
+ stroke: $success-color;
+ }
+
+ .success-conditional {
+ fill: white;
+ stroke: $success-color;
+ }
+
+ .success.djs-connection {
+ .djs-visual path {
+ stroke: $success-color!important;
+ //marker-end: url(#sequenceflow-end-white-success)!important;
+ }
+ }
+
+ .success.djs-connection.condition-expression {
+ .djs-visual path {
+ //marker-start: url(#conditional-flow-marker-white-success)!important;
+ }
+ }
+
+ .success.djs-shape {
+ .djs-visual rect {
+ stroke: $success-color!important;
+ fill: $success-color!important;
+ fill-opacity: 0.15!important;
+ }
+
+ .djs-visual polygon {
+ stroke: $success-color!important;
+ }
+
+ .djs-visual path:nth-child(2) {
+ stroke: $success-color!important;
+ fill: $success-color!important;
+ }
+
+ .djs-visual circle {
+ stroke: $success-color!important;
+ fill: $success-color!important;
+ fill-opacity: 0.15!important;
+ }
+ }
+
+ .primary.djs-shape {
+ .djs-visual rect {
+ stroke: $primary-color!important;
+ fill: $primary-color!important;
+ fill-opacity: 0.15!important;
+ }
+
+ .djs-visual polygon {
+ stroke: $primary-color!important;
+ }
+
+ .djs-visual circle {
+ stroke: $primary-color!important;
+ fill: $primary-color!important;
+ fill-opacity: 0.15!important;
+ }
+ }
+
+ .danger.djs-shape {
+ .djs-visual rect {
+ stroke: $danger-color!important;
+ fill: $danger-color!important;
+ fill-opacity: 0.15!important;
+ }
+
+ .djs-visual polygon {
+ stroke: $danger-color!important;
+ }
+
+ .djs-visual circle {
+ stroke: $danger-color!important;
+ fill: $danger-color!important;
+ fill-opacity: 0.15!important;
+ }
+ }
+
+ .cancel.djs-shape {
+ .djs-visual rect {
+ stroke: $cancel-color!important;
+ fill: $cancel-color!important;
+ fill-opacity: 0.15!important;
+ }
+
+ .djs-visual polygon {
+ stroke: $cancel-color!important;
+ }
+
+ .djs-visual circle {
+ stroke: $cancel-color!important;
+ fill: $cancel-color!important;
+ fill-opacity: 0.15!important;
+ }
+ }
+}
+
+.process-viewer .djs-tooltip-container, .process-viewer .djs-overlay-container, .process-viewer .djs-palette {
+ display: none;
+}
diff --git a/src/components/bpmnProcessDesigner/package/theme/process-designer.scss b/src/components/bpmnProcessDesigner/package/theme/process-designer.scss
new file mode 100644
index 0000000..bca0258
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/theme/process-designer.scss
@@ -0,0 +1,159 @@
+@use 'bpmn-js-token-simulation/assets/css/bpmn-js-token-simulation.css';
+
+// 杈规琚� token-simulation 鏍峰紡瑕嗙洊浜�
+.djs-palette {
+ background: var(--palette-background-color);
+ border: solid 1px var(--palette-border-color) !important;
+ border-radius: 2px;
+}
+
+.my-process-designer {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ .my-process-designer__header {
+ width: 100%;
+ min-height: 36px;
+ .el-button {
+ text-align: center;
+ }
+ .el-button-group {
+ margin: 4px;
+ }
+ .el-tooltip__popper {
+ .el-button {
+ width: 100%;
+ text-align: left;
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+ .el-button:hover {
+ background: rgba(64, 158, 255, 0.8);
+ color: #ffffff;
+ }
+ }
+ .align {
+ position: relative;
+ i {
+ &:after {
+ content: '|';
+ position: absolute;
+ // transform: rotate(90deg) translate(200%, 60%);
+ transform: rotate(180deg) translate(271%, -10%);
+ }
+ }
+ }
+ .align.align-left i {
+ transform: rotate(90deg);
+ }
+ .align.align-right i {
+ transform: rotate(-90deg);
+ }
+ .align.align-top i {
+ transform: rotate(180deg);
+ }
+ .align.align-bottom i {
+ transform: rotate(0deg);
+ }
+ .align.align-center i {
+ transform: rotate(0deg);
+ &:after {
+ // transform: rotate(90deg) translate(0, 60%);
+ transform: rotate(0deg) translate(-0%, -5%);
+ }
+ }
+ .align.align-middle i {
+ transform: rotate(-90deg);
+ &:after {
+ // transform: rotate(90deg) translate(0, 60%);
+ transform: rotate(0deg) translate(0, -10%);
+ }
+ }
+ }
+ .my-process-designer__container {
+ display: inline-flex;
+ width: 100%;
+ flex: 1;
+ .my-process-designer__canvas {
+ flex: 1;
+ height: 100%;
+ position: relative;
+ background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHBhdHRlcm4gaWQ9ImEiIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCIgcGF0dGVyblVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHBhdGggZD0iTTAgMTBoNDBNMTAgMHY0ME0wIDIwaDQwTTIwIDB2NDBNMCAzMGg0ME0zMCAwdjQwIiBmaWxsPSJub25lIiBzdHJva2U9IiNlMGUwZTAiIG9wYWNpdHk9Ii4yIi8+PHBhdGggZD0iTTQwIDBIMHY0MCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZTBlMGUwIi8+PC9wYXR0ZXJuPjwvZGVmcz48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJ1cmwoI2EpIi8+PC9zdmc+')
+ repeat !important;
+ div.toggle-mode {
+ display: none;
+ }
+ }
+ .my-process-designer__property-panel {
+ height: 100%;
+ overflow: scroll;
+ overflow-y: auto;
+ z-index: 10;
+ * {
+ box-sizing: border-box;
+ }
+ }
+ // svg {
+ // width: 100%;
+ // height: 100%;
+ // min-height: 100%;
+ // overflow: hidden;
+ // }
+ }
+}
+
+//渚ц竟鏍忛厤缃�
+// .djs-palette .two-column .open {
+.open {
+ // .djs-palette.open {
+ .djs-palette-entries {
+ div[class^='bpmn-icon-']:before,
+ div[class*='bpmn-icon-']:before {
+ line-height: unset;
+ }
+ div.entry {
+ position: relative;
+ }
+ div.entry:hover {
+ &::after {
+ width: max-content;
+ content: attr(title);
+ vertical-align: text-bottom;
+ position: absolute;
+ right: -10px;
+ top: 0;
+ bottom: 0;
+ overflow: hidden;
+ transform: translateX(100%);
+ font-size: 0.5em;
+ display: inline-block;
+ text-decoration: inherit;
+ font-variant: normal;
+ text-transform: none;
+ background: #fafafa;
+ box-shadow: 0 0 6px #eeeeee;
+ border: 1px solid #cccccc;
+ box-sizing: border-box;
+ padding: 0 16px;
+ border-radius: 4px;
+ z-index: 100;
+ }
+ }
+ }
+}
+pre {
+ margin: 0;
+ height: 100%;
+ overflow: hidden;
+ max-height: calc(80vh - 32px);
+ overflow-y: auto;
+}
+.hljs {
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+.hljs * {
+ font-family: Consolas, Monaco, monospace;
+}
diff --git a/src/components/bpmnProcessDesigner/package/theme/process-panel.scss b/src/components/bpmnProcessDesigner/package/theme/process-panel.scss
new file mode 100644
index 0000000..f840cdd
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/theme/process-panel.scss
@@ -0,0 +1,107 @@
+.process-panel__container {
+ box-sizing: border-box;
+ padding: 0 8px;
+ border-left: 1px solid #eeeeee;
+ box-shadow: 0 0 8px #cccccc;
+ max-height: 100%;
+ overflow-y: scroll;
+}
+.panel-tab__title {
+ font-weight: 600;
+ padding: 0 8px;
+ font-size: 1.1em;
+ line-height: 1.2em;
+ i {
+ margin-right: 8px;
+ font-size: 1.2em;
+ }
+}
+.panel-tab__content {
+ width: 100%;
+ box-sizing: border-box;
+ border-top: 1px solid #eeeeee;
+ padding: 8px 16px;
+ .panel-tab__content--title {
+ display: flex;
+ justify-content: space-between;
+ padding-bottom: 8px;
+ span {
+ flex: 1;
+ text-align: left;
+ }
+ }
+}
+.element-property {
+ width: 100%;
+ display: flex;
+ align-items: flex-start;
+ margin: 8px 0;
+ .element-property__label {
+ display: block;
+ width: 90px;
+ text-align: right;
+ overflow: hidden;
+ padding-right: 12px;
+ line-height: 32px;
+ font-size: 14px;
+ box-sizing: border-box;
+ }
+ .element-property__value {
+ flex: 1;
+ line-height: 32px;
+ }
+ .el-form-item {
+ width: 100%;
+ margin-bottom: 0;
+ padding-bottom: 18px;
+ }
+}
+.list-property {
+ flex-direction: column;
+ .element-listener-item {
+ width: 100%;
+ display: inline-grid;
+ grid-template-columns: 16px auto 32px 32px;
+ grid-column-gap: 8px;
+ }
+ .element-listener-item + .element-listener-item {
+ margin-top: 8px;
+ }
+}
+.listener-filed__title {
+ display: inline-flex;
+ width: 100%;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 0;
+ span {
+ width: 200px;
+ text-align: left;
+ font-size: 14px;
+ }
+ i {
+ margin-right: 8px;
+ }
+}
+.element-drawer__button {
+ margin-top: 8px;
+ width: 100%;
+ display: inline-flex;
+ justify-content: space-around;
+}
+.element-drawer__button > .el-button {
+ width: 100%;
+}
+
+.el-collapse-item__content {
+ padding-bottom: 0;
+}
+.el-input.is-disabled .el-input__inner {
+ color: #999999;
+}
+.el-form-item.el-form-item--mini {
+ margin-bottom: 0;
+ & + .el-form-item {
+ margin-top: 16px;
+ }
+}
diff --git a/src/components/bpmnProcessDesigner/package/utils.ts b/src/components/bpmnProcessDesigner/package/utils.ts
new file mode 100644
index 0000000..b5f0eec
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/package/utils.ts
@@ -0,0 +1,77 @@
+const bpmnInstances = () => (window as any)?.bpmnInstances
+// 鍒涘缓鐩戝惉鍣ㄥ疄渚�
+export function createListenerObject(options, isTask, prefix) {
+ const listenerObj = Object.create(null)
+ listenerObj.event = options.event
+ isTask && (listenerObj.id = options.id) // 浠诲姟鐩戝惉鍣ㄧ壒鏈夌殑 id 瀛楁
+ switch (options.listenerType) {
+ case 'scriptListener':
+ listenerObj.script = createScriptObject(options, prefix)
+ break
+ case 'expressionListener':
+ listenerObj.expression = options.expression
+ break
+ case 'delegateExpressionListener':
+ listenerObj.delegateExpression = options.delegateExpression
+ break
+ default:
+ listenerObj.class = options.class
+ }
+ // 娉ㄥ叆瀛楁
+ if (options.fields) {
+ listenerObj.fields = options.fields.map((field) => {
+ return createFieldObject(field, prefix)
+ })
+ }
+ // 浠诲姟鐩戝惉鍣ㄧ殑 瀹氭椂鍣� 璁剧疆
+ if (isTask && options.event === 'timeout' && !!options.eventDefinitionType) {
+ const timeDefinition = bpmnInstances().moddle.create('bpmn:FormalExpression', {
+ body: options.eventTimeDefinitions
+ })
+ const TimerEventDefinition = bpmnInstances().moddle.create('bpmn:TimerEventDefinition', {
+ id: `TimerEventDefinition_${uuid(8)}`,
+ [`time${options.eventDefinitionType.replace(/^\S/, (s) => s.toUpperCase())}`]: timeDefinition
+ })
+ listenerObj.eventDefinitions = [TimerEventDefinition]
+ }
+ return bpmnInstances().moddle.create(
+ `${prefix}:${isTask ? 'TaskListener' : 'ExecutionListener'}`,
+ listenerObj
+ )
+}
+
+// 鍒涘缓 鐩戝惉鍣ㄧ殑娉ㄥ叆瀛楁 瀹炰緥
+export function createFieldObject(option, prefix) {
+ const { name, fieldType, string, expression } = option
+ const fieldConfig = fieldType === 'string' ? { name, string } : { name, expression }
+ return bpmnInstances().moddle.create(`${prefix}:Field`, fieldConfig)
+}
+
+// 鍒涘缓鑴氭湰瀹炰緥
+export function createScriptObject(options, prefix) {
+ const { scriptType, scriptFormat, value, resource } = options
+ const scriptConfig =
+ scriptType === 'inlineScript' ? { scriptFormat, value } : { scriptFormat, resource }
+ return bpmnInstances().moddle.create(`${prefix}:Script`, scriptConfig)
+}
+
+// 鏇存柊鍏冪礌鎵╁睍灞炴��
+export function updateElementExtensions(element, extensionList) {
+ const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+ values: extensionList
+ })
+ // 鐩存帴浣跨敤鍘熷鍏冪礌瀵硅薄锛屼笉闇�瑕乼oRaw鍖呰
+ bpmnInstances().modeling.updateProperties(element, {
+ extensionElements: extensions
+ })
+}
+
+// 鍒涘缓涓�涓猧d
+export function uuid(length = 8, chars?) {
+ let result = ''
+ const charsString = chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ for (let i = length; i > 0; --i) {
+ result += charsString[Math.floor(Math.random() * charsString.length)]
+ }
+ return result
+}
diff --git a/src/components/bpmnProcessDesigner/src/highlight/index.js b/src/components/bpmnProcessDesigner/src/highlight/index.js
new file mode 100644
index 0000000..5df38c9
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/highlight/index.js
@@ -0,0 +1,5 @@
+const hljs = require('highlight.js/lib/core')
+hljs.registerLanguage('xml', require('highlight.js/lib/languages/xml'))
+hljs.registerLanguage('json', require('highlight.js/lib/languages/json'))
+
+module.exports = hljs
diff --git a/src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js b/src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js
new file mode 100644
index 0000000..e876031
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/modules/custom-renderer/CustomRenderer.js
@@ -0,0 +1,14 @@
+import BpmnRenderer from 'bpmn-js/lib/draw/BpmnRenderer'
+
+export default function CustomRenderer(config, eventBus, styles, pathMap, canvas, textRenderer) {
+ BpmnRenderer.call(this, config, eventBus, styles, pathMap, canvas, textRenderer, 2000)
+
+ this.handlers['label'] = function () {
+ return null
+ }
+}
+
+const F = function () {} // 鏍稿績锛屽埄鐢ㄧ┖瀵硅薄浣滀负涓粙锛�
+F.prototype = BpmnRenderer.prototype // 鏍稿績锛屽皢鐖剁被鐨勫師鍨嬭祴鍊肩粰绌哄璞锛�
+CustomRenderer.prototype = new F() // 鏍稿績锛屽皢 F鐨勫疄渚嬭祴鍊肩粰瀛愮被锛�
+CustomRenderer.prototype.constructor = CustomRenderer // 淇瀛愮被CustomRenderer鐨勬瀯閫犲櫒鎸囧悜锛岄槻姝㈠師鍨嬮摼鐨勬贩涔憋紱
diff --git a/src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js b/src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js
new file mode 100644
index 0000000..79d8bd0
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/modules/custom-renderer/index.js
@@ -0,0 +1,6 @@
+import CustomRenderer from './CustomRenderer'
+
+export default {
+ __init__: ['customRenderer'],
+ customRenderer: ['type', CustomRenderer]
+}
diff --git a/src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js b/src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js
new file mode 100644
index 0000000..9fa1d14
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/modules/rules/CustomRules.js
@@ -0,0 +1,16 @@
+import BpmnRules from 'bpmn-js/lib/features/rules/BpmnRules'
+import inherits from 'inherits'
+
+export default function CustomRules(eventBus) {
+ BpmnRules.call(this, eventBus)
+}
+
+inherits(CustomRules, BpmnRules)
+
+CustomRules.prototype.canDrop = function () {
+ return false
+}
+
+CustomRules.prototype.canMove = function () {
+ return false
+}
diff --git a/src/components/bpmnProcessDesigner/src/modules/rules/index.js b/src/components/bpmnProcessDesigner/src/modules/rules/index.js
new file mode 100644
index 0000000..12cf05a
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/modules/rules/index.js
@@ -0,0 +1,6 @@
+import CustomRules from './CustomRules'
+
+export default {
+ __init__: ['customRules'],
+ customRules: ['type', CustomRules]
+}
diff --git a/src/components/bpmnProcessDesigner/src/translations.ts b/src/components/bpmnProcessDesigner/src/translations.ts
new file mode 100644
index 0000000..5f9b9a5
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/translations.ts
@@ -0,0 +1,25 @@
+/**
+ * This is a sample file that should be replaced with the actual translation.
+ *
+ * Checkout https://github.com/bpmn-io/bpmn-js-i18n for a list of available
+ * translations and labels to translate.
+ */
+export default {
+ 'Exclusive Gateway': 'Exklusives Gateway',
+ 'Parallel Gateway': 'Paralleles Gateway',
+ 'Inclusive Gateway': 'Inklusives Gateway',
+ 'Complex Gateway': 'Komplexes Gateway',
+ 'Event based Gateway': 'Ereignis-basiertes Gateway',
+ 'Message Start Event': '娑堟伅鍚姩浜嬩欢',
+ 'Timer Start Event': '瀹氭椂鍚姩浜嬩欢',
+ 'Conditional Start Event': '鏉′欢鍚姩浜嬩欢',
+ 'Signal Start Event': '淇″彿鍚姩浜嬩欢',
+ 'Error Start Event': '閿欒鍚姩浜嬩欢',
+ 'Escalation Start Event': '鍗囩骇鍚姩浜嬩欢',
+ 'Compensation Start Event': '琛ュ伩鍚姩浜嬩欢',
+ 'Message Start Event (non-interrupting)': '娑堟伅鍚姩浜嬩欢 (闈炰腑鏂�)',
+ 'Timer Start Event (non-interrupting)': '瀹氭椂鍚姩浜嬩欢 (闈炰腑鏂�)',
+ 'Conditional Start Event (non-interrupting)': '鏉′欢鍚姩浜嬩欢 (闈炰腑鏂�)',
+ 'Signal Start Event (non-interrupting)': '淇″彿鍚姩浜嬩欢 (闈炰腑鏂�)',
+ 'Escalation Start Event (non-interrupting)': '鍗囩骇鍚姩浜嬩欢 (闈炰腑鏂�)'
+}
diff --git a/src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js b/src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js
new file mode 100644
index 0000000..bb71d44
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/utils/directive/clickOutSide.js
@@ -0,0 +1,39 @@
+//outside.js
+
+const ctx = '@@clickoutsideContext'
+
+export default {
+ bind(el, binding, vnode) {
+ const ele = el
+ const documentHandler = (e) => {
+ if (!vnode.context || ele.contains(e.target)) {
+ return false
+ }
+ // 璋冪敤鎸囦护鍥炶皟
+ if (binding.expression) {
+ vnode.context[el[ctx].methodName](e)
+ } else {
+ el[ctx].bindingFn(e)
+ }
+ }
+ // 灏嗘柟娉曟坊鍔犲埌ele
+ ele[ctx] = {
+ documentHandler,
+ methodName: binding.expression,
+ bindingFn: binding.value
+ }
+
+ setTimeout(() => {
+ document.addEventListener('touchstart', documentHandler) // 涓篸ocument缁戝畾浜嬩欢
+ })
+ },
+ update(el, binding) {
+ const ele = el
+ ele[ctx].methodName = binding.expression
+ ele[ctx].bindingFn = binding.value
+ },
+ unbind(el) {
+ document.removeEventListener('touchstart', el[ctx].documentHandler) // 瑙g粦
+ delete el[ctx]
+ }
+}
diff --git a/src/components/bpmnProcessDesigner/src/utils/index.js b/src/components/bpmnProcessDesigner/src/utils/index.js
new file mode 100644
index 0000000..7d970ec
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/utils/index.js
@@ -0,0 +1,10 @@
+export function debounce(fn, delay = 500) {
+ let timer
+ return function (...args) {
+ if (timer) {
+ clearTimeout(timer)
+ timer = null
+ }
+ timer = setTimeout(fn.bind(this, ...args), delay)
+ }
+}
diff --git a/src/components/bpmnProcessDesigner/src/utils/xml2json.js b/src/components/bpmnProcessDesigner/src/utils/xml2json.js
new file mode 100644
index 0000000..fe1a52f
--- /dev/null
+++ b/src/components/bpmnProcessDesigner/src/utils/xml2json.js
@@ -0,0 +1,50 @@
+function xmlStr2XmlObj(xmlStr) {
+ let xmlObj = {}
+ if (document.all) {
+ const xmlDom = new window.ActiveXObject('Microsoft.XMLDOM')
+ xmlDom.loadXML(xmlStr)
+ xmlObj = xmlDom
+ } else {
+ xmlObj = new DOMParser().parseFromString(xmlStr, 'text/xml')
+ }
+ return xmlObj
+}
+
+function xml2json(xml) {
+ try {
+ let obj = {}
+ if (xml.children.length > 0) {
+ for (let i = 0; i < xml.children.length; i++) {
+ const item = xml.children.item(i)
+ const nodeName = item.nodeName
+ if (typeof obj[nodeName] == 'undefined') {
+ obj[nodeName] = xml2json(item)
+ } else {
+ if (typeof obj[nodeName].push == 'undefined') {
+ const old = obj[nodeName]
+ obj[nodeName] = []
+ obj[nodeName].push(old)
+ }
+ obj[nodeName].push(xml2json(item))
+ }
+ }
+ } else {
+ obj = xml.textContent
+ }
+ return obj
+ } catch (e) {
+ console.log(e.message)
+ }
+}
+
+function xmlObj2json(xml) {
+ const xmlObj = xmlStr2XmlObj(xml)
+ console.log(xmlObj)
+ let jsonObj = {}
+ if (xmlObj.childNodes.length > 0) {
+ jsonObj = xml2json(xmlObj)
+ }
+ return jsonObj
+}
+
+export default xmlObj2json
diff --git a/src/components/index.ts b/src/components/index.ts
new file mode 100644
index 0000000..4d030c3
--- /dev/null
+++ b/src/components/index.ts
@@ -0,0 +1,6 @@
+import type { App } from 'vue'
+import { Icon } from './Icon'
+
+export const setupGlobCom = (app: App<Element>): void => {
+ app.component('Icon', Icon)
+}
diff --git a/src/config/axios/config.ts b/src/config/axios/config.ts
new file mode 100644
index 0000000..8116508
--- /dev/null
+++ b/src/config/axios/config.ts
@@ -0,0 +1,28 @@
+const config: {
+ base_url: string
+ result_code: number | string
+ default_headers: AxiosHeaders
+ request_timeout: number
+} = {
+ /**
+ * api璇锋眰鍩虹璺緞
+ */
+ base_url: import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL,
+ /**
+ * 鎺ュ彛鎴愬姛杩斿洖鐘舵�佺爜
+ */
+ result_code: 200,
+
+ /**
+ * 鎺ュ彛璇锋眰瓒呮椂鏃堕棿
+ */
+ request_timeout: 30000,
+
+ /**
+ * 榛樿鎺ュ彛璇锋眰绫诲瀷
+ * 鍙�夊�硷細application/x-www-form-urlencoded multipart/form-data
+ */
+ default_headers: 'application/json'
+}
+
+export { config }
diff --git a/src/config/axios/errorCode.ts b/src/config/axios/errorCode.ts
new file mode 100644
index 0000000..94d719f
--- /dev/null
+++ b/src/config/axios/errorCode.ts
@@ -0,0 +1,6 @@
+export default {
+ '401': '璁よ瘉澶辫触锛屾棤娉曡闂郴缁熻祫婧�',
+ '403': '褰撳墠鎿嶄綔娌℃湁鏉冮檺',
+ '404': '璁块棶璧勬簮涓嶅瓨鍦�',
+ default: '绯荤粺鏈煡閿欒锛岃鍙嶉缁欑鐞嗗憳'
+}
diff --git a/src/config/axios/index.ts b/src/config/axios/index.ts
new file mode 100644
index 0000000..36bddc2
--- /dev/null
+++ b/src/config/axios/index.ts
@@ -0,0 +1,48 @@
+import { service } from './service'
+
+import { config } from './config'
+
+const { default_headers } = config
+
+const request = (option: any) => {
+ const { headersType, headers, ...otherOption } = option
+ return service({
+ ...otherOption,
+ headers: {
+ 'Content-Type': headersType || default_headers,
+ ...headers
+ }
+ })
+}
+export default {
+ get: async <T = any>(option: any) => {
+ const res = await request({ method: 'GET', ...option })
+ return res.data as unknown as T
+ },
+ post: async <T = any>(option: any) => {
+ const res = await request({ method: 'POST', ...option })
+ return res.data as unknown as T
+ },
+ postOriginal: async (option: any) => {
+ const res = await request({ method: 'POST', ...option })
+ return res
+ },
+ delete: async <T = any>(option: any) => {
+ const res = await request({ method: 'DELETE', ...option })
+ return res.data as unknown as T
+ },
+ put: async <T = any>(option: any) => {
+ const res = await request({ method: 'PUT', ...option })
+ return res.data as unknown as T
+ },
+ download: async <T = any>(option: any) => {
+ const res = await request({ method: 'GET', responseType: 'blob', ...option })
+ return res as unknown as Promise<T>
+ },
+ upload: async <T = any>(option: any) => {
+ option.headersType = 'multipart/form-data'
+ console.log(option)
+ const res = await request({ method: 'POST', ...option })
+ return res as unknown as Promise<T>
+ }
+}
diff --git a/src/config/axios/service.ts b/src/config/axios/service.ts
new file mode 100644
index 0000000..9214bf8
--- /dev/null
+++ b/src/config/axios/service.ts
@@ -0,0 +1,270 @@
+import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
+
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+import qs from 'qs'
+import { config } from '@/config/axios/config'
+import {
+ getAccessToken,
+ getRefreshToken,
+ getTenantId,
+ getVisitTenantId,
+ removeToken,
+ setToken
+} from '@/utils/auth'
+import errorCode from './errorCode'
+
+import { resetRouter } from '@/router'
+import { deleteUserCache } from '@/hooks/web/useCache'
+import { ApiEncrypt } from '@/utils/encrypt'
+
+const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
+const { result_code, base_url, request_timeout } = config
+
+// 闇�瑕佸拷鐣ョ殑鎻愮ず銆傚拷鐣ュ悗锛岃嚜鍔� Promise.reject('error')
+const ignoreMsgs = [
+ '鏃犳晥鐨勫埛鏂颁护鐗�', // 鍒锋柊浠ょ墝琚垹闄ゆ椂锛屼笉鐢ㄦ彁绀�
+ '鍒锋柊浠ょ墝宸茶繃鏈�' // 浣跨敤鍒锋柊浠ょ墝锛屽埛鏂拌幏鍙栨柊鐨勮闂护鐗屾椂锛岀粨鏋滃洜涓鸿繃鏈熷け璐ワ紝姝ゆ椂闇�瑕佸拷鐣ャ�傚惁鍒欙紝浼氬鑷寸户缁� 401锛屾棤娉曡烦杞埌鐧诲嚭鐣岄潰
+]
+// 鏄惁鏄剧ず閲嶆柊鐧诲綍
+export const isRelogin = { show: false }
+// Axios 鏃犳劅鐭ュ埛鏂颁护鐗岋紝鍙傝�� https://www.dashingdog.cn/article/11 涓� https://segmentfault.com/a/1190000020210980 瀹炵幇
+// 璇锋眰闃熷垪
+let requestList: any[] = []
+// 鏄惁姝e湪鍒锋柊涓�
+let isRefreshToken = false
+// 璇锋眰鐧藉悕鍗曪紝鏃犻』 token 鐨勬帴鍙�
+const whiteList: string[] = ['/login', '/refresh-token']
+
+// 鍒涘缓axios瀹炰緥
+const service: AxiosInstance = axios.create({
+ baseURL: base_url, // api 鐨� base_url
+ timeout: request_timeout, // 璇锋眰瓒呮椂鏃堕棿
+ withCredentials: false, // 绂佺敤 Cookie 绛変俊鎭�
+ // 鑷畾涔夊弬鏁板簭鍒楀寲鍑芥暟
+ paramsSerializer: (params) => {
+ return qs.stringify(params, { allowDots: true })
+ }
+})
+
+// request鎷︽埅鍣�
+service.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ // 鏄惁闇�瑕佽缃� token
+ let isToken = (config!.headers || {}).isToken === false
+ whiteList.some((v) => {
+ if (config.url && config.url.indexOf(v) > -1) {
+ return (isToken = false)
+ }
+ })
+ if (getAccessToken() && !isToken) {
+ config.headers.Authorization = 'Bearer ' + getAccessToken() // 璁╂瘡涓姹傛惡甯﹁嚜瀹氫箟token
+ }
+ // 璁剧疆绉熸埛
+ if (tenantEnable && tenantEnable === 'true') {
+ const tenantId = getTenantId()
+ if (tenantId) config.headers['tenant-id'] = tenantId
+ // 鍙湁鐧诲綍鏃讹紝鎵嶈缃� visit-tenant-id 璁块棶绉熸埛
+ const visitTenantId = getVisitTenantId()
+ if (config.headers.Authorization && visitTenantId) {
+ config.headers['visit-tenant-id'] = visitTenantId
+ }
+ }
+ const method = config.method?.toUpperCase()
+ // 闃叉 GET 璇锋眰缂撳瓨
+ if (method === 'GET') {
+ config.headers['Cache-Control'] = 'no-cache'
+ config.headers['Pragma'] = 'no-cache'
+ }
+ // 鑷畾涔夊弬鏁板簭鍒楀寲鍑芥暟
+ else if (method === 'POST') {
+ const contentType = config.headers['Content-Type'] || config.headers['content-type']
+ if (contentType === 'application/x-www-form-urlencoded') {
+ if (config.data && typeof config.data !== 'string') {
+ config.data = qs.stringify(config.data)
+ }
+ }
+ }
+ // 鏄惁 API 鍔犲瘑
+ if ((config!.headers || {}).isEncrypt) {
+ try {
+ // 鍔犲瘑璇锋眰鏁版嵁
+ if (config.data) {
+ config.data = ApiEncrypt.encryptRequest(config.data)
+ // 璁剧疆鍔犲瘑鏍囪瘑澶�
+ config.headers[ApiEncrypt.getEncryptHeader()] = 'true'
+ }
+ } catch (error) {
+ console.error('璇锋眰鏁版嵁鍔犲瘑澶辫触:', error)
+ throw error
+ }
+ }
+ return config
+ },
+ (error: AxiosError) => {
+ // Do something with request error
+ console.log(error) // for debug
+ return Promise.reject(error)
+ }
+)
+
+// response 鎷︽埅鍣�
+service.interceptors.response.use(
+ async (response: AxiosResponse<any>) => {
+ let { data } = response
+ const config = response.config
+ if (!data) {
+ // 杩斿洖鈥淸HTTP]璇锋眰娌℃湁杩斿洖鍊尖��;
+ throw new Error()
+ }
+
+ // 妫�鏌ユ槸鍚﹂渶瑕佽В瀵嗗搷搴旀暟鎹�
+ const encryptHeader = ApiEncrypt.getEncryptHeader()
+ const isEncryptResponse =
+ response.headers[encryptHeader] === 'true' ||
+ response.headers[encryptHeader.toLowerCase()] === 'true'
+ if (isEncryptResponse && typeof data === 'string') {
+ try {
+ // 瑙e瘑鍝嶅簲鏁版嵁
+ data = ApiEncrypt.decryptResponse(data)
+ } catch (error) {
+ console.error('鍝嶅簲鏁版嵁瑙e瘑澶辫触:', error)
+ throw new Error('鍝嶅簲鏁版嵁瑙e瘑澶辫触: ' + (error as Error).message)
+ }
+ }
+
+ const { t } = useI18n()
+ // 鏈缃姸鎬佺爜鍒欓粯璁ゆ垚鍔熺姸鎬�
+ // 浜岃繘鍒舵暟鎹垯鐩存帴杩斿洖锛屼緥濡傝 Excel 瀵煎嚭
+ if (
+ response.request.responseType === 'blob' ||
+ response.request.responseType === 'arraybuffer'
+ ) {
+ // 娉ㄦ剰锛氬鏋滃鍑虹殑鍝嶅簲涓� json锛岃鏄庡彲鑳藉け璐ヤ簡锛屼笉鐩存帴杩斿洖杩涜涓嬭浇
+ if (response.data.type !== 'application/json') {
+ return response.data
+ }
+ data = await new Response(response.data).json()
+ }
+ const code = data.code || result_code
+ // 鑾峰彇閿欒淇℃伅
+ const msg = data.msg || errorCode[code] || errorCode['default']
+ if (ignoreMsgs.indexOf(msg) !== -1) {
+ // 濡傛灉鏄拷鐣ョ殑閿欒鐮侊紝鐩存帴杩斿洖 msg 寮傚父
+ return Promise.reject(msg)
+ } else if (code === 401) {
+ // 濡傛灉鏈璇侊紝骞朵笖鏈繘琛屽埛鏂颁护鐗岋紝璇存槑鍙兘鏄闂护鐗岃繃鏈熶簡
+ if (!isRefreshToken) {
+ isRefreshToken = true
+ // 1. 濡傛灉鑾峰彇涓嶅埌鍒锋柊浠ょ墝锛屽垯鍙兘鎵ц鐧诲嚭鎿嶄綔
+ if (!getRefreshToken()) {
+ return handleAuthorized()
+ }
+ // 2. 杩涜鍒锋柊璁块棶浠ょ墝
+ try {
+ const refreshTokenRes = await refreshToken()
+ // 2.1 鍒锋柊鎴愬姛锛屽垯鍥炴斁闃熷垪鐨勮姹� + 褰撳墠璇锋眰
+ setToken((await refreshTokenRes).data.data)
+ config.headers!.Authorization = 'Bearer ' + getAccessToken()
+ requestList.forEach((cb: any) => {
+ cb()
+ })
+ requestList = []
+ return service(config)
+ } catch (e) {
+ // 涓轰粈涔堥渶瑕� catch 寮傚父鍛紵鍒锋柊澶辫触鏃讹紝璇锋眰鍥犱负 Promise.reject 瑙﹀彂寮傚父銆�
+ // 2.2 鍒锋柊澶辫触锛屽彧鍥炴斁闃熷垪鐨勮姹�
+ requestList.forEach((cb: any) => {
+ cb()
+ })
+ // 鎻愮ず鏄惁瑕佺櫥鍑恒�傚嵆涓嶅洖鏀惧綋鍓嶈姹傦紒涓嶇劧浼氬舰鎴愰�掑綊
+ return handleAuthorized()
+ } finally {
+ requestList = []
+ isRefreshToken = false
+ }
+ } else {
+ // 娣诲姞鍒伴槦鍒楋紝绛夊緟鍒锋柊鑾峰彇鍒版柊鐨勪护鐗�
+ return new Promise((resolve) => {
+ requestList.push(() => {
+ config.headers!.Authorization = 'Bearer ' + getAccessToken() // 璁╂瘡涓姹傛惡甯﹁嚜瀹氫箟token 璇锋牴鎹疄闄呮儏鍐佃嚜琛屼慨鏀�
+ resolve(service(config))
+ })
+ })
+ }
+ } else if (code === 500) {
+ ElMessage.error(t('sys.api.errMsg500'))
+ return Promise.reject(new Error(msg))
+ } else if (code === 901) {
+ ElMessage.error({
+ offset: 300,
+ dangerouslyUseHTMLString: true,
+ message:
+ '<div>' +
+ t('sys.api.errMsg901') +
+ '</div>' +
+ '<div> </div>' +
+ '<div>鍙傝�� https://doc.iocoder.cn/ 鏁欑▼</div>' +
+ '<div> </div>' +
+ '<div>5 鍒嗛挓鎼缓鏈湴鐜</div>'
+ })
+ return Promise.reject(new Error(msg))
+ } else if (code !== 200) {
+ if (msg === '鏃犳晥鐨勫埛鏂颁护鐗�') {
+ // hard coding锛氬拷鐣ヨ繖涓彁绀猴紝鐩存帴鐧诲嚭
+ console.log(msg)
+ return handleAuthorized()
+ } else {
+ ElNotification.error({ title: msg })
+ }
+ return Promise.reject('error')
+ } else {
+ return data
+ }
+ },
+ (error: AxiosError) => {
+ console.log('err' + error) // for debug
+ let { message } = error
+ const { t } = useI18n()
+ if (message === 'Network Error') {
+ message = t('sys.api.errorMessage')
+ } else if (message.includes('timeout')) {
+ message = t('sys.api.apiTimeoutMessage')
+ } else if (message.includes('Request failed with status code')) {
+ message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3)
+ }
+ ElMessage.error(message)
+ return Promise.reject(error)
+ }
+)
+
+const refreshToken = async () => {
+ axios.defaults.headers.common['tenant-id'] = getTenantId()
+ return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
+}
+const handleAuthorized = () => {
+ const { t } = useI18n()
+ if (!isRelogin.show) {
+ // 濡傛灉宸茬粡鍒扮櫥褰曢〉闈㈠垯涓嶈繘琛屽脊绐楁彁绀�
+ if (window.location.href.includes('login')) {
+ return
+ }
+ isRelogin.show = true
+ ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
+ showCancelButton: false,
+ closeOnClickModal: false,
+ showClose: false,
+ closeOnPressEscape: false,
+ confirmButtonText: t('login.relogin'),
+ type: 'warning'
+ }).then(() => {
+ resetRouter() // 閲嶇疆闈欐�佽矾鐢辫〃
+ deleteUserCache() // 鍒犻櫎鐢ㄦ埛缂撳瓨
+ removeToken()
+ isRelogin.show = false
+ // 骞叉帀token鍚庡啀璧颁竴娆¤矾鐢辫瀹冭繃router.beforeEach鐨勬牎楠�
+ window.location.href = window.location.href
+ })
+ }
+ return Promise.reject(t('sys.api.timeoutMessage'))
+}
+export { service }
diff --git a/src/directives/index.ts b/src/directives/index.ts
new file mode 100644
index 0000000..1b99988
--- /dev/null
+++ b/src/directives/index.ts
@@ -0,0 +1,24 @@
+import type { App } from 'vue'
+import { hasRole } from './permission/hasRole'
+import { hasPermi } from './permission/hasPermi'
+
+/**
+ * 瀵煎嚭鎸囦护锛歷-xxx
+ * @methods hasRole 鐢ㄦ埛鏉冮檺锛岀敤娉�: v-hasRole
+ * @methods hasPermi 鎸夐挳鏉冮檺锛岀敤娉�: v-hasPermi
+ */
+export const setupAuth = (app: App<Element>) => {
+ hasRole(app)
+ hasPermi(app)
+}
+
+/**
+ * 瀵煎嚭鎸囦护锛歷-mountedFocus
+ */
+export const setupMountedFocus = (app: App<Element>) => {
+ app.directive('mountedFocus', {
+ mounted(el) {
+ el.focus()
+ }
+ })
+}
diff --git a/src/directives/permission/hasPermi.ts b/src/directives/permission/hasPermi.ts
new file mode 100644
index 0000000..90cd025
--- /dev/null
+++ b/src/directives/permission/hasPermi.ts
@@ -0,0 +1,31 @@
+import type { App } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+
+const { t } = useI18n() // 鍥介檯鍖�
+
+/** 鍒ゆ柇鏉冮檺鐨勬寚浠� directive */
+export function hasPermi(app: App<Element>) {
+ app.directive('hasPermi', (el, binding) => {
+ const { value } = binding
+
+ if (value && value instanceof Array && value.length > 0) {
+ const hasPermissions = hasPermission(value)
+
+ if (!hasPermissions) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(t('permission.hasPermission'))
+ }
+ })
+}
+
+/** 鍒ゆ柇鏉冮檺鐨勬柟娉� function */
+const userStore = useUserStore()
+const all_permission = '*:*:*'
+export const hasPermission = (permission: string[]) => {
+ return (
+ userStore.permissions.has(all_permission) ||
+ permission.some((permission) => userStore.permissions.has(permission))
+ )
+}
diff --git a/src/directives/permission/hasRole.ts b/src/directives/permission/hasRole.ts
new file mode 100644
index 0000000..a512811
--- /dev/null
+++ b/src/directives/permission/hasRole.ts
@@ -0,0 +1,28 @@
+import type { App } from 'vue'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+
+const { t } = useI18n() // 鍥介檯鍖�
+
+export function hasRole(app: App<Element>) {
+ app.directive('hasRole', (el, binding) => {
+ const { wsCache } = useCache()
+ const { value } = binding
+ const super_admin = 'super_admin'
+ const userInfo = wsCache.get(CACHE_KEY.USER)
+ const roles = userInfo?.roles || []
+
+ if (value && value instanceof Array && value.length > 0) {
+ const roleFlag = value
+
+ const hasRole = roles.some((role: string) => {
+ return super_admin === role || roleFlag.includes(role)
+ })
+
+ if (!hasRole) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(t('permission.hasRole'))
+ }
+ })
+}
diff --git a/src/hooks/event/useScrollTo.ts b/src/hooks/event/useScrollTo.ts
new file mode 100644
index 0000000..92aec87
--- /dev/null
+++ b/src/hooks/event/useScrollTo.ts
@@ -0,0 +1,60 @@
+export interface ScrollToParams {
+ el: HTMLElement
+ to: number
+ position: string
+ duration?: number
+ callback?: () => void
+}
+
+const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
+ t /= d / 2
+ if (t < 1) {
+ return (c / 2) * t * t + b
+ }
+ t--
+ return (-c / 2) * (t * (t - 2) - 1) + b
+}
+const move = (el: HTMLElement, position: string, amount: number) => {
+ el[position] = amount
+}
+
+export function useScrollTo({
+ el,
+ position = 'scrollLeft',
+ to,
+ duration = 500,
+ callback
+}: ScrollToParams) {
+ const isActiveRef = ref(false)
+ const start = el[position]
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+
+ function animateScroll() {
+ if (!unref(isActiveRef)) {
+ return
+ }
+ currentTime += increment
+ const val = easeInOutQuad(currentTime, start, change, duration)
+ move(el, position, val)
+ if (currentTime < duration && unref(isActiveRef)) {
+ requestAnimationFrame(animateScroll)
+ } else {
+ if (callback) {
+ callback()
+ }
+ }
+ }
+
+ function run() {
+ isActiveRef.value = true
+ animateScroll()
+ }
+
+ function stop() {
+ isActiveRef.value = false
+ }
+
+ return { start: run, stop }
+}
diff --git a/src/hooks/web/useCache.ts b/src/hooks/web/useCache.ts
new file mode 100644
index 0000000..1acb03b
--- /dev/null
+++ b/src/hooks/web/useCache.ts
@@ -0,0 +1,41 @@
+/**
+ * 閰嶇疆娴忚鍣ㄦ湰鍦板瓨鍌ㄧ殑鏂瑰紡锛屽彲鐩存帴瀛樺偍瀵硅薄鏁扮粍銆�
+ */
+
+import WebStorageCache from 'web-storage-cache'
+
+type CacheType = 'localStorage' | 'sessionStorage'
+
+export const CACHE_KEY = {
+ // 鐢ㄦ埛鐩稿叧
+ ROLE_ROUTERS: 'roleRouters',
+ USER: 'user',
+ VisitTenantId: 'visitTenantId',
+ // 绯荤粺璁剧疆
+ IS_DARK: 'isDark',
+ LANG: 'lang',
+ THEME: 'theme',
+ LAYOUT: 'layout',
+ DICT_CACHE: 'dictCache',
+ // 鐧诲綍琛ㄥ崟
+ LoginForm: 'loginForm',
+ TenantId: 'tenantId'
+}
+
+export const useCache = (type: CacheType = 'localStorage') => {
+ const wsCache: WebStorageCache = new WebStorageCache({
+ storage: type
+ })
+
+ return {
+ wsCache
+ }
+}
+
+export const deleteUserCache = () => {
+ const { wsCache } = useCache()
+ wsCache.delete(CACHE_KEY.USER)
+ wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+ wsCache.delete(CACHE_KEY.VisitTenantId)
+ // 娉ㄦ剰锛屼笉瑕佹竻鐞� LoginForm 鐧诲綍琛ㄥ崟
+}
diff --git a/src/hooks/web/useConfigGlobal.ts b/src/hooks/web/useConfigGlobal.ts
new file mode 100644
index 0000000..afb3db3
--- /dev/null
+++ b/src/hooks/web/useConfigGlobal.ts
@@ -0,0 +1,9 @@
+import { ConfigGlobalTypes } from '@/types/configGlobal'
+
+export const useConfigGlobal = () => {
+ const configGlobal = inject('configGlobal', {}) as ConfigGlobalTypes
+
+ return {
+ configGlobal
+ }
+}
diff --git a/src/hooks/web/useCrudSchemas.ts b/src/hooks/web/useCrudSchemas.ts
new file mode 100644
index 0000000..458b57e
--- /dev/null
+++ b/src/hooks/web/useCrudSchemas.ts
@@ -0,0 +1,326 @@
+import { reactive } from 'vue'
+import { AxiosPromise } from 'axios'
+import { findIndex } from '@/utils'
+import { eachTree, filter, treeMap } from '@/utils/tree'
+import { getBoolDictOptions, getDictOptions, getIntDictOptions } from '@/utils/dict'
+
+import { FormSchema } from '@/types/form'
+import { TableColumn } from '@/types/table'
+import { DescriptionsSchema } from '@/types/descriptions'
+import { ComponentOptions, ComponentProps } from '@/types/components'
+import { DictTag } from '@/components/DictTag'
+import { cloneDeep, merge } from 'lodash-es'
+
+export type CrudSchema = Omit<TableColumn, 'children'> & {
+ isSearch?: boolean // 鏄惁鍦ㄦ煡璇㈡樉绀�
+ search?: CrudSearchParams // 鏌ヨ鐨勮缁嗛厤缃�
+ isTable?: boolean // 鏄惁鍦ㄥ垪琛ㄦ樉绀�
+ table?: CrudTableParams // 鍒楄〃鐨勮缁嗛厤缃�
+ isForm?: boolean // 鏄惁鍦ㄨ〃鍗曟樉绀�
+ form?: CrudFormParams // 琛ㄥ崟鐨勮缁嗛厤缃�
+ isDetail?: boolean // 鏄惁鍦ㄨ鎯呮樉绀�
+ detail?: CrudDescriptionsParams // 璇︽儏鐨勮缁嗛厤缃�
+ children?: CrudSchema[]
+ dictType?: string // 瀛楀吀绫诲瀷
+ dictClass?: 'string' | 'number' | 'boolean' // 瀛楀吀鏁版嵁绫诲瀷 string | number | boolean
+}
+
+type CrudSearchParams = {
+ // 鏄惁鏄剧ず鍦ㄦ煡璇㈤」
+ show?: boolean
+ // 鎺ュ彛
+ api?: () => Promise<any>
+ // 鎼滅储瀛楁
+ field?: string
+} & Omit<FormSchema, 'field'>
+
+type CrudTableParams = {
+ // 鏄惁鏄剧ず琛ㄥご
+ show?: boolean
+ // 鍒楀閰嶇疆
+ width?: number | string
+ // 鍒楁槸鍚﹀浐瀹氬湪宸︿晶鎴栬�呭彸渚�
+ fixed?: 'left' | 'right'
+} & Omit<FormSchema, 'field'>
+type CrudFormParams = {
+ // 鏄惁鏄剧ず琛ㄥ崟椤�
+ show?: boolean
+ // 鎺ュ彛
+ api?: () => Promise<any>
+} & Omit<FormSchema, 'field'>
+
+type CrudDescriptionsParams = {
+ // 鏄惁鏄剧ず琛ㄥ崟椤�
+ show?: boolean
+} & Omit<DescriptionsSchema, 'field'>
+
+interface AllSchemas {
+ searchSchema: FormSchema[]
+ tableColumns: TableColumn[]
+ formSchema: FormSchema[]
+ detailSchema: DescriptionsSchema[]
+}
+
+const { t } = useI18n()
+
+// 杩囨护鎵�鏈夌粨鏋�
+export const useCrudSchemas = (
+ crudSchema: CrudSchema[]
+): {
+ allSchemas: AllSchemas
+} => {
+ // 鎵�鏈夌粨鏋勬暟鎹�
+ const allSchemas = reactive<AllSchemas>({
+ searchSchema: [],
+ tableColumns: [],
+ formSchema: [],
+ detailSchema: []
+ })
+
+ const searchSchema = filterSearchSchema(crudSchema, allSchemas)
+ allSchemas.searchSchema = searchSchema || []
+
+ const tableColumns = filterTableSchema(crudSchema)
+ allSchemas.tableColumns = tableColumns || []
+
+ const formSchema = filterFormSchema(crudSchema, allSchemas)
+ allSchemas.formSchema = formSchema
+
+ const detailSchema = filterDescriptionsSchema(crudSchema)
+ allSchemas.detailSchema = detailSchema
+
+ return {
+ allSchemas
+ }
+}
+
+// 杩囨护 Search 缁撴瀯
+const filterSearchSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
+ const searchSchema: FormSchema[] = []
+
+ // 鑾峰彇瀛楀吀鍒楄〃闃熷垪
+ const searchRequestTask: Array<() => Promise<void>> = []
+ eachTree(crudSchema, (schemaItem: CrudSchema) => {
+ // 鍒ゆ柇鏄惁鏄剧ず
+ if (schemaItem?.isSearch || schemaItem.search?.show) {
+ let component = schemaItem?.search?.component || 'Input'
+ const options: ComponentOptions[] = []
+ let comonentProps: ComponentProps = {}
+ if (schemaItem.dictType) {
+ const allOptions: ComponentOptions = { label: '鍏ㄩ儴', value: '' }
+ options.push(allOptions)
+ getDictOptions(schemaItem.dictType).forEach((dict) => {
+ options.push(dict)
+ })
+ comonentProps = {
+ options: options
+ }
+ if (!schemaItem.search?.component) component = 'Select'
+ }
+
+ // updated by AKing: 瑙e喅浜嗗綋浣跨敤榛樿鐨刣ict閫夐」鏃讹紝form涓簨浠朵笉鑳借Е鍙戠殑闂
+ const searchSchemaItem = merge(
+ {
+ // 榛樿涓� input
+ component,
+ ...schemaItem.search,
+ field: schemaItem.field,
+ label: schemaItem.search?.label || schemaItem.label
+ },
+ { componentProps: comonentProps }
+ )
+ if (searchSchemaItem.api) {
+ searchRequestTask.push(async () => {
+ const res = await (searchSchemaItem.api as () => AxiosPromise)()
+ if (res) {
+ const index = findIndex(allSchemas.searchSchema, (v: FormSchema) => {
+ return v.field === searchSchemaItem.field
+ })
+ if (index !== -1) {
+ allSchemas.searchSchema[index]!.componentProps!.options = filterOptions(
+ res,
+ searchSchemaItem.componentProps.optionsAlias?.labelField
+ )
+ }
+ }
+ })
+ }
+ // 鍒犻櫎涓嶅繀瑕佺殑瀛楁
+ delete searchSchemaItem.show
+
+ searchSchema.push(searchSchemaItem)
+ }
+ })
+ for (const task of searchRequestTask) {
+ task()
+ }
+ return searchSchema
+}
+
+// 杩囨护 table 缁撴瀯
+const filterTableSchema = (crudSchema: CrudSchema[]): TableColumn[] => {
+ const tableColumns = treeMap<CrudSchema>(crudSchema, {
+ conversion: (schema: CrudSchema) => {
+ if (schema?.isTable !== false && schema?.table?.show !== false) {
+ // add by 鑺嬭壙锛氬鍔犲 dict 瀛楀吀鏁版嵁鐨勬敮鎸�
+ if (!schema.formatter && schema.dictType) {
+ schema.formatter = (_: Recordable, __: TableColumn, cellValue: any) => {
+ return h(DictTag, {
+ type: schema.dictType!, // ! 琛ㄧず涓�瀹氫笉涓虹┖
+ value: cellValue
+ })
+ }
+ }
+ return {
+ ...schema.table,
+ ...schema
+ }
+ }
+ }
+ })
+
+ // 绗竴娆¤繃婊や細鏈� undefined 鎵�浠ラ渶瑕佷簩娆¤繃婊�
+ return filter<TableColumn>(tableColumns as TableColumn[], (data) => {
+ if (data.children === void 0) {
+ delete data.children
+ }
+ return !!data.field
+ })
+}
+
+// 杩囨护 form 缁撴瀯
+const filterFormSchema = (crudSchema: CrudSchema[], allSchemas: AllSchemas): FormSchema[] => {
+ const formSchema: FormSchema[] = []
+
+ // 鑾峰彇瀛楀吀鍒楄〃闃熷垪
+ const formRequestTask: Array<() => Promise<void>> = []
+
+ eachTree(crudSchema, (schemaItem: CrudSchema) => {
+ // 鍒ゆ柇鏄惁鏄剧ず
+ if (schemaItem?.isForm !== false && schemaItem?.form?.show !== false) {
+ let component = schemaItem?.form?.component || 'Input'
+ let defaultValue: any = ''
+ if (schemaItem.form?.value) {
+ defaultValue = schemaItem.form?.value
+ } else {
+ if (component === 'InputNumber') {
+ defaultValue = 0
+ }
+ }
+ let comonentProps: ComponentProps = {}
+ if (schemaItem.dictType) {
+ const options: ComponentOptions[] = []
+ if (schemaItem.dictClass && schemaItem.dictClass === 'number') {
+ getIntDictOptions(schemaItem.dictType).forEach((dict) => {
+ options.push(dict)
+ })
+ } else if (schemaItem.dictClass && schemaItem.dictClass === 'boolean') {
+ getBoolDictOptions(schemaItem.dictType).forEach((dict) => {
+ options.push(dict)
+ })
+ } else {
+ getDictOptions(schemaItem.dictType).forEach((dict) => {
+ options.push(dict)
+ })
+ }
+ comonentProps = {
+ options: options
+ }
+ if (!(schemaItem.form && schemaItem.form.component)) component = 'Select'
+ }
+
+ // updated by AKing: 瑙e喅浜嗗綋浣跨敤榛樿鐨刣ict閫夐」鏃讹紝form涓簨浠朵笉鑳借Е鍙戠殑闂
+ const formSchemaItem = merge(
+ {
+ // 榛樿涓� input
+ component,
+ value: defaultValue,
+ ...schemaItem.form,
+ field: schemaItem.field,
+ label: schemaItem.form?.label || schemaItem.label
+ },
+ { componentProps: comonentProps }
+ )
+
+ if (formSchemaItem.api) {
+ formRequestTask.push(async () => {
+ const res = await (formSchemaItem.api as () => AxiosPromise)()
+ if (res) {
+ const index = findIndex(allSchemas.formSchema, (v: FormSchema) => {
+ return v.field === formSchemaItem.field
+ })
+ if (index !== -1) {
+ allSchemas.formSchema[index]!.componentProps!.options = filterOptions(
+ res,
+ formSchemaItem.componentProps.optionsAlias?.labelField
+ )
+ }
+ }
+ })
+ }
+
+ // 鍒犻櫎涓嶅繀瑕佺殑瀛楁
+ delete formSchemaItem.show
+
+ formSchema.push(formSchemaItem)
+ }
+ })
+
+ for (const task of formRequestTask) {
+ task()
+ }
+ return formSchema
+}
+
+// 杩囨护 descriptions 缁撴瀯
+const filterDescriptionsSchema = (crudSchema: CrudSchema[]): DescriptionsSchema[] => {
+ const descriptionsSchema: FormSchema[] = []
+
+ eachTree(crudSchema, (schemaItem: CrudSchema) => {
+ // 鍒ゆ柇鏄惁鏄剧ず
+ if (schemaItem?.isDetail !== false && schemaItem.detail?.show !== false) {
+ const descriptionsSchemaItem = {
+ ...schemaItem.detail,
+ field: schemaItem.field,
+ label: schemaItem.detail?.label || schemaItem.label
+ }
+ if (schemaItem.dictType) {
+ descriptionsSchemaItem.dictType = schemaItem.dictType
+ }
+ if (schemaItem.detail?.dateFormat || schemaItem.formatter == 'formatDate') {
+ // 浼樺厛浣跨敤 detail 涓嬬殑閰嶇疆锛屽鏋滄病鏈夐粯璁や负 YYYY-MM-DD HH:mm:ss
+ descriptionsSchemaItem.dateFormat = schemaItem?.detail?.dateFormat
+ ? schemaItem?.detail?.dateFormat
+ : 'YYYY-MM-DD HH:mm:ss'
+ }
+
+ // 鍒犻櫎涓嶅繀瑕佺殑瀛楁
+ delete descriptionsSchemaItem.show
+
+ descriptionsSchema.push(descriptionsSchemaItem)
+ }
+ })
+
+ return descriptionsSchema
+}
+
+// 缁檕ptions娣诲姞鍥介檯鍖�
+const filterOptions = (options: Recordable, labelField?: string) => {
+ return options?.map((v: Recordable) => {
+ if (labelField) {
+ v['labelField'] = t(v.labelField)
+ } else {
+ v['label'] = t(v.label)
+ }
+ return v
+ })
+}
+
+// 灏� tableColumns 鎸囧畾 fields 鏀惧埌鏈�鍓嶉潰
+export const sortTableColumns = (tableColumns: TableColumn[], field: string) => {
+ const fieldIndex = tableColumns.findIndex((item) => item.field === field)
+ const fieldColumn = cloneDeep(tableColumns[fieldIndex])
+ tableColumns.splice(fieldIndex, 1)
+ // 娣诲姞鍒板紑澶�
+ tableColumns.unshift(fieldColumn)
+}
diff --git a/src/hooks/web/useDesign.ts b/src/hooks/web/useDesign.ts
new file mode 100644
index 0000000..8ee3b38
--- /dev/null
+++ b/src/hooks/web/useDesign.ts
@@ -0,0 +1,18 @@
+import variables from '@/styles/global.module.scss'
+
+export const useDesign = () => {
+ const scssVariables = variables
+
+ /**
+ * @param scope 绫诲悕
+ * @returns 杩斿洖绌洪棿鍚�-绫诲悕
+ */
+ const getPrefixCls = (scope: string) => {
+ return `${scssVariables.namespace}-${scope}`
+ }
+
+ return {
+ variables: scssVariables,
+ getPrefixCls
+ }
+}
diff --git a/src/hooks/web/useEmitt.ts b/src/hooks/web/useEmitt.ts
new file mode 100644
index 0000000..d4efea7
--- /dev/null
+++ b/src/hooks/web/useEmitt.ts
@@ -0,0 +1,22 @@
+import mitt from 'mitt'
+
+interface Option {
+ name: string // 浜嬩欢鍚嶇О
+ callback: Fn // 鍥炶皟
+}
+
+const emitter = mitt()
+
+export const useEmitt = (option?: Option) => {
+ if (option) {
+ emitter.on(option.name, option.callback)
+
+ onBeforeUnmount(() => {
+ emitter.off(option.name)
+ })
+ }
+
+ return {
+ emitter
+ }
+}
diff --git a/src/hooks/web/useForm.ts b/src/hooks/web/useForm.ts
new file mode 100644
index 0000000..53a8a94
--- /dev/null
+++ b/src/hooks/web/useForm.ts
@@ -0,0 +1,94 @@
+import type { Form, FormExpose } from '@/components/Form'
+import type { ElForm } from 'element-plus'
+import type { FormProps } from '@/components/Form/src/types'
+import { FormSchema, FormSetPropsType } from '@/types/form'
+
+export const useForm = (props?: FormProps) => {
+ // From瀹炰緥
+ const formRef = ref<typeof Form & FormExpose>()
+
+ // ElForm瀹炰緥
+ const elFormRef = ref<ComponentRef<typeof ElForm>>()
+
+ /**
+ * @param ref Form瀹炰緥
+ * @param elRef ElForm瀹炰緥
+ */
+ const register = (ref: typeof Form & FormExpose, elRef: ComponentRef<typeof ElForm>) => {
+ formRef.value = ref
+ elFormRef.value = elRef
+ }
+
+ const getForm = async () => {
+ await nextTick()
+ const form = unref(formRef)
+ if (!form) {
+ console.error('The form is not registered. Please use the register method to register')
+ }
+ return form
+ }
+
+ // 涓�浜涘唴缃殑鏂规硶
+ const methods: {
+ setProps: (props: Recordable) => void
+ setValues: (data: Recordable) => void
+ getFormData: <T = Recordable | undefined>() => Promise<T>
+ setSchema: (schemaProps: FormSetPropsType[]) => void
+ addSchema: (formSchema: FormSchema, index?: number) => void
+ delSchema: (field: string) => void
+ } = {
+ setProps: async (props: FormProps = {}) => {
+ const form = await getForm()
+ form?.setProps(props)
+ if (props.model) {
+ form?.setValues(props.model)
+ }
+ },
+
+ setValues: async (data: Recordable) => {
+ const form = await getForm()
+ form?.setValues(data)
+ },
+
+ /**
+ * @param schemaProps 闇�瑕佽缃殑schemaProps
+ */
+ setSchema: async (schemaProps: FormSetPropsType[]) => {
+ const form = await getForm()
+ form?.setSchema(schemaProps)
+ },
+
+ /**
+ * @param formSchema 闇�瑕佹柊澧炴暟鎹�
+ * @param index 鍦ㄥ摢閲屾柊澧�
+ */
+ addSchema: async (formSchema: FormSchema, index?: number) => {
+ const form = await getForm()
+ form?.addSchema(formSchema, index)
+ },
+
+ /**
+ * @param field 鍒犻櫎鍝釜鏁版嵁
+ */
+ delSchema: async (field: string) => {
+ const form = await getForm()
+ form?.delSchema(field)
+ },
+
+ /**
+ * @returns form data
+ */
+ getFormData: async <T = Recordable>(): Promise<T> => {
+ const form = await getForm()
+ return form?.formModel as T
+ }
+ }
+
+ props && methods.setProps(props)
+
+ return {
+ register,
+ elFormRef,
+ methods
+ }
+}
diff --git a/src/hooks/web/useGuide.ts b/src/hooks/web/useGuide.ts
new file mode 100644
index 0000000..7fd2fb0
--- /dev/null
+++ b/src/hooks/web/useGuide.ts
@@ -0,0 +1,49 @@
+import { Config, driver } from 'driver.js'
+import 'driver.js/dist/driver.css'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useI18n } from '@/hooks/web/useI18n'
+
+const { t } = useI18n()
+
+const { variables } = useDesign()
+
+export const useGuide = (options?: Config) => {
+ const driverObj = driver(
+ options || {
+ showProgress: true,
+ nextBtnText: t('common.nextLabel'),
+ prevBtnText: t('common.prevLabel'),
+ doneBtnText: t('common.doneLabel'),
+ steps: [
+ {
+ element: `#${variables.namespace}-menu`,
+ popover: {
+ title: t('common.menu'),
+ description: t('common.menuDes'),
+ side: 'right'
+ }
+ },
+ {
+ element: `#${variables.namespace}-tool-header`,
+ popover: {
+ title: t('common.tool'),
+ description: t('common.toolDes'),
+ side: 'left'
+ }
+ },
+ {
+ element: `#${variables.namespace}-tags-view`,
+ popover: {
+ title: t('common.tagsView'),
+ description: t('common.tagsViewDes'),
+ side: 'bottom'
+ }
+ }
+ ]
+ }
+ )
+
+ return {
+ ...driverObj
+ }
+}
diff --git a/src/hooks/web/useI18n.ts b/src/hooks/web/useI18n.ts
new file mode 100644
index 0000000..d1ab70f
--- /dev/null
+++ b/src/hooks/web/useI18n.ts
@@ -0,0 +1,53 @@
+import { i18n } from '@/plugins/vueI18n'
+
+type I18nGlobalTranslation = {
+ (key: string): string
+ (key: string, locale: string): string
+ (key: string, locale: string, list: unknown[]): string
+ (key: string, locale: string, named: Record<string, unknown>): string
+ (key: string, list: unknown[]): string
+ (key: string, named: Record<string, unknown>): string
+}
+
+type I18nTranslationRestParameters = [string, any]
+
+const getKey = (namespace: string | undefined, key: string) => {
+ if (!namespace) {
+ return key
+ }
+ if (key.startsWith(namespace)) {
+ return key
+ }
+ return `${namespace}.${key}`
+}
+
+export const useI18n = (
+ namespace?: string
+): {
+ t: I18nGlobalTranslation
+} => {
+ const normalFn = {
+ t: (key: string) => {
+ return getKey(namespace, key)
+ }
+ }
+
+ if (!i18n) {
+ return normalFn
+ }
+
+ const { t, ...methods } = i18n.global
+
+ const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
+ if (!key) return ''
+ if (!key.includes('.') && !namespace) return key
+ //@ts-ignore
+ return t(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
+ }
+ return {
+ ...methods,
+ t: tFn
+ }
+}
+
+export const t = (key: string) => key
diff --git a/src/hooks/web/useIcon.ts b/src/hooks/web/useIcon.ts
new file mode 100644
index 0000000..3500204
--- /dev/null
+++ b/src/hooks/web/useIcon.ts
@@ -0,0 +1,8 @@
+import { h } from 'vue'
+import type { VNode } from 'vue'
+import { Icon } from '@/components/Icon'
+import { IconTypes } from '@/types/icon'
+
+export const useIcon = (props: IconTypes): VNode => {
+ return h(Icon, props)
+}
diff --git a/src/hooks/web/useLocale.ts b/src/hooks/web/useLocale.ts
new file mode 100644
index 0000000..c65070e
--- /dev/null
+++ b/src/hooks/web/useLocale.ts
@@ -0,0 +1,35 @@
+import { i18n } from '@/plugins/vueI18n'
+import { useLocaleStoreWithOut } from '@/store/modules/locale'
+import { setHtmlPageLang } from '@/plugins/vueI18n/helper'
+
+const setI18nLanguage = (locale: LocaleType) => {
+ const localeStore = useLocaleStoreWithOut()
+
+ if (i18n.mode === 'legacy') {
+ i18n.global.locale = locale
+ } else {
+ ;(i18n.global.locale as any).value = locale
+ }
+ localeStore.setCurrentLocale({
+ lang: locale
+ })
+ setHtmlPageLang(locale)
+}
+
+export const useLocale = () => {
+ // Switching the language will change the locale of useI18n
+ // And submit to configuration modification
+ const changeLocale = async (locale: LocaleType) => {
+ const globalI18n = i18n.global
+
+ const langModule = await import(`../../locales/${locale}.ts`)
+
+ globalI18n.setLocaleMessage(locale, langModule.default)
+
+ setI18nLanguage(locale)
+ }
+
+ return {
+ changeLocale
+ }
+}
diff --git a/src/hooks/web/useMessage.ts b/src/hooks/web/useMessage.ts
new file mode 100644
index 0000000..ac2b552
--- /dev/null
+++ b/src/hooks/web/useMessage.ts
@@ -0,0 +1,95 @@
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+import { useI18n } from './useI18n'
+export const useMessage = () => {
+ const { t } = useI18n()
+ return {
+ // 娑堟伅鎻愮ず
+ info(content: string) {
+ ElMessage.info(content)
+ },
+ // 閿欒娑堟伅
+ error(content: string) {
+ ElMessage.error(content)
+ },
+ // 鎴愬姛娑堟伅
+ success(content: string) {
+ ElMessage.success(content)
+ },
+ // 璀﹀憡娑堟伅
+ warning(content: string) {
+ ElMessage.warning(content)
+ },
+ // 寮瑰嚭鎻愮ず
+ alert(content: string) {
+ ElMessageBox.alert(content, t('common.confirmTitle'))
+ },
+ // 閿欒鎻愮ず
+ alertError(content: string) {
+ ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'error' })
+ },
+ // 鎴愬姛鎻愮ず
+ alertSuccess(content: string) {
+ ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'success' })
+ },
+ // 璀﹀憡鎻愮ず
+ alertWarning(content: string) {
+ ElMessageBox.alert(content, t('common.confirmTitle'), { type: 'warning' })
+ },
+ // 閫氱煡鎻愮ず
+ notify(content: string) {
+ ElNotification.info(content)
+ },
+ // 閿欒閫氱煡
+ notifyError(content: string) {
+ ElNotification.error(content)
+ },
+ // 鎴愬姛閫氱煡
+ notifySuccess(content: string) {
+ ElNotification.success(content)
+ },
+ // 璀﹀憡閫氱煡
+ notifyWarning(content: string) {
+ ElNotification.warning(content)
+ },
+ // 纭绐椾綋
+ confirm(content: string, tip?: string) {
+ return ElMessageBox.confirm(content, tip ? tip : t('common.confirmTitle'), {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ })
+ },
+ // 鍒犻櫎绐椾綋
+ delConfirm(content?: string, tip?: string) {
+ return ElMessageBox.confirm(
+ content ? content : t('common.delMessage'),
+ tip ? tip : t('common.confirmTitle'),
+ {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ }
+ )
+ },
+ // 瀵煎嚭绐椾綋
+ exportConfirm(content?: string, tip?: string) {
+ return ElMessageBox.confirm(
+ content ? content : t('common.exportMessage'),
+ tip ? tip : t('common.confirmTitle'),
+ {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ }
+ )
+ },
+ // 鎻愪氦鍐呭
+ prompt(content: string, tip: string) {
+ return ElMessageBox.prompt(content, tip, {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ })
+ }
+ }
+}
diff --git a/src/hooks/web/useNProgress.ts b/src/hooks/web/useNProgress.ts
new file mode 100644
index 0000000..6d8c0b9
--- /dev/null
+++ b/src/hooks/web/useNProgress.ts
@@ -0,0 +1,33 @@
+import { useCssVar } from '@vueuse/core'
+import type { NProgressOptions } from 'nprogress'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+
+const primaryColor = useCssVar('--el-color-primary', document.documentElement)
+
+export const useNProgress = () => {
+ NProgress.configure({ showSpinner: false } as NProgressOptions)
+
+ const initColor = async () => {
+ await nextTick()
+ const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
+ if (bar) {
+ bar.style.background = unref(primaryColor.value)
+ }
+ }
+
+ initColor()
+
+ const start = () => {
+ NProgress.start()
+ }
+
+ const done = () => {
+ NProgress.done()
+ }
+
+ return {
+ start,
+ done
+ }
+}
diff --git a/src/hooks/web/useNetwork.ts b/src/hooks/web/useNetwork.ts
new file mode 100644
index 0000000..66fa446
--- /dev/null
+++ b/src/hooks/web/useNetwork.ts
@@ -0,0 +1,21 @@
+import { ref, onBeforeUnmount } from 'vue'
+
+const useNetwork = () => {
+ const online = ref(true)
+
+ const updateNetwork = () => {
+ online.value = navigator.onLine
+ }
+
+ window.addEventListener('online', updateNetwork)
+ window.addEventListener('offline', updateNetwork)
+
+ onBeforeUnmount(() => {
+ window.removeEventListener('online', updateNetwork)
+ window.removeEventListener('offline', updateNetwork)
+ })
+
+ return { online }
+}
+
+export { useNetwork }
diff --git a/src/hooks/web/useNow.ts b/src/hooks/web/useNow.ts
new file mode 100644
index 0000000..09d3176
--- /dev/null
+++ b/src/hooks/web/useNow.ts
@@ -0,0 +1,60 @@
+import { dateUtil } from '@/utils/dateUtil'
+import { reactive, toRefs } from 'vue'
+import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
+
+export const useNow = (immediate = true) => {
+ let timer: IntervalHandle
+
+ const state = reactive({
+ year: 0,
+ month: 0,
+ week: '',
+ day: 0,
+ hour: '',
+ minute: '',
+ second: 0,
+ meridiem: ''
+ })
+
+ const update = () => {
+ const now = dateUtil()
+
+ const h = now.format('HH')
+ const m = now.format('mm')
+ const s = now.get('s')
+
+ state.year = now.get('y')
+ state.month = now.get('M') + 1
+ state.week = '鏄熸湡' + ['鏃�', '涓�', '浜�', '涓�', '鍥�', '浜�', '鍏�'][now.day()]
+ state.day = now.get('date')
+ state.hour = h
+ state.minute = m
+ state.second = s
+
+ state.meridiem = now.format('A')
+ }
+
+ function start() {
+ update()
+ clearInterval(timer)
+ timer = setInterval(() => update(), 1000)
+ }
+
+ function stop() {
+ clearInterval(timer)
+ }
+
+ tryOnMounted(() => {
+ immediate && start()
+ })
+
+ tryOnUnmounted(() => {
+ stop()
+ })
+
+ return {
+ ...toRefs(state),
+ start,
+ stop
+ }
+}
diff --git a/src/hooks/web/usePageLoading.ts b/src/hooks/web/usePageLoading.ts
new file mode 100644
index 0000000..bb89457
--- /dev/null
+++ b/src/hooks/web/usePageLoading.ts
@@ -0,0 +1,18 @@
+import { useAppStoreWithOut } from '@/store/modules/app'
+
+const appStore = useAppStoreWithOut()
+
+export const usePageLoading = () => {
+ const loadStart = () => {
+ appStore.setPageLoading(true)
+ }
+
+ const loadDone = () => {
+ appStore.setPageLoading(false)
+ }
+
+ return {
+ loadStart,
+ loadDone
+ }
+}
diff --git a/src/hooks/web/useTable.ts b/src/hooks/web/useTable.ts
new file mode 100644
index 0000000..361dd67
--- /dev/null
+++ b/src/hooks/web/useTable.ts
@@ -0,0 +1,223 @@
+import download from '@/utils/download'
+import { Table, TableExpose } from '@/components/Table'
+import { ElMessage, ElMessageBox, ElTable } from 'element-plus'
+import { computed, nextTick, reactive, ref, unref, watch } from 'vue'
+import type { TableProps } from '@/components/Table/src/types'
+
+import { TableSetPropsType } from '@/types/table'
+
+const { t } = useI18n()
+interface ResponseType<T = any> {
+ list: T[]
+ total?: number
+}
+
+interface UseTableConfig<T = any> {
+ getListApi: (option: any) => Promise<T>
+ delListApi?: (option: any) => Promise<T>
+ exportListApi?: (option: any) => Promise<T>
+ // 杩斿洖鏁版嵁鏍煎紡閰嶇疆
+ response?: ResponseType
+ // 榛樿浼犻�掔殑鍙傛暟
+ defaultParams?: Recordable
+ props?: TableProps
+}
+
+interface TableObject<T = any> {
+ pageSize: number
+ currentPage: number
+ total: number
+ tableList: T[]
+ params: any
+ loading: boolean
+ exportLoading: boolean
+ currentRow: Nullable<T>
+}
+
+export const useTable = <T = any>(config?: UseTableConfig<T>) => {
+ const tableObject = reactive<TableObject<T>>({
+ // 椤垫暟
+ pageSize: 10,
+ // 褰撳墠椤�
+ currentPage: 1,
+ // 鎬绘潯鏁�
+ total: 10,
+ // 琛ㄦ牸鏁版嵁
+ tableList: [],
+ // AxiosConfig 閰嶇疆
+ params: {
+ ...(config?.defaultParams || {})
+ },
+ // 鍔犺浇涓�
+ loading: true,
+ // 瀵煎嚭鍔犺浇涓�
+ exportLoading: false,
+ // 褰撳墠琛岀殑鏁版嵁
+ currentRow: null
+ })
+
+ const paramsObj = computed(() => {
+ return {
+ ...tableObject.params,
+ pageSize: tableObject.pageSize,
+ pageNo: tableObject.currentPage
+ }
+ })
+
+ watch(
+ () => tableObject.currentPage,
+ () => {
+ methods.getList()
+ }
+ )
+
+ watch(
+ () => tableObject.pageSize,
+ () => {
+ // 褰撳墠椤典笉涓�1鏃讹紝淇敼椤垫暟鍚庝細瀵艰嚧澶氭璋冪敤getList鏂规硶
+ if (tableObject.currentPage === 1) {
+ methods.getList()
+ } else {
+ tableObject.currentPage = 1
+ methods.getList()
+ }
+ }
+ )
+
+ // Table瀹炰緥
+ const tableRef = ref<typeof Table & TableExpose>()
+
+ // ElTable瀹炰緥
+ const elTableRef = ref<ComponentRef<typeof ElTable>>()
+
+ const register = (ref: typeof Table & TableExpose, elRef: ComponentRef<typeof ElTable>) => {
+ tableRef.value = ref
+ elTableRef.value = elRef
+ }
+
+ const getTable = async () => {
+ await nextTick()
+ const table = unref(tableRef)
+ if (!table) {
+ console.error('The table is not registered. Please use the register method to register')
+ }
+ return table
+ }
+
+ const delData = async (ids: string | number | string[] | number[]) => {
+ let idsLength = 1
+ if (ids instanceof Array) {
+ idsLength = ids.length
+ await Promise.all(
+ ids.map(async (id: string | number) => {
+ await (config?.delListApi && config?.delListApi(id))
+ })
+ )
+ } else {
+ await (config?.delListApi && config?.delListApi(ids))
+ }
+ ElMessage.success(t('common.delSuccess'))
+
+ // 璁$畻鍑轰复鐣岀偣
+ tableObject.currentPage =
+ tableObject.total % tableObject.pageSize === idsLength || tableObject.pageSize === 1
+ ? tableObject.currentPage > 1
+ ? tableObject.currentPage - 1
+ : tableObject.currentPage
+ : tableObject.currentPage
+ await methods.getList()
+ }
+
+ const methods = {
+ getList: async () => {
+ tableObject.loading = true
+ const res = await config?.getListApi(unref(paramsObj)).finally(() => {
+ tableObject.loading = false
+ })
+ if (res) {
+ tableObject.tableList = (res as unknown as ResponseType).list
+ tableObject.total = (res as unknown as ResponseType).total ?? 0
+ }
+ },
+ setProps: async (props: TableProps = {}) => {
+ const table = await getTable()
+ table?.setProps(props)
+ },
+ setColumn: async (columnProps: TableSetPropsType[]) => {
+ const table = await getTable()
+ table?.setColumn(columnProps)
+ },
+ getSelections: async () => {
+ const table = await getTable()
+ return (table?.selections || []) as T[]
+ },
+ // 涓嶴earch缁勪欢缁撳悎
+ setSearchParams: (data: Recordable) => {
+ tableObject.params = Object.assign(tableObject.params, {
+ pageSize: tableObject.pageSize,
+ pageNo: 1,
+ ...data
+ })
+ // 椤电爜涓嶇瓑浜�1鏃舵洿鏂伴〉鐮侀噸鏂拌幏鍙栨暟鎹紝椤电爜绛変簬1鏃堕噸鏂拌幏鍙栨暟鎹�
+ if (tableObject.currentPage !== 1) {
+ tableObject.currentPage = 1
+ } else {
+ methods.getList()
+ }
+ },
+ // 鍒犻櫎鏁版嵁
+ delList: async (
+ ids: string | number | string[] | number[],
+ multiple: boolean,
+ message = true
+ ) => {
+ const tableRef = await getTable()
+ if (multiple) {
+ if (!tableRef?.selections.length) {
+ ElMessage.warning(t('common.delNoData'))
+ return
+ }
+ }
+ if (message) {
+ ElMessageBox.confirm(t('common.delMessage'), t('common.confirmTitle'), {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ }).then(async () => {
+ await delData(ids)
+ })
+ } else {
+ await delData(ids)
+ }
+ },
+ // 瀵煎嚭鍒楄〃
+ exportList: async (fileName: string) => {
+ tableObject.exportLoading = true
+ ElMessageBox.confirm(t('common.exportMessage'), t('common.confirmTitle'), {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ })
+ .then(async () => {
+ const res = await config?.exportListApi?.(unref(paramsObj) as unknown as T)
+ if (res) {
+ download.excel(res as unknown as Blob, fileName)
+ }
+ })
+ .finally(() => {
+ tableObject.exportLoading = false
+ })
+ }
+ }
+
+ config?.props && methods.setProps(config.props)
+
+ return {
+ register,
+ elTableRef,
+ tableObject,
+ methods,
+ // add by 鑺嬭壙锛氳繑鍥� tableMethods 灞炴�э紝鍜� tableObject 鏇寸粺涓�
+ tableMethods: methods
+ }
+}
diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts
new file mode 100644
index 0000000..31eadb0
--- /dev/null
+++ b/src/hooks/web/useTagsView.ts
@@ -0,0 +1,63 @@
+import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
+import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
+import { computed, nextTick, unref } from 'vue'
+
+export const useTagsView = () => {
+ const tagsViewStore = useTagsViewStoreWithOut()
+
+ const { replace, currentRoute } = useRouter()
+
+ const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+ const closeAll = (callback?: Fn) => {
+ tagsViewStore.delAllViews()
+ callback?.()
+ }
+
+ const closeLeft = (callback?: Fn) => {
+ tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+ callback?.()
+ }
+
+ const closeRight = (callback?: Fn) => {
+ tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+ callback?.()
+ }
+
+ const closeOther = (callback?: Fn) => {
+ tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
+ callback?.()
+ }
+
+ const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
+ if (view?.meta?.affix) return
+ tagsViewStore.delView(view || unref(currentRoute))
+
+ callback?.()
+ }
+
+ const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
+ tagsViewStore.delCachedView()
+ const { path, query } = view || unref(currentRoute)
+ await nextTick()
+ replace({
+ path: '/redirect' + path,
+ query: query
+ })
+ callback?.()
+ }
+
+ const setTitle = (title: string, path?: string) => {
+ tagsViewStore.setTitle(title, path)
+ }
+
+ return {
+ closeAll,
+ closeLeft,
+ closeRight,
+ closeOther,
+ closeCurrent,
+ refreshPage,
+ setTitle
+ }
+}
diff --git a/src/hooks/web/useTimeAgo.ts b/src/hooks/web/useTimeAgo.ts
new file mode 100644
index 0000000..a6da281
--- /dev/null
+++ b/src/hooks/web/useTimeAgo.ts
@@ -0,0 +1,49 @@
+import { useTimeAgo as useTimeAgoCore, UseTimeAgoMessages } from '@vueuse/core'
+import { useLocaleStoreWithOut } from '@/store/modules/locale'
+
+const TIME_AGO_MESSAGE_MAP: {
+ 'zh-CN': UseTimeAgoMessages
+ en: UseTimeAgoMessages
+} = {
+ // @ts-ignore
+ 'zh-CN': {
+ justNow: '鍒氬垰',
+ past: (n) => (n.match(/\d/) ? `${n}鍓峘 : n),
+ future: (n) => (n.match(/\d/) ? `${n}鍚巂 : n),
+ month: (n, past) => (n === 1 ? (past ? '涓婁釜鏈�' : '涓嬩釜鏈�') : `${n} 涓湀`),
+ year: (n, past) => (n === 1 ? (past ? '鍘诲勾' : '鏄庡勾') : `${n} 骞碻),
+ day: (n, past) => (n === 1 ? (past ? '鏄ㄥぉ' : '鏄庡ぉ') : `${n} 澶ー),
+ week: (n, past) => (n === 1 ? (past ? '涓婂懆' : '涓嬪懆') : `${n} 鍛╜),
+ hour: (n) => `${n} 灏忔椂`,
+ minute: (n) => `${n} 鍒嗛挓`,
+ second: (n) => `${n} 绉抈
+ },
+ // @ts-ignore
+ en: {
+ justNow: 'just now',
+ past: (n) => (n.match(/\d/) ? `${n} ago` : n),
+ future: (n) => (n.match(/\d/) ? `in ${n}` : n),
+ month: (n, past) =>
+ n === 1 ? (past ? 'last month' : 'next month') : `${n} month${n > 1 ? 's' : ''}`,
+ year: (n, past) =>
+ n === 1 ? (past ? 'last year' : 'next year') : `${n} year${n > 1 ? 's' : ''}`,
+ day: (n, past) => (n === 1 ? (past ? 'yesterday' : 'tomorrow') : `${n} day${n > 1 ? 's' : ''}`),
+ week: (n, past) =>
+ n === 1 ? (past ? 'last week' : 'next week') : `${n} week${n > 1 ? 's' : ''}`,
+ hour: (n) => `${n} hour${n > 1 ? 's' : ''}`,
+ minute: (n) => `${n} minute${n > 1 ? 's' : ''}`,
+ second: (n) => `${n} second${n > 1 ? 's' : ''}`
+ }
+}
+
+export const useTimeAgo = (time: Date | number | string) => {
+ const localeStore = useLocaleStoreWithOut()
+
+ const currentLocale = computed(() => localeStore.getCurrentLocale)
+
+ const timeAgo = useTimeAgoCore(time, {
+ messages: TIME_AGO_MESSAGE_MAP[unref(currentLocale).lang]
+ })
+
+ return timeAgo
+}
diff --git a/src/hooks/web/useTitle.ts b/src/hooks/web/useTitle.ts
new file mode 100644
index 0000000..020a9b7
--- /dev/null
+++ b/src/hooks/web/useTitle.ts
@@ -0,0 +1,24 @@
+import { watch, ref } from 'vue'
+import { isString } from '@/utils/is'
+import { useAppStoreWithOut } from '@/store/modules/app'
+
+const appStore = useAppStoreWithOut()
+
+export const useTitle = (newTitle?: string) => {
+ const { t } = useI18n()
+ const title = ref(
+ newTitle ? `${appStore.getTitle} - ${t(newTitle as string)}` : appStore.getTitle
+ )
+
+ watch(
+ title,
+ (n, o) => {
+ if (isString(n) && n !== o && document) {
+ document.title = n
+ }
+ },
+ { immediate: true }
+ )
+
+ return title
+}
diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts
new file mode 100644
index 0000000..151e35b
--- /dev/null
+++ b/src/hooks/web/useValidator.ts
@@ -0,0 +1,60 @@
+import { useI18n } from '@/hooks/web/useI18n'
+import { FormItemRule } from 'element-plus'
+
+const { t } = useI18n()
+
+interface LengthRange {
+ min: number
+ max: number
+ message?: string
+}
+
+export const useValidator = () => {
+ const required = (message?: string): FormItemRule => {
+ return {
+ required: true,
+ message: message || t('common.required')
+ }
+ }
+
+ const lengthRange = (options: LengthRange): FormItemRule => {
+ const { min, max, message } = options
+
+ return {
+ min,
+ max,
+ message: message || t('common.lengthRange', { min, max })
+ }
+ }
+
+ const notSpace = (message?: string): FormItemRule => {
+ return {
+ validator: (_, val, callback) => {
+ if (val?.indexOf(' ') !== -1) {
+ callback(new Error(message || t('common.notSpace')))
+ } else {
+ callback()
+ }
+ }
+ }
+ }
+
+ const notSpecialCharacters = (message?: string): FormItemRule => {
+ return {
+ validator: (_, val, callback) => {
+ if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
+ callback(new Error(message || t('common.notSpecialCharacters')))
+ } else {
+ callback()
+ }
+ }
+ }
+ }
+
+ return {
+ required,
+ lengthRange,
+ notSpace,
+ notSpecialCharacters
+ }
+}
diff --git a/src/hooks/web/useWatermark.ts b/src/hooks/web/useWatermark.ts
new file mode 100644
index 0000000..028926b
--- /dev/null
+++ b/src/hooks/web/useWatermark.ts
@@ -0,0 +1,72 @@
+import { useAppStore } from '@/store/modules/app'
+import { watch } from 'vue'
+
+const domSymbol = Symbol('watermark-dom')
+
+export function useWatermark(appendEl: HTMLElement | null = document.body) {
+ let func: Fn = () => {}
+ const id = domSymbol.toString()
+ const appStore = useAppStore()
+ let watermarkStr = ''
+
+ const clear = () => {
+ const domId = document.getElementById(id)
+ if (domId) {
+ const el = appendEl
+ el && el.removeChild(domId)
+ }
+ window.removeEventListener('resize', func)
+ }
+ const createWatermark = (str: string) => {
+ clear()
+
+ const can = document.createElement('canvas')
+ can.width = 300
+ can.height = 240
+
+ const cans = can.getContext('2d')
+ if (cans) {
+ cans.rotate((-20 * Math.PI) / 120)
+ cans.font = '15px Vedana'
+ cans.fillStyle = appStore.getIsDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'
+ cans.textAlign = 'left'
+ cans.textBaseline = 'middle'
+ cans.fillText(str, can.width / 20, can.height)
+ }
+
+ const div = document.createElement('div')
+ div.id = id
+ div.style.pointerEvents = 'none'
+ div.style.top = '0px'
+ div.style.left = '0px'
+ div.style.position = 'absolute'
+ div.style.zIndex = '100000000'
+ div.style.width = document.documentElement.clientWidth + 'px'
+ div.style.height = document.documentElement.clientHeight + 'px'
+ div.style.background = 'url(' + can.toDataURL('image/png') + ') left top repeat'
+ const el = appendEl
+ el && el.appendChild(div)
+ return id
+ }
+
+ function setWatermark(str: string) {
+ watermarkStr = str
+ createWatermark(str)
+ func = () => {
+ createWatermark(str)
+ }
+ window.addEventListener('resize', func)
+ }
+
+ // 鐩戝惉涓婚鍙樺寲
+ watch(
+ () => appStore.getIsDark,
+ () => {
+ if (watermarkStr) {
+ createWatermark(watermarkStr)
+ }
+ }
+ )
+
+ return { setWatermark, clear }
+}
diff --git a/src/layout/Layout.vue b/src/layout/Layout.vue
new file mode 100644
index 0000000..d15025c
--- /dev/null
+++ b/src/layout/Layout.vue
@@ -0,0 +1,75 @@
+<script lang="tsx">
+import { computed, defineComponent, unref } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { Backtop } from '@/components/Backtop'
+import { Setting } from '@/layout/components/Setting'
+import { useRenderLayout } from './components/useRenderLayout'
+import { useDesign } from '@/hooks/web/useDesign'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('layout')
+
+const appStore = useAppStore()
+
+// 鏄惁鏄Щ鍔ㄧ
+const mobile = computed(() => appStore.getMobile)
+
+// 鑿滃崟鎶樺彔
+const collapse = computed(() => appStore.getCollapse)
+
+const layout = computed(() => appStore.getLayout)
+
+const handleClickOutside = () => {
+ appStore.setCollapse(true)
+}
+
+const renderLayout = () => {
+ switch (unref(layout)) {
+ case 'classic':
+ const { renderClassic } = useRenderLayout()
+ return renderClassic()
+ case 'topLeft':
+ const { renderTopLeft } = useRenderLayout()
+ return renderTopLeft()
+ case 'top':
+ const { renderTop } = useRenderLayout()
+ return renderTop()
+ case 'cutMenu':
+ const { renderCutMenu } = useRenderLayout()
+ return renderCutMenu()
+ default:
+ break
+ }
+}
+
+export default defineComponent({
+ name: 'Layout',
+ setup() {
+ return () => (
+ <section class={[prefixCls, `${prefixCls}__${layout.value}`, 'w-[100%] h-[100%] relative']}>
+ {mobile.value && !collapse.value ? (
+ <div
+ class="absolute left-0 top-0 z-99 h-full w-full bg-[var(--el-color-black)] opacity-30"
+ onClick={handleClickOutside}
+ ></div>
+ ) : undefined}
+
+ {renderLayout()}
+
+ <Backtop></Backtop>
+
+ <Setting></Setting>
+ </section>
+ )
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-layout;
+
+.#{$prefix-cls} {
+ background-color: var(--app-content-bg-color);
+}
+</style>
diff --git a/src/layout/components/AppView.vue b/src/layout/components/AppView.vue
new file mode 100644
index 0000000..df720a1
--- /dev/null
+++ b/src/layout/components/AppView.vue
@@ -0,0 +1,55 @@
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useAppStore } from '@/store/modules/app'
+import { Footer } from '@/layout/components/Footer'
+
+defineOptions({ name: 'AppView' })
+
+const appStore = useAppStore()
+
+const layout = computed(() => appStore.getLayout)
+
+const fixedHeader = computed(() => appStore.getFixedHeader)
+
+const footer = computed(() => appStore.getFooter)
+
+const tagsViewStore = useTagsViewStore()
+
+const getCaches = computed((): string[] => {
+ return tagsViewStore.getCachedViews
+})
+
+const tagsView = computed(() => appStore.getTagsView)
+
+//region 鏃犳劅鍒锋柊
+const routerAlive = ref(true)
+// 鏃犳劅鍒锋柊锛岄槻姝㈠嚭鐜伴〉闈㈤棯鐑佺櫧灞�
+const reload = () => {
+ routerAlive.value = false
+ nextTick(() => (routerAlive.value = true))
+}
+// 涓虹粍浠跺悗浠f彁渚涘埛鏂版柟娉�
+provide('reload', reload)
+//endregion
+</script>
+
+<template>
+ <section
+ :class="[
+ 'p-[var(--app-content-padding)] w-full bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
+ {
+ '!min-h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))] pb-0':
+ footer
+ }
+ ]"
+ >
+ <router-view v-if="routerAlive">
+ <template #default="{ Component, route }">
+ <keep-alive :include="getCaches">
+ <component :is="Component" :key="route.fullPath" />
+ </keep-alive>
+ </template>
+ </router-view>
+ </section>
+ <Footer v-if="footer" />
+</template>
diff --git a/src/layout/components/Breadcrumb/index.ts b/src/layout/components/Breadcrumb/index.ts
new file mode 100644
index 0000000..93ffe70
--- /dev/null
+++ b/src/layout/components/Breadcrumb/index.ts
@@ -0,0 +1,3 @@
+import Breadcrumb from './src/Breadcrumb.vue'
+
+export { Breadcrumb }
diff --git a/src/layout/components/Breadcrumb/src/Breadcrumb.vue b/src/layout/components/Breadcrumb/src/Breadcrumb.vue
new file mode 100644
index 0000000..80770a8
--- /dev/null
+++ b/src/layout/components/Breadcrumb/src/Breadcrumb.vue
@@ -0,0 +1,130 @@
+<script lang="tsx">
+import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
+import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
+import { useRouter } from 'vue-router'
+import { usePermissionStore } from '@/store/modules/permission'
+import { filterBreadcrumb } from './helper'
+import { filter, treeToList } from '@/utils/tree'
+import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
+
+import { Icon } from '@/components/Icon'
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('breadcrumb')
+
+const appStore = useAppStore()
+
+// 闈㈠寘灞戝浘鏍�
+const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon)
+
+export default defineComponent({
+ name: 'Breadcrumb',
+ setup() {
+ const { currentRoute } = useRouter()
+
+ const { t } = useI18n()
+
+ const levelList = ref<AppRouteRecordRaw[]>([])
+
+ const permissionStore = usePermissionStore()
+
+ const menuRouters = computed(() => {
+ const routers = permissionStore.getRouters
+ return filterBreadcrumb(routers)
+ })
+
+ const getBreadcrumb = () => {
+ const currentPath = currentRoute.value.matched.slice(-1)[0].path
+
+ levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => {
+ return node.path === currentPath
+ })
+ }
+
+ const renderBreadcrumb = () => {
+ const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList))
+ return breadcrumbList.map((v) => {
+ const disabled = !v.redirect || v.redirect === 'noredirect'
+ const meta = v.meta as RouteMeta
+ return (
+ <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
+ {meta?.icon && breadcrumbIcon.value ? (
+ <div class="flex items-center">
+ <Icon icon={meta.icon} class="mr-[2px]" svgClass="inline-block"></Icon>
+ {t(v?.meta?.title)}
+ </div>
+ ) : (
+ t(v?.meta?.title)
+ )}
+ </ElBreadcrumbItem>
+ )
+ })
+ }
+
+ watch(
+ () => currentRoute.value,
+ (route: RouteLocationNormalizedLoaded) => {
+ if (route.path.startsWith('/redirect/')) {
+ return
+ }
+ getBreadcrumb()
+ },
+ {
+ immediate: true
+ }
+ )
+
+ return () => (
+ <ElBreadcrumb separator="/" class={`${prefixCls} flex items-center h-full ml-[10px]`}>
+ <TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight">
+ {renderBreadcrumb()}
+ </TransitionGroup>
+ </ElBreadcrumb>
+ )
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$elNamespace}-breadcrumb;
+
+.#{$prefix-cls} {
+ :deep(.#{$prefix-cls}__item) {
+ display: flex;
+ .#{$prefix-cls}__inner {
+ display: flex;
+ align-items: center;
+ color: var(--top-header-text-color);
+
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ }
+ }
+
+ :deep(.#{$prefix-cls}__item):not(:last-child) {
+ .#{$prefix-cls}__inner {
+ color: var(--top-header-text-color);
+
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ }
+ }
+
+ :deep(.#{$prefix-cls}__item):last-child {
+ .#{$prefix-cls}__inner {
+ display: flex;
+ align-items: center;
+ color: var(--el-text-color-placeholder);
+
+ &:hover {
+ color: var(--el-text-color-placeholder);
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Breadcrumb/src/helper.ts b/src/layout/components/Breadcrumb/src/helper.ts
new file mode 100644
index 0000000..fb3ec19
--- /dev/null
+++ b/src/layout/components/Breadcrumb/src/helper.ts
@@ -0,0 +1,31 @@
+import { pathResolve } from '@/utils/routerHelper'
+import type { RouteMeta } from 'vue-router'
+
+export const filterBreadcrumb = (
+ routes: AppRouteRecordRaw[],
+ parentPath = ''
+): AppRouteRecordRaw[] => {
+ const res: AppRouteRecordRaw[] = []
+
+ for (const route of routes) {
+ const meta = route?.meta as RouteMeta
+ if (meta.hidden && !meta.canTo) {
+ continue
+ }
+
+ const data: AppRouteRecordRaw =
+ !meta.alwaysShow && route.children?.length === 1
+ ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) }
+ : { ...route }
+
+ data.path = pathResolve(parentPath, data.path)
+
+ if (data.children) {
+ data.children = filterBreadcrumb(data.children, data.path)
+ }
+ if (data) {
+ res.push(data)
+ }
+ }
+ return res
+}
diff --git a/src/layout/components/Collapse/index.ts b/src/layout/components/Collapse/index.ts
new file mode 100644
index 0000000..73f65a3
--- /dev/null
+++ b/src/layout/components/Collapse/index.ts
@@ -0,0 +1,3 @@
+import Collapse from './src/Collapse.vue'
+
+export { Collapse }
diff --git a/src/layout/components/Collapse/src/Collapse.vue b/src/layout/components/Collapse/src/Collapse.vue
new file mode 100644
index 0000000..a8fc7ee
--- /dev/null
+++ b/src/layout/components/Collapse/src/Collapse.vue
@@ -0,0 +1,35 @@
+<script lang="ts" setup>
+import { useAppStore } from '@/store/modules/app'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'Collapse' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('collapse')
+
+defineProps({
+ color: propTypes.string.def('')
+})
+
+const appStore = useAppStore()
+
+const collapse = computed(() => appStore.getCollapse)
+
+const toggleCollapse = () => {
+ const collapsed = unref(collapse)
+ appStore.setCollapse(!collapsed)
+}
+</script>
+
+<template>
+ <div :class="prefixCls" @click="toggleCollapse">
+ <Icon
+ :color="color"
+ :icon="collapse ? 'ep:expand' : 'ep:fold'"
+ :size="18"
+ class="cursor-pointer"
+ />
+ </div>
+</template>
diff --git a/src/layout/components/ContextMenu/index.ts b/src/layout/components/ContextMenu/index.ts
new file mode 100644
index 0000000..2a7c1f0
--- /dev/null
+++ b/src/layout/components/ContextMenu/index.ts
@@ -0,0 +1,10 @@
+import ContextMenu from './src/ContextMenu.vue'
+import { ElDropdown } from 'element-plus'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+export interface ContextMenuExpose {
+ elDropdownMenuRef: ComponentRef<typeof ElDropdown>
+ tagItem: RouteLocationNormalizedLoaded
+}
+
+export { ContextMenu }
diff --git a/src/layout/components/ContextMenu/src/ContextMenu.vue b/src/layout/components/ContextMenu/src/ContextMenu.vue
new file mode 100644
index 0000000..90eea4c
--- /dev/null
+++ b/src/layout/components/ContextMenu/src/ContextMenu.vue
@@ -0,0 +1,76 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+import { contextMenuSchema } from '@/types/contextMenu'
+import type { ElDropdown } from 'element-plus'
+
+defineOptions({ name: 'ContextMenu' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('context-menu')
+
+const { t } = useI18n()
+
+const emit = defineEmits(['visibleChange'])
+
+const props = defineProps({
+ schema: {
+ type: Array as PropType<contextMenuSchema[]>,
+ default: () => []
+ },
+ trigger: {
+ type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>,
+ default: 'contextmenu'
+ },
+ tagItem: {
+ type: Object as PropType<RouteLocationNormalizedLoaded>,
+ default: () => ({})
+ }
+})
+
+const command = (item: contextMenuSchema) => {
+ item.command && item.command(item)
+}
+
+const visibleChange = (visible: boolean) => {
+ emit('visibleChange', visible, props.tagItem)
+}
+
+const elDropdownMenuRef = ref<ComponentRef<typeof ElDropdown>>()
+
+defineExpose({
+ elDropdownMenuRef,
+ tagItem: props.tagItem
+})
+</script>
+
+<template>
+ <ElDropdown
+ ref="elDropdownMenuRef"
+ :class="prefixCls"
+ :trigger="trigger"
+ placement="bottom-start"
+ popper-class="v-context-menu-popper"
+ @command="command"
+ @visible-change="visibleChange"
+ >
+ <slot></slot>
+ <template #dropdown>
+ <ElDropdownMenu>
+ <ElDropdownItem
+ v-for="(item, index) in schema"
+ :key="`dropdown${index}`"
+ :command="item"
+ :disabled="item.disabled"
+ :divided="item.divided"
+ >
+ <Icon :icon="item.icon" />
+ {{ t(item.label) }}
+ </ElDropdownItem>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+</template>
diff --git a/src/layout/components/Footer/index.ts b/src/layout/components/Footer/index.ts
new file mode 100644
index 0000000..bd052e0
--- /dev/null
+++ b/src/layout/components/Footer/index.ts
@@ -0,0 +1,3 @@
+import Footer from './src/Footer.vue'
+
+export { Footer }
diff --git a/src/layout/components/Footer/src/Footer.vue b/src/layout/components/Footer/src/Footer.vue
new file mode 100644
index 0000000..98ce7e5
--- /dev/null
+++ b/src/layout/components/Footer/src/Footer.vue
@@ -0,0 +1,27 @@
+<script lang="ts" setup>
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+
+// eslint-disable-next-line vue/no-reserved-component-names
+defineOptions({ name: 'Footer' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('footer')
+
+const appStore = useAppStore()
+
+const title = computed(() => appStore.getTitle)
+
+// 娣诲姞褰撳墠骞翠唤璁$畻灞炴��
+const currentYear = computed(() => new Date().getFullYear())
+</script>
+
+<template>
+ <div
+ :class="prefixCls"
+ class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
+ >
+ <span class="text-14px">Copyright 漏{{ currentYear }} {{ title }}</span>
+ </div>
+</template>
diff --git a/src/layout/components/LocaleDropdown/index.ts b/src/layout/components/LocaleDropdown/index.ts
new file mode 100644
index 0000000..d02e640
--- /dev/null
+++ b/src/layout/components/LocaleDropdown/index.ts
@@ -0,0 +1,3 @@
+import LocaleDropdown from './src/LocaleDropdown.vue'
+
+export { LocaleDropdown }
diff --git a/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue b/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue
new file mode 100644
index 0000000..95132db
--- /dev/null
+++ b/src/layout/components/LocaleDropdown/src/LocaleDropdown.vue
@@ -0,0 +1,52 @@
+<script lang="ts" setup>
+import { useLocaleStore } from '@/store/modules/locale'
+import { useLocale } from '@/hooks/web/useLocale'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'LocaleDropdown' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('locale-dropdown')
+
+defineProps({
+ color: propTypes.string.def('')
+})
+
+const localeStore = useLocaleStore()
+
+const langMap = computed(() => localeStore.getLocaleMap)
+
+const currentLang = computed(() => localeStore.getCurrentLocale)
+
+const setLang = (lang: LocaleType) => {
+ if (lang === unref(currentLang).lang) return
+ // 闇�瑕侀噸鏂板姞杞介〉闈㈣鏁翠釜璇█澶氬垵濮嬪寲
+ window.location.reload()
+ localeStore.setCurrentLocale({
+ lang
+ })
+ const { changeLocale } = useLocale()
+ changeLocale(lang)
+}
+</script>
+
+<template>
+ <ElDropdown :class="prefixCls" trigger="click" @command="setLang">
+ <Icon
+ :class="$attrs.class"
+ :color="color"
+ :size="18"
+ class="cursor-pointer !p-0"
+ icon="ion:language-sharp"
+ />
+ <template #dropdown>
+ <ElDropdownMenu>
+ <ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang">
+ {{ item.name }}
+ </ElDropdownItem>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+</template>
diff --git a/src/layout/components/Logo/index.ts b/src/layout/components/Logo/index.ts
new file mode 100644
index 0000000..1c4224c
--- /dev/null
+++ b/src/layout/components/Logo/index.ts
@@ -0,0 +1,3 @@
+import Logo from './src/Logo.vue'
+
+export { Logo }
diff --git a/src/layout/components/Logo/src/Logo.vue b/src/layout/components/Logo/src/Logo.vue
new file mode 100644
index 0000000..d241130
--- /dev/null
+++ b/src/layout/components/Logo/src/Logo.vue
@@ -0,0 +1,88 @@
+<script lang="ts" setup>
+import { computed, onMounted, ref, unref, watch } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'Logo' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('logo')
+
+const appStore = useAppStore()
+
+const show = ref(true)
+
+const title = computed(() => appStore.getTitle)
+
+const layout = computed(() => appStore.getLayout)
+
+const collapse = computed(() => appStore.getCollapse)
+
+onMounted(() => {
+ if (unref(collapse)) show.value = false
+})
+
+watch(
+ () => collapse.value,
+ (collapse: boolean) => {
+ if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') {
+ show.value = true
+ return
+ }
+ if (!collapse) {
+ setTimeout(() => {
+ show.value = !collapse
+ }, 400)
+ } else {
+ show.value = !collapse
+ }
+ }
+)
+
+watch(
+ () => layout.value,
+ (layout) => {
+ if (layout === 'top' || layout === 'cutMenu') {
+ show.value = true
+ } else {
+ if (unref(collapse)) {
+ show.value = false
+ } else {
+ show.value = true
+ }
+ }
+ }
+)
+</script>
+
+<template>
+ <div>
+ <router-link
+ :class="[
+ prefixCls,
+ layout !== 'classic' ? `${prefixCls}__Top` : '',
+ 'flex !h-[var(--logo-height)] items-center cursor-pointer pl-8px relative decoration-none overflow-hidden'
+ ]"
+ to="/"
+ >
+ <img
+ class="h-[calc(var(--logo-height)-10px)] w-[calc(var(--logo-height)-10px)]"
+ src="@/assets/imgs/logo.png"
+ />
+ <div
+ v-if="show"
+ :class="[
+ 'ml-10px text-16px font-700',
+ {
+ 'text-[var(--logo-title-text-color)]': layout === 'classic',
+ 'text-[var(--top-header-text-color)]':
+ layout === 'topLeft' || layout === 'top' || layout === 'cutMenu'
+ }
+ ]"
+ >
+ {{ title }}
+ </div>
+ </router-link>
+ </div>
+</template>
diff --git a/src/layout/components/Menu/index.ts b/src/layout/components/Menu/index.ts
new file mode 100644
index 0000000..a6ec696
--- /dev/null
+++ b/src/layout/components/Menu/index.ts
@@ -0,0 +1,3 @@
+import Menu from './src/Menu.vue'
+
+export { Menu }
diff --git a/src/layout/components/Menu/src/Menu.vue b/src/layout/components/Menu/src/Menu.vue
new file mode 100644
index 0000000..94a1da4
--- /dev/null
+++ b/src/layout/components/Menu/src/Menu.vue
@@ -0,0 +1,272 @@
+<script lang="tsx">
+import { PropType } from 'vue'
+import { ElMenu, ElScrollbar } from 'element-plus'
+import { useAppStore } from '@/store/modules/app'
+import { usePermissionStore } from '@/store/modules/permission'
+import { useRenderMenuItem } from './components/useRenderMenuItem'
+import { isUrl } from '@/utils/is'
+import { useDesign } from '@/hooks/web/useDesign'
+import { LayoutType } from '@/types/layout'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('menu')
+
+export default defineComponent({
+ // eslint-disable-next-line vue/no-reserved-component-names
+ name: 'Menu',
+ props: {
+ menuSelect: {
+ type: Function as PropType<(index: string) => void>,
+ default: undefined
+ }
+ },
+ setup(props) {
+ const appStore = useAppStore()
+
+ const layout = computed(() => appStore.getLayout)
+
+ const { push, currentRoute } = useRouter()
+
+ const permissionStore = usePermissionStore()
+
+ const menuMode = computed((): 'vertical' | 'horizontal' => {
+ // 绔�
+ const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
+
+ if (vertical.includes(unref(layout))) {
+ return 'vertical'
+ } else {
+ return 'horizontal'
+ }
+ })
+
+ const routers = computed(() =>
+ unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
+ )
+
+ const collapse = computed(() => appStore.getCollapse)
+
+ const uniqueOpened = computed(() => appStore.getUniqueOpened)
+
+ const activeMenu = computed(() => {
+ const { meta, path } = unref(currentRoute)
+ // if set path, the sidebar will highlight the path you set
+ if (meta.activeMenu) {
+ return meta.activeMenu as string
+ }
+ return path
+ })
+
+ const menuSelect = (index: string) => {
+ if (props.menuSelect) {
+ props.menuSelect(index)
+ }
+ // 鑷畾涔変簨浠�
+ if (isUrl(index)) {
+ window.open(index)
+ } else {
+ push(index)
+ }
+ }
+
+ const renderMenuWrap = () => {
+ if (unref(layout) === 'top') {
+ return renderMenu()
+ } else {
+ return <ElScrollbar>{renderMenu()}</ElScrollbar>
+ }
+ }
+
+ const renderMenu = () => {
+ return (
+ <ElMenu
+ defaultActive={unref(activeMenu)}
+ mode={unref(menuMode)}
+ collapse={
+ unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse)
+ }
+ uniqueOpened={unref(layout) === 'top' ? false : unref(uniqueOpened)}
+ backgroundColor="var(--left-menu-bg-color)"
+ textColor="var(--left-menu-text-color)"
+ activeTextColor="var(--left-menu-text-active-color)"
+ popperClass={
+ unref(menuMode) === 'vertical'
+ ? `${prefixCls}-popper--vertical`
+ : `${prefixCls}-popper--horizontal`
+ }
+ onSelect={menuSelect}
+ >
+ {{
+ default: () => {
+ const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
+ return renderMenuItem(unref(routers))
+ }
+ }}
+ </ElMenu>
+ )
+ }
+
+ return () => (
+ <div
+ id={prefixCls}
+ class={[
+ `${prefixCls} ${prefixCls}__${unref(menuMode)}`,
+ 'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]',
+ {
+ 'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
+ 'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
+ }
+ ]}
+ >
+ {renderMenuWrap()}
+ </div>
+ )
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-menu;
+
+.#{$prefix-cls} {
+ position: relative;
+ transition: width var(--transition-time-02);
+
+ :deep(.#{$elNamespace}-menu) {
+ width: 100% !important;
+ border-right: none;
+
+ // 璁剧疆閫変腑鏃跺瓙鏍囬鐨勯鑹�
+ .is-active {
+ & > .#{$elNamespace}-sub-menu__title {
+ color: var(--left-menu-text-active-color) !important;
+ }
+ }
+
+ // 璁剧疆瀛愯彍鍗曟偓鍋滅殑楂樹寒鍜岃儗鏅壊
+ .#{$elNamespace}-sub-menu__title,
+ .#{$elNamespace}-menu-item {
+ &:hover {
+ color: var(--left-menu-text-active-color) !important;
+ background-color: var(--left-menu-bg-color) !important;
+ }
+ }
+
+ // 璁剧疆閫変腑鏃剁殑楂樹寒鑳屾櫙鍜岄珮浜鑹�
+ .#{$elNamespace}-menu-item.is-active {
+ color: var(--left-menu-text-active-color) !important;
+ background-color: var(--left-menu-bg-active-color) !important;
+
+ &:hover {
+ background-color: var(--left-menu-bg-active-color) !important;
+ }
+ }
+
+ .#{$elNamespace}-menu-item.is-active {
+ position: relative;
+ }
+
+ // 璁剧疆瀛愯彍鍗曠殑鑳屾櫙棰滆壊
+ .#{$elNamespace}-menu {
+ .#{$elNamespace}-sub-menu__title,
+ .#{$elNamespace}-menu-item:not(.is-active) {
+ background-color: var(--left-menu-bg-light-color) !important;
+ }
+ }
+ }
+
+ // 鎶樺彔鏃剁殑鏈�灏忓搴�
+ :deep(.#{$elNamespace}-menu--collapse) {
+ width: var(--left-menu-min-width);
+
+ & > .is-active,
+ & > .is-active > .#{$elNamespace}-sub-menu__title {
+ position: relative;
+ background-color: var(--left-menu-collapse-bg-active-color) !important;
+ }
+ }
+
+ // 鎶樺彔鍔ㄧ敾鐨勬椂鍊欙紝灏遍渶瑕佹妸鏂囧瓧缁欓殣钘忔帀
+ :deep(.horizontal-collapse-transition) {
+ // transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important;
+ .#{$prefix-cls}__title {
+ display: none;
+ }
+ }
+
+ // 鍨傜洿鑿滃崟
+ &__vertical {
+ :deep(.#{$elNamespace}-menu--vertical) {
+ &:not(.#{$elNamespace}-menu--collapse) .#{$elNamespace}-sub-menu__title,
+ .#{$elNamespace}-menu-item {
+ padding-right: 0;
+ }
+ }
+ }
+
+ // 姘村钩鑿滃崟
+ &__horizontal {
+ height: calc(var(--top-tool-height)) !important;
+
+ :deep(.#{$elNamespace}-menu--horizontal) {
+ height: calc(var(--top-tool-height));
+ border-bottom: none;
+ // 閲嶆柊璁剧疆搴曢儴楂樹寒棰滆壊
+ & > .#{$elNamespace}-sub-menu.is-active {
+ .#{$elNamespace}-sub-menu__title {
+ border-bottom-color: var(--el-color-primary) !important;
+ }
+ }
+
+ .#{$elNamespace}-menu-item.is-active {
+ position: relative;
+
+ &::after {
+ display: none !important;
+ }
+ }
+
+ .#{$prefix-cls}__title {
+ /* stylelint-disable-next-line */
+ max-height: calc(var(--top-tool-height) - 2px) !important;
+ /* stylelint-disable-next-line */
+ line-height: calc(var(--top-tool-height) - 2px);
+ }
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+$prefix-cls: #{$namespace}-menu-popper;
+
+.#{$prefix-cls}--vertical,
+.#{$prefix-cls}--horizontal {
+ // 璁剧疆閫変腑鏃跺瓙鏍囬鐨勯鑹�
+ .is-active {
+ & > .el-sub-menu__title {
+ color: var(--left-menu-text-active-color) !important;
+ }
+ }
+
+ // 璁剧疆瀛愯彍鍗曟偓鍋滅殑楂樹寒鍜岃儗鏅壊
+ .el-sub-menu__title,
+ .el-menu-item {
+ &:hover {
+ color: var(--left-menu-text-active-color) !important;
+ background-color: var(--left-menu-bg-color) !important;
+ }
+ }
+
+ // 璁剧疆閫変腑鏃剁殑楂樹寒鑳屾櫙
+ .el-menu-item.is-active {
+ position: relative;
+ background-color: var(--left-menu-bg-active-color) !important;
+
+ &:hover {
+ background-color: var(--left-menu-bg-active-color) !important;
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Menu/src/components/useRenderMenuItem.tsx b/src/layout/components/Menu/src/components/useRenderMenuItem.tsx
new file mode 100644
index 0000000..301313f
--- /dev/null
+++ b/src/layout/components/Menu/src/components/useRenderMenuItem.tsx
@@ -0,0 +1,50 @@
+import { ElSubMenu, ElMenuItem } from 'element-plus'
+import { hasOneShowingChild } from '../helper'
+import { isUrl } from '@/utils/is'
+import { useRenderMenuTitle } from './useRenderMenuTitle'
+import { pathResolve } from '@/utils/routerHelper'
+
+const { renderMenuTitle } = useRenderMenuTitle()
+
+export const useRenderMenuItem = () =>
+ // allRouters: AppRouteRecordRaw[] = [],
+ {
+ const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
+ return routers
+ .filter((v) => !v.meta?.hidden)
+ .map((v) => {
+ const meta = v.meta ?? {}
+ const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v)
+ const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/')
+
+ if (
+ oneShowingChild &&
+ (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
+ !meta?.alwaysShow
+ ) {
+ return (
+ <ElMenuItem
+ index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
+ >
+ {{
+ default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
+ }}
+ </ElMenuItem>
+ )
+ } else {
+ return (
+ <ElSubMenu index={fullPath}>
+ {{
+ title: () => renderMenuTitle(meta),
+ default: () => renderMenuItem(v.children!, fullPath)
+ }}
+ </ElSubMenu>
+ )
+ }
+ })
+ }
+
+ return {
+ renderMenuItem
+ }
+ }
diff --git a/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx b/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx
new file mode 100644
index 0000000..8941d9d
--- /dev/null
+++ b/src/layout/components/Menu/src/components/useRenderMenuTitle.tsx
@@ -0,0 +1,27 @@
+import type { RouteMeta } from 'vue-router'
+import { Icon } from '@/components/Icon'
+import { useI18n } from '@/hooks/web/useI18n'
+
+export const useRenderMenuTitle = () => {
+ const renderMenuTitle = (meta: RouteMeta) => {
+ const { t } = useI18n()
+ const { title = 'Please set title', icon } = meta
+
+ return icon ? (
+ <>
+ <Icon icon={meta.icon}></Icon>
+ <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
+ {t(title as string)}
+ </span>
+ </>
+ ) : (
+ <span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
+ {t(title as string)}
+ </span>
+ )
+ }
+
+ return {
+ renderMenuTitle
+ }
+}
diff --git a/src/layout/components/Menu/src/helper.ts b/src/layout/components/Menu/src/helper.ts
new file mode 100644
index 0000000..c26f5f4
--- /dev/null
+++ b/src/layout/components/Menu/src/helper.ts
@@ -0,0 +1,54 @@
+import type { RouteMeta } from 'vue-router'
+import { findPath } from '@/utils/tree'
+
+type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean }
+
+interface HasOneShowingChild {
+ oneShowingChild?: boolean
+ onlyOneChild?: OnlyOneChildType
+}
+
+export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => {
+ const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[]
+ return (menuList || []).map((item) => item.path)
+}
+
+export const hasOneShowingChild = (
+ children: AppRouteRecordRaw[] = [],
+ parent: AppRouteRecordRaw
+): HasOneShowingChild => {
+ const onlyOneChild = ref<OnlyOneChildType>()
+
+ const showingChildren = children.filter((v) => {
+ const meta = (v.meta ?? {}) as RouteMeta
+ if (meta.hidden) {
+ return false
+ } else {
+ // Temp set(will be used if only has one showing child)
+ onlyOneChild.value = v
+ return true
+ }
+ })
+
+ // When there is only one child router, the child router is displayed by default
+ if (showingChildren.length === 1) {
+ return {
+ oneShowingChild: true,
+ onlyOneChild: unref(onlyOneChild)
+ }
+ }
+
+ // Show parent if there are no child router to display
+ if (!showingChildren.length) {
+ onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
+ return {
+ oneShowingChild: true,
+ onlyOneChild: unref(onlyOneChild)
+ }
+ }
+
+ return {
+ oneShowingChild: false,
+ onlyOneChild: unref(onlyOneChild)
+ }
+}
diff --git a/src/layout/components/Message/index.ts b/src/layout/components/Message/index.ts
new file mode 100644
index 0000000..dfe0207
--- /dev/null
+++ b/src/layout/components/Message/index.ts
@@ -0,0 +1,3 @@
+import Message from './src/Message.vue'
+
+export { Message }
diff --git a/src/layout/components/Message/src/Message.vue b/src/layout/components/Message/src/Message.vue
new file mode 100644
index 0000000..d33b3b6
--- /dev/null
+++ b/src/layout/components/Message/src/Message.vue
@@ -0,0 +1,137 @@
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import * as NotifyMessageApi from '@/api/system/notify/message'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'Message' })
+
+defineProps({
+ color: propTypes.string.def('')
+})
+
+const { push } = useRouter()
+const userStore = useUserStoreWithOut()
+const activeName = ref('notice')
+const unreadCount = ref(0) // 鏈娑堟伅鏁伴噺
+const list = ref<any[]>([]) // 娑堟伅鍒楄〃
+
+// 鑾峰緱娑堟伅鍒楄〃
+const getList = async () => {
+ list.value = await NotifyMessageApi.getUnreadNotifyMessageList()
+ // 寮哄埗璁剧疆 unreadCount 涓� 0锛岄伩鍏嶅皬绾㈢偣鍥犱负杞澶參锛屼笉娑堥櫎
+ unreadCount.value = 0
+}
+
+// 鑾峰緱鏈娑堟伅鏁�
+const getUnreadCount = async () => {
+ NotifyMessageApi.getUnreadNotifyMessageCount().then((data) => {
+ unreadCount.value = data
+ })
+}
+
+// 璺宠浆鎴戠殑绔欏唴淇�
+const goMyList = () => {
+ push({
+ name: 'MyNotifyMessage'
+ })
+}
+
+// ========== 鍒濆鍖� =========
+onMounted(() => {
+ // 棣栨鍔犺浇灏忕孩鐐�
+ getUnreadCount()
+ // 杞鍒锋柊灏忕孩鐐�
+ setInterval(
+ () => {
+ if (userStore.getIsSetUser) {
+ getUnreadCount()
+ } else {
+ unreadCount.value = 0
+ }
+ },
+ 1000 * 60 * 2
+ )
+})
+</script>
+<template>
+ <div class="message">
+ <ElPopover :width="400" placement="bottom" trigger="click">
+ <template #reference>
+ <ElBadge :is-dot="unreadCount > 0" class="item">
+ <Icon :size="18" class="cursor-pointer" icon="ep:bell" :color="color" @click="getList" />
+ </ElBadge>
+ </template>
+ <ElTabs v-model="activeName">
+ <ElTabPane label="鎴戠殑绔欏唴淇�" name="notice">
+ <el-scrollbar class="message-list">
+ <template v-for="item in list" :key="item.id">
+ <div class="message-item">
+ <img alt="" class="message-icon" src="@/assets/imgs/avatar.gif" />
+ <div class="message-content">
+ <span class="message-title">
+ {{ item.templateNickname }}锛歿{ item.templateContent }}
+ </span>
+ <span class="message-date">
+ {{ formatDate(item.createTime) }}
+ </span>
+ </div>
+ </div>
+ </template>
+ </el-scrollbar>
+ </ElTabPane>
+ </ElTabs>
+ <!-- 鏇村 -->
+ <div style="margin-top: 10px; text-align: right">
+ <XButton preIcon="ep:view" title="鏌ョ湅鍏ㄩ儴" type="primary" @click="goMyList" />
+ </div>
+ </ElPopover>
+ </div>
+</template>
+<style lang="scss" scoped>
+.message-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 260px;
+ line-height: 45px;
+}
+
+.message-list {
+ display: flex;
+ height: 400px;
+ flex-direction: column;
+
+ .message-item {
+ display: flex;
+ align-items: center;
+ padding: 20px 0;
+ border-bottom: 1px solid var(--el-border-color-light);
+
+ &:last-child {
+ border: none;
+ }
+
+ .message-icon {
+ width: 40px;
+ height: 40px;
+ margin: 0 20px 0 5px;
+ }
+
+ .message-content {
+ display: flex;
+ flex-direction: column;
+
+ .message-title {
+ margin-bottom: 5px;
+ }
+
+ .message-date {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Screenfull/index.ts b/src/layout/components/Screenfull/index.ts
new file mode 100644
index 0000000..faec2d8
--- /dev/null
+++ b/src/layout/components/Screenfull/index.ts
@@ -0,0 +1,3 @@
+import Screenfull from './src/Screenfull.vue'
+
+export { Screenfull }
diff --git a/src/layout/components/Screenfull/src/Screenfull.vue b/src/layout/components/Screenfull/src/Screenfull.vue
new file mode 100644
index 0000000..4c045f2
--- /dev/null
+++ b/src/layout/components/Screenfull/src/Screenfull.vue
@@ -0,0 +1,32 @@
+<script lang="ts" setup>
+import { Icon } from '@/components/Icon'
+import { useFullscreen } from '@vueuse/core'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'ScreenFull' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('screenfull')
+
+defineProps({
+ color: propTypes.string.def('')
+})
+
+const { toggle, isFullscreen } = useFullscreen()
+
+const toggleFullscreen = () => {
+ toggle()
+}
+</script>
+
+<template>
+ <div :class="prefixCls" @click="toggleFullscreen">
+ <Icon
+ :color="color"
+ :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
+ :size="18"
+ />
+ </div>
+</template>
diff --git a/src/layout/components/Setting/index.ts b/src/layout/components/Setting/index.ts
new file mode 100644
index 0000000..b64c9ad
--- /dev/null
+++ b/src/layout/components/Setting/index.ts
@@ -0,0 +1,3 @@
+import Setting from './src/Setting.vue'
+
+export { Setting }
diff --git a/src/layout/components/Setting/src/Setting.vue b/src/layout/components/Setting/src/Setting.vue
new file mode 100644
index 0000000..92ecf41
--- /dev/null
+++ b/src/layout/components/Setting/src/Setting.vue
@@ -0,0 +1,303 @@
+<script lang="ts" setup>
+import { ElMessage } from 'element-plus'
+import { useClipboard, useCssVar } from '@vueuse/core'
+
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { useDesign } from '@/hooks/web/useDesign'
+
+import { setCssVar, trim } from '@/utils'
+import { colorIsDark, hexToRGB, lighten } from '@/utils/color'
+import { useAppStore } from '@/store/modules/app'
+import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
+import ColorRadioPicker from './components/ColorRadioPicker.vue'
+import InterfaceDisplay from './components/InterfaceDisplay.vue'
+import LayoutRadioPicker from './components/LayoutRadioPicker.vue'
+
+defineOptions({ name: 'Setting' })
+
+const { t } = useI18n()
+const appStore = useAppStore()
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('setting')
+const layout = computed(() => appStore.getLayout)
+const drawer = ref(false)
+
+// 涓婚鑹茬浉鍏�
+const systemTheme = ref(appStore.getTheme.elColorPrimary)
+
+const setSystemTheme = (color: string) => {
+ setCssVar('--el-color-primary', color)
+ appStore.setTheme({ elColorPrimary: color })
+ const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement)
+ setMenuTheme(trim(unref(leftMenuBgColor)))
+}
+
+// 澶撮儴涓婚鐩稿叧
+const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '')
+
+const setHeaderTheme = (color: string) => {
+ const isDarkColor = colorIsDark(color)
+ const textColor = isDarkColor ? '#fff' : 'inherit'
+ const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
+ const topToolBorderColor = isDarkColor ? color : '#eee'
+ setCssVar('--top-header-bg-color', color)
+ setCssVar('--top-header-text-color', textColor)
+ setCssVar('--top-header-hover-color', textHoverColor)
+ appStore.setTheme({
+ topHeaderBgColor: color,
+ topHeaderTextColor: textColor,
+ topHeaderHoverColor: textHoverColor,
+ topToolBorderColor
+ })
+ if (unref(layout) === 'top') {
+ setMenuTheme(color)
+ }
+}
+
+// 鑿滃崟涓婚鐩稿叧
+const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '')
+
+const setMenuTheme = (color: string) => {
+ const primaryColor = useCssVar('--el-color-primary', document.documentElement)
+ const isDarkColor = colorIsDark(color)
+ const theme: Recordable = {
+ // 宸︿晶鑿滃崟杈规棰滆壊
+ leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee',
+ // 宸︿晶鑿滃崟鑳屾櫙棰滆壊
+ leftMenuBgColor: color,
+ // 宸︿晶鑿滃崟娴呰壊鑳屾櫙棰滆壊
+ leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color,
+ // 宸︿晶鑿滃崟閫変腑鑳屾櫙棰滆壊
+ leftMenuBgActiveColor: isDarkColor
+ ? 'var(--el-color-primary)'
+ : hexToRGB(unref(primaryColor), 0.1),
+ // 宸︿晶鑿滃崟鏀惰捣閫変腑鑳屾櫙棰滆壊
+ leftMenuCollapseBgActiveColor: isDarkColor
+ ? 'var(--el-color-primary)'
+ : hexToRGB(unref(primaryColor), 0.1),
+ // 宸︿晶鑿滃崟瀛椾綋棰滆壊
+ leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333',
+ // 宸︿晶鑿滃崟閫変腑瀛椾綋棰滆壊
+ leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)',
+ // logo瀛椾綋棰滆壊
+ logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
+ // logo杈规棰滆壊
+ logoBorderColor: isDarkColor ? color : '#eee'
+ }
+ appStore.setTheme(theme)
+ appStore.setCssVarTheme()
+}
+if (layout.value === 'top' && !appStore.getIsDark) {
+ headerTheme.value = '#fff'
+ setHeaderTheme('#fff')
+}
+
+// 鐩戝惉layout鍙樺寲锛岄噸缃竴浜涗富棰樿壊
+watch(
+ () => layout.value,
+ (n) => {
+ if (n === 'top' && !appStore.getIsDark) {
+ headerTheme.value = '#fff'
+ setHeaderTheme('#fff')
+ } else {
+ setMenuTheme(unref(menuTheme))
+ }
+ }
+)
+
+// 鎷疯礉
+const copyConfig = async () => {
+ const { copy, copied, isSupported } = useClipboard({
+ legacy: true,
+ source: `
+ // 闈㈠寘灞�
+ breadcrumb: ${appStore.getBreadcrumb},
+ // 闈㈠寘灞戝浘鏍�
+ breadcrumbIcon: ${appStore.getBreadcrumbIcon},
+ // 鎶樺彔鍥炬爣
+ hamburger: ${appStore.getHamburger},
+ // 鍏ㄥ睆鍥炬爣
+ screenfull: ${appStore.getScreenfull},
+ // 灏哄鍥炬爣
+ size: ${appStore.getSize},
+ // 澶氳瑷�鍥炬爣
+ locale: ${appStore.getLocale},
+ // 娑堟伅鍥炬爣
+ message: ${appStore.getMessage},
+ // 鏍囩椤�
+ tagsView: ${appStore.getTagsView},
+ // 鏍囩椤�
+ tagsViewImmerse: ${appStore.getTagsViewImmerse},
+ // 鏍囩椤靛浘鏍�
+ tagsViewIcon: ${appStore.getTagsViewIcon},
+ // logo
+ logo: ${appStore.getLogo},
+ // 鑿滃崟鎵嬮鐞�
+ uniqueOpened: ${appStore.getUniqueOpened},
+ // 鍥哄畾header
+ fixedHeader: ${appStore.getFixedHeader},
+ // 椤佃剼
+ footer: ${appStore.getFooter},
+ // 鐏拌壊妯″紡
+ greyMode: ${appStore.getGreyMode},
+ // layout甯冨眬
+ layout: '${appStore.getLayout}',
+ // 鏆楅粦妯″紡
+ isDark: ${appStore.getIsDark},
+ // 缁勪欢灏哄
+ currentSize: '${appStore.getCurrentSize}',
+ // 涓婚鐩稿叧
+ theme: {
+ // 涓婚鑹�
+ elColorPrimary: '${appStore.getTheme.elColorPrimary}',
+ // 宸︿晶鑿滃崟杈规棰滆壊
+ leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}',
+ // 宸︿晶鑿滃崟鑳屾櫙棰滆壊
+ leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}',
+ // 宸︿晶鑿滃崟娴呰壊鑳屾櫙棰滆壊
+ leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}',
+ // 宸︿晶鑿滃崟閫変腑鑳屾櫙棰滆壊
+ leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}',
+ // 宸︿晶鑿滃崟鏀惰捣閫変腑鑳屾櫙棰滆壊
+ leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}',
+ // 宸︿晶鑿滃崟瀛椾綋棰滆壊
+ leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}',
+ // 宸︿晶鑿滃崟閫変腑瀛椾綋棰滆壊
+ leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}',
+ // logo瀛椾綋棰滆壊
+ logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}',
+ // logo杈规棰滆壊
+ logoBorderColor: '${appStore.getTheme.logoBorderColor}',
+ // 澶撮儴鑳屾櫙棰滆壊
+ topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}',
+ // 澶撮儴瀛椾綋棰滆壊
+ topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}',
+ // 澶撮儴鎮仠棰滆壊
+ topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}',
+ // 澶撮儴杈规棰滆壊
+ topToolBorderColor: '${appStore.getTheme.topToolBorderColor}'
+ }
+ `
+ })
+ if (!isSupported) {
+ ElMessage.error(t('setting.copyFailed'))
+ } else {
+ await copy()
+ if (unref(copied)) {
+ ElMessage.success(t('setting.copySuccess'))
+ }
+ }
+}
+
+// 娓呯┖缂撳瓨
+const clear = () => {
+ const { wsCache } = useCache()
+ wsCache.delete(CACHE_KEY.LAYOUT)
+ wsCache.delete(CACHE_KEY.THEME)
+ wsCache.delete(CACHE_KEY.IS_DARK)
+ window.location.reload()
+}
+</script>
+
+<template>
+ <div
+ :class="prefixCls"
+ class="fixed right-0 top-[45%] h-40px w-40px cursor-pointer bg-[var(--el-color-primary)] text-center leading-40px"
+ @click="drawer = true"
+ >
+ <Icon color="#fff" icon="ep:setting" />
+ </div>
+
+ <ElDrawer v-model="drawer" :z-index="4000" direction="rtl" size="350px">
+ <template #header>
+ <span class="text-16px font-700">{{ t('setting.projectSetting') }}</span>
+ </template>
+
+ <div class="text-center">
+ <!-- 涓婚 -->
+ <ElDivider>{{ t('setting.theme') }}</ElDivider>
+ <ThemeSwitch />
+
+ <!-- 甯冨眬 -->
+ <ElDivider>{{ t('setting.layout') }}</ElDivider>
+ <LayoutRadioPicker />
+
+ <!-- 绯荤粺涓婚 -->
+ <ElDivider>{{ t('setting.systemTheme') }}</ElDivider>
+ <ColorRadioPicker
+ v-model="systemTheme"
+ :schema="[
+ '#409eff',
+ '#009688',
+ '#536dfe',
+ '#ff5c93',
+ '#ee4f12',
+ '#0096c7',
+ '#9c27b0',
+ '#ff9800'
+ ]"
+ @change="setSystemTheme"
+ />
+
+ <!-- 澶撮儴涓婚 -->
+ <ElDivider>{{ t('setting.headerTheme') }}</ElDivider>
+ <ColorRadioPicker
+ v-model="headerTheme"
+ :schema="[
+ '#fff',
+ '#151515',
+ '#5172dc',
+ '#e74c3c',
+ '#24292e',
+ '#394664',
+ '#009688',
+ '#383f45'
+ ]"
+ @change="setHeaderTheme"
+ />
+
+ <!-- 鑿滃崟涓婚 -->
+ <template v-if="layout !== 'top'">
+ <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
+ <ColorRadioPicker
+ v-model="menuTheme"
+ :schema="[
+ '#fff',
+ '#001529',
+ '#212121',
+ '#273352',
+ '#191b24',
+ '#383f45',
+ '#001628',
+ '#344058'
+ ]"
+ @change="setMenuTheme"
+ />
+ </template>
+ </div>
+
+ <!-- 鐣岄潰鏄剧ず -->
+ <ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider>
+ <InterfaceDisplay />
+
+ <ElDivider />
+ <div>
+ <ElButton class="w-full" type="primary" @click="copyConfig">{{ t('setting.copy') }}</ElButton>
+ </div>
+ <div class="mt-5px">
+ <ElButton class="w-full" type="danger" @click="clear">
+ {{ t('setting.clearAndReset') }}
+ </ElButton>
+ </div>
+ </ElDrawer>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-setting;
+
+.#{$prefix-cls} {
+ z-index: 1200; /* 淇娌℃湁z-index浼氳琛ㄦ牸灞傝鐩�,鍊间笉瑕佽秴杩�4000 */
+ border-radius: 6px 0 0 6px;
+}
+</style>
diff --git a/src/layout/components/Setting/src/components/ColorRadioPicker.vue b/src/layout/components/Setting/src/components/ColorRadioPicker.vue
new file mode 100644
index 0000000..fcc5e75
--- /dev/null
+++ b/src/layout/components/Setting/src/components/ColorRadioPicker.vue
@@ -0,0 +1,67 @@
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'ColorRadioPicker' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('color-radio-picker')
+
+const props = defineProps({
+ schema: {
+ type: Array as PropType<string[]>,
+ default: () => []
+ },
+ modelValue: propTypes.string.def('')
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const colorVal = ref(props.modelValue)
+
+watch(
+ () => props.modelValue,
+ (val: string) => {
+ if (val === unref(colorVal)) return
+ colorVal.value = val
+ }
+)
+
+// 鐩戝惉
+watch(
+ () => colorVal.value,
+ (val: string) => {
+ emit('update:modelValue', val)
+ emit('change', val)
+ }
+)
+</script>
+
+<template>
+ <div :class="prefixCls" class="flex flex-wrap space-x-14px">
+ <span
+ v-for="(item, i) in schema"
+ :key="`radio-${i}`"
+ :class="{ 'is-active': colorVal === item }"
+ :style="{
+ background: item
+ }"
+ class="mb-5px h-20px w-20px cursor-pointer border-2px border-gray-300 rounded-2px border-solid text-center leading-20px"
+ @click="colorVal = item"
+ >
+ <Icon v-if="colorVal === item" :size="16" color="#fff" icon="ep:check" />
+ </span>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-color-radio-picker;
+
+.#{$prefix-cls} {
+ .is-active {
+ border-color: var(--el-color-primary);
+ }
+}
+</style>
diff --git a/src/layout/components/Setting/src/components/InterfaceDisplay.vue b/src/layout/components/Setting/src/components/InterfaceDisplay.vue
new file mode 100644
index 0000000..3ba5c6b
--- /dev/null
+++ b/src/layout/components/Setting/src/components/InterfaceDisplay.vue
@@ -0,0 +1,236 @@
+<script lang="ts" setup>
+import { setCssVar } from '@/utils'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import { useWatermark } from '@/hooks/web/useWatermark'
+import { useAppStore } from '@/store/modules/app'
+
+defineOptions({ name: 'InterfaceDisplay' })
+
+const { t } = useI18n()
+const { getPrefixCls } = useDesign()
+const { setWatermark } = useWatermark()
+const prefixCls = getPrefixCls('interface-display')
+const appStore = useAppStore()
+
+const water = ref()
+
+// 闈㈠寘灞�
+const breadcrumb = ref(appStore.getBreadcrumb)
+
+const breadcrumbChange = (show: boolean) => {
+ appStore.setBreadcrumb(show)
+}
+
+// 闈㈠寘灞戝浘鏍�
+const breadcrumbIcon = ref(appStore.getBreadcrumbIcon)
+
+const breadcrumbIconChange = (show: boolean) => {
+ appStore.setBreadcrumbIcon(show)
+}
+
+// 鎶樺彔鍥炬爣
+const hamburger = ref(appStore.getHamburger)
+
+const hamburgerChange = (show: boolean) => {
+ appStore.setHamburger(show)
+}
+
+// 鍏ㄥ睆鍥炬爣
+const screenfull = ref(appStore.getScreenfull)
+
+const screenfullChange = (show: boolean) => {
+ appStore.setScreenfull(show)
+}
+
+// 灏哄鍥炬爣
+const size = ref(appStore.getSize)
+
+const sizeChange = (show: boolean) => {
+ appStore.setSize(show)
+}
+
+// 澶氳瑷�鍥炬爣
+const locale = ref(appStore.getLocale)
+
+const localeChange = (show: boolean) => {
+ appStore.setLocale(show)
+}
+
+// 娑堟伅鍥炬爣
+const message = ref(appStore.getMessage)
+
+const messageChange = (show: boolean) => {
+ appStore.setMessage(show)
+}
+
+// 鏍囩椤�
+const tagsView = ref(appStore.getTagsView)
+
+const tagsViewChange = (show: boolean) => {
+ // 鍒囨崲鏍囩鏍忔樉绀烘椂锛屽悓姝ュ垏鎹㈡爣绛炬爮鐨勯珮搴�
+ setCssVar('--tags-view-height', show ? '35px' : '0px')
+ appStore.setTagsView(show)
+}
+
+// 鏍囩椤垫矇娴�
+const tagsViewImmerse = ref(appStore.getTagsViewImmerse)
+
+const tagsViewImmerseChange = (immerse: boolean) => {
+ appStore.setTagsViewImmerse(immerse)
+}
+
+// 鏍囩椤靛浘鏍�
+const tagsViewIcon = ref(appStore.getTagsViewIcon)
+
+const tagsViewIconChange = (show: boolean) => {
+ appStore.setTagsViewIcon(show)
+}
+
+// logo
+const logo = ref(appStore.getLogo)
+
+const logoChange = (show: boolean) => {
+ appStore.setLogo(show)
+}
+
+// 鑿滃崟鎵嬮鐞�
+const uniqueOpened = ref(appStore.getUniqueOpened)
+
+const uniqueOpenedChange = (uniqueOpened: boolean) => {
+ appStore.setUniqueOpened(uniqueOpened)
+}
+
+// 鍥哄畾澶撮儴
+const fixedHeader = ref(appStore.getFixedHeader)
+
+const fixedHeaderChange = (show: boolean) => {
+ appStore.setFixedHeader(show)
+}
+
+// 椤佃剼
+const footer = ref(appStore.getFooter)
+
+const footerChange = (show: boolean) => {
+ appStore.setFooter(show)
+}
+
+// 鐏拌壊妯″紡
+const greyMode = ref(appStore.getGreyMode)
+
+const greyModeChange = (show: boolean) => {
+ appStore.setGreyMode(show)
+}
+
+// 鍥哄畾鑿滃崟
+const fixedMenu = ref(appStore.getFixedMenu)
+
+const fixedMenuChange = (show: boolean) => {
+ appStore.setFixedMenu(show)
+}
+
+// 璁剧疆姘村嵃
+const setWater = () => {
+ setWatermark(water.value)
+}
+
+const layout = computed(() => appStore.getLayout)
+
+watch(
+ () => layout.value,
+ (n) => {
+ if (n === 'top') {
+ appStore.setCollapse(false)
+ }
+ }
+)
+</script>
+
+<template>
+ <div :class="prefixCls">
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.breadcrumb') }}</span>
+ <ElSwitch v-model="breadcrumb" @change="breadcrumbChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span>
+ <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
+ <ElSwitch v-model="hamburger" @change="hamburgerChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.screenfullIcon') }}</span>
+ <ElSwitch v-model="screenfull" @change="screenfullChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.sizeIcon') }}</span>
+ <ElSwitch v-model="size" @change="sizeChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.localeIcon') }}</span>
+ <ElSwitch v-model="locale" @change="localeChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.messageIcon') }}</span>
+ <ElSwitch v-model="message" @change="messageChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.tagsView') }}</span>
+ <ElSwitch v-model="tagsView" @change="tagsViewChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.tagsViewImmerse') }}</span>
+ <ElSwitch v-model="tagsViewImmerse" @change="tagsViewImmerseChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.tagsViewIcon') }}</span>
+ <ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.logo') }}</span>
+ <ElSwitch v-model="logo" @change="logoChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.uniqueOpened') }}</span>
+ <ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.fixedHeader') }}</span>
+ <ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.footer') }}</span>
+ <ElSwitch v-model="footer" @change="footerChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.greyMode') }}</span>
+ <ElSwitch v-model="greyMode" @change="greyModeChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('setting.fixedMenu') }}</span>
+ <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" />
+ </div>
+
+ <div class="flex items-center justify-between">
+ <span class="text-14px">{{ t('watermark.watermark') }}</span>
+ <ElInput v-model="water" class="right-1 w-20" @change="setWater()" />
+ </div>
+ </div>
+</template>
diff --git a/src/layout/components/Setting/src/components/LayoutRadioPicker.vue b/src/layout/components/Setting/src/components/LayoutRadioPicker.vue
new file mode 100644
index 0000000..801686c
--- /dev/null
+++ b/src/layout/components/Setting/src/components/LayoutRadioPicker.vue
@@ -0,0 +1,172 @@
+<script lang="ts" setup>
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'LayoutRadioPicker' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('layout-radio-picker')
+
+const appStore = useAppStore()
+
+const layout = computed(() => appStore.getLayout)
+</script>
+
+<template>
+ <div :class="prefixCls" class="flex flex-wrap space-x-14px">
+ <div
+ :class="[
+ `${prefixCls}__classic`,
+ 'relative w-56px h-48px cursor-pointer bg-gray-300',
+ {
+ 'is-acitve': layout === 'classic'
+ }
+ ]"
+ @click="appStore.setLayout('classic')"
+ ></div>
+ <div
+ :class="[
+ `${prefixCls}__top-left`,
+ 'relative w-56px h-48px cursor-pointer bg-gray-300',
+ {
+ 'is-acitve': layout === 'topLeft'
+ }
+ ]"
+ @click="appStore.setLayout('topLeft')"
+ ></div>
+ <div
+ :class="[
+ `${prefixCls}__top`,
+ 'relative w-56px h-48px cursor-pointer bg-gray-300',
+ {
+ 'is-acitve': layout === 'top'
+ }
+ ]"
+ @click="appStore.setLayout('top')"
+ ></div>
+ <div
+ :class="[
+ `${prefixCls}__cut-menu`,
+ 'relative w-56px h-48px cursor-pointer bg-gray-300',
+ {
+ 'is-acitve': layout === 'cutMenu'
+ }
+ ]"
+ @click="appStore.setLayout('cutMenu')"
+ >
+ <div class="absolute left-[10%] top-0 h-full w-[33%] bg-gray-200"></div>
+ </div>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-layout-radio-picker;
+
+.#{$prefix-cls} {
+ &__classic {
+ border: 2px solid #e5e7eb;
+ border-radius: 4px;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 33%;
+ height: 100%;
+ background-color: #273352;
+ border-radius: 4px 0 0 4px;
+ content: '';
+ }
+
+ &::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 25%;
+ background-color: #fff;
+ border-radius: 4px 4px 0;
+ content: '';
+ }
+ }
+
+ &__top-left {
+ border: 2px solid #e5e7eb;
+ border-radius: 4px;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 100%;
+ height: 33%;
+ background-color: #273352;
+ border-radius: 4px 4px 0 0;
+ content: '';
+ }
+
+ &::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 33%;
+ height: 100%;
+ background-color: #fff;
+ border-radius: 4px 0 0 4px;
+ content: '';
+ }
+ }
+
+ &__top {
+ border: 2px solid #e5e7eb;
+ border-radius: 4px;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 100%;
+ height: 33%;
+ background-color: #273352;
+ border-radius: 4px 4px 0 0;
+ content: '';
+ }
+ }
+
+ &__cut-menu {
+ border: 2px solid #e5e7eb;
+ border-radius: 4px;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ width: 100%;
+ height: 33%;
+ background-color: #273352;
+ border-radius: 4px 4px 0 0;
+ content: '';
+ }
+
+ &::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 10%;
+ height: 100%;
+ background-color: #fff;
+ border-radius: 4px 0 0 4px;
+ content: '';
+ }
+ }
+
+ .is-acitve {
+ border-color: var(--el-color-primary);
+ }
+}
+</style>
diff --git a/src/layout/components/SizeDropdown/index.ts b/src/layout/components/SizeDropdown/index.ts
new file mode 100644
index 0000000..516488d
--- /dev/null
+++ b/src/layout/components/SizeDropdown/index.ts
@@ -0,0 +1,3 @@
+import SizeDropdown from './src/SizeDropdown.vue'
+
+export { SizeDropdown }
diff --git a/src/layout/components/SizeDropdown/src/SizeDropdown.vue b/src/layout/components/SizeDropdown/src/SizeDropdown.vue
new file mode 100644
index 0000000..3e15224
--- /dev/null
+++ b/src/layout/components/SizeDropdown/src/SizeDropdown.vue
@@ -0,0 +1,40 @@
+<script lang="ts" setup>
+import { useAppStore } from '@/store/modules/app'
+
+import { propTypes } from '@/utils/propTypes'
+import { useDesign } from '@/hooks/web/useDesign'
+import { ElementPlusSize } from '@/types/elementPlus'
+
+defineOptions({ name: 'SizeDropdown' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('size-dropdown')
+
+defineProps({
+ color: propTypes.string.def('')
+})
+
+const { t } = useI18n()
+
+const appStore = useAppStore()
+
+const sizeMap = computed(() => appStore.sizeMap)
+
+const setCurrentSize = (size: ElementPlusSize) => {
+ appStore.setCurrentSize(size)
+}
+</script>
+
+<template>
+ <ElDropdown :class="prefixCls" trigger="click" @command="setCurrentSize">
+ <Icon :color="color" :size="18" class="cursor-pointer" icon="mdi:format-size" />
+ <template #dropdown>
+ <ElDropdownMenu>
+ <ElDropdownItem v-for="item in sizeMap" :key="item" :command="item">
+ {{ t(`size.${item}`) }}
+ </ElDropdownItem>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+</template>
diff --git a/src/layout/components/TabMenu/index.ts b/src/layout/components/TabMenu/index.ts
new file mode 100644
index 0000000..b5fd71c
--- /dev/null
+++ b/src/layout/components/TabMenu/index.ts
@@ -0,0 +1,3 @@
+import TabMenu from './src/TabMenu.vue'
+
+export { TabMenu }
diff --git a/src/layout/components/TabMenu/src/TabMenu.vue b/src/layout/components/TabMenu/src/TabMenu.vue
new file mode 100644
index 0000000..efad6a6
--- /dev/null
+++ b/src/layout/components/TabMenu/src/TabMenu.vue
@@ -0,0 +1,240 @@
+<script lang="tsx">
+import { usePermissionStore } from '@/store/modules/permission'
+import { useAppStore } from '@/store/modules/app'
+
+import { ElScrollbar } from 'element-plus'
+import { Icon } from '@/components/Icon'
+import { Menu } from '@/layout/components/Menu'
+import { pathResolve } from '@/utils/routerHelper'
+import { cloneDeep } from 'lodash-es'
+import { filterMenusPath, initTabMap, tabPathMap } from './helper'
+import { useDesign } from '@/hooks/web/useDesign'
+import { isUrl } from '@/utils/is'
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('tab-menu')
+
+export default defineComponent({
+ name: 'TabMenu',
+ setup() {
+ const { push, currentRoute } = useRouter()
+
+ const { t } = useI18n()
+
+ const appStore = useAppStore()
+
+ const collapse = computed(() => appStore.getCollapse)
+
+ const fixedMenu = computed(() => appStore.getFixedMenu)
+
+ const permissionStore = usePermissionStore()
+
+ const routers = computed(() => permissionStore.getRouters)
+
+ const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
+
+ const setCollapse = () => {
+ appStore.setCollapse(!unref(collapse))
+ }
+
+ onMounted(() => {
+ if (unref(fixedMenu)) {
+ const path = `/${unref(currentRoute).path.split('/')[1]}`
+ const children = unref(tabRouters).find(
+ (v) =>
+ (v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
+ v.path === path
+ )?.children
+
+ tabActive.value = path
+ if (children) {
+ permissionStore.setMenuTabRouters(
+ cloneDeep(children).map((v) => {
+ v.path = pathResolve(unref(tabActive), v.path)
+ return v
+ })
+ )
+ }
+ }
+ })
+
+ watch(
+ () => routers.value,
+ (routers: AppRouteRecordRaw[]) => {
+ initTabMap(routers)
+ filterMenusPath(routers, routers)
+ },
+ {
+ immediate: true,
+ deep: true
+ }
+ )
+
+ const showTitle = ref(true)
+
+ watch(
+ () => collapse.value,
+ (collapse: boolean) => {
+ if (!collapse) {
+ setTimeout(() => {
+ showTitle.value = !collapse
+ }, 200)
+ } else {
+ showTitle.value = !collapse
+ }
+ }
+ )
+
+ // 鏄惁鏄剧ず鑿滃崟
+ const showMenu = ref(unref(fixedMenu) ? true : false)
+
+ // tab楂樹寒
+ const tabActive = ref('')
+
+ // tab鐐瑰嚮浜嬩欢
+ const tabClick = (item: AppRouteRecordRaw) => {
+ if (isUrl(item.path)) {
+ window.open(item.path)
+ return
+ }
+ const newPath = item.children ? item.path : item.path.split('/')[0]
+ const oldPath = unref(tabActive)
+ tabActive.value = item.children ? item.path : item.path.split('/')[0]
+ if (item.children) {
+ if (newPath === oldPath || !unref(showMenu)) {
+ showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
+ }
+ if (unref(showMenu)) {
+ permissionStore.setMenuTabRouters(
+ cloneDeep(item.children).map((v) => {
+ v.path = pathResolve(unref(tabActive), v.path)
+ return v
+ })
+ )
+ }
+ } else {
+ push(item.path)
+ permissionStore.setMenuTabRouters([])
+ showMenu.value = false
+ }
+ }
+
+ // 璁剧疆楂樹寒
+ const isActive = (currentPath: string) => {
+ const { path } = unref(currentRoute)
+ if (tabPathMap[currentPath].includes(path)) {
+ return true
+ }
+ return false
+ }
+
+ const mouseleave = () => {
+ if (!unref(showMenu) || unref(fixedMenu)) return
+ showMenu.value = false
+ }
+
+ return () => (
+ <div
+ id={`${variables.namespace}-menu`}
+ class={[
+ prefixCls,
+ 'relative bg-[var(--left-menu-bg-color)] layout-border__right',
+ {
+ 'w-[var(--tab-menu-max-width)]': !unref(collapse),
+ 'w-[var(--tab-menu-min-width)]': unref(collapse)
+ }
+ ]}
+ onMouseleave={mouseleave}
+ >
+ <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height))]">
+ <div>
+ {() => {
+ return unref(tabRouters).map((v) => {
+ const item = (
+ v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)
+ ? v
+ : {
+ ...(v?.children && v?.children[0]),
+ path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string)
+ }
+ ) as AppRouteRecordRaw
+ return (
+ <div
+ class={[
+ `${prefixCls}__item`,
+ 'text-center text-12px relative py-12px cursor-pointer',
+ {
+ 'is-active': isActive(v.path)
+ }
+ ]}
+ onClick={() => {
+ tabClick(item)
+ }}
+ >
+ <div>
+ <Icon icon={item?.meta?.icon}></Icon>
+ </div>
+ {!unref(showTitle) ? undefined : (
+ <p class="mt-5px break-words px-2px">{t(item.meta?.title)}</p>
+ )}
+ </div>
+ )
+ })
+ }}
+ </div>
+ </ElScrollbar>
+ <div
+ class={[
+ `${prefixCls}--collapse`,
+ 'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
+ ]}
+ onClick={setCollapse}
+ >
+ <Icon icon={unref(collapse) ? 'ep:d-arrow-right' : 'ep:d-arrow-left'}></Icon>
+ </div>
+ <Menu
+ class={[
+ '!absolute top-0 z-11',
+ {
+ '!left-[var(--tab-menu-min-width)]': unref(collapse),
+ '!left-[var(--tab-menu-max-width)]': !unref(collapse),
+ '!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu),
+ '!w-0': !unref(showMenu) && !unref(fixedMenu)
+ }
+ ]}
+ style="transition: width var(--transition-time-02), left var(--transition-time-02);"
+ ></Menu>
+ </div>
+ )
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-tab-menu;
+
+.#{$prefix-cls} {
+ transition: all var(--transition-time-02);
+
+ &__item {
+ color: var(--left-menu-text-color);
+ transition: all var(--transition-time-02);
+
+ &:hover {
+ color: var(--left-menu-text-active-color);
+ // background-color: var(--left-menu-bg-active-color);
+ }
+ }
+
+ &--collapse {
+ color: var(--left-menu-text-color);
+ background-color: var(--left-menu-bg-light-color);
+ }
+
+ .is-active {
+ color: var(--left-menu-text-active-color);
+ background-color: var(--left-menu-bg-active-color);
+ }
+}
+</style>
diff --git a/src/layout/components/TabMenu/src/helper.ts b/src/layout/components/TabMenu/src/helper.ts
new file mode 100644
index 0000000..cce3932
--- /dev/null
+++ b/src/layout/components/TabMenu/src/helper.ts
@@ -0,0 +1,51 @@
+import { getAllParentPath } from '@/layout/components/Menu/src/helper'
+import type { RouteMeta } from 'vue-router'
+import { isUrl } from '@/utils/is'
+import { cloneDeep } from 'lodash-es'
+
+export type TabMapTypes = {
+ [key: string]: string[]
+}
+
+export const tabPathMap = reactive<TabMapTypes>({})
+
+export const initTabMap = (routes: AppRouteRecordRaw[]) => {
+ for (const v of routes) {
+ const meta = (v.meta ?? {}) as RouteMeta
+ if (!meta?.hidden) {
+ tabPathMap[v.path] = []
+ }
+ }
+}
+
+export const filterMenusPath = (
+ routes: AppRouteRecordRaw[],
+ allRoutes: AppRouteRecordRaw[]
+): AppRouteRecordRaw[] => {
+ const res: AppRouteRecordRaw[] = []
+ for (const v of routes) {
+ let data: Nullable<AppRouteRecordRaw> = null
+ const meta = (v.meta ?? {}) as RouteMeta
+ if (!meta.hidden || meta.canTo) {
+ const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
+
+ const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
+
+ data = cloneDeep(v)
+ data.path = fullPath
+ if (v.children && data) {
+ data.children = filterMenusPath(v.children, allRoutes)
+ }
+
+ if (data) {
+ res.push(data)
+ }
+
+ if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) {
+ tabPathMap[allParentPath[0]].push(fullPath)
+ }
+ }
+ }
+
+ return res
+}
diff --git a/src/layout/components/TagsView/index.ts b/src/layout/components/TagsView/index.ts
new file mode 100644
index 0000000..30e604a
--- /dev/null
+++ b/src/layout/components/TagsView/index.ts
@@ -0,0 +1,3 @@
+import TagsView from './src/TagsView.vue'
+
+export { TagsView }
diff --git a/src/layout/components/TagsView/src/TagsView.vue b/src/layout/components/TagsView/src/TagsView.vue
new file mode 100644
index 0000000..69f94bf
--- /dev/null
+++ b/src/layout/components/TagsView/src/TagsView.vue
@@ -0,0 +1,661 @@
+<script lang="ts" setup>
+import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
+import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
+import { useRouter } from 'vue-router'
+import { usePermissionStore } from '@/store/modules/permission'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useAppStore } from '@/store/modules/app'
+import { useI18n } from '@/hooks/web/useI18n'
+import { filterAffixTags } from './helper'
+import { ContextMenu, ContextMenuExpose } from '@/layout/components/ContextMenu'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useTemplateRefsList } from '@vueuse/core'
+import { ElScrollbar } from 'element-plus'
+import { useScrollTo } from '@/hooks/event/useScrollTo'
+import { useTagsView } from '@/hooks/web/useTagsView'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'TagsView' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('tags-view')
+
+const { t } = useI18n()
+
+const { currentRoute, push } = useRouter()
+
+const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
+
+const permissionStore = usePermissionStore()
+
+const routers = computed(() => permissionStore.getRouters)
+
+const tagsViewStore = useTagsViewStore()
+
+const visitedViews = computed(() => tagsViewStore.getVisitedViews)
+
+const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
+
+const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+const setSelectTag = tagsViewStore.setSelectedTag
+
+const appStore = useAppStore()
+
+const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
+
+const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
+
+const isDark = computed(() => appStore.getIsDark)
+
+// 鍒濆鍖杢ag
+const initTags = () => {
+ affixTagArr.value = filterAffixTags(unref(routers))
+ for (const tag of unref(affixTagArr)) {
+ // Must have tag name
+ if (tag.name) {
+ tagsViewStore.addVisitedView(cloneDeep(tag))
+ }
+ }
+}
+
+// 鏂板tag
+const addTags = () => {
+ const { name } = unref(currentRoute)
+ if (name) {
+ setSelectTag(unref(currentRoute))
+ tagsViewStore.addView(unref(currentRoute))
+ }
+}
+
+// 鍏抽棴閫変腑鐨則ag
+const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
+ closeCurrent(view, () => {
+ if (isActive(view)) {
+ toLastView()
+ }
+ })
+}
+
+// 鍘绘渶鍚庝竴涓�
+const toLastView = () => {
+ const visitedViews = tagsViewStore.getVisitedViews
+ const latestView = visitedViews.slice(-1)[0]
+ if (latestView) {
+ push(latestView)
+ } else {
+ if (
+ unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
+ unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
+ ) {
+ addTags()
+ return
+ }
+ // You can set another route
+ push(permissionStore.getAddRouters[0].path)
+ }
+}
+
+// 鍏抽棴鍏ㄩ儴
+const closeAllTags = () => {
+ closeAll(() => {
+ toLastView()
+ })
+}
+
+// 鍏抽棴鍏跺畠
+const closeOthersTags = () => {
+ closeOther()
+}
+
+// 閲嶆柊鍔犺浇
+const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
+ refreshPage(view)
+}
+
+// 鍏抽棴宸︿晶
+const closeLeftTags = () => {
+ closeLeft()
+}
+
+// 鍏抽棴鍙充晶
+const closeRightTags = () => {
+ closeRight()
+}
+
+// 婊氬姩鍒伴�変腑鐨則ag
+const moveToCurrentTag = async () => {
+ await nextTick()
+ for (const v of unref(visitedViews)) {
+ if (v.fullPath === unref(currentRoute).fullPath) {
+ moveToTarget(v)
+ break
+ }
+ }
+}
+
+const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
+
+const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
+ const wrap$ = unref(scrollbarRef)?.wrapRef
+ let firstTag: Nullable<RouterLinkProps> = null
+ let lastTag: Nullable<RouterLinkProps> = null
+
+ const tagList = unref(tagLinksRefs)
+ // find first tag and last tag
+ if (tagList.length > 0) {
+ firstTag = tagList[0]
+ lastTag = tagList[tagList.length - 1]
+ }
+ if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
+ // 鐩存帴婊氬姩鍒�0鐨勪綅缃�
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: 0,
+ duration: 500
+ })
+ start()
+ } else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
+ // 婊氬姩鍒版渶鍚庣殑浣嶇疆
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: wrap$!.scrollWidth - wrap$!.offsetWidth,
+ duration: 500
+ })
+ start()
+ } else {
+ // find preTag and nextTag
+ const currentIndex: number = tagList.findIndex(
+ (item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
+ )
+ const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
+
+ const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
+ const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
+
+ // the tag's offsetLeft after of nextTag
+ const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
+
+ // the tag's offsetLeft before of prevTag
+ const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
+
+ if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
+ duration: 500
+ })
+ start()
+ } else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: beforePrevTagOffsetLeft,
+ duration: 500
+ })
+ start()
+ }
+ }
+}
+
+// 鏄惁鏄綋鍓峵ag
+const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
+ return route.fullPath === unref(currentRoute).fullPath
+}
+
+// 鎵�鏈夊彸閿彍鍗曠粍浠剁殑鍏冪礌
+const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
+
+// 鍙抽敭鑿滃崟鐘舵�佹敼鍙樼殑鏃跺��
+const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
+ if (visible) {
+ for (const v of unref(itemRefs)) {
+ const elDropdownMenuRef = v.elDropdownMenuRef
+ if (tagItem.fullPath !== v.tagItem.fullPath) {
+ elDropdownMenuRef?.handleClose()
+ setSelectTag(tagItem)
+ }
+ }
+ }
+}
+
+// elscroll 瀹炰緥
+const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
+
+// 淇濆瓨婊氬姩浣嶇疆
+const scrollLeftNumber = ref(0)
+
+const scroll = ({ scrollLeft }) => {
+ scrollLeftNumber.value = scrollLeft as number
+}
+
+// 绉诲姩鍒版煇涓綅缃�
+const move = (to: number) => {
+ const wrap$ = unref(scrollbarRef)?.wrapRef
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: unref(scrollLeftNumber) + to,
+ duration: 500
+ })
+ start()
+}
+
+const canShowIcon = (item: RouteLocationNormalizedLoaded) => {
+ if (
+ (item?.matched?.[1]?.meta?.icon && unref(tagsViewIcon)) ||
+ (item?.meta?.affix && unref(tagsViewIcon) && item?.meta?.icon)
+ ) {
+ return true
+ }
+ return false
+}
+
+const closeTabOnMouseMidClick = (e: MouseEvent, item) => {
+ // 涓敭锛歜utton === 1
+ if (e.button === 1) {
+ e.preventDefault()
+ e.stopPropagation()
+ closeSelectedTag(item)
+ }
+}
+
+onBeforeMount(() => {
+ initTags()
+ addTags()
+})
+
+watch(
+ () => currentRoute.value,
+ () => {
+ addTags()
+ moveToCurrentTag()
+ }
+)
+</script>
+
+<template>
+ <div
+ :id="prefixCls"
+ :class="prefixCls"
+ class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
+ >
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`"
+ class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ @click="move(-200)"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:d-arrow-left"
+ />
+ </span>
+ <div class="flex-1 overflow-hidden">
+ <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
+ <div class="h-[var(--tags-view-height)] flex">
+ <ContextMenu
+ v-for="item in visitedViews"
+ :key="item.fullPath"
+ :ref="itemRefs.set"
+ @auxclick="closeTabOnMouseMidClick($event, item)"
+ :class="[
+ `${prefixCls}__item`,
+ tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
+ tagsViewIcon ? `${prefixCls}__item--icon` : '',
+ tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '',
+ item?.meta?.affix ? `${prefixCls}__item--affix` : '',
+ {
+ 'is-active': isActive(item)
+ }
+ ]"
+ :schema="[
+ {
+ icon: 'ep:refresh',
+ label: t('common.reload'),
+ disabled: selectedTag?.fullPath !== item.fullPath,
+ command: () => {
+ refreshSelectedTag(item)
+ }
+ },
+ {
+ icon: 'ep:close',
+ label: t('common.closeTab'),
+ disabled: !!visitedViews?.length && selectedTag?.meta.affix,
+ command: () => {
+ closeSelectedTag(item)
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:d-arrow-left',
+ label: t('common.closeTheLeftTab'),
+ disabled:
+ !!visitedViews?.length &&
+ (item.fullPath === visitedViews[0].fullPath ||
+ selectedTag?.fullPath !== item.fullPath),
+ command: () => {
+ closeLeftTags()
+ }
+ },
+ {
+ icon: 'ep:d-arrow-right',
+ label: t('common.closeTheRightTab'),
+ disabled:
+ !!visitedViews?.length &&
+ (item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
+ selectedTag?.fullPath !== item.fullPath),
+ command: () => {
+ closeRightTags()
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:discount',
+ label: t('common.closeOther'),
+ disabled: selectedTag?.fullPath !== item.fullPath,
+ command: () => {
+ closeOthersTags()
+ }
+ },
+ {
+ icon: 'ep:minus',
+ label: t('common.closeAll'),
+ command: () => {
+ closeAllTags()
+ }
+ }
+ ]"
+ :tag-item="item"
+ @visible-change="visibleChange"
+ >
+ <div>
+ <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom>
+ <div
+ :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
+ @click="navigate"
+ >
+ <Icon
+ v-if="
+ tagsViewIcon &&
+ (item?.meta?.icon ||
+ (item?.matched &&
+ item.matched[0] &&
+ item.matched[item.matched.length - 1].meta?.icon))
+ "
+ :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon"
+ :size="12"
+ class="mr-5px"
+ />
+ {{
+ t(item?.meta?.title as string) +
+ (item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
+ }}
+ <Icon
+ :class="`${prefixCls}__item--close`"
+ :size="12"
+ color="#333"
+ icon="ep:close"
+ @click.prevent.stop="closeSelectedTag(item)"
+ />
+ </div>
+ </router-link>
+ </div>
+ </ContextMenu>
+ </div>
+ </ElScrollbar>
+ </div>
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
+ class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ @click="move(200)"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:d-arrow-right"
+ />
+ </span>
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
+ class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ @click="refreshSelectedTag(selectedTag)"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:refresh-right"
+ />
+ </span>
+ <ContextMenu
+ :schema="[
+ {
+ icon: 'ep:refresh',
+ label: t('common.reload'),
+ command: () => {
+ refreshSelectedTag(selectedTag)
+ }
+ },
+ {
+ icon: 'ep:close',
+ label: t('common.closeTab'),
+ disabled: !!visitedViews?.length && selectedTag?.meta.affix,
+ command: () => {
+ closeSelectedTag(selectedTag!)
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:d-arrow-left',
+ label: t('common.closeTheLeftTab'),
+ disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
+ command: () => {
+ closeLeftTags()
+ }
+ },
+ {
+ icon: 'ep:d-arrow-right',
+ label: t('common.closeTheRightTab'),
+ disabled:
+ !!visitedViews?.length &&
+ selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
+ command: () => {
+ closeRightTags()
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:discount',
+ label: t('common.closeOther'),
+ command: () => {
+ closeOthersTags()
+ }
+ },
+ {
+ icon: 'ep:minus',
+ label: t('common.closeAll'),
+ command: () => {
+ closeAllTags()
+ }
+ }
+ ]"
+ trigger="click"
+ >
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
+ class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:menu"
+ />
+ </span>
+ </ContextMenu>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-tags-view;
+
+.#{$prefix-cls} {
+ :deep(.#{$elNamespace}-scrollbar__view) {
+ height: 100%;
+ }
+
+ &__tool {
+ position: relative;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-left: 1px solid var(--el-border-color);
+ content: '';
+ }
+
+ &--first {
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-right: 1px solid var(--el-border-color);
+ border-left: none;
+ content: '';
+ }
+ }
+ }
+
+ &__item {
+ position: relative;
+ top: 3px;
+ height: calc(100% - 6px);
+ padding-right: 15px;
+ margin-left: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ border: 1px solid #d9d9d9;
+ border-radius: 2px;
+ box-sizing: border-box;
+
+ &--close {
+ position: absolute;
+ top: 50%;
+ right: 5px;
+ display: none;
+ transform: translate(0, -50%);
+ }
+
+ &:not(.#{$prefix-cls}__item--affix):hover {
+ .#{$prefix-cls}__item--close {
+ display: block;
+ }
+ }
+ }
+
+ &__item--icon {
+ padding-right: 20px;
+ }
+
+ &__item:not(.is-active) {
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ }
+
+ &__item.is-active {
+ color: var(--el-color-white);
+ background-color: var(--el-color-primary);
+ border: 1px solid var(--el-color-primary);
+
+ .#{$prefix-cls}__item--close {
+ :deep(span) {
+ color: var(--el-color-white) !important;
+ }
+ }
+ }
+
+ &__item--immerse {
+ top: 2px;
+ height: calc(100% - 3px);
+ padding-right: 35px;
+ margin: 0 -10px;
+ border: none !important;
+ -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
+ 12 27 15;
+
+ .#{$prefix-cls}__item--label {
+ padding-left: 35px;
+ }
+
+ .#{$prefix-cls}__item--close {
+ right: 20px;
+ }
+ }
+
+ &__item--immerse--icon {
+ padding-right: 35px;
+ }
+
+ &__item--immerse:not(.is-active) {
+ &:hover {
+ color: var(--el-color-white);
+ background-color: var(--el-color-primary);
+
+ .#{$prefix-cls}__item--close {
+ :deep(span) {
+ color: var(--el-color-white) !important;
+ }
+ }
+ }
+ }
+}
+
+.dark {
+ .#{$prefix-cls} {
+ &__tool {
+ &--first {
+ &::after {
+ display: none;
+ }
+ }
+ }
+
+ &__item {
+ border: 1px solid var(--el-border-color);
+ }
+
+ &__item:not(.is-active) {
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ }
+
+ &__item.is-active {
+ color: var(--el-color-white);
+ background-color: var(--el-color-primary);
+ border: 1px solid var(--el-color-primary);
+
+ .#{$prefix-cls}__item--close {
+ :deep(span) {
+ color: var(--el-color-white) !important;
+ }
+ }
+ }
+
+ &__item--immerse:not(.is-active) {
+ &:hover {
+ color: var(--el-color-white);
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/TagsView/src/helper.ts b/src/layout/components/TagsView/src/helper.ts
new file mode 100644
index 0000000..22f6a50
--- /dev/null
+++ b/src/layout/components/TagsView/src/helper.ts
@@ -0,0 +1,21 @@
+import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router'
+import { pathResolve } from '@/utils/routerHelper'
+
+export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => {
+ let tags: RouteLocationNormalizedLoaded[] = []
+ routes.forEach((route) => {
+ const meta = route.meta as RouteMeta
+ const tagPath = pathResolve(parentPath, route.path)
+ if (meta?.affix) {
+ tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded)
+ }
+ if (route.children) {
+ const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath)
+ if (tempTags.length >= 1) {
+ tags = [...tags, ...tempTags]
+ }
+ }
+ })
+
+ return tags
+}
diff --git a/src/layout/components/TenantVisit/index.vue b/src/layout/components/TenantVisit/index.vue
new file mode 100644
index 0000000..81e04a9
--- /dev/null
+++ b/src/layout/components/TenantVisit/index.vue
@@ -0,0 +1,46 @@
+<template>
+ <div>
+ <el-select
+ filterable
+ placeholder="璇烽�夋嫨绉熸埛"
+ class="!w-180px"
+ v-model="value"
+ @change="handleChange"
+ clearable
+ >
+ <el-option v-for="item in tenants" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted } from 'vue'
+import * as TenantApi from '@/api/system/tenant'
+import { getVisitTenantId, setVisitTenantId } from '@/utils/auth'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useTagsView } from '@/hooks/web/useTagsView'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const tagsView = useTagsView() // 鏍囩椤垫搷浣�
+
+const value = ref(getVisitTenantId()) // 褰撳墠閫変腑鐨勭鎴� ID
+const tenants = ref<any[]>([]) // 绉熸埛鍒楄〃
+
+const handleChange = (id: number) => {
+ // 璁剧疆璁块棶绉熸埛 ID
+ setVisitTenantId(id)
+ // 鍏抽棴鍏朵粬鏍囩椤碉紝鍙繚鐣欏綋鍓嶉〉
+ tagsView.closeOther()
+ // 鍒锋柊褰撳墠椤甸潰
+ tagsView.refreshPage()
+ // 鎻愮ず鍒囨崲鎴愬姛
+ const tenant = tenants.value.find((item) => item.id === id)
+ if (tenant) {
+ message.success(`鍒囨崲褰撳墠绉熸埛涓�: ${tenant.name}`)
+ }
+}
+
+onMounted(async () => {
+ tenants.value = await TenantApi.getTenantList()
+})
+</script>
diff --git a/src/layout/components/ThemeSwitch/index.ts b/src/layout/components/ThemeSwitch/index.ts
new file mode 100644
index 0000000..823a276
--- /dev/null
+++ b/src/layout/components/ThemeSwitch/index.ts
@@ -0,0 +1,3 @@
+import ThemeSwitch from './src/ThemeSwitch.vue'
+
+export { ThemeSwitch }
diff --git a/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue b/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue
new file mode 100644
index 0000000..39a8cfd
--- /dev/null
+++ b/src/layout/components/ThemeSwitch/src/ThemeSwitch.vue
@@ -0,0 +1,46 @@
+<script lang="ts" setup>
+import { useAppStore } from '@/store/modules/app'
+import { useIcon } from '@/hooks/web/useIcon'
+import { useDesign } from '@/hooks/web/useDesign'
+
+defineOptions({ name: 'ThemeSwitch' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('theme-switch')
+
+const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' })
+
+const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' })
+
+const appStore = useAppStore()
+
+// 鍒濆鍖栬幏鍙栨槸鍚︽槸鏆楅粦涓婚
+const isDark = ref(appStore.getIsDark)
+
+// 璁剧疆switch鐨勮儗鏅鑹�
+const blackColor = 'var(--el-color-black)'
+
+const themeChange = (val: boolean) => {
+ appStore.setIsDark(val)
+}
+</script>
+
+<template>
+ <ElSwitch
+ v-model="isDark"
+ :active-color="blackColor"
+ :active-icon="Sun"
+ :border-color="blackColor"
+ :class="prefixCls"
+ :inactive-color="blackColor"
+ :inactive-icon="CrescentMoon"
+ inline-prompt
+ @change="themeChange"
+ />
+</template>
+<style lang="scss" scoped>
+:deep(.el-switch__core .el-switch__inner .is-icon) {
+ overflow: visible;
+}
+</style>
diff --git a/src/layout/components/ToolHeader.vue b/src/layout/components/ToolHeader.vue
new file mode 100644
index 0000000..be0d5b7
--- /dev/null
+++ b/src/layout/components/ToolHeader.vue
@@ -0,0 +1,103 @@
+<script lang="tsx">
+import { defineComponent, computed } from 'vue'
+import { Message } from '@/layout/components//Message'
+import { Collapse } from '@/layout/components/Collapse'
+import { UserInfo } from '@/layout/components/UserInfo'
+import { Screenfull } from '@/layout/components/Screenfull'
+import { Breadcrumb } from '@/layout/components/Breadcrumb'
+import { SizeDropdown } from '@/layout/components/SizeDropdown'
+import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+import RouterSearch from '@/components/RouterSearch/index.vue'
+import TenantVisit from '@/layout/components/TenantVisit/index.vue'
+import { useAppStore } from '@/store/modules/app'
+import { useDesign } from '@/hooks/web/useDesign'
+import { checkPermi } from '@/utils/permission'
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('tool-header')
+
+const appStore = useAppStore()
+
+// 闈㈠寘灞�
+const breadcrumb = computed(() => appStore.getBreadcrumb)
+
+// 鎶樺彔鍥炬爣
+const hamburger = computed(() => appStore.getHamburger)
+
+// 鍏ㄥ睆鍥炬爣
+const screenfull = computed(() => appStore.getScreenfull)
+
+// 鎼滅储鍥剧墖
+const search = computed(() => appStore.search)
+
+// 灏哄鍥炬爣
+const size = computed(() => appStore.getSize)
+
+// 甯冨眬
+const layout = computed(() => appStore.getLayout)
+
+// 澶氳瑷�鍥炬爣
+const locale = computed(() => appStore.getLocale)
+
+// 娑堟伅鍥炬爣
+const message = computed(() => appStore.getMessage)
+
+// 绉熸埛鍒囨崲鏉冮檺
+const hasTenantVisitPermission = computed(
+ () => import.meta.env.VITE_APP_TENANT_ENABLE === 'true' && checkPermi(['system:tenant:visit'])
+)
+
+export default defineComponent({
+ name: 'ToolHeader',
+ setup() {
+ return () => (
+ <div
+ id={`${variables.namespace}-tool-header`}
+ class={[
+ prefixCls,
+ 'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between',
+ 'dark:bg-[var(--el-bg-color)]'
+ ]}
+ >
+ {layout.value !== 'top' ? (
+ <div class="h-full flex items-center">
+ {hamburger.value && layout.value !== 'cutMenu' ? (
+ <Collapse class="custom-hover" color="var(--top-header-text-color)"></Collapse>
+ ) : undefined}
+ {breadcrumb.value ? <Breadcrumb class="lt-md:hidden"></Breadcrumb> : undefined}
+ </div>
+ ) : undefined}
+ <div class="h-full flex items-center">
+ {hasTenantVisitPermission.value ? <TenantVisit /> : undefined}
+ {screenfull.value ? (
+ <Screenfull class="custom-hover" color="var(--top-header-text-color)"></Screenfull>
+ ) : undefined}
+ {search.value ? <RouterSearch isModal={false} color="var(--top-header-text-color)"/> : undefined}
+ {size.value ? (
+ <SizeDropdown class="custom-hover" color="var(--top-header-text-color)"></SizeDropdown>
+ ) : undefined}
+ {locale.value ? (
+ <LocaleDropdown
+ class="custom-hover"
+ color="var(--top-header-text-color)"
+ ></LocaleDropdown>
+ ) : undefined}
+ {message.value ? (
+ <Message class="custom-hover" color="var(--top-header-text-color)"></Message>
+ ) : undefined}
+ <UserInfo></UserInfo>
+ </div>
+ </div>
+ )
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-tool-header;
+
+.#{$prefix-cls} {
+ transition: left var(--transition-time-02);
+}
+</style>
diff --git a/src/layout/components/UserInfo/index.ts b/src/layout/components/UserInfo/index.ts
new file mode 100644
index 0000000..c3a34ab
--- /dev/null
+++ b/src/layout/components/UserInfo/index.ts
@@ -0,0 +1,3 @@
+import UserInfo from './src/UserInfo.vue'
+
+export { UserInfo }
diff --git a/src/layout/components/UserInfo/src/UserInfo.vue b/src/layout/components/UserInfo/src/UserInfo.vue
new file mode 100644
index 0000000..355aabc
--- /dev/null
+++ b/src/layout/components/UserInfo/src/UserInfo.vue
@@ -0,0 +1,113 @@
+<script lang="ts" setup>
+import { ElMessageBox } from 'element-plus'
+
+import avatarImg from '@/assets/imgs/avatar.gif'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useUserStore } from '@/store/modules/user'
+import LockDialog from './components/LockDialog.vue'
+import LockPage from './components/LockPage.vue'
+import { useLockStore } from '@/store/modules/lock'
+
+defineOptions({ name: 'UserInfo' })
+
+const { t } = useI18n()
+
+const { push, replace } = useRouter()
+
+const userStore = useUserStore()
+
+const tagsViewStore = useTagsViewStore()
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('user-info')
+
+const avatar = computed(() => userStore.user.avatar || avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+// 閿佸畾灞忓箷
+const lockStore = useLockStore()
+const getIsLock = computed(() => lockStore.getLockInfo?.isLock ?? false)
+const dialogVisible = ref<boolean>(false)
+const lockScreen = () => {
+ dialogVisible.value = true
+}
+
+const loginOut = async () => {
+ try {
+ await ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ type: 'warning'
+ })
+ await userStore.loginOut()
+ tagsViewStore.delAllViews()
+ replace('/login?redirect=/index')
+ } catch {}
+}
+const toProfile = async () => {
+ push('/user/profile')
+}
+const toDocument = () => {
+ window.open('https://doc.iocoder.cn/')
+}
+</script>
+
+<template>
+ <ElDropdown class="custom-hover" :class="prefixCls" trigger="click">
+ <div class="flex items-center">
+ <ElAvatar :src="avatar" alt="" class="w-[calc(var(--logo-height)-25px)] rounded-[50%]" />
+ <span class="pl-[5px] text-14px text-[var(--top-header-text-color)] <lg:hidden">
+ {{ userName }}
+ </span>
+ </div>
+ <template #dropdown>
+ <ElDropdownMenu>
+ <ElDropdownItem>
+ <Icon icon="ep:tools" />
+ <div @click="toProfile">{{ t('common.profile') }}</div>
+ </ElDropdownItem>
+ <ElDropdownItem>
+ <Icon icon="ep:menu" />
+ <div @click="toDocument">{{ t('common.document') }}</div>
+ </ElDropdownItem>
+ <ElDropdownItem divided>
+ <Icon icon="ep:lock" />
+ <div @click="lockScreen">{{ t('lock.lockScreen') }}</div>
+ </ElDropdownItem>
+ <ElDropdownItem divided @click="loginOut">
+ <Icon icon="ep:switch-button" />
+ <div>{{ t('common.loginOut') }}</div>
+ </ElDropdownItem>
+ </ElDropdownMenu>
+ </template>
+ </ElDropdown>
+
+ <LockDialog v-if="dialogVisible" v-model="dialogVisible" />
+
+ <teleport to="body">
+ <transition name="fade-bottom" mode="out-in">
+ <LockPage v-if="getIsLock" />
+ </transition>
+ </teleport>
+</template>
+
+<style scoped lang="scss">
+.fade-bottom-enter-active,
+.fade-bottom-leave-active {
+ transition:
+ opacity 0.25s,
+ transform 0.3s;
+}
+
+.fade-bottom-enter-from {
+ opacity: 0;
+ transform: translateY(-10%);
+}
+
+.fade-bottom-leave-to {
+ opacity: 0;
+ transform: translateY(10%);
+}
+</style>
diff --git a/src/layout/components/UserInfo/src/components/LockDialog.vue b/src/layout/components/UserInfo/src/components/LockDialog.vue
new file mode 100644
index 0000000..7257be1
--- /dev/null
+++ b/src/layout/components/UserInfo/src/components/LockDialog.vue
@@ -0,0 +1,98 @@
+<script setup lang="ts">
+import { useValidator } from '@/hooks/web/useValidator'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useLockStore } from '@/store/modules/lock'
+import avatarImg from '@/assets/imgs/avatar.gif'
+import { useUserStore } from '@/store/modules/user'
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-dialog')
+
+const { required } = useValidator()
+
+const { t } = useI18n()
+
+const lockStore = useLockStore()
+
+const props = defineProps({
+ modelValue: {
+ type: Boolean
+ }
+})
+
+const userStore = useUserStore()
+const avatar = computed(() => userStore.user.avatar || avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+const emit = defineEmits(['update:modelValue'])
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => {
+ console.log('set: ', val)
+ emit('update:modelValue', val)
+ }
+})
+
+const dialogTitle = ref(t('lock.lockScreen'))
+
+const formData = ref({
+ password: undefined
+})
+const formRules = reactive({
+ password: [required()]
+})
+
+const formRef = ref() // 琛ㄥ崟 Ref
+const handleLock = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ dialogVisible.value = false
+ lockStore.setLockInfo({
+ ...formData.value,
+ isLock: true
+ })
+}
+</script>
+
+<template>
+ <Dialog
+ v-model="dialogVisible"
+ width="500px"
+ max-height="170px"
+ :class="prefixCls"
+ :title="dialogTitle"
+ >
+ <div class="flex flex-col items-center">
+ <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
+ <span class="text-14px my-10px text-[var(--top-header-text-color)]">
+ {{ userName }}
+ </span>
+ </div>
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+ <el-form-item :label="t('lock.lockPassword')" prop="password">
+ <el-input
+ type="password"
+ v-model="formData.password"
+ :placeholder="'璇疯緭鍏�' + t('lock.lockPassword')"
+ clearable
+ show-password
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <ElButton type="primary" @click="handleLock">{{ t('lock.lock') }}</ElButton>
+ </template>
+ </Dialog>
+</template>
+
+<style lang="scss" scoped>
+:global(.v-lock-dialog) {
+ @media (max-width: 767px) {
+ max-width: calc(100vw - 16px);
+ }
+}
+</style>
diff --git a/src/layout/components/UserInfo/src/components/LockPage.vue b/src/layout/components/UserInfo/src/components/LockPage.vue
new file mode 100644
index 0000000..27d0a43
--- /dev/null
+++ b/src/layout/components/UserInfo/src/components/LockPage.vue
@@ -0,0 +1,270 @@
+<script lang="ts" setup>
+import { resetRouter } from '@/router'
+import { deleteUserCache } from '@/hooks/web/useCache'
+import { useLockStore } from '@/store/modules/lock'
+import { useNow } from '@/hooks/web/useNow'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useUserStore } from '@/store/modules/user'
+import avatarImg from '@/assets/imgs/avatar.gif'
+
+const tagsViewStore = useTagsViewStore()
+
+const { replace } = useRouter()
+
+const userStore = useUserStore()
+
+const password = ref('')
+const loading = ref(false)
+const errMsg = ref(false)
+const showDate = ref(true)
+
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('lock-page')
+
+const avatar = computed(() => userStore.user.avatar || avatarImg)
+const userName = computed(() => userStore.user.nickname ?? 'Admin')
+
+const lockStore = useLockStore()
+
+const { hour, month, minute, meridiem, year, day, week } = useNow(true)
+
+const { t } = useI18n()
+
+// 瑙i攣
+async function unLock() {
+ if (!password.value) {
+ return
+ }
+ let pwd = password.value
+ try {
+ loading.value = true
+ const res = await lockStore.unLock(pwd)
+ errMsg.value = !res
+ } finally {
+ loading.value = false
+ }
+}
+
+// 杩斿洖鐧诲綍
+async function goLogin() {
+ await userStore.loginOut().catch(() => {})
+ // 鐧诲嚭鍚庢竻鐞�
+ deleteUserCache() // 娓呯┖鐢ㄦ埛缂撳瓨
+ tagsViewStore.delAllViews()
+ // resetRouter() // 閲嶇疆闈欐�佽矾鐢辫〃
+ lockStore.resetLockInfo()
+ replace('/login')
+}
+
+function handleShowForm(show = false) {
+ showDate.value = show
+}
+</script>
+
+<template>
+ <div
+ :class="prefixCls"
+ class="fixed inset-0 flex h-screen w-screen bg-black items-center justify-center"
+ >
+ <div
+ :class="`${prefixCls}__unlock`"
+ class="absolute top-0 left-1/2 flex pt-5 h-16 items-center justify-center sm:text-md xl:text-xl text-white flex-col cursor-pointer transform translate-x-1/2"
+ @click="handleShowForm(false)"
+ v-show="showDate"
+ >
+ <Icon icon="ep:lock" />
+ <span>{{ t('lock.unlock') }}</span>
+ </div>
+
+ <div class="flex w-screen h-screen justify-center items-center">
+ <div :class="`${prefixCls}__hour`" class="relative mr-5 md:mr-20 w-2/5 h-2/5 md:h-4/5">
+ <span>{{ hour }}</span>
+ <span class="meridiem absolute left-5 top-5 text-md xl:text-xl" v-show="showDate">
+ {{ meridiem }}
+ </span>
+ </div>
+ <div :class="`${prefixCls}__minute w-2/5 h-2/5 md:h-4/5 `">
+ <span> {{ minute }}</span>
+ </div>
+ </div>
+ <transition name="fade-slide">
+ <div :class="`${prefixCls}-entry`" v-show="!showDate">
+ <div :class="`${prefixCls}-entry-content`">
+ <div class="flex flex-col items-center">
+ <img :src="avatar" alt="" class="w-70px h-70px rounded-[50%]" />
+ <span class="text-14px my-10px text-[var(--logo-title-text-color)]">
+ {{ userName }}
+ </span>
+ </div>
+ <ElInput
+ type="password"
+ :placeholder="t('lock.placeholder')"
+ class="enter-x"
+ v-model="password"
+ />
+ <span :class="`text-14px ${prefixCls}-entry__err-msg enter-x`" v-if="errMsg">
+ {{ t('lock.message') }}
+ </span>
+ <div :class="`${prefixCls}-entry__footer enter-x`">
+ <ElButton
+ type="primary"
+ size="small"
+ class="mt-2 mr-2 enter-x"
+ link
+ :disabled="loading"
+ @click="handleShowForm(true)"
+ >
+ {{ t('common.back') }}
+ </ElButton>
+ <ElButton
+ type="primary"
+ size="small"
+ class="mt-2 mr-2 enter-x"
+ link
+ :disabled="loading"
+ @click="goLogin"
+ >
+ {{ t('lock.backToLogin') }}
+ </ElButton>
+ <ElButton
+ type="primary"
+ class="mt-2"
+ size="small"
+ link
+ @click="unLock()"
+ :disabled="loading"
+ >
+ {{ t('lock.entrySystem') }}
+ </ElButton>
+ </div>
+ </div>
+ </div>
+ </transition>
+
+ <div class="absolute bottom-5 w-full text-gray-300 xl:text-xl 2xl:text-3xl text-center enter-y">
+ <div class="text-5xl mb-4 enter-x" v-show="!showDate">
+ {{ hour }}:{{ minute }} <span class="text-3xl">{{ meridiem }}</span>
+ </div>
+ <div class="text-2xl">{{ year }}/{{ month }}/{{ day }} {{ week }}</div>
+ </div>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: '#{$namespace}-lock-page';
+
+// Small screen / tablet
+$screen-sm: 576px;
+
+// Medium screen / desktop
+$screen-md: 768px;
+
+// Large screen / wide desktop
+$screen-lg: 992px;
+
+// Extra large screen / full hd
+$screen-xl: 1200px;
+
+// Extra extra large screen / large desktop
+$screen-2xl: 1600px;
+
+$error-color: #ed6f6f;
+
+.#{$prefix-cls} {
+ z-index: 3000;
+
+ &__unlock {
+ transform: translate(-50%, 0);
+ }
+
+ &__hour,
+ &__minute {
+ display: flex;
+ font-weight: 700;
+ color: #bababa;
+ background-color: #141313;
+ border-radius: 30px;
+ justify-content: center;
+ align-items: center;
+
+ @media screen and (max-width: $screen-md) {
+ span:not(.meridiem) {
+ font-size: 160px;
+ }
+ }
+
+ @media screen and (min-width: $screen-md) {
+ span:not(.meridiem) {
+ font-size: 160px;
+ }
+ }
+
+ @media screen and (max-width: $screen-sm) {
+ span:not(.meridiem) {
+ font-size: 90px;
+ }
+ }
+ @media screen and (min-width: $screen-lg) {
+ span:not(.meridiem) {
+ font-size: 220px;
+ }
+ }
+
+ @media screen and (min-width: $screen-xl) {
+ span:not(.meridiem) {
+ font-size: 260px;
+ }
+ }
+ @media screen and (min-width: $screen-2xl) {
+ span:not(.meridiem) {
+ font-size: 320px;
+ }
+ }
+ }
+
+ &-entry {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(8px);
+ justify-content: center;
+ align-items: center;
+
+ &-content {
+ width: 260px;
+ }
+
+ &__header {
+ text-align: center;
+
+ &-img {
+ width: 70px;
+ margin: 0 auto;
+ border-radius: 50%;
+ }
+
+ &-name {
+ margin-top: 5px;
+ font-weight: 500;
+ color: #bababa;
+ }
+ }
+
+ &__err-msg {
+ display: inline-block;
+ margin-top: 10px;
+ color: $error-color;
+ }
+
+ &__footer {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/useRenderLayout.tsx b/src/layout/components/useRenderLayout.tsx
new file mode 100644
index 0000000..a631aa0
--- /dev/null
+++ b/src/layout/components/useRenderLayout.tsx
@@ -0,0 +1,294 @@
+import { computed } from 'vue'
+import { useAppStore } from '@/store/modules/app'
+import { Menu } from '@/layout/components/Menu'
+import { TabMenu } from '@/layout/components/TabMenu'
+import { TagsView } from '@/layout/components/TagsView'
+import { Logo } from '@/layout/components/Logo'
+import AppView from './AppView.vue'
+import ToolHeader from './ToolHeader.vue'
+import { ElScrollbar } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('layout')
+
+const appStore = useAppStore()
+
+const pageLoading = computed(() => appStore.getPageLoading)
+
+// 鏍囩椤�
+const tagsView = computed(() => appStore.getTagsView)
+
+// 鑿滃崟鎶樺彔
+const collapse = computed(() => appStore.getCollapse)
+
+// logo
+const logo = computed(() => appStore.logo)
+
+// 鍥哄畾澶撮儴
+const fixedHeader = computed(() => appStore.getFixedHeader)
+
+// 鏄惁鏄Щ鍔ㄧ
+const mobile = computed(() => appStore.getMobile)
+
+// 鍥哄畾鑿滃崟
+const fixedMenu = computed(() => appStore.getFixedMenu)
+
+export const useRenderLayout = () => {
+ const renderClassic = () => {
+ return (
+ <>
+ <div
+ class={[
+ 'absolute top-0 left-0 h-full layout-border__right',
+ { '!fixed z-3000': mobile.value }
+ ]}
+ >
+ {logo.value ? (
+ <Logo
+ class={[
+ 'bg-[var(--left-menu-bg-color)] relative',
+ {
+ '!pl-0': mobile.value && collapse.value,
+ 'w-[var(--left-menu-min-width)]': appStore.getCollapse,
+ 'w-[var(--left-menu-max-width)]': !appStore.getCollapse
+ }
+ ]}
+ style="transition: all var(--transition-time-02);"
+ ></Logo>
+ ) : undefined}
+ <Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu>
+ </div>
+ <div
+ class={[
+ `${prefixCls}-content`,
+ 'absolute top-0 h-[100%]',
+ {
+ 'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
+ collapse.value && !mobile.value && !mobile.value,
+ 'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
+ !collapse.value && !mobile.value && !mobile.value,
+ 'fixed !w-full !left-0': mobile.value
+ }
+ ]}
+ style="transition: all var(--transition-time-02);"
+ >
+ <ElScrollbar
+ v-loading={pageLoading.value}
+ class={[
+ `${prefixCls}-content-scrollbar`,
+ {
+ '!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]':
+ fixedHeader.value
+ }
+ ]}
+ >
+ <div
+ class={[
+ {
+ 'fixed top-0 left-0 z-10': fixedHeader.value,
+ 'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)]':
+ collapse.value && fixedHeader.value && !mobile.value,
+ 'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)]':
+ !collapse.value && fixedHeader.value && !mobile.value,
+ '!w-full !left-0': mobile.value
+ }
+ ]}
+ style="transition: all var(--transition-time-02);"
+ >
+ <ToolHeader
+ class={[
+ 'bg-[var(--top-header-bg-color)]',
+ {
+ 'layout-border__bottom': !tagsView.value
+ }
+ ]}
+ ></ToolHeader>
+
+ {tagsView.value ? (
+ <TagsView class="layout-border__top layout-border__bottom"></TagsView>
+ ) : undefined}
+ </div>
+
+ <AppView></AppView>
+ </ElScrollbar>
+ </div>
+ </>
+ )
+ }
+
+ const renderTopLeft = () => {
+ return (
+ <>
+ <div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom dark:bg-[var(--el-bg-color)]">
+ {logo.value ? <Logo class="custom-hover"></Logo> : undefined}
+
+ <ToolHeader class="flex-1"></ToolHeader>
+ </div>
+ <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex">
+ <Menu class="relative layout-border__right !h-full"></Menu>
+ <div
+ class={[
+ `${prefixCls}-content`,
+ 'h-[100%]',
+ {
+ 'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]':
+ collapse.value,
+ 'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]':
+ !collapse.value
+ }
+ ]}
+ style="transition: all var(--transition-time-02);"
+ >
+ <ElScrollbar
+ v-loading={pageLoading.value}
+ class={[
+ `${prefixCls}-content-scrollbar`,
+ {
+ '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
+ fixedHeader.value && tagsView.value
+ }
+ ]}
+ >
+ {tagsView.value ? (
+ <TagsView
+ class={[
+ 'layout-border__bottom absolute',
+ {
+ '!fixed top-0 left-0 z-10': fixedHeader.value,
+ 'w-[calc(100%-var(--left-menu-min-width))] !left-[var(--left-menu-min-width)] mt-[var(--logo-height)]':
+ collapse.value && fixedHeader.value,
+ 'w-[calc(100%-var(--left-menu-max-width))] !left-[var(--left-menu-max-width)] mt-[var(--logo-height)]':
+ !collapse.value && fixedHeader.value
+ }
+ ]}
+ style="transition: width var(--transition-time-02), left var(--transition-time-02);"
+ ></TagsView>
+ ) : undefined}
+
+ <AppView></AppView>
+ </ElScrollbar>
+ </div>
+ </div>
+ </>
+ )
+ }
+
+ const renderTop = () => {
+ return (
+ <>
+ <div
+ class={[
+ 'flex items-center justify-between bg-[var(--top-header-bg-color)] relative',
+ {
+ 'layout-border__bottom': !tagsView.value
+ }
+ ]}
+ >
+ {logo.value ? <Logo class="custom-hover"></Logo> : undefined}
+ <Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu>
+ <ToolHeader></ToolHeader>
+ </div>
+ <div class={[`${prefixCls}-content`, 'w-full h-[calc(100%-var(--top-tool-height))]']}>
+ <ElScrollbar
+ v-loading={pageLoading.value}
+ class={[
+ `${prefixCls}-content-scrollbar`,
+ {
+ '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
+ fixedHeader.value && tagsView.value
+ }
+ ]}
+ >
+ {tagsView.value ? (
+ <TagsView
+ class={[
+ 'layout-border__bottom layout-border__top relative',
+ {
+ '!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value
+ }
+ ]}
+ style="transition: width var(--transition-time-02), left var(--transition-time-02);"
+ ></TagsView>
+ ) : undefined}
+
+ <AppView></AppView>
+ </ElScrollbar>
+ </div>
+ </>
+ )
+ }
+
+ const renderCutMenu = () => {
+ return (
+ <>
+ <div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom">
+ {logo.value ? <Logo class="custom-hover !pr-15px"></Logo> : undefined}
+
+ <ToolHeader class="flex-1"></ToolHeader>
+ </div>
+ <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-full flex">
+ <TabMenu></TabMenu>
+ <div
+ class={[
+ `${prefixCls}-content`,
+ 'h-[100%]',
+ {
+ 'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
+ collapse.value && !fixedMenu.value,
+ 'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]':
+ !collapse.value && !fixedMenu.value,
+ 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
+ collapse.value && fixedMenu.value,
+ 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]':
+ !collapse.value && fixedMenu.value
+ }
+ ]}
+ style="transition: all var(--transition-time-02);"
+ >
+ <ElScrollbar
+ v-loading={pageLoading.value}
+ class={[
+ `${prefixCls}-content-scrollbar`,
+ {
+ '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
+ fixedHeader.value && tagsView.value
+ }
+ ]}
+ >
+ {tagsView.value ? (
+ <TagsView
+ class={[
+ 'relative layout-border__bottom',
+ {
+ '!fixed top-0 left-0 z-10': fixedHeader.value,
+ 'w-[calc(100%-var(--tab-menu-min-width))] !left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
+ collapse.value && fixedHeader.value && !fixedMenu.value,
+ 'w-[calc(100%-var(--tab-menu-max-width))] !left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
+ !collapse.value && fixedHeader.value && !fixedMenu.value,
+ 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-min-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]':
+ collapse.value && fixedHeader.value && fixedMenu.value,
+ 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] !left-[calc(var(--tab-menu-max-width)+var(--left-menu-max-width))] mt-[var(--logo-height)]':
+ !collapse.value && fixedHeader.value && fixedMenu.value
+ }
+ ]}
+ style="transition: width var(--transition-time-02), left var(--transition-time-02);"
+ ></TagsView>
+ ) : undefined}
+
+ <AppView></AppView>
+ </ElScrollbar>
+ </div>
+ </div>
+ </>
+ )
+ }
+
+ return {
+ renderClassic,
+ renderTopLeft,
+ renderTop,
+ renderCutMenu
+ }
+}
diff --git a/src/locales/en.ts b/src/locales/en.ts
new file mode 100644
index 0000000..bd4c0b4
--- /dev/null
+++ b/src/locales/en.ts
@@ -0,0 +1,462 @@
+export default {
+ common: {
+ inputText: 'Please input',
+ selectText: 'Please select',
+ startTimeText: 'Start time',
+ endTimeText: 'End time',
+ login: 'Login',
+ required: 'This is required',
+ loginOut: 'Login out',
+ document: 'Document',
+ profile: 'User Center',
+ reminder: 'Reminder',
+ loginOutMessage: 'Exit the system?',
+ back: 'Back',
+ ok: 'OK',
+ save: 'Save',
+ cancel: 'Cancel',
+ close: 'Close',
+ reload: 'Reload current',
+ success: 'Success',
+ closeTab: 'Close current',
+ closeTheLeftTab: 'Close left',
+ closeTheRightTab: 'Close right',
+ closeOther: 'Close other',
+ closeAll: 'Close all',
+ prevLabel: 'Prev',
+ nextLabel: 'Next',
+ skipLabel: 'Jump',
+ doneLabel: 'End',
+ menu: 'Menu',
+ menuDes: 'Menu bar rendered in routed structure',
+ collapse: 'Collapse',
+ collapseDes: 'Expand and zoom the menu bar',
+ tagsView: 'Tags view',
+ tagsViewDes: 'Used to record routing history',
+ tool: 'Tool',
+ toolDes: 'Used to set up custom systems',
+ query: 'Query',
+ reset: 'Reset',
+ shrink: 'Put away',
+ expand: 'Expand',
+ confirmTitle: 'System Hint',
+ exportMessage: 'Whether to confirm export data item?',
+ importMessage: 'Whether to confirm import data item?',
+ createSuccess: 'Create Success',
+ updateSuccess: 'Update Success',
+ delMessage: 'Delete the selected data?',
+ delDataMessage: 'Delete the data?',
+ delNoData: 'Please select the data to delete',
+ delSuccess: 'Deleted successfully',
+ index: 'Index',
+ status: 'Status',
+ createTime: 'Create Time',
+ updateTime: 'Update Time',
+ copy: 'Copy',
+ copySuccess: 'Copy Success',
+ copyError: 'Copy Error'
+ },
+ lock: {
+ lockScreen: 'Lock screen',
+ lock: 'Lock',
+ lockPassword: 'Lock screen password',
+ unlock: 'Click to unlock',
+ backToLogin: 'Back to login',
+ entrySystem: 'Entry the system',
+ placeholder: 'Please enter the lock screen password',
+ message: 'Lock screen password error'
+ },
+ error: {
+ noPermission: `Sorry, you don't have permission to access this page.`,
+ pageError: 'Sorry, the page you visited does not exist.',
+ networkError: 'Sorry, the server reported an error.',
+ returnToHome: 'Return to home'
+ },
+ permission: {
+ hasPermission: `Please set the operation permission label value`,
+ hasRole: `Please set the role permission tag value`
+ },
+ setting: {
+ projectSetting: 'Project setting',
+ theme: 'Theme',
+ layout: 'Layout',
+ systemTheme: 'System theme',
+ menuTheme: 'Menu theme',
+ interfaceDisplay: 'Interface display',
+ breadcrumb: 'Breadcrumb',
+ breadcrumbIcon: 'Breadcrumb icon',
+ collapseMenu: 'Collapse menu',
+ hamburgerIcon: 'Hamburger icon',
+ screenfullIcon: 'Screenfull icon',
+ sizeIcon: 'Size icon',
+ localeIcon: 'Locale icon',
+ messageIcon: 'Message icon',
+ tagsView: 'Tags view',
+ logo: 'Logo',
+ greyMode: 'Grey mode',
+ fixedHeader: 'Fixed header',
+ headerTheme: 'Header theme',
+ cutMenu: 'Cut Menu',
+ copy: 'Copy',
+ clearAndReset: 'Clear cache and reset',
+ copySuccess: 'Copy success',
+ copyFailed: 'Copy failed',
+ footer: 'Footer',
+ uniqueOpened: 'Unique opened',
+ tagsViewIcon: 'Tags view icon',
+ reExperienced: 'Please exit the login experience again',
+ fixedMenu: 'Fixed menu'
+ },
+ size: {
+ default: 'Default',
+ large: 'Large',
+ small: 'Small'
+ },
+ login: {
+ welcome: 'Welcome to the system',
+ message: 'Backstage management system',
+ tenantname: 'TenantName',
+ username: 'Username',
+ password: 'Password',
+ code: 'verification code',
+ login: 'Sign in',
+ relogin: 'Sign in again',
+ otherLogin: 'Sign in with',
+ register: 'Register',
+ checkPassword: 'Confirm password',
+ remember: 'Remember me',
+ hasUser: 'Existing account? Go to login',
+ forgetPassword: 'Forget password?',
+ tenantNamePlaceholder: 'Please Enter Tenant Name',
+ usernamePlaceholder: 'Please Enter Username',
+ passwordPlaceholder: 'Please Enter Password',
+ codePlaceholder: 'Please Enter Verification Code',
+ mobileTitle: 'Mobile sign in',
+ mobileNumber: 'Mobile Number',
+ mobileNumberPlaceholder: 'Plaease Enter Mobile Number',
+ backLogin: 'back',
+ getSmsCode: 'Get SMS Code',
+ btnMobile: 'Mobile sign in',
+ btnQRCode: 'QR code sign in',
+ qrcode: 'Scan the QR code to log in',
+ btnRegister: 'Sign up',
+ SmsSendMsg: 'code has been sent',
+ resetPassword: "Reset Password",
+ resetPasswordSuccess: "Reset Password Success",
+ invalidTenantName:"Invalid Tenant Name"
+ },
+ captcha: {
+ verify: 'Verify',
+ verification: 'Please complete security verification',
+ slide: 'Swipe right to complete verification',
+ point: 'Please click',
+ code: 'Please enter the verification code',
+ success: 'Verification succeeded',
+ fail: 'verification failed'
+ },
+ router: {
+ login: 'Login',
+ home: 'Home',
+ analysis: 'Analysis',
+ workplace: 'Workplace'
+ },
+ analysis: {
+ newUser: 'New user',
+ unreadInformation: 'Unread information',
+ transactionAmount: 'Transaction amount',
+ totalShopping: 'Total Shopping',
+ monthlySales: 'Monthly sales',
+ userAccessSource: 'User access source',
+ january: 'January',
+ february: 'February',
+ march: 'March',
+ april: 'April',
+ may: 'May',
+ june: 'June',
+ july: 'July',
+ august: 'August',
+ september: 'September',
+ october: 'October',
+ november: 'November',
+ december: 'December',
+ estimate: 'Estimate',
+ actual: 'Actual',
+ directAccess: 'Airect access',
+ mailMarketing: 'Mail marketing',
+ allianceAdvertising: 'Alliance advertising',
+ videoAdvertising: 'Video advertising',
+ searchEngines: 'Search engines',
+ weeklyUserActivity: 'Weekly user activity',
+ activeQuantity: 'Active quantity',
+ monday: 'Monday',
+ tuesday: 'Tuesday',
+ wednesday: 'Wednesday',
+ thursday: 'Thursday',
+ friday: 'Friday',
+ saturday: 'Saturday',
+ sunday: 'Sunday'
+ },
+ workplace: {
+ welcome: 'Hello',
+ happyDay: 'Wish you happy every day!',
+ toady: `It's sunny today`,
+ notice: 'Announcement',
+ project: 'Project',
+ access: 'Project access',
+ toDo: 'To do',
+ introduction: 'A serious introduction',
+ shortcutOperation: 'Quick entry',
+ operation: 'Operation',
+ index: 'Index',
+ personal: 'Personal',
+ team: 'Team',
+ quote: 'Quote',
+ contribution: 'Contribution',
+ hot: 'Hot',
+ yield: 'Yield',
+ dynamic: 'Dynamic',
+ push: 'push',
+ follow: 'Follow'
+ },
+ form: {
+ input: 'Input',
+ inputNumber: 'InputNumber',
+ default: 'Default',
+ icon: 'Icon',
+ mixed: 'Mixed',
+ textarea: 'Textarea',
+ slot: 'Slot',
+ position: 'Position',
+ autocomplete: 'Autocomplete',
+ select: 'Select',
+ selectGroup: 'Select Group',
+ selectV2: 'SelectV2',
+ cascader: 'Cascader',
+ switch: 'Switch',
+ rate: 'Rate',
+ colorPicker: 'Color Picker',
+ transfer: 'Transfer',
+ render: 'Render',
+ radio: 'Radio',
+ button: 'Button',
+ checkbox: 'Checkbox',
+ slider: 'Slider',
+ datePicker: 'Date Picker',
+ shortcuts: 'Shortcuts',
+ today: 'Today',
+ yesterday: 'Yesterday',
+ aWeekAgo: 'A week ago',
+ week: 'Week',
+ year: 'Year',
+ month: 'Month',
+ dates: 'Dates',
+ daterange: 'Date Range',
+ monthrange: 'Month Range',
+ dateTimePicker: 'DateTimePicker',
+ dateTimerange: 'Datetime Range',
+ timePicker: 'Time Picker',
+ timeSelect: 'Time Select',
+ inputPassword: 'input Password',
+ passwordStrength: 'Password Strength',
+ operate: 'operate',
+ change: 'Change',
+ restore: 'Restore',
+ disabled: 'Disabled',
+ disablement: 'Disablement',
+ delete: 'Delete',
+ add: 'Add',
+ setValue: 'Set value',
+ resetValue: 'Reset value',
+ set: 'Set',
+ subitem: 'Subitem',
+ formValidation: 'Form validation',
+ verifyReset: 'Verify reset',
+ remark: 'Remark'
+ },
+ watermark: {
+ watermark: 'Watermark'
+ },
+ table: {
+ table: 'Table',
+ index: 'Index',
+ title: 'Title',
+ author: 'Author',
+ createTime: 'Create time',
+ action: 'Action',
+ pagination: 'pagination',
+ reserveIndex: 'Reserve index',
+ restoreIndex: 'Restore index',
+ showSelections: 'Show selections',
+ hiddenSelections: 'Restore selections',
+ showExpandedRows: 'Show expanded rows',
+ hiddenExpandedRows: 'Hidden expanded rows',
+ header: 'Header'
+ },
+ action: {
+ create: 'Create',
+ add: 'Add',
+ del: 'Delete',
+ delete: 'Delete',
+ edit: 'Edit',
+ update: 'Update',
+ preview: 'Preview',
+ more: 'More',
+ sync: 'Sync',
+ save: 'Save',
+ detail: 'Detail',
+ export: 'Export',
+ import: 'Import',
+ generate: 'Generate',
+ logout: 'Login Out',
+ test: 'Test',
+ typeCreate: 'Dict Type Create',
+ typeUpdate: 'Dict Type Eidt',
+ dataCreate: 'Dict Data Create',
+ dataUpdate: 'Dict Data Eidt',
+ fileUpload: 'File Upload'
+ },
+ dialog: {
+ dialog: 'Dialog',
+ open: 'Open',
+ close: 'Close'
+ },
+ sys: {
+ api: {
+ operationFailed: 'Operation failed',
+ errorTip: 'Error Tip',
+ errorMessage: 'The operation failed, the system is abnormal!',
+ timeoutMessage: 'Login timed out, please log in again!',
+ apiTimeoutMessage: 'The interface request timed out, please refresh the page and try again!',
+ apiRequestFailed: 'The interface request failed, please try again later!',
+ networkException: 'network anomaly',
+ networkExceptionMsg:
+ 'Please check if your network connection is normal! The network is abnormal',
+
+ errMsg401: 'The user does not have permission (token, user name, password error)!',
+ errMsg403: 'The user is authorized, but access is forbidden!',
+ errMsg404: 'Network request error, the resource was not found!',
+ errMsg405: 'Network request error, request method not allowed!',
+ errMsg408: 'Network request timed out!',
+ errMsg500: 'Server error, please contact the administrator!',
+ errMsg501: 'The network is not implemented!',
+ errMsg502: 'Network Error!',
+ errMsg503: 'The service is unavailable, the server is temporarily overloaded or maintained!',
+ errMsg504: 'Network timeout!',
+ errMsg505: 'The http version does not support the request!',
+ errMsg901: 'Demo mode, no write operations are possible!'
+ },
+ app: {
+ logoutTip: 'Reminder',
+ logoutMessage: 'Confirm to exit the system?',
+ menuLoading: 'Menu loading...'
+ },
+ exception: {
+ backLogin: 'Back Login',
+ backHome: 'Back Home',
+ subTitle403: "Sorry, you don't have access to this page.",
+ subTitle404: 'Sorry, the page you visited does not exist.',
+ subTitle500: 'Sorry, the server is reporting an error.',
+ noDataTitle: 'No data on the current page.',
+ networkErrorTitle: 'Network Error',
+ networkErrorSubTitle:
+ 'Sorry, Your network connection has been disconnected, please check your network!'
+ },
+ lock: {
+ unlock: 'Click to unlock',
+ alert: 'Lock screen password error',
+ backToLogin: 'Back to login',
+ entry: 'Enter the system',
+ placeholder: 'Please enter the lock screen password or user password'
+ },
+ login: {
+ backSignIn: 'Back sign in',
+ mobileSignInFormTitle: 'Mobile sign in',
+ qrSignInFormTitle: 'Qr code sign in',
+ signInFormTitle: 'Sign in',
+ signUpFormTitle: 'Sign up',
+ forgetFormTitle: 'Reset password',
+
+ signInTitle: 'Backstage management system',
+ signInDesc: 'Enter your personal details and get started!',
+ policy: 'I agree to the xxx Privacy Policy',
+ scanSign: `scanning the code to complete the login`,
+
+ loginButton: 'Sign in',
+ registerButton: 'Sign up',
+ rememberMe: 'Remember me',
+ forgetPassword: 'Forget Password?',
+ otherSignIn: 'Sign in with',
+
+ // notify
+ loginSuccessTitle: 'Login successful',
+ loginSuccessDesc: 'Welcome back',
+
+ // placeholder
+ accountPlaceholder: 'Please input username',
+ passwordPlaceholder: 'Please input password',
+ smsPlaceholder: 'Please input sms code',
+ mobilePlaceholder: 'Please input mobile',
+ policyPlaceholder: 'Register after checking',
+ diffPwd: 'The two passwords are inconsistent',
+
+ userName: 'Username',
+ password: 'Password',
+ confirmPassword: 'Confirm Password',
+ email: 'Email',
+ smsCode: 'SMS code',
+ mobile: 'Mobile'
+ }
+ },
+ profile: {
+ user: {
+ title: 'Personal Information',
+ username: 'User Name',
+ nickname: 'Nick Name',
+ mobile: 'Phone Number',
+ email: 'User Mail',
+ dept: 'Department',
+ posts: 'Position',
+ roles: 'Own Role',
+ sex: 'Sex',
+ man: 'Man',
+ woman: 'Woman',
+ createTime: 'Created Date'
+ },
+ info: {
+ title: 'Basic Information',
+ basicInfo: 'Basic Information',
+ resetPwd: 'Reset Password',
+ userSocial: 'Social Information'
+ },
+ rules: {
+ nickname: 'Please Enter User Nickname',
+ mail: 'Please Input The Email Address',
+ truemail: 'Please Input The Correct Email Address',
+ phone: 'Please Enter The Phone Number',
+ truephone: 'Please Enter The Correct Phone Number'
+ },
+ password: {
+ oldPassword: 'Old PassWord',
+ newPassword: 'New Password',
+ confirmPassword: 'Confirm Password',
+ oldPwdMsg: 'Please Enter Old Password',
+ newPwdMsg: 'Please Enter New Password',
+ cfPwdMsg: 'Please Enter Confirm Password',
+ diffPwd: 'The Passwords Entered Twice No Match'
+ }
+ },
+ cropper: {
+ selectImage: 'Select Image',
+ uploadSuccess: 'Uploaded success!',
+ modalTitle: 'Avatar upload',
+ okText: 'Confirm and upload',
+ btn_reset: 'Reset',
+ btn_rotate_left: 'Counterclockwise rotation',
+ btn_rotate_right: 'Clockwise rotation',
+ btn_scale_x: 'Flip horizontal',
+ btn_scale_y: 'Flip vertical',
+ btn_zoom_in: 'Zoom in',
+ btn_zoom_out: 'Zoom out',
+ preview: 'Preivew'
+ }
+}
\ No newline at end of file
diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts
new file mode 100644
index 0000000..3b6c2e9
--- /dev/null
+++ b/src/locales/zh-CN.ts
@@ -0,0 +1,458 @@
+export default {
+ common: {
+ inputText: '璇疯緭鍏�',
+ selectText: '璇烽�夋嫨',
+ startTimeText: '寮�濮嬫椂闂�',
+ endTimeText: '缁撴潫鏃堕棿',
+ login: '鐧诲綍',
+ required: '璇ラ」涓哄繀濉」',
+ loginOut: '閫�鍑虹郴缁�',
+ document: '椤圭洰鏂囨。',
+ profile: '涓汉涓績',
+ reminder: '娓╅Θ鎻愮ず',
+ loginOutMessage: '鏄惁閫�鍑烘湰绯荤粺锛�',
+ back: '杩斿洖',
+ ok: '纭畾',
+ save: '淇濆瓨',
+ cancel: '鍙栨秷',
+ close: '鍏抽棴',
+ reload: '閲嶆柊鍔犺浇',
+ success: '鎴愬姛',
+ closeTab: '鍏抽棴鏍囩椤�',
+ closeTheLeftTab: '鍏抽棴宸︿晶鏍囩椤�',
+ closeTheRightTab: '鍏抽棴鍙充晶鏍囩椤�',
+ closeOther: '鍏抽棴鍏朵粬鏍囩椤�',
+ closeAll: '鍏抽棴鍏ㄩ儴鏍囩椤�',
+ prevLabel: '涓婁竴姝�',
+ nextLabel: '涓嬩竴姝�',
+ skipLabel: '璺宠繃',
+ doneLabel: '缁撴潫',
+ menu: '鑿滃崟',
+ menuDes: '浠ヨ矾鐢辩殑缁撴瀯娓叉煋鐨勮彍鍗曟爮',
+ collapse: '灞曞紑缂╂敹',
+ collapseDes: '灞曞紑鍜岀缉鏀捐彍鍗曟爮',
+ tagsView: '鏍囩椤�',
+ tagsViewDes: '鐢ㄤ簬璁板綍璺敱鍘嗗彶璁板綍',
+ tool: '宸ュ叿',
+ toolDes: '鐢ㄤ簬璁剧疆瀹氬埗绯荤粺',
+ query: '鏌ヨ',
+ reset: '閲嶇疆',
+ shrink: '鏀惰捣',
+ expand: '灞曞紑',
+ confirmTitle: '绯荤粺鎻愮ず',
+ exportMessage: '鏄惁纭瀵煎嚭鏁版嵁椤癸紵',
+ importMessage: '鏄惁纭瀵煎叆鏁版嵁椤癸紵',
+ createSuccess: '鏂板鎴愬姛',
+ updateSuccess: '淇敼鎴愬姛',
+ delMessage: '鏄惁鍒犻櫎鎵�閫変腑鏁版嵁锛�',
+ delDataMessage: '鏄惁鍒犻櫎鏁版嵁锛�',
+ delNoData: '璇烽�夋嫨闇�瑕佸垹闄ょ殑鏁版嵁',
+ delSuccess: '鍒犻櫎鎴愬姛',
+ index: '搴忓彿',
+ status: '鐘舵��',
+ createTime: '鍒涘缓鏃堕棿',
+ updateTime: '鏇存柊鏃堕棿',
+ copy: '澶嶅埗',
+ copySuccess: '澶嶅埗鎴愬姛',
+ copyError: '澶嶅埗澶辫触'
+ },
+ lock: {
+ lockScreen: '閿佸畾灞忓箷',
+ lock: '閿佸畾',
+ lockPassword: '閿佸睆瀵嗙爜',
+ unlock: '鐐瑰嚮瑙i攣',
+ backToLogin: '杩斿洖鐧诲綍',
+ entrySystem: '杩涘叆绯荤粺',
+ placeholder: '璇疯緭鍏ラ攣灞忓瘑鐮�',
+ message: '閿佸睆瀵嗙爜閿欒'
+ },
+ error: {
+ noPermission: `鎶辨瓑锛屾偍鏃犳潈璁块棶姝ら〉闈€�俙,
+ pageError: '鎶辨瓑锛屾偍璁块棶鐨勯〉闈笉瀛樺湪銆�',
+ networkError: '鎶辨瓑锛屾湇鍔″櫒鎶ュ憡閿欒銆�',
+ returnToHome: '杩斿洖棣栭〉'
+ },
+ permission: {
+ hasPermission: `璇疯缃搷浣滄潈闄愭爣绛惧�糮,
+ hasRole: `璇疯缃鑹叉潈闄愭爣绛惧�糮
+ },
+ setting: {
+ projectSetting: '椤圭洰閰嶇疆',
+ theme: '涓婚',
+ layout: '甯冨眬',
+ systemTheme: '绯荤粺涓婚',
+ menuTheme: '鑿滃崟涓婚',
+ interfaceDisplay: '鐣岄潰鏄剧ず',
+ breadcrumb: '闈㈠寘灞�',
+ breadcrumbIcon: '闈㈠寘灞戝浘鏍�',
+ collapseMenu: '鎶樺彔鑿滃崟',
+ hamburgerIcon: '鎶樺彔鍥炬爣',
+ screenfullIcon: '鍏ㄥ睆鍥炬爣',
+ sizeIcon: '灏哄鍥炬爣',
+ localeIcon: '澶氳瑷�鍥炬爣',
+ messageIcon: '娑堟伅鍥炬爣',
+ tagsView: '鏍囩椤�',
+ tagsViewImmerse: '鏍囩椤垫矇娴�',
+ logo: '鏍囧織',
+ greyMode: '鐏拌壊妯″紡',
+ fixedHeader: '鍥哄畾澶撮儴',
+ headerTheme: '澶撮儴涓婚',
+ cutMenu: '鍒囧壊鑿滃崟',
+ copy: '鎷疯礉',
+ clearAndReset: '娓呴櫎缂撳瓨骞朵笖閲嶇疆',
+ copySuccess: '鎷疯礉鎴愬姛',
+ copyFailed: '鎷疯礉澶辫触',
+ footer: '椤佃剼',
+ uniqueOpened: '鑿滃崟鎵嬮鐞�',
+ tagsViewIcon: '鏍囩椤靛浘鏍�',
+ reExperienced: '璇烽噸鏂伴��鍑虹櫥褰曚綋楠�',
+ fixedMenu: '鍥哄畾鑿滃崟'
+ },
+ size: {
+ default: '榛樿',
+ large: '澶�',
+ small: '灏�'
+ },
+ login: {
+ welcome: '娆㈣繋浣跨敤鏈郴缁�',
+ message: '寮�绠卞嵆鐢ㄧ殑涓悗鍙扮鐞嗙郴缁�',
+ tenantname: '绉熸埛鍚嶇О',
+ username: '鐢ㄦ埛鍚�',
+ password: '瀵嗙爜',
+ code: '楠岃瘉鐮�',
+ login: '鐧诲綍',
+ relogin: '閲嶆柊鐧诲綍',
+ otherLogin: '鍏朵粬鐧诲綍鏂瑰紡',
+ register: '娉ㄥ唽',
+ checkPassword: '纭瀵嗙爜',
+ remember: '璁颁綇鎴�',
+ hasUser: '宸叉湁璐﹀彿锛熷幓鐧诲綍',
+ forgetPassword: '蹇樿瀵嗙爜?',
+ tenantNamePlaceholder: '璇疯緭鍏ョ鎴峰悕绉�',
+ usernamePlaceholder: '璇疯緭鍏ョ敤鎴峰悕',
+ passwordPlaceholder: '璇疯緭鍏ュ瘑鐮�',
+ codePlaceholder: '璇疯緭鍏ラ獙璇佺爜',
+ mobileTitle: '鎵嬫満鐧诲綍',
+ mobileNumber: '鎵嬫満鍙风爜',
+ mobileNumberPlaceholder: '璇疯緭鍏ユ墜鏈哄彿鐮�',
+ backLogin: '杩斿洖',
+ getSmsCode: '鑾峰彇楠岃瘉鐮�',
+ btnMobile: '鎵嬫満鐧诲綍',
+ btnQRCode: '浜岀淮鐮佺櫥褰�',
+ qrcode: '鎵弿浜岀淮鐮佺櫥褰�',
+ btnRegister: '娉ㄥ唽',
+ SmsSendMsg: '楠岃瘉鐮佸凡鍙戦��',
+ resetPassword: '閲嶇疆瀵嗙爜',
+ resetPasswordSuccess: '閲嶇疆瀵嗙爜鎴愬姛',
+ invalidTenantName: '鏃犳晥鐨勭鎴峰悕绉�'
+ },
+ captcha: {
+ verify: '楠岃瘉',
+ verification: '璇峰畬鎴愬畨鍏ㄩ獙璇�',
+ slide: '鍚戝彸婊戝姩瀹屾垚楠岃瘉',
+ point: '璇蜂緷娆$偣鍑�',
+ code: '璇疯緭鍏ラ獙璇佺爜',
+ success: '楠岃瘉鎴愬姛',
+ fail: '楠岃瘉澶辫触'
+ },
+ router: {
+ login: '鐧诲綍',
+ socialLogin: '绀句氦鐧诲綍',
+ home: '棣栭〉',
+ analysis: '鍒嗘瀽椤�',
+ workplace: '宸ヤ綔鍙�'
+ },
+ analysis: {
+ newUser: '鏂板鐢ㄦ埛',
+ unreadInformation: '鏈娑堟伅',
+ transactionAmount: '鎴愪氦閲戦',
+ totalShopping: '璐墿鎬婚噺',
+ monthlySales: '姣忔湀閿�鍞',
+ userAccessSource: '鐢ㄦ埛璁块棶鏉ユ簮',
+ january: '涓�鏈�',
+ february: '浜屾湀',
+ march: '涓夋湀',
+ april: '鍥涙湀',
+ may: '浜旀湀',
+ june: '鍏湀',
+ july: '涓冩湀',
+ august: '鍏湀',
+ september: '涔濇湀',
+ october: '鍗佹湀',
+ november: '鍗佷竴鏈�',
+ december: '鍗佷簩鏈�',
+ estimate: '棰勮',
+ actual: '瀹為檯',
+ directAccess: '鐩存帴璁块棶',
+ mailMarketing: '閭欢钀ラ攢',
+ allianceAdvertising: '鑱旂洘骞垮憡',
+ videoAdvertising: '瑙嗛骞垮憡',
+ searchEngines: '鎼滅储寮曟搸',
+ weeklyUserActivity: '姣忓懆鐢ㄦ埛娲昏穬閲�',
+ activeQuantity: '娲昏穬閲�',
+ monday: '鍛ㄤ竴',
+ tuesday: '鍛ㄤ簩',
+ wednesday: '鍛ㄤ笁',
+ thursday: '鍛ㄥ洓',
+ friday: '鍛ㄤ簲',
+ saturday: '鍛ㄥ叚',
+ sunday: '鍛ㄦ棩'
+ },
+ workplace: {
+ welcome: '浣犲ソ',
+ happyDay: '绁濅綘寮�蹇冩瘡涓�澶�!',
+ toady: '浠婃棩鏅�',
+ notice: '閫氱煡鍏憡',
+ project: '椤圭洰鏁�',
+ access: '椤圭洰璁块棶',
+ toDo: '寰呭姙',
+ introduction: '涓�涓缁忕殑绠�浠�',
+ shortcutOperation: '蹇嵎鍏ュ彛',
+ operation: '鎿嶄綔',
+ index: '鎸囨暟',
+ personal: '涓汉',
+ team: '鍥㈤槦',
+ quote: '寮曠敤',
+ contribution: '璐$尞',
+ hot: '鐑害',
+ yield: '浜ч噺',
+ dynamic: '鍔ㄦ��',
+ push: '鎺ㄩ��',
+ follow: '鍏虫敞'
+ },
+ form: {
+ input: '杈撳叆妗�',
+ inputNumber: '鏁板瓧杈撳叆妗�',
+ default: '榛樿',
+ icon: '鍥炬爣',
+ mixed: '澶嶅悎鍨�',
+ textarea: '澶氳鏂囨湰',
+ slot: '鎻掓Ы',
+ position: '浣嶇疆',
+ autocomplete: '鑷姩琛ュ叏',
+ select: '閫夋嫨鍣�',
+ selectGroup: '閫夐」鍒嗙粍',
+ selectV2: '铏氭嫙鍒楄〃閫夋嫨鍣�',
+ cascader: '绾ц仈閫夋嫨鍣�',
+ switch: '寮�鍏�',
+ rate: '璇勫垎',
+ colorPicker: '棰滆壊閫夋嫨鍣�',
+ transfer: '绌挎妗�',
+ render: '娓叉煋鍣�',
+ radio: '鍗曢�夋',
+ button: '鎸夐挳',
+ checkbox: '澶氶�夋',
+ slider: '婊戝潡',
+ datePicker: '鏃ユ湡閫夋嫨鍣�',
+ shortcuts: '蹇嵎閫夐」',
+ today: '浠婂ぉ',
+ yesterday: '鏄ㄥぉ',
+ aWeekAgo: '涓�鍛ㄥ墠',
+ week: '鍛�',
+ year: '骞�',
+ month: '鏈�',
+ dates: '鏃ユ湡',
+ daterange: '鏃ユ湡鑼冨洿',
+ monthrange: '鏈堜唤鑼冨洿',
+ dateTimePicker: '鏃ユ湡鏃堕棿閫夋嫨鍣�',
+ dateTimerange: '鏃ユ湡鏃堕棿鑼冨洿',
+ timePicker: '鏃堕棿閫夋嫨鍣�',
+ timeSelect: '鏃堕棿閫夋嫨',
+ inputPassword: '瀵嗙爜杈撳叆妗�',
+ passwordStrength: '瀵嗙爜寮哄害',
+ operate: '鎿嶄綔',
+ change: '鏇存敼',
+ restore: '杩樺師',
+ disabled: '绂佺敤',
+ disablement: '瑙i櫎绂佺敤',
+ delete: '鍒犻櫎',
+ add: '娣诲姞',
+ setValue: '璁剧疆鍊�',
+ resetValue: '閲嶇疆鍊�',
+ set: '璁剧疆',
+ subitem: '瀛愰」',
+ formValidation: '琛ㄥ崟楠岃瘉',
+ verifyReset: '楠岃瘉閲嶇疆',
+ remark: '澶囨敞'
+ },
+ watermark: {
+ watermark: '姘村嵃'
+ },
+ table: {
+ table: '琛ㄦ牸',
+ index: '搴忓彿',
+ title: '鏍囬',
+ author: '浣滆��',
+ createTime: '鍒涘缓鏃堕棿',
+ action: '鎿嶄綔',
+ pagination: '鍒嗛〉',
+ reserveIndex: '鍙犲姞搴忓彿',
+ restoreIndex: '杩樺師搴忓彿',
+ showSelections: '鏄剧ず澶氶��',
+ hiddenSelections: '闅愯棌澶氶��',
+ showExpandedRows: '鏄剧ず灞曞紑琛�',
+ hiddenExpandedRows: '闅愯棌灞曞紑琛�',
+ header: '澶撮儴'
+ },
+ action: {
+ create: '鏂板',
+ add: '鏂板',
+ del: '鍒犻櫎',
+ delete: '鍒犻櫎',
+ edit: '缂栬緫',
+ update: '缂栬緫',
+ preview: '棰勮',
+ more: '鏇村',
+ sync: '鍚屾',
+ save: '淇濆瓨',
+ detail: '璇︽儏',
+ export: '瀵煎嚭',
+ import: '瀵煎叆',
+ generate: '鐢熸垚',
+ logout: '寮哄埗閫�鍑�',
+ test: '娴嬭瘯',
+ typeCreate: '瀛楀吀绫诲瀷鏂板',
+ typeUpdate: '瀛楀吀绫诲瀷缂栬緫',
+ dataCreate: '瀛楀吀鏁版嵁鏂板',
+ dataUpdate: '瀛楀吀鏁版嵁缂栬緫'
+ },
+ dialog: {
+ dialog: '寮圭獥',
+ open: '鎵撳紑',
+ close: '鍏抽棴'
+ },
+ sys: {
+ api: {
+ operationFailed: '鎿嶄綔澶辫触',
+ errorTip: '閿欒鎻愮ず',
+ errorMessage: '鎿嶄綔澶辫触,绯荤粺寮傚父!',
+ timeoutMessage: '鐧诲綍瓒呮椂,璇烽噸鏂扮櫥褰�!',
+ apiTimeoutMessage: '鎺ュ彛璇锋眰瓒呮椂,璇峰埛鏂伴〉闈㈤噸璇�!',
+ apiRequestFailed: '璇锋眰鍑洪敊锛岃绋嶅�欓噸璇�',
+ networkException: '缃戠粶寮傚父',
+ networkExceptionMsg: '缃戠粶寮傚父锛岃妫�鏌ユ偍鐨勭綉缁滆繛鎺ユ槸鍚︽甯�!',
+ errMsg401: '鐢ㄦ埛娌℃湁鏉冮檺锛堜护鐗屻�佺敤鎴峰悕銆佸瘑鐮侀敊璇級!',
+ errMsg403: '鐢ㄦ埛寰楀埌鎺堟潈锛屼絾鏄闂槸琚姝㈢殑銆�!',
+ errMsg404: '缃戠粶璇锋眰閿欒,鏈壘鍒拌璧勬簮!',
+ errMsg405: '缃戠粶璇锋眰閿欒,璇锋眰鏂规硶鏈厑璁�!',
+ errMsg408: '缃戠粶璇锋眰瓒呮椂!',
+ errMsg500: '鏈嶅姟鍣ㄩ敊璇�,璇疯仈绯荤鐞嗗憳!',
+ errMsg501: '缃戠粶鏈疄鐜�!',
+ errMsg502: '缃戠粶閿欒!',
+ errMsg503: '鏈嶅姟涓嶅彲鐢紝鏈嶅姟鍣ㄦ殏鏃惰繃杞芥垨缁存姢!',
+ errMsg504: '缃戠粶瓒呮椂!',
+ errMsg505: 'http鐗堟湰涓嶆敮鎸佽璇锋眰!',
+ errMsg901: '婕旂ず妯″紡锛屾棤娉曡繘琛屽啓鎿嶄綔!'
+ },
+ app: {
+ logoutTip: '娓╅Θ鎻愰啋',
+ logoutMessage: '鏄惁纭閫�鍑虹郴缁�?',
+ menuLoading: '鑿滃崟鍔犺浇涓�...'
+ },
+ exception: {
+ backLogin: '杩斿洖鐧诲綍',
+ backHome: '杩斿洖棣栭〉',
+ subTitle403: '鎶辨瓑锛屾偍鏃犳潈璁块棶姝ら〉闈€��',
+ subTitle404: '鎶辨瓑锛屾偍璁块棶鐨勯〉闈笉瀛樺湪銆�',
+ subTitle500: '鎶辨瓑锛屾湇鍔″櫒鎶ュ憡閿欒銆�',
+ noDataTitle: '褰撳墠椤垫棤鏁版嵁',
+ networkErrorTitle: '缃戠粶閿欒',
+ networkErrorSubTitle: '鎶辨瓑锛屾偍鐨勭綉缁滆繛鎺ュ凡鏂紑锛岃妫�鏌ユ偍鐨勭綉缁滐紒'
+ },
+ lock: {
+ unlock: '鐐瑰嚮瑙i攣',
+ alert: '閿佸睆瀵嗙爜閿欒',
+ backToLogin: '杩斿洖鐧诲綍',
+ entry: '杩涘叆绯荤粺',
+ placeholder: '璇疯緭鍏ラ攣灞忓瘑鐮佹垨鑰呯敤鎴峰瘑鐮�'
+ },
+ login: {
+ backSignIn: '杩斿洖',
+ signInFormTitle: '鐧诲綍',
+ ssoFormTitle: '涓夋柟鎺堟潈',
+ mobileSignInFormTitle: '鎵嬫満鐧诲綍',
+ qrSignInFormTitle: '浜岀淮鐮佺櫥褰�',
+ signUpFormTitle: '娉ㄥ唽',
+ forgetFormTitle: '閲嶇疆瀵嗙爜',
+ signInTitle: '寮�绠卞嵆鐢ㄧ殑涓悗鍙扮鐞嗙郴缁�',
+ signInDesc: '杈撳叆鎮ㄧ殑涓汉璇︾粏淇℃伅寮�濮嬩娇鐢紒',
+ policy: '鎴戝悓鎰弜xx闅愮鏀跨瓥',
+ scanSign: `鎵爜鍚庣偣鍑�"纭"锛屽嵆鍙畬鎴愮櫥褰昤,
+ loginButton: '鐧诲綍',
+ registerButton: '娉ㄥ唽',
+ rememberMe: '璁颁綇鎴�',
+ forgetPassword: '蹇樿瀵嗙爜?',
+ otherSignIn: '鍏朵粬鐧诲綍鏂瑰紡',
+ // notify
+ loginSuccessTitle: '鐧诲綍鎴愬姛',
+ loginSuccessDesc: '娆㈣繋鍥炴潵',
+ // placeholder
+ accountPlaceholder: '璇疯緭鍏ヨ处鍙�',
+ passwordPlaceholder: '璇疯緭鍏ュ瘑鐮�',
+ smsPlaceholder: '璇疯緭鍏ラ獙璇佺爜',
+ mobilePlaceholder: '璇疯緭鍏ユ墜鏈哄彿鐮�',
+ policyPlaceholder: '鍕鹃�夊悗鎵嶈兘娉ㄥ唽',
+ diffPwd: '涓ゆ杈撳叆瀵嗙爜涓嶄竴鑷�',
+ userName: '璐﹀彿',
+ password: '瀵嗙爜',
+ confirmPassword: '纭瀵嗙爜',
+ email: '閭',
+ smsCode: '鐭俊楠岃瘉鐮�',
+ mobile: '鎵嬫満鍙风爜'
+ }
+ },
+ profile: {
+ user: {
+ title: '涓汉淇℃伅',
+ username: '鐢ㄦ埛鍚嶇О',
+ nickname: '鐢ㄦ埛鏄电О',
+ mobile: '鎵嬫満鍙风爜',
+ email: '鐢ㄦ埛閭',
+ dept: '鎵�灞為儴闂�',
+ posts: '鎵�灞炲矖浣�',
+ roles: '鎵�灞炶鑹�',
+ sex: '鎬у埆',
+ man: '鐢�',
+ woman: '濂�',
+ createTime: '鍒涘缓鏃ユ湡'
+ },
+ info: {
+ title: '鍩烘湰淇℃伅',
+ basicInfo: '鍩烘湰璁剧疆',
+ resetPwd: '瀵嗙爜璁剧疆',
+ userSocial: '绀句氦缁戝畾'
+ },
+ rules: {
+ nickname: '璇疯緭鍏ョ敤鎴锋樀绉�',
+ mail: '璇疯緭鍏ラ偖绠卞湴鍧�',
+ truemail: '璇疯緭鍏ユ纭殑閭鍦板潃',
+ phone: '璇疯緭鍏ユ纭殑鎵嬫満鍙风爜',
+ truephone: '璇疯緭鍏ユ纭殑鎵嬫満鍙风爜'
+ },
+ password: {
+ oldPassword: '鏃у瘑鐮�',
+ newPassword: '鏂板瘑鐮�',
+ confirmPassword: '纭瀵嗙爜',
+ oldPwdMsg: '璇疯緭鍏ユ棫瀵嗙爜',
+ newPwdMsg: '璇疯緭鍏ユ柊瀵嗙爜',
+ cfPwdMsg: '璇疯緭鍏ョ‘璁ゅ瘑鐮�',
+ pwdRules: '闀垮害鍦� 6 鍒� 20 涓瓧绗�',
+ diffPwd: '涓ゆ杈撳叆瀵嗙爜涓嶄竴鑷�'
+ }
+ },
+ cropper: {
+ selectImage: '閫夋嫨鍥剧墖',
+ uploadSuccess: '涓婁紶鎴愬姛',
+ modalTitle: '澶村儚涓婁紶',
+ okText: '纭骞朵笂浼�',
+ btn_reset: '閲嶇疆',
+ btn_rotate_left: '閫嗘椂閽堟棆杞�',
+ btn_rotate_right: '椤烘椂閽堟棆杞�',
+ btn_scale_x: '姘村钩缈昏浆',
+ btn_scale_y: '鍨傜洿缈昏浆',
+ btn_zoom_in: '鏀惧ぇ',
+ btn_zoom_out: '缂╁皬',
+ preview: '棰勮'
+ },
+ 'OAuth 2.0': 'OAuth 2.0' // 閬垮厤鑿滃崟鍚嶆槸 OAuth 2.0 鏃讹紝涓�鐩� warn 鎶ラ敊
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..fcfd780
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,85 @@
+// 寮曞叆unocss css
+import '@/plugins/unocss'
+
+// 瀵煎叆鍏ㄥ眬鐨剆vg鍥炬爣
+import '@/plugins/svgIcon'
+
+// 鍒濆鍖栧璇█
+import { setupI18n } from '@/plugins/vueI18n'
+
+// 寮曞叆鐘舵�佺鐞�
+import { setupStore } from '@/store'
+
+// 鍏ㄥ眬缁勪欢
+import { setupGlobCom } from '@/components'
+
+// 寮曞叆 element-plus
+import { setupElementPlus } from '@/plugins/elementPlus'
+
+// 寮曞叆 form-create
+import { setupFormCreate } from '@/plugins/formCreate'
+
+// 寮曞叆鍏ㄥ眬鏍峰紡
+import '@/styles/index.scss'
+
+// 寮曞叆鍔ㄧ敾
+import '@/plugins/animate.css'
+
+// 璺敱
+import router, { setupRouter } from '@/router'
+
+// 鎸囦护
+import { setupAuth, setupMountedFocus } from '@/directives'
+
+import { createApp } from 'vue'
+
+import App from './App.vue'
+
+import './permission'
+
+import '@/plugins/tongji' // 鐧惧害缁熻
+import Logger from '@/utils/Logger'
+
+import VueDOMPurifyHTML from 'vue-dompurify-html' // 瑙e喅v-html 鐨勫畨鍏ㄩ殣鎮�
+
+// wangEditor 鎻掍欢娉ㄥ唽
+import { setupWangEditorPlugin } from '@/views/bpm/model/form/PrintTemplate'
+
+import print from 'vue3-print-nb' // 鎵撳嵃鎻掍欢
+
+// 鍒涘缓瀹炰緥
+const setupAll = async () => {
+ const app = createApp(App)
+
+ await setupI18n(app)
+
+ setupStore(app)
+
+ setupGlobCom(app)
+
+ setupElementPlus(app)
+
+ setupFormCreate(app)
+
+ setupRouter(app)
+
+ // directives 鎸囦护
+ setupAuth(app)
+ setupMountedFocus(app)
+
+ // wangEditor 鎻掍欢娉ㄥ唽
+ setupWangEditorPlugin()
+
+ await router.isReady()
+
+ app.use(VueDOMPurifyHTML)
+
+ // 鎵撳嵃
+ app.use(print)
+
+ app.mount('#app')
+}
+
+setupAll()
+
+Logger.prettyPrimary(`娆㈣繋浣跨敤`, import.meta.env.VITE_APP_TITLE)
diff --git a/src/permission.ts b/src/permission.ts
new file mode 100644
index 0000000..d2e7d31
--- /dev/null
+++ b/src/permission.ts
@@ -0,0 +1,107 @@
+import router from './router'
+import type { RouteRecordRaw } from 'vue-router'
+import { isRelogin } from '@/config/axios/service'
+import { getAccessToken } from '@/utils/auth'
+import { useTitle } from '@/hooks/web/useTitle'
+import { useNProgress } from '@/hooks/web/useNProgress'
+import { usePageLoading } from '@/hooks/web/usePageLoading'
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { usePermissionStoreWithOut } from '@/store/modules/permission'
+
+const { start, done } = useNProgress()
+
+const { loadStart, loadDone } = usePageLoading()
+
+const parseURL = (
+ url: string | null | undefined
+): { basePath: string; paramsObject: { [key: string]: string } } => {
+ // 濡傛灉杈撳叆涓� null 鎴� undefined锛岃繑鍥炵┖瀛楃涓插拰绌哄璞�
+ if (url == null) {
+ return { basePath: '', paramsObject: {} }
+ }
+
+ // 鎵惧埌闂彿 (?) 鐨勪綅缃紝瀹冧箣鍓嶆槸鍩虹璺緞锛屼箣鍚庢槸鏌ヨ鍙傛暟
+ const questionMarkIndex = url.indexOf('?')
+ let basePath = url
+ const paramsObject: { [key: string]: string } = {}
+
+ // 濡傛灉鎵惧埌浜嗛棶鍙凤紝璇存槑鏈夋煡璇㈠弬鏁�
+ if (questionMarkIndex !== -1) {
+ // 鑾峰彇 basePath
+ basePath = url.substring(0, questionMarkIndex)
+
+ // 浠� URL 涓幏鍙栨煡璇㈠瓧绗︿覆閮ㄥ垎
+ const queryString = url.substring(questionMarkIndex + 1)
+
+ // 浣跨敤 URLSearchParams 閬嶅巻鍙傛暟
+ const searchParams = new URLSearchParams(queryString)
+ searchParams.forEach((value, key) => {
+ // 灏佽杩� paramsObject 瀵硅薄
+ paramsObject[key] = value
+ })
+ }
+
+ // 杩斿洖 basePath 鍜� paramsObject
+ return { basePath, paramsObject }
+}
+
+// 璺敱涓嶉噸瀹氬悜鐧藉悕鍗�
+const whiteList = [
+ '/login',
+ '/social-login',
+ '/auth-redirect',
+ '/bind',
+ '/register',
+ '/oauthLogin/gitee'
+]
+
+// 璺敱鍔犺浇鍓�
+router.beforeEach(async (to, from, next) => {
+ start()
+ loadStart()
+ if (getAccessToken()) {
+ if (to.path === '/login') {
+ next({ path: '/' })
+ } else {
+ const dictStore = useDictStoreWithOut()
+ const userStore = useUserStoreWithOut()
+ const permissionStore = usePermissionStoreWithOut()
+ // 寮傛鍔犺浇瀛楀吀
+ // 鍙﹀锛岄棿鎺� issue锛歨ttps://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/ID9FLI
+ if (!dictStore.getIsSetDict) {
+ dictStore.setDictMap().then()
+ }
+ if (!userStore.getIsSetUser) {
+ isRelogin.show = true
+ await userStore.setUserInfoAction()
+ isRelogin.show = false
+ // 鍚庣杩囨护鑿滃崟
+ await permissionStore.generateRoutes()
+ permissionStore.getAddRouters.forEach((route) => {
+ router.addRoute(route as unknown as RouteRecordRaw) // 鍔ㄦ�佹坊鍔犲彲璁块棶璺敱琛�
+ })
+ const redirectPath = from.query.redirect || to.path
+ // 淇璺宠浆鏃朵笉甯﹀弬鏁扮殑闂
+ const redirect = decodeURIComponent(redirectPath as string)
+ const { paramsObject: query } = parseURL(redirect)
+ const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect, query }
+ next(nextData)
+ } else {
+ next()
+ }
+ }
+ } else {
+ if (whiteList.indexOf(to.path) !== -1) {
+ next()
+ } else {
+ next(`/login?redirect=${to.fullPath}`) // 鍚﹀垯鍏ㄩ儴閲嶅畾鍚戝埌鐧诲綍椤�
+ }
+ }
+})
+
+router.afterEach((to) => {
+ useTitle(to?.meta?.title as string)
+ done() // 缁撴潫Progress
+ loadDone()
+})
diff --git a/src/plugins/animate.css/index.ts b/src/plugins/animate.css/index.ts
new file mode 100644
index 0000000..3e93451
--- /dev/null
+++ b/src/plugins/animate.css/index.ts
@@ -0,0 +1 @@
+import 'animate.css'
diff --git a/src/plugins/echarts/index.ts b/src/plugins/echarts/index.ts
new file mode 100644
index 0000000..3a402de
--- /dev/null
+++ b/src/plugins/echarts/index.ts
@@ -0,0 +1,51 @@
+import * as echarts from 'echarts/core'
+
+import {
+ BarChart,
+ FunnelChart,
+ GaugeChart,
+ LineChart,
+ MapChart,
+ PictorialBarChart,
+ PieChart,
+ RadarChart
+} from 'echarts/charts'
+
+import {
+ AriaComponent,
+ DataZoomComponent,
+ GridComponent,
+ LegendComponent,
+ ParallelComponent,
+ PolarComponent,
+ TitleComponent,
+ ToolboxComponent,
+ TooltipComponent,
+ VisualMapComponent
+} from 'echarts/components'
+
+import { CanvasRenderer } from 'echarts/renderers'
+
+echarts.use([
+ LegendComponent,
+ TitleComponent,
+ TooltipComponent,
+ ToolboxComponent,
+ DataZoomComponent,
+ GridComponent,
+ PolarComponent,
+ AriaComponent,
+ ParallelComponent,
+ VisualMapComponent,
+ BarChart,
+ LineChart,
+ PieChart,
+ MapChart,
+ CanvasRenderer,
+ PictorialBarChart,
+ RadarChart,
+ GaugeChart,
+ FunnelChart
+])
+
+export default echarts
diff --git a/src/plugins/elementPlus/index.ts b/src/plugins/elementPlus/index.ts
new file mode 100644
index 0000000..0ae2a8b
--- /dev/null
+++ b/src/plugins/elementPlus/index.ts
@@ -0,0 +1,17 @@
+import type { App } from 'vue'
+// 闇�瑕佸叏灞�寮曞叆涓�浜涚粍浠讹紝濡侲lScrollbar锛屼笉鐒朵竴浜涗笅鎷夐」鏍峰紡鏈夐棶棰�
+import { ElLoading, ElScrollbar, ElButton } from 'element-plus'
+
+const plugins = [ElLoading]
+
+const components = [ElScrollbar, ElButton]
+
+export const setupElementPlus = (app: App<Element>) => {
+ plugins.forEach((plugin) => {
+ app.use(plugin)
+ })
+
+ components.forEach((component) => {
+ app.component(component.name, component)
+ })
+}
diff --git a/src/plugins/formCreate/index.ts b/src/plugins/formCreate/index.ts
new file mode 100644
index 0000000..01a57be
--- /dev/null
+++ b/src/plugins/formCreate/index.ts
@@ -0,0 +1,135 @@
+import type { App } from 'vue'
+// 馃憞浣跨敤 form-create 闇�棰濆鍏ㄥ眬寮曞叆 element plus 缁勪欢
+import {
+ // ElAutocomplete,
+ // ElButton,
+ // ElCascader,
+ // ElCheckbox,
+ // ElCheckboxButton,
+ // ElCheckboxGroup,
+ // ElCol,
+ // ElColorPicker,
+ // ElDatePicker,
+ // ElDialog,
+ // ElForm,
+ // ElInput,
+ // ElInputNumber,
+ // ElPopover,
+ // ElRadio,
+ // ElRadioButton,
+ // ElRadioGroup,
+ // ElRate,
+ // ElRow,
+ // ElSelect,
+ // ElSlider,
+ // ElSwitch,
+ // ElTimePicker,
+ // ElTooltip,
+ // ElTree,
+ // ElUpload,
+ // ElIcon,
+ // ElProgress,
+ // 浠ヤ笂浼氱敱 @form-create/element-ui/auto-import 鑷姩寮曞叆
+ ElAlert,
+ ElTransfer,
+ ElAside,
+ ElContainer,
+ ElDivider,
+ ElHeader,
+ ElMain,
+ ElPopconfirm,
+ ElTable,
+ ElTableColumn,
+ ElTabPane,
+ ElTabs,
+ ElDropdown,
+ ElDropdownMenu,
+ ElDropdownItem,
+ ElBadge,
+ ElTag,
+ ElText,
+ ElMenu,
+ ElMenuItem,
+ ElFooter,
+ ElMessage,
+ ElCollapse,
+ ElCollapseItem,
+ ElCard,
+ ElTreeSelect
+ // ElFormItem,
+ // ElOption
+} from 'element-plus'
+import FcDesigner from '@form-create/designer'
+import formCreate from '@form-create/element-ui'
+import install from '@form-create/element-ui/auto-import'
+
+//======================= 鑷畾涔夌粍浠� =======================
+import { UploadFile, UploadImg, UploadImgs } from '@/components/UploadFile'
+import { useApiSelect } from '@/components/FormCreate'
+import { Editor } from '@/components/Editor'
+import DictSelect from '@/components/FormCreate/src/components/DictSelect.vue'
+
+const UserSelect = useApiSelect({
+ name: 'UserSelect',
+ labelField: 'nickname',
+ valueField: 'id',
+ url: '/system/user/simple-list'
+})
+const DeptSelect = useApiSelect({
+ name: 'DeptSelect',
+ labelField: 'name',
+ valueField: 'id',
+ url: '/system/dept/simple-list'
+})
+const ApiSelect = useApiSelect({
+ name: 'ApiSelect'
+})
+
+const components = [
+ ElAlert,
+ ElTransfer,
+ ElAside,
+ ElContainer,
+ ElDivider,
+ ElHeader,
+ ElMain,
+ ElPopconfirm,
+ ElTable,
+ ElTableColumn,
+ ElTabPane,
+ ElTabs,
+ ElTreeSelect,
+ ElDropdown,
+ ElDropdownMenu,
+ ElDropdownItem,
+ ElBadge,
+ ElTag,
+ ElText,
+ ElMenu,
+ ElMenuItem,
+ ElFooter,
+ ElMessage,
+ // ElFormItem,
+ // ElOption,
+ UploadImg,
+ UploadImgs,
+ UploadFile,
+ DictSelect,
+ UserSelect,
+ DeptSelect,
+ ApiSelect,
+ Editor,
+ ElCollapse,
+ ElCollapseItem,
+ ElCard
+]
+
+// 鍙傝�� http://www.form-create.com/v3/element-ui/auto-import.html 鏂囨。
+export const setupFormCreate = (app: App<Element>) => {
+ components.forEach((component) => {
+ app.component(component.name, component)
+ })
+ formCreate.use(install)
+ app.use(formCreate)
+ app.use(FcDesigner)
+}
diff --git a/src/plugins/svgIcon/index.ts b/src/plugins/svgIcon/index.ts
new file mode 100644
index 0000000..b5b7f70
--- /dev/null
+++ b/src/plugins/svgIcon/index.ts
@@ -0,0 +1,3 @@
+import 'virtual:svg-icons-register'
+
+import '@purge-icons/generated'
diff --git a/src/plugins/tongji/index.ts b/src/plugins/tongji/index.ts
new file mode 100644
index 0000000..ec261a1
--- /dev/null
+++ b/src/plugins/tongji/index.ts
@@ -0,0 +1,23 @@
+import router from '@/router'
+
+// 鐢ㄤ簬 router push
+window._hmt = window._hmt || []
+// HM_ID
+const HM_ID = import.meta.env.VITE_APP_BAIDU_CODE
+;(function () {
+ // 鏈夊�肩殑鏃跺�欙紝鎵嶅紑鍚�
+ if (!HM_ID) {
+ return
+ }
+ const hm = document.createElement('script')
+ hm.src = 'https://hm.baidu.com/hm.js?' + HM_ID
+ const s = document.getElementsByTagName('script')[0]
+ s.parentNode.insertBefore(hm, s)
+})()
+
+router.afterEach(function (to) {
+ if (!HM_ID) {
+ return
+ }
+ _hmt.push(['_trackPageview', to.fullPath])
+})
diff --git a/src/plugins/unocss/index.ts b/src/plugins/unocss/index.ts
new file mode 100644
index 0000000..d366b5a
--- /dev/null
+++ b/src/plugins/unocss/index.ts
@@ -0,0 +1 @@
+import 'virtual:uno.css'
diff --git a/src/plugins/vueI18n/helper.ts b/src/plugins/vueI18n/helper.ts
new file mode 100644
index 0000000..da6bc8c
--- /dev/null
+++ b/src/plugins/vueI18n/helper.ts
@@ -0,0 +1,3 @@
+export const setHtmlPageLang = (locale: LocaleType) => {
+ document.querySelector('html')?.setAttribute('lang', locale)
+}
diff --git a/src/plugins/vueI18n/index.ts b/src/plugins/vueI18n/index.ts
new file mode 100644
index 0000000..f845b13
--- /dev/null
+++ b/src/plugins/vueI18n/index.ts
@@ -0,0 +1,42 @@
+import type { App } from 'vue'
+import { createI18n } from 'vue-i18n'
+import { useLocaleStoreWithOut } from '@/store/modules/locale'
+import type { I18n, I18nOptions } from 'vue-i18n'
+import { setHtmlPageLang } from './helper'
+
+export let i18n: ReturnType<typeof createI18n>
+
+const createI18nOptions = async (): Promise<I18nOptions> => {
+ const localeStore = useLocaleStoreWithOut()
+ const locale = localeStore.getCurrentLocale
+ const localeMap = localeStore.getLocaleMap
+ const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
+ const message = defaultLocal.default ?? {}
+
+ setHtmlPageLang(locale.lang)
+
+ localeStore.setCurrentLocale({
+ lang: locale.lang
+ // elLocale: elLocal
+ })
+
+ return {
+ legacy: false,
+ locale: locale.lang,
+ fallbackLocale: locale.lang,
+ messages: {
+ [locale.lang]: message
+ },
+ availableLocales: localeMap.map((v) => v.lang),
+ sync: true,
+ silentTranslationWarn: true,
+ missingWarn: false,
+ silentFallbackWarn: true
+ }
+}
+
+export const setupI18n = async (app: App<Element>) => {
+ const options = await createI18nOptions()
+ i18n = createI18n(options) as I18n
+ app.use(i18n)
+}
diff --git a/src/router/index.ts b/src/router/index.ts
new file mode 100644
index 0000000..4e861c6
--- /dev/null
+++ b/src/router/index.ts
@@ -0,0 +1,36 @@
+import type { App } from 'vue'
+import type { RouteRecordRaw } from 'vue-router'
+import { createRouter, createWebHistory } from 'vue-router'
+import remainingRouter from './modules/remaining'
+
+// 鍒涘缓璺敱瀹炰緥
+const router = createRouter({
+ history: createWebHistory(import.meta.env.VITE_BASE_PATH), // createWebHashHistory URL甯�#锛宑reateWebHistory URL涓嶅甫#
+ strict: true,
+ routes: remainingRouter as RouteRecordRaw[],
+ scrollBehavior: () => {
+ // 鏂板紑鏍囩鏃躲�佽繑鍥炴爣绛炬椂锛屾粴鍔ㄦ潯鍥炲埌椤堕儴锛屽惁鍒欎細淇濈暀涓婃鏍囩鐨勬粴鍔ㄤ綅缃��
+ const scrollbarWrap = document.querySelector('.v-layout-content-scrollbar .el-scrollbar__wrap')
+ if (scrollbarWrap) {
+ // scrollbarWrap.scrollTo({ left: 0, top: 0, behavior: 'auto' })
+ scrollbarWrap.scrollTop = 0
+ }
+ return { left: 0, top: 0 }
+ }
+})
+
+export const resetRouter = (): void => {
+ const resetWhiteNameList = ['Redirect', 'Login', 'NoFound', 'Home']
+ router.getRoutes().forEach((route) => {
+ const { name } = route
+ if (name && !resetWhiteNameList.includes(name as string)) {
+ router.hasRoute(name) && router.removeRoute(name)
+ }
+ })
+}
+
+export const setupRouter = (app: App<Element>) => {
+ app.use(router)
+}
+
+export default router
diff --git a/src/router/modules/remaining.ts b/src/router/modules/remaining.ts
new file mode 100644
index 0000000..794778c
--- /dev/null
+++ b/src/router/modules/remaining.ts
@@ -0,0 +1,752 @@
+import { Layout } from '@/utils/routerHelper'
+
+const { t } = useI18n()
+/**
+ * redirect: noredirect 褰撹缃� noredirect 鐨勬椂鍊欒璺敱鍦ㄩ潰鍖呭睉瀵艰埅涓笉鍙鐐瑰嚮
+ * name:'router-name' 璁惧畾璺敱鐨勫悕瀛楋紝涓�瀹氳濉啓涓嶇劧浣跨敤<keep-alive>鏃朵細鍑虹幇鍚勭闂
+ * meta : {
+ hidden: true 褰撹缃� true 鐨勬椂鍊欒璺敱涓嶄細鍐嶄晶杈规爮鍑虹幇 濡�404锛宭ogin绛夐〉闈�(榛樿 false)
+
+ alwaysShow: true 褰撲綘涓�涓矾鐢变笅闈㈢殑 children 澹版槑鐨勮矾鐢卞ぇ浜�1涓椂锛岃嚜鍔ㄤ細鍙樻垚宓屽鐨勬ā寮忥紝
+ 鍙湁涓�涓椂锛屼細灏嗛偅涓瓙璺敱褰撳仛鏍硅矾鐢辨樉绀哄湪渚ц竟鏍忥紝
+ 鑻ヤ綘鎯充笉绠¤矾鐢变笅闈㈢殑 children 澹版槑鐨勪釜鏁伴兘鏄剧ず浣犵殑鏍硅矾鐢憋紝
+ 浣犲彲浠ヨ缃� alwaysShow: true锛岃繖鏍峰畠灏变細蹇界暐涔嬪墠瀹氫箟鐨勮鍒欙紝
+ 涓�鐩存樉绀烘牴璺敱(榛樿 false)
+
+ title: 'title' 璁剧疆璇ヨ矾鐢卞湪渚ц竟鏍忓拰闈㈠寘灞戜腑灞曠ず鐨勫悕瀛�
+
+ icon: 'svg-name' 璁剧疆璇ヨ矾鐢辩殑鍥炬爣
+
+ noCache: true 濡傛灉璁剧疆涓簍rue锛屽垯涓嶄細琚� <keep-alive> 缂撳瓨(榛樿 false)
+
+ breadcrumb: false 濡傛灉璁剧疆涓篺alse锛屽垯涓嶄細鍦╞readcrumb闈㈠寘灞戜腑鏄剧ず(榛樿 true)
+
+ affix: true 濡傛灉璁剧疆涓簍rue锛屽垯浼氫竴鐩村浐瀹氬湪tag椤逛腑(榛樿 false)
+
+ noTagsView: true 濡傛灉璁剧疆涓簍rue锛屽垯涓嶄細鍑虹幇鍦╰ag涓�(榛樿 false)
+
+ activeMenu: '/dashboard' 鏄剧ず楂樹寒鐨勮矾鐢辫矾寰�
+
+ followAuth: '/dashboard' 璺熼殢鍝釜璺敱杩涜鏉冮檺杩囨护
+
+ canTo: true 璁剧疆涓簍rue鍗充娇hidden涓簍rue锛屼篃渚濈劧鍙互杩涜璺敱璺宠浆(榛樿 false)
+ }
+ **/
+const remainingRouter: AppRouteRecordRaw[] = [
+ {
+ path: '/redirect',
+ component: Layout,
+ name: 'Redirect',
+ children: [
+ {
+ path: '/redirect/:path(.*)',
+ name: 'Redirect',
+ component: () => import('@/views/Redirect/Redirect.vue'),
+ meta: {}
+ }
+ ],
+ meta: {
+ hidden: true,
+ noTagsView: true
+ }
+ },
+ {
+ path: '/',
+ component: Layout,
+ redirect: '/index',
+ name: 'Home',
+ meta: {},
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/Home/Index.vue'),
+ name: 'Index',
+ meta: {
+ title: t('router.home'),
+ icon: 'ep:home-filled',
+ noCache: false,
+ affix: true
+ }
+ }
+ ]
+ },
+ {
+ path: '/user',
+ component: Layout,
+ name: 'UserInfo',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'profile',
+ component: () => import('@/views/Profile/Index.vue'),
+ name: 'Profile',
+ meta: {
+ canTo: true,
+ hidden: true,
+ noTagsView: false,
+ icon: 'ep:user',
+ title: t('common.profile')
+ }
+ },
+ {
+ path: 'notify-message',
+ component: () => import('@/views/system/notify/my/index.vue'),
+ name: 'MyNotifyMessage',
+ meta: {
+ canTo: true,
+ hidden: true,
+ noTagsView: false,
+ icon: 'ep:message',
+ title: '鎴戠殑绔欏唴淇�'
+ }
+ }
+ ]
+ },
+ {
+ path: '/dict',
+ component: Layout,
+ name: 'dict',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'type/data/:dictType',
+ component: () => import('@/views/system/dict/data/index.vue'),
+ name: 'SystemDictData',
+ meta: {
+ title: '瀛楀吀鏁版嵁',
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: '',
+ activeMenu: '/system/dict'
+ }
+ }
+ ]
+ },
+
+ {
+ path: '/codegen',
+ component: Layout,
+ name: 'CodegenEdit',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'edit',
+ component: () => import('@/views/infra/codegen/EditTable.vue'),
+ name: 'InfraCodegenEditTable',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: 'ep:edit',
+ title: '淇敼鐢熸垚閰嶇疆',
+ activeMenu: 'infra/codegen/index'
+ }
+ }
+ ]
+ },
+ {
+ path: '/job',
+ component: Layout,
+ name: 'JobL',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'job-log',
+ component: () => import('@/views/infra/job/logger/index.vue'),
+ name: 'InfraJobLog',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: 'ep:edit',
+ title: '璋冨害鏃ュ織',
+ activeMenu: 'infra/job/index'
+ }
+ }
+ ]
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/Login/Login.vue'),
+ name: 'Login',
+ meta: {
+ hidden: true,
+ title: t('router.login'),
+ noTagsView: true
+ }
+ },
+ {
+ path: '/sso',
+ component: () => import('@/views/Login/Login.vue'),
+ name: 'SSOLogin',
+ meta: {
+ hidden: true,
+ title: t('router.login'),
+ noTagsView: true
+ }
+ },
+ {
+ path: '/social-login',
+ component: () => import('@/views/Login/SocialLogin.vue'),
+ name: 'SocialLogin',
+ meta: {
+ hidden: true,
+ title: t('router.socialLogin'),
+ noTagsView: true
+ }
+ },
+ {
+ path: '/403',
+ component: () => import('@/views/Error/403.vue'),
+ name: 'NoAccess',
+ meta: {
+ hidden: true,
+ title: '403',
+ noTagsView: true
+ }
+ },
+ {
+ path: '/404',
+ component: () => import('@/views/Error/404.vue'),
+ name: 'NoFound',
+ meta: {
+ hidden: true,
+ title: '404',
+ noTagsView: true
+ }
+ },
+ {
+ path: '/500',
+ component: () => import('@/views/Error/500.vue'),
+ name: 'Error',
+ meta: {
+ hidden: true,
+ title: '500',
+ noTagsView: true
+ }
+ },
+ {
+ path: '/bpm',
+ component: Layout,
+ name: 'bpm',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'manager/form/edit',
+ component: () => import('@/views/bpm/form/editor/index.vue'),
+ name: 'BpmFormEditor',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '璁捐娴佺▼琛ㄥ崟',
+ activeMenu: '/bpm/manager/form'
+ }
+ },
+ {
+ path: 'manager/definition',
+ component: () => import('@/views/bpm/model/definition/index.vue'),
+ name: 'BpmProcessDefinition',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '娴佺▼瀹氫箟',
+ activeMenu: '/bpm/manager/model'
+ }
+ },
+ {
+ path: 'process-instance/detail',
+ component: () => import('@/views/bpm/processInstance/detail/index.vue'),
+ name: 'BpmProcessInstanceDetail',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '娴佺▼璇︽儏',
+ activeMenu: '/bpm/task/my'
+ },
+ props: (route) => ({
+ id: route.query.id,
+ taskId: route.query.taskId,
+ activityId: route.query.activityId
+ })
+ },
+ {
+ path: 'process-instance/report',
+ component: () => import('@/views/bpm/processInstance/report/index.vue'),
+ name: 'BpmProcessInstanceReport',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '鏁版嵁鎶ヨ〃',
+ activeMenu: '/bpm/manager/model'
+ }
+ },
+ {
+ path: 'oa/leave/create',
+ component: () => import('@/views/bpm/oa/leave/create.vue'),
+ name: 'OALeaveCreate',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '鍙戣捣 OA 璇峰亣',
+ activeMenu: '/bpm/oa/leave'
+ }
+ },
+ {
+ path: 'oa/leave/detail',
+ component: () => import('@/views/bpm/oa/leave/detail.vue'),
+ name: 'OALeaveDetail',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '鏌ョ湅 OA 璇峰亣',
+ activeMenu: '/bpm/oa/leave'
+ }
+ },
+ {
+ path: 'manager/model/create',
+ component: () => import('@/views/bpm/model/form/index.vue'),
+ name: 'BpmModelCreate',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '鍒涘缓娴佺▼',
+ activeMenu: '/bpm/manager/model'
+ }
+ },
+ {
+ path: 'manager/model/:type/:id',
+ component: () => import('@/views/bpm/model/form/index.vue'),
+ name: 'BpmModelUpdate',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '淇敼娴佺▼',
+ activeMenu: '/bpm/manager/model'
+ }
+ }
+ ]
+ },
+ {
+ path: '/mall/product', // 鍟嗗搧涓績
+ component: Layout,
+ name: 'ProductCenter',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'spu/add',
+ component: () => import('@/views/mall/product/spu/form/index.vue'),
+ name: 'ProductSpuAdd',
+ meta: {
+ noCache: false, // 闇�瑕佺紦瀛�
+ hidden: true,
+ canTo: true,
+ icon: 'ep:edit',
+ title: '鍟嗗搧娣诲姞',
+ activeMenu: '/mall/product/spu'
+ }
+ },
+ {
+ path: 'spu/edit/:id(\\d+)',
+ component: () => import('@/views/mall/product/spu/form/index.vue'),
+ name: 'ProductSpuEdit',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: 'ep:edit',
+ title: '鍟嗗搧缂栬緫',
+ activeMenu: '/mall/product/spu'
+ }
+ },
+ {
+ path: 'spu/detail/:id(\\d+)',
+ component: () => import('@/views/mall/product/spu/form/index.vue'),
+ name: 'ProductSpuDetail',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: 'ep:view',
+ title: '鍟嗗搧璇︽儏',
+ activeMenu: '/mall/product/spu'
+ }
+ },
+ {
+ path: 'property/value/:propertyId(\\d+)',
+ component: () => import('@/views/mall/product/property/value/index.vue'),
+ name: 'ProductPropertyValue',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ icon: 'ep:view',
+ title: '鍟嗗搧灞炴�у��',
+ activeMenu: '/product/property'
+ }
+ }
+ ]
+ },
+ {
+ path: '/mall/trade', // 浜ゆ槗涓績
+ component: Layout,
+ name: 'TradeCenter',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'order/detail/:id(\\d+)',
+ component: () => import('@/views/mall/trade/order/detail/index.vue'),
+ name: 'TradeOrderDetail',
+ meta: { title: '璁㈠崟璇︽儏', icon: 'ep:view', activeMenu: '/mall/trade/order' }
+ },
+ {
+ path: 'after-sale/detail/:id(\\d+)',
+ component: () => import('@/views/mall/trade/afterSale/detail/index.vue'),
+ name: 'TradeAfterSaleDetail',
+ meta: { title: '閫�娆捐鎯�', icon: 'ep:view', activeMenu: '/mall/trade/after-sale' }
+ }
+ ]
+ },
+ {
+ path: '/member',
+ component: Layout,
+ name: 'MemberCenter',
+ meta: { hidden: true },
+ children: [
+ {
+ path: 'user/detail/:id',
+ name: 'MemberUserDetail',
+ meta: {
+ title: '浼氬憳璇︽儏',
+ noCache: true,
+ hidden: true
+ },
+ component: () => import('@/views/member/user/detail/index.vue')
+ }
+ ]
+ },
+ {
+ path: '/pay',
+ component: Layout,
+ name: 'pay',
+ meta: { hidden: true },
+ children: [
+ {
+ path: 'cashier',
+ name: 'PayCashier',
+ meta: {
+ title: '鏀堕摱鍙�',
+ noCache: true,
+ hidden: true
+ },
+ component: () => import('@/views/pay/cashier/index.vue')
+ }
+ ]
+ },
+ {
+ path: '/diy',
+ name: 'DiyCenter',
+ meta: { hidden: true },
+ component: Layout,
+ children: [
+ {
+ path: 'template/decorate/:id',
+ name: 'DiyTemplateDecorate',
+ meta: {
+ title: '妯℃澘瑁呬慨',
+ noCache: false,
+ hidden: true,
+ activeMenu: '/mall/promotion/diy-template/diy-template'
+ },
+ component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
+ },
+ {
+ path: 'page/decorate/:id',
+ name: 'DiyPageDecorate',
+ meta: {
+ title: '椤甸潰瑁呬慨',
+ noCache: false,
+ hidden: true,
+ activeMenu: '/mall/promotion/diy-template/diy-page'
+ },
+ component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
+ }
+ ]
+ },
+ {
+ path: '/crm',
+ component: Layout,
+ name: 'CrmCenter',
+ meta: { hidden: true },
+ children: [
+ {
+ path: 'clue/detail/:id',
+ name: 'CrmClueDetail',
+ meta: {
+ title: '绾跨储璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/clue'
+ },
+ component: () => import('@/views/crm/clue/detail/index.vue')
+ },
+ {
+ path: 'customer/detail/:id',
+ name: 'CrmCustomerDetail',
+ meta: {
+ title: '瀹㈡埛璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/customer'
+ },
+ component: () => import('@/views/crm/customer/detail/index.vue')
+ },
+ {
+ path: 'business/detail/:id',
+ name: 'CrmBusinessDetail',
+ meta: {
+ title: '鍟嗘満璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/business'
+ },
+ component: () => import('@/views/crm/business/detail/index.vue')
+ },
+ {
+ path: 'contract/detail/:id',
+ name: 'CrmContractDetail',
+ meta: {
+ title: '鍚堝悓璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/contract'
+ },
+ component: () => import('@/views/crm/contract/detail/index.vue')
+ },
+ {
+ path: 'receivable-plan/detail/:id',
+ name: 'CrmReceivablePlanDetail',
+ meta: {
+ title: '鍥炴璁″垝璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/receivable-plan'
+ },
+ component: () => import('@/views/crm/receivable/plan/detail/index.vue')
+ },
+ {
+ path: 'receivable/detail/:id',
+ name: 'CrmReceivableDetail',
+ meta: {
+ title: '鍥炴璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/receivable'
+ },
+ component: () => import('@/views/crm/receivable/detail/index.vue')
+ },
+ {
+ path: 'contact/detail/:id',
+ name: 'CrmContactDetail',
+ meta: {
+ title: '鑱旂郴浜鸿鎯�',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/contact'
+ },
+ component: () => import('@/views/crm/contact/detail/index.vue')
+ },
+ {
+ path: 'product/detail/:id',
+ name: 'CrmProductDetail',
+ meta: {
+ title: '浜у搧璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/crm/product'
+ },
+ component: () => import('@/views/crm/product/detail/index.vue')
+ }
+ ]
+ },
+ {
+ path: '/ai',
+ component: Layout,
+ name: 'Ai',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'image/square',
+ component: () => import('@/views/ai/image/square/index.vue'),
+ name: 'AiImageSquare',
+ meta: {
+ title: '缁樺浘浣滃搧',
+ icon: 'ep:home-filled',
+ noCache: false
+ }
+ },
+ {
+ path: 'knowledge/document',
+ component: () => import('@/views/ai/knowledge/document/index.vue'),
+ name: 'AiKnowledgeDocument',
+ meta: {
+ title: '鐭ヨ瘑搴撴枃妗�',
+ icon: 'ep:document',
+ noCache: false,
+ activeMenu: '/ai/knowledge'
+ }
+ },
+ {
+ path: 'knowledge/document/create',
+ component: () => import('@/views/ai/knowledge/document/form/index.vue'),
+ name: 'AiKnowledgeDocumentCreate',
+ meta: {
+ title: '鍒涘缓鏂囨。',
+ icon: 'ep:plus',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/ai/knowledge'
+ }
+ },
+ {
+ path: 'knowledge/document/update',
+ component: () => import('@/views/ai/knowledge/document/form/index.vue'),
+ name: 'AiKnowledgeDocumentUpdate',
+ meta: {
+ title: '淇敼鏂囨。',
+ icon: 'ep:edit',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/ai/knowledge'
+ }
+ },
+ {
+ path: 'knowledge/retrieval',
+ component: () => import('@/views/ai/knowledge/knowledge/retrieval/index.vue'),
+ name: 'AiKnowledgeRetrieval',
+ meta: {
+ title: '鏂囨。鍙洖娴嬭瘯',
+ icon: 'ep:search',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/ai/knowledge'
+ }
+ },
+ {
+ path: 'knowledge/segment',
+ component: () => import('@/views/ai/knowledge/segment/index.vue'),
+ name: 'AiKnowledgeSegment',
+ meta: {
+ title: '鐭ヨ瘑搴撳垎娈�',
+ icon: 'ep:tickets',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/ai/knowledge'
+ }
+ },
+ {
+ path: 'console/workflow/create',
+ component: () => import('@/views/ai/workflow/form/index.vue'),
+ name: 'AiWorkflowCreate',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '璁捐 AI 宸ヤ綔娴�',
+ activeMenu: '/ai/console/workflow'
+ }
+ },
+ {
+ path: 'console/workflow/:type/:id',
+ component: () => import('@/views/ai/workflow/form/index.vue'),
+ name: 'AiWorkflowUpdate',
+ meta: {
+ noCache: true,
+ hidden: true,
+ canTo: true,
+ title: '璁捐 AI 宸ヤ綔娴�',
+ activeMenu: '/ai/console/workflow'
+ }
+ }
+ ]
+ },
+ {
+ path: '/:pathMatch(.*)*',
+ component: () => import('@/views/Error/404.vue'),
+ name: '',
+ meta: {
+ title: '404',
+ hidden: true,
+ breadcrumb: false
+ }
+ },
+ {
+ path: '/iot',
+ component: Layout,
+ name: 'IOT',
+ meta: {
+ hidden: true
+ },
+ children: [
+ {
+ path: 'product/product/detail/:id',
+ name: 'IoTProductDetail',
+ meta: {
+ title: '浜у搧璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/iot/device/product'
+ },
+ component: () => import('@/views/iot/product/product/detail/index.vue')
+ },
+ {
+ path: 'device/detail/:id',
+ name: 'IoTDeviceDetail',
+ meta: {
+ title: '璁惧璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/iot/device/device'
+ },
+ component: () => import('@/views/iot/device/device/detail/index.vue')
+ },
+ {
+ path: 'ota/operation/firmware/detail/:id',
+ name: 'IoTOtaFirmwareDetail',
+ meta: {
+ title: '鍥轰欢璇︽儏',
+ noCache: true,
+ hidden: true,
+ activeMenu: '/iot/operation/ota/firmware'
+ },
+ component: () => import('@/views/iot/ota/firmware/detail/index.vue')
+ }
+ ]
+ }
+]
+
+export default remainingRouter
diff --git a/src/store/index.ts b/src/store/index.ts
new file mode 100644
index 0000000..63f0045
--- /dev/null
+++ b/src/store/index.ts
@@ -0,0 +1,12 @@
+import type { App } from 'vue'
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+
+const store = createPinia()
+store.use(piniaPluginPersistedstate)
+
+export const setupStore = (app: App<Element>) => {
+ app.use(store)
+}
+
+export { store }
diff --git a/src/store/modules/app.ts b/src/store/modules/app.ts
new file mode 100644
index 0000000..4a2ca9a
--- /dev/null
+++ b/src/store/modules/app.ts
@@ -0,0 +1,322 @@
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { ElementPlusSize } from '@/types/elementPlus'
+import { LayoutType } from '@/types/layout'
+import { ThemeTypes } from '@/types/theme'
+import { humpToUnderline, setCssVar } from '@/utils'
+import { getCssColorVariable, hexToRGB, mix } from '@/utils/color'
+import { ElMessage } from 'element-plus'
+import { defineStore } from 'pinia'
+import { store } from '../index'
+
+const { wsCache } = useCache()
+
+interface AppState {
+ breadcrumb: boolean
+ breadcrumbIcon: boolean
+ collapse: boolean
+ uniqueOpened: boolean
+ hamburger: boolean
+ screenfull: boolean
+ search: boolean
+ size: boolean
+ locale: boolean
+ message: boolean
+ tagsView: boolean
+ tagsViewImmerse: boolean
+ tagsViewIcon: boolean
+ logo: boolean
+ fixedHeader: boolean
+ greyMode: boolean
+ pageLoading: boolean
+ layout: LayoutType
+ title: string
+ userInfo: string
+ isDark: boolean
+ currentSize: ElementPlusSize
+ sizeMap: ElementPlusSize[]
+ mobile: boolean
+ footer: boolean
+ theme: ThemeTypes
+ fixedMenu: boolean
+}
+
+export const useAppStore = defineStore('app', {
+ state: (): AppState => {
+ return {
+ userInfo: 'userInfo', // 鐧诲綍淇℃伅瀛樺偍瀛楁-寤鸿姣忎釜椤圭洰鎹竴涓瓧娈碉紝閬垮厤涓庡叾浠栭」鐩啿绐�
+ sizeMap: ['default', 'large', 'small'],
+ mobile: false, // 鏄惁鏄Щ鍔ㄧ
+ title: import.meta.env.VITE_APP_TITLE, // 鏍囬
+ pageLoading: false, // 璺敱璺宠浆loading
+
+ breadcrumb: true, // 闈㈠寘灞�
+ breadcrumbIcon: true, // 闈㈠寘灞戝浘鏍�
+ collapse: false, // 鎶樺彔鑿滃崟
+ uniqueOpened: true, // 鏄惁鍙繚鎸佷竴涓瓙鑿滃崟鐨勫睍寮�
+ hamburger: true, // 鎶樺彔鍥炬爣
+ screenfull: true, // 鍏ㄥ睆鍥炬爣
+ search: true, // 鎼滅储鍥炬爣
+ size: true, // 灏哄鍥炬爣
+ locale: true, // 澶氳瑷�鍥炬爣
+ message: true, // 娑堟伅鍥炬爣
+ tagsView: true, // 鏍囩椤�
+ tagsViewImmerse: false, // 鏍囩椤垫矇娴�
+ tagsViewIcon: true, // 鏄惁鏄剧ず鏍囩鍥炬爣
+ logo: true, // logo
+ fixedHeader: true, // 鍥哄畾toolheader
+ footer: true, // 鏄剧ず椤佃剼
+ greyMode: false, // 鏄惁寮�濮嬬伆鑹叉ā寮忥紝鐢ㄤ簬鐗规畩鎮煎康鏃�
+ fixedMenu: wsCache.get('fixedMenu') || false, // 鏄惁鍥哄畾鑿滃崟
+
+ layout: wsCache.get(CACHE_KEY.LAYOUT) || 'classic', // layout甯冨眬
+ isDark: wsCache.get(CACHE_KEY.IS_DARK) || false, // 鏄惁鏄殫榛戞ā寮�
+ currentSize: wsCache.get('default') || 'default', // 缁勪欢灏哄
+ theme: wsCache.get(CACHE_KEY.THEME) || {
+ // 涓婚鑹�
+ elColorPrimary: '#409eff',
+ // 宸︿晶鑿滃崟杈规棰滆壊
+ leftMenuBorderColor: 'inherit',
+ // 宸︿晶鑿滃崟鑳屾櫙棰滆壊
+ leftMenuBgColor: '#001529',
+ // 宸︿晶鑿滃崟娴呰壊鑳屾櫙棰滆壊
+ leftMenuBgLightColor: '#0f2438',
+ // 宸︿晶鑿滃崟閫変腑鑳屾櫙棰滆壊
+ leftMenuBgActiveColor: 'var(--el-color-primary)',
+ // 宸︿晶鑿滃崟鏀惰捣閫変腑鑳屾櫙棰滆壊
+ leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
+ // 宸︿晶鑿滃崟瀛椾綋棰滆壊
+ leftMenuTextColor: '#bfcbd9',
+ // 宸︿晶鑿滃崟閫変腑瀛椾綋棰滆壊
+ leftMenuTextActiveColor: '#fff',
+ // logo瀛椾綋棰滆壊
+ logoTitleTextColor: '#fff',
+ // logo杈规棰滆壊
+ logoBorderColor: 'inherit',
+ // 澶撮儴鑳屾櫙棰滆壊
+ topHeaderBgColor: '#fff',
+ // 澶撮儴瀛椾綋棰滆壊
+ topHeaderTextColor: 'inherit',
+ // 澶撮儴鎮仠棰滆壊
+ topHeaderHoverColor: '#f6f6f6',
+ // 澶撮儴杈规棰滆壊
+ topToolBorderColor: '#eee'
+ }
+ }
+ },
+ getters: {
+ getBreadcrumb(): boolean {
+ return this.breadcrumb
+ },
+ getBreadcrumbIcon(): boolean {
+ return this.breadcrumbIcon
+ },
+ getCollapse(): boolean {
+ return this.collapse
+ },
+ getUniqueOpened(): boolean {
+ return this.uniqueOpened
+ },
+ getHamburger(): boolean {
+ return this.hamburger
+ },
+ getScreenfull(): boolean {
+ return this.screenfull
+ },
+ getSize(): boolean {
+ return this.size
+ },
+ getLocale(): boolean {
+ return this.locale
+ },
+ getMessage(): boolean {
+ return this.message
+ },
+ getTagsView(): boolean {
+ return this.tagsView
+ },
+ getTagsViewImmerse(): boolean {
+ return this.tagsViewImmerse
+ },
+ getTagsViewIcon(): boolean {
+ return this.tagsViewIcon
+ },
+ getLogo(): boolean {
+ return this.logo
+ },
+ getFixedHeader(): boolean {
+ return this.fixedHeader
+ },
+ getGreyMode(): boolean {
+ return this.greyMode
+ },
+ getFixedMenu(): boolean {
+ return this.fixedMenu
+ },
+ getPageLoading(): boolean {
+ return this.pageLoading
+ },
+ getLayout(): LayoutType {
+ return this.layout
+ },
+ getTitle(): string {
+ return this.title
+ },
+ getUserInfo(): string {
+ return this.userInfo
+ },
+ getIsDark(): boolean {
+ return this.isDark
+ },
+ getCurrentSize(): ElementPlusSize {
+ return this.currentSize
+ },
+ getSizeMap(): ElementPlusSize[] {
+ return this.sizeMap
+ },
+ getMobile(): boolean {
+ return this.mobile
+ },
+ getTheme(): ThemeTypes {
+ return this.theme
+ },
+ getFooter(): boolean {
+ return this.footer
+ }
+ },
+ actions: {
+ setPrimaryLight() {
+ if (this.theme.elColorPrimary) {
+ const elColorPrimary = this.theme.elColorPrimary
+ const color = this.isDark ? '#000000' : '#ffffff'
+ const lightList = [3, 5, 7, 8, 9]
+ lightList.forEach((v) => {
+ setCssVar(`--el-color-primary-light-${v}`, mix(color, elColorPrimary, v / 10))
+ })
+ setCssVar(`--el-color-primary-dark-2`, mix(color, elColorPrimary, 0.2))
+
+ this.setAllColorRgbVars()
+ }
+ },
+
+ // 澶勭悊element鑷甫鐨勪富棰樿壊鍜岃緟鍔╄壊鐨�-rgb鍒囨崲涓婚鍙樺寲锛屽锛�--el-color-primary-rgb
+ setAllColorRgbVars() {
+ // 闇�瑕佸鐞嗙殑棰滆壊绫诲瀷鍒楄〃
+ const colorTypes = ['primary', 'success', 'warning', 'danger', 'error', 'info']
+
+ colorTypes.forEach((type) => {
+ // 鑾峰彇褰撳墠棰滆壊鍊�
+ const colorValue = getCssColorVariable(`--el-color-${type}`)
+ if (colorValue) {
+ // 杞崲涓簉gba骞舵彁鍙朢GB閮ㄥ垎
+ const rgbaString = hexToRGB(colorValue, 1)
+ const rgbValues = rgbaString.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i)
+ if (rgbValues) {
+ const [, r, g, b] = rgbValues
+ // 璁剧疆瀵瑰簲鐨凴GB鍙橀噺
+ setCssVar(`--el-color-${type}-rgb`, `${r}, ${g}, ${b}`)
+ }
+ }
+ })
+ },
+ setBreadcrumb(breadcrumb: boolean) {
+ this.breadcrumb = breadcrumb
+ },
+ setBreadcrumbIcon(breadcrumbIcon: boolean) {
+ this.breadcrumbIcon = breadcrumbIcon
+ },
+ setCollapse(collapse: boolean) {
+ this.collapse = collapse
+ },
+ setUniqueOpened(uniqueOpened: boolean) {
+ this.uniqueOpened = uniqueOpened
+ },
+ setHamburger(hamburger: boolean) {
+ this.hamburger = hamburger
+ },
+ setScreenfull(screenfull: boolean) {
+ this.screenfull = screenfull
+ },
+ setSize(size: boolean) {
+ this.size = size
+ },
+ setLocale(locale: boolean) {
+ this.locale = locale
+ },
+ setMessage(message: boolean) {
+ this.message = message
+ },
+ setTagsView(tagsView: boolean) {
+ this.tagsView = tagsView
+ },
+ setTagsViewImmerse(tagsViewImmerse: boolean) {
+ this.tagsViewImmerse = tagsViewImmerse
+ },
+ setTagsViewIcon(tagsViewIcon: boolean) {
+ this.tagsViewIcon = tagsViewIcon
+ },
+ setLogo(logo: boolean) {
+ this.logo = logo
+ },
+ setFixedHeader(fixedHeader: boolean) {
+ this.fixedHeader = fixedHeader
+ },
+ setGreyMode(greyMode: boolean) {
+ this.greyMode = greyMode
+ },
+ setFixedMenu(fixedMenu: boolean) {
+ wsCache.set('fixedMenu', fixedMenu)
+ this.fixedMenu = fixedMenu
+ },
+ setPageLoading(pageLoading: boolean) {
+ this.pageLoading = pageLoading
+ },
+ setLayout(layout: LayoutType) {
+ if (this.mobile && layout !== 'classic') {
+ ElMessage.warning('绉诲姩绔ā寮忎笅涓嶆敮鎸佸垏鎹㈠叾浠栧竷灞�')
+ return
+ }
+ this.layout = layout
+ wsCache.set(CACHE_KEY.LAYOUT, this.layout)
+ },
+ setTitle(title: string) {
+ this.title = title
+ },
+ setIsDark(isDark: boolean) {
+ this.isDark = isDark
+ if (this.isDark) {
+ document.documentElement.classList.add('dark')
+ document.documentElement.classList.remove('light')
+ } else {
+ document.documentElement.classList.add('light')
+ document.documentElement.classList.remove('dark')
+ }
+ wsCache.set(CACHE_KEY.IS_DARK, this.isDark)
+ this.setPrimaryLight()
+ },
+ setCurrentSize(currentSize: ElementPlusSize) {
+ this.currentSize = currentSize
+ wsCache.set('currentSize', this.currentSize)
+ },
+ setMobile(mobile: boolean) {
+ this.mobile = mobile
+ },
+ setTheme(theme: ThemeTypes) {
+ this.theme = Object.assign(this.theme, theme)
+ wsCache.set(CACHE_KEY.THEME, this.theme)
+ },
+ setCssVarTheme() {
+ for (const key in this.theme) {
+ setCssVar(`--${humpToUnderline(key)}`, this.theme[key])
+ }
+ this.setPrimaryLight()
+ },
+ setFooter(footer: boolean) {
+ this.footer = footer
+ }
+ },
+ persist: false
+})
+
+export const useAppStoreWithOut = () => {
+ return useAppStore(store)
+}
diff --git a/src/store/modules/bpm/simpleWorkflow.ts b/src/store/modules/bpm/simpleWorkflow.ts
new file mode 100644
index 0000000..2942951
--- /dev/null
+++ b/src/store/modules/bpm/simpleWorkflow.ts
@@ -0,0 +1,55 @@
+import { store } from '../../index'
+import { defineStore } from 'pinia'
+
+export const useWorkFlowStore = defineStore('simpleWorkflow', {
+ state: () => ({
+ tableId: '',
+ isTried: false,
+ promoterDrawer: false,
+ approverDrawer: false,
+ approverConfig1: {},
+ copyerDrawer: false,
+ copyerConfig: {},
+ conditionDrawer: false,
+ conditionsConfig1: {
+ conditionNodes: []
+ },
+ userTaskConfig: {}
+ }),
+ actions: {
+ setTableId(payload) {
+ this.tableId = payload
+ },
+ setIsTried(payload) {
+ this.isTried = payload
+ },
+ setPromoter(payload) {
+ this.promoterDrawer = payload
+ },
+ setApproverDrawer(payload) {
+ this.approverDrawer = payload
+ },
+ setApproverConfig(payload) {
+ this.approverConfig1 = payload
+ },
+ setCopyerDrawer(payload) {
+ this.copyerDrawer = payload
+ },
+ setCopyerConfig(payload) {
+ this.copyerConfig = payload
+ },
+ setCondition(payload) {
+ this.conditionDrawer = payload
+ },
+ setConditionsConfig(payload) {
+ this.conditionsConfig1 = payload
+ },
+ setUserTaskConfig(payload) {
+ this.userTaskConfig = payload
+ }
+ }
+})
+
+export const useWorkFlowStoreWithOut = () => {
+ return useWorkFlowStore(store)
+}
diff --git a/src/store/modules/dict.ts b/src/store/modules/dict.ts
new file mode 100644
index 0000000..ee7f285
--- /dev/null
+++ b/src/store/modules/dict.ts
@@ -0,0 +1,110 @@
+import { defineStore } from 'pinia'
+import { store } from '../index'
+// @ts-ignore
+import { DictDataVO } from '@/api/system/dict/types'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+const { wsCache } = useCache('sessionStorage')
+import { getSimpleDictDataList } from '@/api/system/dict/dict.data'
+
+export interface DictValueType {
+ value: any
+ label: string
+ clorType?: string
+ cssClass?: string
+}
+export interface DictTypeType {
+ dictType: string
+ dictValue: DictValueType[]
+}
+export interface DictState {
+ dictMap: Map<string, any>
+ isSetDict: boolean
+}
+
+export const useDictStore = defineStore('dict', {
+ state: (): DictState => ({
+ dictMap: new Map<string, any>(),
+ isSetDict: false
+ }),
+ getters: {
+ getDictMap(): Recordable {
+ const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
+ if (dictMap) {
+ this.dictMap = dictMap
+ }
+ return this.dictMap
+ },
+ getIsSetDict(): boolean {
+ return this.isSetDict
+ }
+ },
+ actions: {
+ async setDictMap() {
+ const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
+ if (dictMap) {
+ this.dictMap = dictMap
+ this.isSetDict = true
+ } else {
+ const res = await getSimpleDictDataList()
+ if (!res || res.length === 0) {
+ return
+ }
+ // 璁剧疆鏁版嵁
+ const dictDataMap = new Map<string, any>()
+ res.forEach((dictData: DictDataVO) => {
+ // 鑾峰緱 dictType 灞傜骇
+ const enumValueObj = dictDataMap[dictData.dictType]
+ if (!enumValueObj) {
+ dictDataMap[dictData.dictType] = []
+ }
+ // 澶勭悊 dictValue 灞傜骇
+ dictDataMap[dictData.dictType].push({
+ value: dictData.value,
+ label: dictData.label,
+ colorType: dictData.colorType,
+ cssClass: dictData.cssClass
+ })
+ })
+ this.dictMap = dictDataMap
+ this.isSetDict = true
+ wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 绉� 杩囨湡
+ }
+ },
+ getDictByType(type: string) {
+ if (!this.isSetDict) {
+ this.setDictMap()
+ }
+ return this.dictMap[type]
+ },
+ async resetDict() {
+ wsCache.delete(CACHE_KEY.DICT_CACHE)
+ const res = await getSimpleDictDataList()
+ if (!res || res.length === 0) {
+ return
+ }
+ // 璁剧疆鏁版嵁
+ const dictDataMap = new Map<string, any>()
+ res.forEach((dictData: DictDataVO) => {
+ // 鑾峰緱 dictType 灞傜骇
+ const enumValueObj = dictDataMap[dictData.dictType]
+ if (!enumValueObj) {
+ dictDataMap[dictData.dictType] = []
+ }
+ // 澶勭悊 dictValue 灞傜骇
+ dictDataMap[dictData.dictType].push({
+ value: dictData.value,
+ label: dictData.label,
+ colorType: dictData.colorType,
+ cssClass: dictData.cssClass
+ })
+ })
+ this.dictMap = dictDataMap
+ this.isSetDict = true
+ wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 绉� 杩囨湡
+ }
+ }
+})
+
+export const useDictStoreWithOut = () => {
+ return useDictStore(store)
+}
diff --git a/src/store/modules/locale.ts b/src/store/modules/locale.ts
new file mode 100644
index 0000000..1fc772a
--- /dev/null
+++ b/src/store/modules/locale.ts
@@ -0,0 +1,59 @@
+import { defineStore } from 'pinia'
+import { store } from '../index'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import en from 'element-plus/es/locale/lang/en'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { LocaleDropdownType } from '@/types/localeDropdown'
+
+const { wsCache } = useCache()
+
+const elLocaleMap = {
+ 'zh-CN': zhCn,
+ en: en
+}
+interface LocaleState {
+ currentLocale: LocaleDropdownType
+ localeMap: LocaleDropdownType[]
+}
+
+export const useLocaleStore = defineStore('locales', {
+ state: (): LocaleState => {
+ return {
+ currentLocale: {
+ lang: wsCache.get(CACHE_KEY.LANG) || 'zh-CN',
+ elLocale: elLocaleMap[wsCache.get(CACHE_KEY.LANG) || 'zh-CN']
+ },
+ // 澶氳瑷�
+ localeMap: [
+ {
+ lang: 'zh-CN',
+ name: '绠�浣撲腑鏂�'
+ },
+ {
+ lang: 'en',
+ name: 'English'
+ }
+ ]
+ }
+ },
+ getters: {
+ getCurrentLocale(): LocaleDropdownType {
+ return this.currentLocale
+ },
+ getLocaleMap(): LocaleDropdownType[] {
+ return this.localeMap
+ }
+ },
+ actions: {
+ setCurrentLocale(localeMap: LocaleDropdownType) {
+ // this.locale = Object.assign(this.locale, localeMap)
+ this.currentLocale.lang = localeMap?.lang
+ this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
+ wsCache.set(CACHE_KEY.LANG, localeMap?.lang)
+ }
+ }
+})
+
+export const useLocaleStoreWithOut = () => {
+ return useLocaleStore(store)
+}
diff --git a/src/store/modules/lock.ts b/src/store/modules/lock.ts
new file mode 100644
index 0000000..68ae1d7
--- /dev/null
+++ b/src/store/modules/lock.ts
@@ -0,0 +1,48 @@
+import { defineStore } from 'pinia'
+import { store } from '@/store'
+
+interface lockInfo {
+ isLock?: boolean
+ password?: string
+}
+
+interface LockState {
+ lockInfo: lockInfo
+}
+
+export const useLockStore = defineStore('lock', {
+ state: (): LockState => {
+ return {
+ lockInfo: {
+ // isLock: false, // 鏄惁閿佸畾灞忓箷
+ // password: '' // 閿佸睆瀵嗙爜
+ }
+ }
+ },
+ getters: {
+ getLockInfo(): lockInfo {
+ return this.lockInfo
+ }
+ },
+ actions: {
+ setLockInfo(lockInfo: lockInfo) {
+ this.lockInfo = lockInfo
+ },
+ resetLockInfo() {
+ this.lockInfo = {}
+ },
+ unLock(password: string) {
+ if (this.lockInfo?.password === password) {
+ this.resetLockInfo()
+ return true
+ } else {
+ return false
+ }
+ }
+ },
+ persist: true
+})
+
+export const useLockStoreWithOut = () => {
+ return useLockStore(store)
+}
diff --git a/src/store/modules/mall/kefu.ts b/src/store/modules/mall/kefu.ts
new file mode 100644
index 0000000..2aecee0
--- /dev/null
+++ b/src/store/modules/mall/kefu.ts
@@ -0,0 +1,81 @@
+import { store } from '@/store'
+import { defineStore } from 'pinia'
+import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+import { isEmpty } from '@/utils/is'
+
+interface MallKefuInfoVO {
+ conversationList: KeFuConversationRespVO[] // 浼氳瘽鍒楄〃
+ conversationMessageList: Map<number, KeFuMessageRespVO[]> // 浼氳瘽娑堟伅
+}
+
+export const useMallKefuStore = defineStore('mall-kefu', {
+ state: (): MallKefuInfoVO => ({
+ conversationList: [],
+ conversationMessageList: new Map<number, KeFuMessageRespVO[]>() // key 浼氳瘽锛寁alue 浼氳瘽娑堟伅鍒楄〃
+ }),
+ getters: {
+ getConversationList(): KeFuConversationRespVO[] {
+ return this.conversationList
+ },
+ getConversationMessageList(): (conversationId: number) => KeFuMessageRespVO[] | undefined {
+ return (conversationId: number) => this.conversationMessageList.get(conversationId)
+ }
+ },
+ actions: {
+ // ======================= 浼氳瘽娑堟伅鐩稿叧 =======================
+ /** 缂撳瓨鍘嗗彶娑堟伅 */
+ saveMessageList(conversationId: number, messageList: KeFuMessageRespVO[]) {
+ this.conversationMessageList.set(conversationId, messageList)
+ },
+
+ // ======================= 浼氳瘽鐩稿叧 =======================
+ /** 鍔犺浇浼氳瘽缂撳瓨鍒楄〃 */
+ async setConversationList() {
+ this.conversationList = await KeFuConversationApi.getConversationList()
+ this.conversationSort()
+ },
+ /** 鏇存柊浼氳瘽缂撳瓨宸茶 */
+ async updateConversationStatus(conversationId: number) {
+ if (isEmpty(this.conversationList)) {
+ return
+ }
+ const conversation = this.conversationList.find((item) => item.id === conversationId)
+ conversation && (conversation.adminUnreadMessageCount = 0)
+ },
+ /** 鏇存柊浼氳瘽缂撳瓨 */
+ async updateConversation(conversationId: number) {
+ if (isEmpty(this.conversationList)) {
+ return
+ }
+
+ const conversation = await KeFuConversationApi.getConversation(conversationId)
+ this.deleteConversation(conversationId)
+ conversation && this.conversationList.push(conversation)
+ this.conversationSort()
+ },
+ /** 鍒犻櫎浼氳瘽缂撳瓨 */
+ deleteConversation(conversationId: number) {
+ const index = this.conversationList.findIndex((item) => item.id === conversationId)
+ // 瀛樺湪鍒欏垹闄�
+ if (index > -1) {
+ this.conversationList.splice(index, 1)
+ }
+ },
+ conversationSort() {
+ // 鎸夌疆椤跺睘鎬у拰鏈�鍚庢秷鎭椂闂存帓搴�
+ this.conversationList.sort((a, b) => {
+ // 鎸夌収缃《鎺掑簭锛岀疆椤剁殑浼氬湪鍓嶉潰
+ if (a.adminPinned !== b.adminPinned) {
+ return a.adminPinned ? -1 : 1
+ }
+ // 鎸夌収鏈�鍚庢秷鎭椂闂存帓搴忥紝鏈�杩戠殑浼氬湪鍓嶉潰
+ return (b.lastMessageTime as unknown as number) - (a.lastMessageTime as unknown as number)
+ })
+ }
+ }
+})
+
+export const useMallKefuStoreWithOut = () => {
+ return useMallKefuStore(store)
+}
diff --git a/src/store/modules/permission.ts b/src/store/modules/permission.ts
new file mode 100644
index 0000000..2ff8111
--- /dev/null
+++ b/src/store/modules/permission.ts
@@ -0,0 +1,71 @@
+import { defineStore } from 'pinia'
+import { store } from '@/store'
+import { cloneDeep } from 'lodash-es'
+import remainingRouter from '@/router/modules/remaining'
+import { flatMultiLevelRoutes, generateRoute } from '@/utils/routerHelper'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+
+const { wsCache } = useCache()
+
+export interface PermissionState {
+ routers: AppRouteRecordRaw[]
+ addRouters: AppRouteRecordRaw[]
+ menuTabRouters: AppRouteRecordRaw[]
+}
+
+export const usePermissionStore = defineStore('permission', {
+ state: (): PermissionState => ({
+ routers: [],
+ addRouters: [],
+ menuTabRouters: []
+ }),
+ getters: {
+ getRouters(): AppRouteRecordRaw[] {
+ return this.routers
+ },
+ getAddRouters(): AppRouteRecordRaw[] {
+ return flatMultiLevelRoutes(cloneDeep(this.addRouters))
+ },
+ getMenuTabRouters(): AppRouteRecordRaw[] {
+ return this.menuTabRouters
+ }
+ },
+ actions: {
+ async generateRoutes(): Promise<unknown> {
+ return new Promise<void>(async (resolve) => {
+ // 鑾峰緱鑿滃崟鍒楄〃锛屽畠鍦ㄧ櫥褰曠殑鏃跺�欙紝setUserInfoAction 鏂规硶涓凡缁忚繘琛岃幏鍙�
+ let res: AppCustomRouteRecordRaw[] = []
+ const roleRouters = wsCache.get(CACHE_KEY.ROLE_ROUTERS)
+ if (roleRouters) {
+ res = roleRouters as AppCustomRouteRecordRaw[]
+ }
+ const routerMap: AppRouteRecordRaw[] = generateRoute(res)
+ // 鍔ㄦ�佽矾鐢憋紝404涓�瀹氳鏀惧埌鏈�鍚庨潰
+ // preschooler锛歷ue-router@4浠ュ悗宸叉敮鎸侀潤鎬�404璺敱锛屾澶勫彲涓嶅啀杩藉姞
+ this.addRouters = routerMap.concat([
+ {
+ path: '/:path(.*)*',
+ // redirect: '/404',
+ component: () => import('@/views/Error/404.vue'),
+ name: '404Page',
+ meta: {
+ hidden: true,
+ breadcrumb: false
+ }
+ }
+ ])
+ // 娓叉煋鑿滃崟鐨勬墍鏈夎矾鐢�
+ this.routers = cloneDeep(remainingRouter).concat(routerMap)
+ resolve()
+ })
+ },
+ setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
+ this.menuTabRouters = routers
+ }
+ },
+ persist: false
+})
+
+export const usePermissionStoreWithOut = () => {
+ return usePermissionStore(store)
+}
diff --git a/src/store/modules/tagsView.ts b/src/store/modules/tagsView.ts
new file mode 100644
index 0000000..b540635
--- /dev/null
+++ b/src/store/modules/tagsView.ts
@@ -0,0 +1,183 @@
+import router from '@/router'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+import { getRawRoute } from '@/utils/routerHelper'
+import { defineStore } from 'pinia'
+import { store } from '../index'
+import { findIndex } from '@/utils'
+import { useUserStoreWithOut } from './user'
+
+export interface TagsViewState {
+ visitedViews: RouteLocationNormalizedLoaded[]
+ cachedViews: Set<string>
+ selectedTag?: RouteLocationNormalizedLoaded
+}
+
+export const useTagsViewStore = defineStore('tagsView', {
+ state: (): TagsViewState => ({
+ visitedViews: [],
+ cachedViews: new Set(),
+ selectedTag: undefined
+ }),
+ getters: {
+ getVisitedViews(): RouteLocationNormalizedLoaded[] {
+ return this.visitedViews
+ },
+ getCachedViews(): string[] {
+ return Array.from(this.cachedViews)
+ },
+ getSelectedTag(): RouteLocationNormalizedLoaded | undefined {
+ return this.selectedTag
+ }
+ },
+ actions: {
+ // 鏂板缂撳瓨鍜宼ag
+ addView(view: RouteLocationNormalizedLoaded): void {
+ this.addVisitedView(view)
+ this.addCachedView()
+ },
+ // 鏂板tag
+ addVisitedView(view: RouteLocationNormalizedLoaded) {
+ if (this.visitedViews.some((v) => v.fullPath === view.fullPath)) return
+ if (view.meta?.noTagsView) return
+ const visitedView = Object.assign({}, view, { title: view.meta?.title || 'no-name' })
+
+ if (visitedView.meta) {
+ const titleSuffixList: string[] = []
+ this.visitedViews.forEach((v) => {
+ if (v.path === visitedView.path && v.meta?.title === visitedView.meta?.title) {
+ titleSuffixList.push(v.meta?.titleSuffix || '1')
+ }
+ })
+ if (titleSuffixList.length) {
+ let titleSuffix = 1
+ while (titleSuffixList.includes(`${titleSuffix}`)) {
+ titleSuffix += 1
+ }
+ visitedView.meta.titleSuffix = titleSuffix === 1 ? undefined : `${titleSuffix}`
+ }
+ }
+
+ this.visitedViews.push(visitedView)
+ },
+ // 鏂板缂撳瓨
+ addCachedView() {
+ const cacheMap: Set<string> = new Set()
+ for (const v of this.visitedViews) {
+ const item = getRawRoute(v)
+ const needCache = !item.meta?.noCache
+ if (!needCache) {
+ continue
+ }
+ const name = item.name as string
+ cacheMap.add(name)
+ }
+ if (Array.from(this.cachedViews).sort().toString() === Array.from(cacheMap).sort().toString())
+ return
+ this.cachedViews = cacheMap
+ },
+ // 鍒犻櫎鏌愪釜
+ delView(view: RouteLocationNormalizedLoaded) {
+ this.delVisitedView(view)
+ this.delCachedView()
+ },
+ // 鍒犻櫎tag
+ delVisitedView(view: RouteLocationNormalizedLoaded) {
+ for (const [i, v] of this.visitedViews.entries()) {
+ if (v.fullPath === view.fullPath) {
+ this.visitedViews.splice(i, 1)
+ break
+ }
+ }
+ },
+ // 鍒犻櫎缂撳瓨
+ delCachedView() {
+ const route = router.currentRoute.value
+ const index = findIndex<string>(this.getCachedViews, (v) => v === route.name)
+ // 闇�瑕佹敞閲婏紝瑙e喅鈥滄爣绛鹃〉鍒锋柊鏃犳晥鈥濄�傜浉鍏虫渚嬶細https://github.com/yudaocode/yudao-ui-admin-vue3/issues/180
+ // for (const v of this.visitedViews) {
+ // if (v.name === route.name) {
+ // return
+ // }
+ // }
+ if (index > -1) {
+ this.cachedViews.delete(this.getCachedViews[index])
+ }
+ },
+ // 鍒犻櫎鎵�鏈夌紦瀛樺拰tag
+ delAllViews() {
+ this.delAllVisitedViews()
+ this.delCachedView()
+ },
+ // 鍒犻櫎鎵�鏈塼ag
+ delAllVisitedViews() {
+ const userStore = useUserStoreWithOut()
+
+ // const affixTags = this.visitedViews.filter((tag) => tag.meta.affix)
+ this.visitedViews = userStore.getUser
+ ? this.visitedViews.filter((tag) => tag?.meta?.affix)
+ : []
+ },
+ // 鍒犻櫎鍏朵粬
+ delOthersViews(view: RouteLocationNormalizedLoaded) {
+ this.delOthersVisitedViews(view)
+ this.addCachedView()
+ },
+ // 鍒犻櫎鍏朵粬tag
+ delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
+ this.visitedViews = this.visitedViews.filter((v) => {
+ return v?.meta?.affix || v.fullPath === view.fullPath
+ })
+ },
+ // 鍒犻櫎宸︿晶
+ delLeftViews(view: RouteLocationNormalizedLoaded) {
+ const index = findIndex<RouteLocationNormalizedLoaded>(
+ this.visitedViews,
+ (v) => v.fullPath === view.fullPath
+ )
+ if (index > -1) {
+ this.visitedViews = this.visitedViews.filter((v, i) => {
+ return v?.meta?.affix || v.fullPath === view.fullPath || i > index
+ })
+ this.addCachedView()
+ }
+ },
+ // 鍒犻櫎鍙充晶
+ delRightViews(view: RouteLocationNormalizedLoaded) {
+ const index = findIndex<RouteLocationNormalizedLoaded>(
+ this.visitedViews,
+ (v) => v.fullPath === view.fullPath
+ )
+ if (index > -1) {
+ this.visitedViews = this.visitedViews.filter((v, i) => {
+ return v?.meta?.affix || v.fullPath === view.fullPath || i < index
+ })
+ this.addCachedView()
+ }
+ },
+ updateVisitedView(view: RouteLocationNormalizedLoaded) {
+ for (let v of this.visitedViews) {
+ if (v.fullPath === view.fullPath) {
+ v = Object.assign(v, view)
+ break
+ }
+ }
+ },
+ // 璁剧疆褰撳墠閫変腑鐨� tag
+ setSelectedTag(tag: RouteLocationNormalizedLoaded) {
+ this.selectedTag = tag
+ },
+ setTitle(title: string, path?: string) {
+ for (const v of this.visitedViews) {
+ if (v.path === (path ?? this.selectedTag?.path)) {
+ v.meta.title = title
+ break
+ }
+ }
+ }
+ },
+ persist: false
+})
+
+export const useTagsViewStoreWithOut = () => {
+ return useTagsViewStore(store)
+}
diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts
new file mode 100644
index 0000000..aea2eda
--- /dev/null
+++ b/src/store/modules/user.ts
@@ -0,0 +1,108 @@
+import { store } from '@/store'
+import { defineStore } from 'pinia'
+import { getAccessToken, removeToken } from '@/utils/auth'
+import { CACHE_KEY, useCache, deleteUserCache } from '@/hooks/web/useCache'
+import { getInfo, loginOut } from '@/api/login'
+
+const { wsCache } = useCache()
+
+interface UserVO {
+ id: number
+ avatar: string
+ nickname: string
+ deptId: number
+}
+
+interface UserInfoVO {
+ // USER 缂撳瓨
+ permissions: Set<string>
+ roles: string[]
+ isSetUser: boolean
+ user: UserVO
+}
+
+export const useUserStore = defineStore('admin-user', {
+ state: (): UserInfoVO => ({
+ permissions: new Set<string>(),
+ roles: [],
+ isSetUser: false,
+ user: {
+ id: 0,
+ avatar: '',
+ nickname: '',
+ deptId: 0
+ }
+ }),
+ getters: {
+ getPermissions(): Set<string> {
+ return this.permissions
+ },
+ getRoles(): string[] {
+ return this.roles
+ },
+ getIsSetUser(): boolean {
+ return this.isSetUser
+ },
+ getUser(): UserVO {
+ return this.user
+ }
+ },
+ actions: {
+ async setUserInfoAction() {
+ if (!getAccessToken()) {
+ this.resetState()
+ return null
+ }
+ let userInfo = wsCache.get(CACHE_KEY.USER)
+ if (!userInfo) {
+ userInfo = await getInfo()
+ } else {
+ // 鐗规畩锛氬湪鏈夌紦瀛樼殑鎯呭喌涓嬶紝杩涜鍔犺浇銆備絾鏄嵆浣垮姞杞藉け璐ワ紝涔熶笉褰卞搷鍚庣画鐨勬搷浣滐紝淇濊瘉鍙互杩涘叆绯荤粺
+ try {
+ userInfo = await getInfo()
+ } catch (error) {}
+ }
+ this.permissions = new Set(userInfo.permissions || []) // 鍏滃簳涓� [] https://t.zsxq.com/xCJew
+ this.roles = userInfo.roles
+ this.user = userInfo.user
+ this.isSetUser = true
+ wsCache.set(CACHE_KEY.USER, userInfo)
+ wsCache.set(CACHE_KEY.ROLE_ROUTERS, userInfo.menus)
+ },
+ async setUserAvatarAction(avatar: string) {
+ const userInfo = wsCache.get(CACHE_KEY.USER)
+ // NOTE: 鏄惁闇�瑕佸儚`setUserInfoAction`涓�鏍峰垽鏂璥userInfo != null`
+ this.user.avatar = avatar
+ userInfo.user.avatar = avatar
+ wsCache.set(CACHE_KEY.USER, userInfo)
+ },
+ async setUserNicknameAction(nickname: string) {
+ const userInfo = wsCache.get(CACHE_KEY.USER)
+ // NOTE: 鏄惁闇�瑕佸儚`setUserInfoAction`涓�鏍峰垽鏂璥userInfo != null`
+ this.user.nickname = nickname
+ userInfo.user.nickname = nickname
+ wsCache.set(CACHE_KEY.USER, userInfo)
+ },
+ async loginOut() {
+ await loginOut()
+ removeToken()
+ deleteUserCache() // 鍒犻櫎鐢ㄦ埛缂撳瓨
+ this.resetState()
+ },
+ resetState() {
+ this.permissions = new Set<string>()
+ this.roles = []
+ this.isSetUser = false
+ this.user = {
+ id: 0,
+ avatar: '',
+ nickname: '',
+ deptId: 0
+ }
+ }
+ }
+})
+
+export const useUserStoreWithOut = () => {
+ return useUserStore(store)
+}
diff --git a/src/styles/FormCreate/fonts/fontello.woff b/src/styles/FormCreate/fonts/fontello.woff
new file mode 100644
index 0000000..1e00f49
--- /dev/null
+++ b/src/styles/FormCreate/fonts/fontello.woff
Binary files differ
diff --git a/src/styles/FormCreate/index.scss b/src/styles/FormCreate/index.scss
new file mode 100644
index 0000000..bb62000
--- /dev/null
+++ b/src/styles/FormCreate/index.scss
@@ -0,0 +1,22 @@
+// 浣跨敤瀛椾綋鍥炬爣鏉ユ簮 https://fontello.com/
+
+@font-face {
+ font-family: 'fc-icon';
+ src: url('@/styles/FormCreate/fonts/fontello.woff') format('woff');
+}
+
+.icon-doc-text:before {
+ content: '\f0f6';
+}
+
+.icon-server:before {
+ content: '\f233';
+}
+
+.icon-address-card-o:before {
+ content: '\f2bc';
+}
+
+.icon-user-o:before {
+ content: '\f2c0';
+}
diff --git a/src/styles/global.module.scss b/src/styles/global.module.scss
new file mode 100644
index 0000000..af793f0
--- /dev/null
+++ b/src/styles/global.module.scss
@@ -0,0 +1,6 @@
+@use './variables.scss' as *;
+// 瀵煎嚭鍙橀噺
+:export {
+ namespace: $namespace;
+ elNamespace: $elNamespace;
+}
diff --git a/src/styles/index.scss b/src/styles/index.scss
new file mode 100644
index 0000000..7607941
--- /dev/null
+++ b/src/styles/index.scss
@@ -0,0 +1,37 @@
+@use './var.css';
+@use './FormCreate/index.scss';
+@use './theme.scss';
+@use 'element-plus/theme-chalk/dark/css-vars.css';
+
+.reset-margin [class*='el-icon'] + span {
+ margin-left: 2px !important;
+}
+
+// 瑙e喅鎶藉眽寮瑰嚭鏃讹紝body瀹藉害鍙樺寲鐨勯棶棰�
+.el-popup-parent--hidden {
+ width: 100% !important;
+}
+
+// 瑙e喅琛ㄦ牸鍐呭瓒呰繃琛ㄦ牸鎬诲搴﹀悗锛屾í鍚戞粴鍔ㄦ潯鍓嶇椤朵笉鍒拌〃鏍艰竟缂樼殑闂
+.el-scrollbar__bar {
+ display: flex;
+ justify-content: flex-start;
+}
+
+/* nprogress 閫傞厤 element-plus 鐨勪富棰樿壊 */
+#nprogress {
+ & .bar {
+ background-color: var(--el-color-primary) !important;
+ }
+
+ & .peg {
+ box-shadow:
+ 0 0 10px var(--el-color-primary),
+ 0 0 5px var(--el-color-primary) !important;
+ }
+
+ & .spinner-icon {
+ border-top-color: var(--el-color-primary);
+ border-left-color: var(--el-color-primary);
+ }
+}
diff --git a/src/styles/theme.scss b/src/styles/theme.scss
new file mode 100644
index 0000000..39b03b3
--- /dev/null
+++ b/src/styles/theme.scss
@@ -0,0 +1,6 @@
+// .text-color {
+// color: var(--el-text-color-regular);
+// }
+// .dark .dark\:text-color {
+// color: rgba(255, 255, 255, var(--dark-text-color));
+// }
diff --git a/src/styles/var.css b/src/styles/var.css
new file mode 100644
index 0000000..44f9405
--- /dev/null
+++ b/src/styles/var.css
@@ -0,0 +1,74 @@
+:root {
+ --login-bg-color: #293146;
+
+ --left-menu-max-width: 200px;
+
+ --left-menu-min-width: 64px;
+
+ --left-menu-bg-color: #001529;
+
+ --left-menu-bg-light-color: #0f2438;
+
+ --left-menu-bg-active-color: var(--el-color-primary);
+
+ --left-menu-text-color: #bfcbd9;
+
+ --left-menu-text-active-color: #fff;
+
+ --left-menu-collapse-bg-active-color: var(--el-color-primary);
+ /* left menu end */
+
+ /* logo start */
+ --logo-height: 50px;
+
+ --logo-title-text-color: #fff;
+ /* logo end */
+
+ /* header start */
+ --top-header-bg-color: '#fff';
+
+ --top-header-text-color: 'inherit';
+
+ --top-header-hover-color: #f6f6f6;
+
+ --top-tool-height: var(--logo-height);
+
+ --top-tool-p-x: 0;
+
+ --tags-view-height: 35px;
+ /* header start */
+
+ /* tab menu start */
+ --tab-menu-max-width: 80px;
+
+ --tab-menu-min-width: 30px;
+
+ --tab-menu-collapse-height: 36px;
+ /* tab menu end */
+
+ --app-content-padding: 20px;
+
+ --app-content-bg-color: #f5f7f9;
+
+ --app-footer-height: 50px;
+
+ --transition-time-02: 0.2s;
+}
+
+.dark {
+ --app-content-bg-color: var(--el-bg-color);
+}
+
+html,
+body {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+*,
+:after,
+:before {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
diff --git a/src/styles/variables.scss b/src/styles/variables.scss
new file mode 100644
index 0000000..00b66f1
--- /dev/null
+++ b/src/styles/variables.scss
@@ -0,0 +1,4 @@
+// 鍛藉悕绌洪棿
+$namespace: v;
+// el鍛藉悕绌洪棿
+$elNamespace: el;
diff --git a/src/types/components.d.ts b/src/types/components.d.ts
new file mode 100644
index 0000000..8de1f33
--- /dev/null
+++ b/src/types/components.d.ts
@@ -0,0 +1,56 @@
+export type ComponentName =
+ | 'Radio'
+ | 'RadioButton'
+ | 'Checkbox'
+ | 'CheckboxButton'
+ | 'Input'
+ | 'Autocomplete'
+ | 'InputNumber'
+ | 'Select'
+ | 'Cascader'
+ | 'Switch'
+ | 'Slider'
+ | 'TimePicker'
+ | 'DatePicker'
+ | 'Rate'
+ | 'ColorPicker'
+ | 'Transfer'
+ | 'Divider'
+ | 'TimeSelect'
+ | 'SelectV2'
+ | 'TreeSelect'
+ | 'InputPassword'
+ | 'Editor'
+ | 'UploadImg'
+ | 'UploadImgs'
+ | 'UploadFile'
+
+export type ColProps = {
+ span?: number
+ xs?: number
+ sm?: number
+ md?: number
+ lg?: number
+ xl?: number
+ tag?: string
+}
+
+export type ComponentOptions = {
+ label?: string
+ value?: FormValueType
+ disabled?: boolean
+ key?: string | number
+ children?: ComponentOptions[]
+ options?: ComponentOptions[]
+} & Recordable
+
+export type ComponentOptionsAlias = {
+ labelField?: string
+ valueField?: string
+}
+
+export type ComponentProps = {
+ optionsAlias?: ComponentOptionsAlias
+ options?: ComponentOptions[]
+ optionsSlot?: boolean
+} & Recordable
diff --git a/src/types/configGlobal.d.ts b/src/types/configGlobal.d.ts
new file mode 100644
index 0000000..f6d7b3c
--- /dev/null
+++ b/src/types/configGlobal.d.ts
@@ -0,0 +1,4 @@
+import { ElementPlusSize } from './elementPlus'
+export interface ConfigGlobalTypes {
+ size?: ElementPlusSize
+}
diff --git a/src/types/contextMenu.d.ts b/src/types/contextMenu.d.ts
new file mode 100644
index 0000000..0738d0e
--- /dev/null
+++ b/src/types/contextMenu.d.ts
@@ -0,0 +1,7 @@
+export type contextMenuSchema = {
+ disabled?: boolean
+ divided?: boolean
+ icon?: string
+ label: string
+ command?: (item: contextMenuSchema) => void
+}
diff --git a/src/types/descriptions.d.ts b/src/types/descriptions.d.ts
new file mode 100644
index 0000000..af6d68c
--- /dev/null
+++ b/src/types/descriptions.d.ts
@@ -0,0 +1,14 @@
+export interface DescriptionsSchema {
+ span?: number // 鍗犲灏戝垎
+ field: string // 瀛楁鍚�
+ label?: string // label鍚�
+ mappedField?: string // 瀛楁鏄犲皠
+ width?: string | number
+ minWidth?: string | number
+ align?: 'left' | 'center' | 'right'
+ labelAlign?: 'left' | 'center' | 'right'
+ className?: string
+ labelClassName?: string
+ dateFormat?: string // add by 鏄熻锛氭敮鎸佹椂闂寸殑鏍煎紡鍖�
+ dictType?: string // add by 鏄熻锛氭敮鎸� dict 瀛楀吀鏁版嵁
+}
diff --git a/src/types/elementPlus.d.ts b/src/types/elementPlus.d.ts
new file mode 100644
index 0000000..2c6b76e
--- /dev/null
+++ b/src/types/elementPlus.d.ts
@@ -0,0 +1,3 @@
+export type ElementPlusSize = 'default' | 'small' | 'large'
+
+export type ElementPlusInfoType = 'success' | 'info' | 'warning' | 'danger'
diff --git a/src/types/form.d.ts b/src/types/form.d.ts
new file mode 100644
index 0000000..980c8cc
--- /dev/null
+++ b/src/types/form.d.ts
@@ -0,0 +1,44 @@
+import type { CSSProperties } from 'vue'
+import { ColProps, ComponentProps, ComponentName } from '@/types/components'
+import type { AxiosPromise } from 'axios'
+
+export type FormSetPropsType = {
+ field: string
+ path: string
+ value: any
+}
+
+export type FormValueType = string | number | string[] | number[] | boolean | undefined | null
+
+export type FormItemProps = {
+ labelWidth?: string | number
+ required?: boolean
+ rules?: Recordable
+ error?: string
+ showMessage?: boolean
+ inlineMessage?: boolean
+ style?: CSSProperties
+}
+
+export type FormSchema = {
+ // 鍞竴鍊�
+ field: string
+ // 鏍囬
+ label?: string
+ // 鎻愮ず
+ labelMessage?: string
+ // col缁勪欢灞炴��
+ colProps?: ColProps
+ // 琛ㄥ崟缁勪欢灞炴�э紝slots瀵瑰簲鐨勬槸琛ㄥ崟缁勪欢鐨勬彃妲斤紝瑙勫垯锛�${field}-xxx锛屽叿浣撳彲浠ユ煡鐪媏lement-plus鏂囨。
+ componentProps?: { slots?: Recordable } & ComponentProps
+ // formItem缁勪欢灞炴��
+ formItemProps?: FormItemProps
+ // 娓叉煋鐨勭粍浠�
+ component?: ComponentName
+ // 鍒濆鍊�
+ value?: FormValueType
+ // 鏄惁闅愯棌
+ hidden?: boolean
+ // 杩滅▼鍔犺浇涓嬫媺椤�
+ api?: <T = any>() => AxiosPromise<T>
+}
diff --git a/src/types/icon.d.ts b/src/types/icon.d.ts
new file mode 100644
index 0000000..d1ffcdb
--- /dev/null
+++ b/src/types/icon.d.ts
@@ -0,0 +1,5 @@
+export interface IconTypes {
+ size?: number
+ color?: string
+ icon: string
+}
diff --git a/src/types/infoTip.d.ts b/src/types/infoTip.d.ts
new file mode 100644
index 0000000..6eff083
--- /dev/null
+++ b/src/types/infoTip.d.ts
@@ -0,0 +1,4 @@
+export interface TipSchema {
+ label: string
+ keys?: string[]
+}
diff --git a/src/types/layout.d.ts b/src/types/layout.d.ts
new file mode 100644
index 0000000..cad3e2a
--- /dev/null
+++ b/src/types/layout.d.ts
@@ -0,0 +1 @@
+export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu'
diff --git a/src/types/localeDropdown.d.ts b/src/types/localeDropdown.d.ts
new file mode 100644
index 0000000..c749dce
--- /dev/null
+++ b/src/types/localeDropdown.d.ts
@@ -0,0 +1,10 @@
+export interface Language {
+ el: Recordable
+ name: string
+}
+
+export interface LocaleDropdownType {
+ lang: LocaleType
+ name?: string
+ elLocale?: Language
+}
diff --git a/src/types/qrcode.d.ts b/src/types/qrcode.d.ts
new file mode 100644
index 0000000..86cdf0b
--- /dev/null
+++ b/src/types/qrcode.d.ts
@@ -0,0 +1,9 @@
+export interface QrcodeLogo {
+ src?: string
+ logoSize?: number
+ bgColor?: string
+ borderSize?: number
+ crossOrigin?: string
+ borderRadius?: number
+ logoRadius?: number
+}
diff --git a/src/types/table.d.ts b/src/types/table.d.ts
new file mode 100644
index 0000000..9cb4205
--- /dev/null
+++ b/src/types/table.d.ts
@@ -0,0 +1,44 @@
+export type TableColumn = {
+ field: string
+ label?: string
+ width?: number | string
+ fixed?: 'left' | 'right'
+ children?: TableColumn[]
+} & Recordable
+
+export type VxeTableColumn = {
+ field: string
+ title?: string
+ children?: TableColumn[]
+} & Recordable
+
+export type TableSlotDefault = {
+ row: Recordable
+ column: TableColumn
+ $index: number
+} & Recordable
+
+export interface Pagination {
+ small?: boolean
+ background?: boolean
+ pageSize?: number
+ defaultPageSize?: number
+ total?: number
+ pageCount?: number
+ pagerCount?: number
+ currentPage?: number
+ defaultCurrentPage?: number
+ layout?: string
+ pageSizes?: number[]
+ popperClass?: string
+ prevText?: string
+ nextText?: string
+ disabled?: boolean
+ hideOnSinglePage?: boolean
+}
+
+export interface TableSetPropsType {
+ field: string
+ path: string
+ value: any
+}
diff --git a/src/types/theme.d.ts b/src/types/theme.d.ts
new file mode 100644
index 0000000..ad649b0
--- /dev/null
+++ b/src/types/theme.d.ts
@@ -0,0 +1,16 @@
+export type ThemeTypes = {
+ elColorPrimary?: string
+ leftMenuBorderColor?: string
+ leftMenuBgColor?: string
+ leftMenuBgLightColor?: string
+ leftMenuBgActiveColor?: string
+ leftMenuCollapseBgActiveColor?: string
+ leftMenuTextColor?: string
+ leftMenuTextActiveColor?: string
+ logoTitleTextColor?: string
+ logoBorderColor?: string
+ topHeaderBgColor?: string
+ topHeaderTextColor?: string
+ topHeaderHoverColor?: string
+ topToolBorderColor?: string
+}
diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts
new file mode 100644
index 0000000..ca58df2
--- /dev/null
+++ b/src/utils/Logger.ts
@@ -0,0 +1,100 @@
+const isArray = function (obj: any): boolean {
+ return Object.prototype.toString.call(obj) === '[object Array]'
+}
+
+const Logger = () => {}
+
+Logger.typeColor = function (type: string) {
+ let color = ''
+ switch (type) {
+ case 'primary':
+ color = '#2d8cf0'
+ break
+ case 'success':
+ color = '#19be6b'
+ break
+ case 'info':
+ color = '#909399'
+ break
+ case 'warn':
+ color = '#ff9900'
+ break
+ case 'error':
+ color = '#f03f14'
+ break
+ default:
+ color = '#35495E'
+ break
+ }
+ return color
+}
+
+Logger.print = function (type = 'default', text: any, back = false) {
+ if (typeof text === 'object') {
+ // 濡傛灉鏄皪璞″墖瑾跨敤鎵撳嵃灏嶈薄鏂瑰紡
+ isArray(text) ? console.table(text) : console.dir(text)
+ return
+ }
+ if (back) {
+ // 濡傛灉鏄墦鍗板付鑳屾櫙鍦栫殑
+ console.log(
+ `%c ${text} `,
+ `background:${Logger.typeColor(type)}; padding: 2px; border-radius: 4px; color: #fff;`
+ )
+ } else {
+ console.log(
+ `%c ${text} `,
+ `border: 1px solid ${Logger.typeColor(type)};
+ padding: 2px; border-radius: 4px;
+ color: ${Logger.typeColor(type)};`
+ )
+ }
+}
+
+Logger.printBack = function (type = 'primary', text) {
+ this.print(type, text, true)
+}
+
+Logger.pretty = function (type = 'primary', title, text) {
+ if (typeof text === 'object') {
+ console.group('Console Group', title)
+ console.log(
+ `%c ${title}`,
+ `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)};
+ padding: 1px; border-radius: 4px; color: #fff;`
+ )
+ isArray(text) ? console.table(text) : console.dir(text)
+ console.groupEnd()
+ return
+ }
+ console.log(
+ `%c ${title} %c ${text} %c`,
+ `background:${Logger.typeColor(type)};border:1px solid ${Logger.typeColor(type)};
+ padding: 1px; border-radius: 4px 0 0 4px; color: #fff;`,
+ `border:1px solid ${Logger.typeColor(type)};
+ padding: 1px; border-radius: 0 4px 4px 0; color: ${Logger.typeColor(type)};`,
+ 'background:transparent'
+ )
+}
+
+Logger.prettyPrimary = function (title, ...text) {
+ text.forEach((t) => this.pretty('primary', title, t))
+}
+
+Logger.prettySuccess = function (title, ...text) {
+ text.forEach((t) => this.pretty('success', title, t))
+}
+
+Logger.prettyWarn = function (title, ...text) {
+ text.forEach((t) => this.pretty('warn', title, t))
+}
+
+Logger.prettyError = function (title, ...text) {
+ text.forEach((t) => this.pretty('error', title, t))
+}
+
+Logger.prettyInfo = function (title, ...text) {
+ text.forEach((t) => this.pretty('info', title, t))
+}
+
+export default Logger
diff --git a/src/utils/auth.ts b/src/utils/auth.ts
new file mode 100644
index 0000000..ad67440
--- /dev/null
+++ b/src/utils/auth.ts
@@ -0,0 +1,80 @@
+import { useCache, CACHE_KEY } from '@/hooks/web/useCache'
+import { TokenType } from '@/api/login/types'
+import { decrypt, encrypt } from '@/utils/jsencrypt'
+
+const { wsCache } = useCache()
+
+const AccessTokenKey = 'ACCESS_TOKEN'
+const RefreshTokenKey = 'REFRESH_TOKEN'
+
+// 鑾峰彇token
+export const getAccessToken = () => {
+ // 姝ゅ涓嶵okenKey鐩稿悓锛屾鍐欐硶瑙e喅鍒濆鍖栨椂Cookies涓笉瀛樺湪TokenKey鎶ラ敊
+ const accessToken = wsCache.get(AccessTokenKey)
+ return accessToken ? accessToken : wsCache.get('ACCESS_TOKEN')
+}
+
+// 鍒锋柊token
+export const getRefreshToken = () => {
+ return wsCache.get(RefreshTokenKey)
+}
+
+// 璁剧疆token
+export const setToken = (token: TokenType) => {
+ wsCache.set(RefreshTokenKey, token.refreshToken)
+ wsCache.set(AccessTokenKey, token.accessToken)
+}
+
+// 鍒犻櫎token
+export const removeToken = () => {
+ wsCache.delete(AccessTokenKey)
+ wsCache.delete(RefreshTokenKey)
+}
+
+/** 鏍煎紡鍖杢oken锛坖wt鏍煎紡锛� */
+export const formatToken = (token: string): string => {
+ return 'Bearer ' + token
+}
+// ========== 璐﹀彿鐩稿叧 ==========
+
+export type LoginFormType = {
+ tenantName: string
+ username: string
+ password: string
+ rememberMe: boolean
+}
+
+export const getLoginForm = () => {
+ const loginForm: LoginFormType = wsCache.get(CACHE_KEY.LoginForm)
+ if (loginForm) {
+ loginForm.password = decrypt(loginForm.password) as string
+ }
+ return loginForm
+}
+
+export const setLoginForm = (loginForm: LoginFormType) => {
+ loginForm.password = encrypt(loginForm.password) as string
+ wsCache.set(CACHE_KEY.LoginForm, loginForm, { exp: 30 * 24 * 60 * 60 })
+}
+
+export const removeLoginForm = () => {
+ wsCache.delete(CACHE_KEY.LoginForm)
+}
+
+// ========== 绉熸埛鐩稿叧 ==========
+
+export const getTenantId = () => {
+ return wsCache.get(CACHE_KEY.TenantId)
+}
+
+export const setTenantId = (tenantId: number) => {
+ wsCache.set(CACHE_KEY.TenantId, tenantId)
+}
+
+export const getVisitTenantId = () => {
+ return wsCache.get(CACHE_KEY.VisitTenantId)
+}
+
+export const setVisitTenantId = (visitTenantId: number) => {
+ wsCache.set(CACHE_KEY.VisitTenantId, visitTenantId)
+}
diff --git a/src/utils/color.ts b/src/utils/color.ts
new file mode 100644
index 0000000..943be97
--- /dev/null
+++ b/src/utils/color.ts
@@ -0,0 +1,217 @@
+/**
+ * 鍒ゆ柇鏄惁 鍗佸叚杩涘埗棰滆壊鍊�.
+ * 杈撳叆褰㈠紡鍙负 #fff000 #f00
+ *
+ * @param String color 鍗佸叚杩涘埗棰滆壊鍊�
+ * @return Boolean
+ */
+export const isHexColor = (color: string) => {
+ const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/
+ return reg.test(color)
+}
+
+/**
+ * RGB 棰滆壊鍊艰浆鎹负 鍗佸叚杩涘埗棰滆壊鍊�.
+ * r, g, 鍜� b 闇�瑕佸湪 [0, 255] 鑼冨洿鍐�
+ *
+ * @return String 绫讳技#ff00ff
+ * @param r
+ * @param g
+ * @param b
+ */
+export const rgbToHex = (r: number, g: number, b: number) => {
+ // tslint:disable-next-line:no-bitwise
+ const hex = ((r << 16) | (g << 8) | b).toString(16)
+ return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex
+}
+
+/**
+ * Transform a HEX color to its RGB representation
+ * @param {string} hex The color to transform
+ * @returns The RGB representation of the passed color
+ */
+export const hexToRGB = (hex: string, opacity?: number) => {
+ let sHex = hex.toLowerCase()
+ if (isHexColor(hex)) {
+ if (sHex.length === 4) {
+ let sColorNew = '#'
+ for (let i = 1; i < 4; i += 1) {
+ sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1))
+ }
+ sHex = sColorNew
+ }
+ const sColorChange: number[] = []
+ for (let i = 1; i < 7; i += 2) {
+ sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2)))
+ }
+ return opacity
+ ? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')'
+ : 'RGB(' + sColorChange.join(',') + ')'
+ }
+ return sHex
+}
+
+export const colorIsDark = (color: string) => {
+ if (!isHexColor(color)) return
+ const [r, g, b] = hexToRGB(color)
+ .replace(/(?:\(|\)|rgb|RGB)*/g, '')
+ .split(',')
+ .map((item) => Number(item))
+ return r * 0.299 + g * 0.578 + b * 0.114 < 192
+}
+
+/**
+ * Darkens a HEX color given the passed percentage
+ * @param {string} color The color to process
+ * @param {number} amount The amount to change the color by
+ * @returns {string} The HEX representation of the processed color
+ */
+export const darken = (color: string, amount: number) => {
+ color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
+ amount = Math.trunc((255 * amount) / 100)
+ return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight(
+ color.substring(2, 4),
+ amount
+ )}${subtractLight(color.substring(4, 6), amount)}`
+}
+
+/**
+ * Lightens a 6 char HEX color according to the passed percentage
+ * @param {string} color The color to change
+ * @param {number} amount The amount to change the color by
+ * @returns {string} The processed color represented as HEX
+ */
+export const lighten = (color: string, amount: number) => {
+ color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
+ amount = Math.trunc((255 * amount) / 100)
+ return `#${addLight(color.substring(0, 2), amount)}${addLight(
+ color.substring(2, 4),
+ amount
+ )}${addLight(color.substring(4, 6), amount)}`
+}
+
+/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */
+/**
+ * Sums the passed percentage to the R, G or B of a HEX color
+ * @param {string} color The color to change
+ * @param {number} amount The amount to change the color by
+ * @returns {string} The processed part of the color
+ */
+const addLight = (color: string, amount: number) => {
+ const cc = parseInt(color, 16) + amount
+ const c = cc > 255 ? 255 : cc
+ return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
+}
+
+/**
+ * Calculates luminance of an rgb color
+ * @param {number} r red
+ * @param {number} g green
+ * @param {number} b blue
+ */
+const luminanace = (r: number, g: number, b: number) => {
+ const a = [r, g, b].map((v) => {
+ v /= 255
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)
+ })
+ return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722
+}
+
+/**
+ * Calculates contrast between two rgb colors
+ * @param {string} rgb1 rgb color 1
+ * @param {string} rgb2 rgb color 2
+ */
+const contrast = (rgb1: string[], rgb2: number[]) => {
+ return (
+ (luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
+ (luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05)
+ )
+}
+
+/**
+ * Determines what the best text color is (black or white) based con the contrast with the background
+ * @param hexColor - Last selected color by the user
+ */
+export const calculateBestTextColor = (hexColor: string) => {
+ const rgbColor = hexToRGB(hexColor.substring(1))
+ const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0])
+
+ return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF'
+}
+
+/**
+ * Subtracts the indicated percentage to the R, G or B of a HEX color
+ * @param {string} color The color to change
+ * @param {number} amount The amount to change the color by
+ * @returns {string} The processed part of the color
+ */
+const subtractLight = (color: string, amount: number) => {
+ const cc = parseInt(color, 16) - amount
+ const c = cc < 0 ? 0 : cc
+ return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
+}
+
+// 棰勮棰滆壊
+export const PREDEFINE_COLORS = [
+ '#ff4500',
+ '#ff8c00',
+ '#ffd700',
+ '#90ee90',
+ '#00ced1',
+ '#1e90ff',
+ '#c71585',
+ '#409EFF',
+ '#909399',
+ '#C0C4CC',
+ '#b7390b',
+ '#ff7800',
+ '#fad400',
+ '#5b8c5f',
+ '#00babd',
+ '#1f73c3',
+ '#711f57'
+]
+
+
+/**
+ * Mixes two colors.
+ *
+ * @param {string} color1 - The first color, should be a 6-digit hexadecimal color code starting with `#`.
+ * @param {string} color2 - The second color, should be a 6-digit hexadecimal color code starting with `#`.
+ * @param {number} [weight=0.5] - The weight of color1 in the mix, should be a number between 0 and 1, where 0 represents 100% of color2, and 1 represents 100% of color1.
+ * @returns {string} The mixed color, a 6-digit hexadecimal color code starting with `#`.
+ */
+export const mix = (color1: string, color2: string, weight: number = 0.5): string => {
+ let color = '#'
+ for (let i = 0; i <= 2; i++) {
+ const c1 = parseInt(color1.substring(1 + i * 2, 3 + i * 2), 16)
+ const c2 = parseInt(color2.substring(1 + i * 2, 3 + i * 2), 16)
+ const c = Math.round(c1 * weight + c2 * (1 - weight))
+ color += c.toString(16).padStart(2, '0')
+ }
+ return color
+}
+
+/**
+ * getCssColorVariable
+ * @description 鑾峰彇css鍙橀噺鐨勯鑹插��
+ * @param colorVariable css鍙橀噺鍚�
+ * @param opacity 閫忔槑搴�
+ * @returns {string} 棰滆壊鍊�
+ * @example getCssColorVariable('--el-color-primary', 0.5)
+ * @example getCssColorVariable('--el-color-primary')
+ * @example getCssColorVariable()
+ */
+export const getCssColorVariable = (
+ colorVariable: string = '--el-color-primary',
+ opacity?: number
+) => {
+ const colorValue = getComputedStyle(document.documentElement)
+ .getPropertyValue(colorVariable)
+ .trim()
+ if (colorValue) {
+ return opacity ? hexToRGB(colorValue, opacity) : colorValue
+ }
+ return ''
+}
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
new file mode 100644
index 0000000..91e1827
--- /dev/null
+++ b/src/utils/constants.ts
@@ -0,0 +1,465 @@
+/**
+ * Created by 鑺嬮亾婧愮爜
+ *
+ * 鏋氫妇绫�
+ */
+
+// ========== COMMON 妯″潡 ==========
+// 鍏ㄥ眬閫氱敤鐘舵�佹灇涓�
+export const CommonStatusEnum = {
+ ENABLE: 0, // 寮�鍚�
+ DISABLE: 1 // 绂佺敤
+}
+
+// 鍏ㄥ眬鐢ㄦ埛绫诲瀷鏋氫妇
+export const UserTypeEnum = {
+ MEMBER: 1, // 浼氬憳
+ ADMIN: 2 // 绠$悊鍛�
+}
+
+// ========== SYSTEM 妯″潡 ==========
+/**
+ * 鑿滃崟鐨勭被鍨嬫灇涓�
+ */
+export const SystemMenuTypeEnum = {
+ DIR: 1, // 鐩綍
+ MENU: 2, // 鑿滃崟
+ BUTTON: 3 // 鎸夐挳
+}
+
+/**
+ * 瑙掕壊鐨勭被鍨嬫灇涓�
+ */
+export const SystemRoleTypeEnum = {
+ SYSTEM: 1, // 鍐呯疆瑙掕壊
+ CUSTOM: 2 // 鑷畾涔夎鑹�
+}
+
+/**
+ * 鏁版嵁鏉冮檺鐨勮寖鍥存灇涓�
+ */
+export const SystemDataScopeEnum = {
+ ALL: 1, // 鍏ㄩ儴鏁版嵁鏉冮檺
+ DEPT_CUSTOM: 2, // 鎸囧畾閮ㄩ棬鏁版嵁鏉冮檺
+ DEPT_ONLY: 3, // 閮ㄩ棬鏁版嵁鏉冮檺
+ DEPT_AND_CHILD: 4, // 閮ㄩ棬鍙婁互涓嬫暟鎹潈闄�
+ DEPT_SELF: 5 // 浠呮湰浜烘暟鎹潈闄�
+}
+
+/**
+ * 鐢ㄦ埛鐨勭ぞ浜ゅ钩鍙扮殑绫诲瀷鏋氫妇
+ */
+export const SystemUserSocialTypeEnum = {
+ DINGTALK: {
+ title: '閽夐拤',
+ type: 20,
+ source: 'dingtalk',
+ img: 'https://s1.ax1x.com/2022/05/22/OzMDRs.png'
+ },
+ WECHAT_ENTERPRISE: {
+ title: '浼佷笟寰俊',
+ type: 30,
+ source: 'wechat_enterprise',
+ img: 'https://s1.ax1x.com/2022/05/22/OzMrzn.png'
+ }
+}
+
+// ========== INFRA 妯″潡 ==========
+/**
+ * 浠g爜鐢熸垚妯℃澘绫诲瀷
+ */
+export const InfraCodegenTemplateTypeEnum = {
+ CRUD: 1, // 鍩虹 CRUD
+ TREE: 2, // 鏍戝舰 CRUD
+ SUB: 15 // 涓诲瓙琛� CRUD
+}
+
+/**
+ * 浠诲姟鐘舵�佺殑鏋氫妇
+ */
+export const InfraJobStatusEnum = {
+ INIT: 0, // 鍒濆鍖栦腑
+ NORMAL: 1, // 杩愯涓�
+ STOP: 2 // 鏆傚仠杩愯
+}
+
+/**
+ * API 寮傚父鏁版嵁鐨勫鐞嗙姸鎬�
+ */
+export const InfraApiErrorLogProcessStatusEnum = {
+ INIT: 0, // 鏈鐞�
+ DONE: 1, // 宸插鐞�
+ IGNORE: 2 // 宸插拷鐣�
+}
+
+// ========== PAY 妯″潡 ==========
+/**
+ * 鏀粯娓犻亾鏋氫妇
+ */
+export const PayChannelEnum = {
+ WX_PUB: {
+ code: 'wx_pub',
+ name: '寰俊 JSAPI 鏀粯'
+ },
+ WX_LITE: {
+ code: 'wx_lite',
+ name: '寰俊灏忕▼搴忔敮浠�'
+ },
+ WX_APP: {
+ code: 'wx_app',
+ name: '寰俊 APP 鏀粯'
+ },
+ WX_NATIVE: {
+ code: 'wx_native',
+ name: '寰俊 Native 鏀粯'
+ },
+ WX_WAP: {
+ code: 'wx_wap',
+ name: '寰俊 WAP 缃戠珯鏀粯'
+ },
+ WX_BAR: {
+ code: 'wx_bar',
+ name: '寰俊鏉$爜鏀粯'
+ },
+ ALIPAY_PC: {
+ code: 'alipay_pc',
+ name: '鏀粯瀹� PC 缃戠珯鏀粯'
+ },
+ ALIPAY_WAP: {
+ code: 'alipay_wap',
+ name: '鏀粯瀹� WAP 缃戠珯鏀粯'
+ },
+ ALIPAY_APP: {
+ code: 'alipay_app',
+ name: '鏀粯瀹� APP 鏀粯'
+ },
+ ALIPAY_QR: {
+ code: 'alipay_qr',
+ name: '鏀粯瀹濇壂鐮佹敮浠�'
+ },
+ ALIPAY_BAR: {
+ code: 'alipay_bar',
+ name: '鏀粯瀹濇潯鐮佹敮浠�'
+ },
+ WALLET: {
+ code: 'wallet',
+ name: '閽卞寘鏀粯'
+ },
+ MOCK: {
+ code: 'mock',
+ name: '妯℃嫙鏀粯'
+ }
+}
+
+/**
+ * 鏀粯鐨勫睍绀烘ā寮忔瘡灞�
+ */
+export const PayDisplayModeEnum = {
+ URL: {
+ mode: 'url'
+ },
+ IFRAME: {
+ mode: 'iframe'
+ },
+ FORM: {
+ mode: 'form'
+ },
+ QR_CODE: {
+ mode: 'qr_code'
+ },
+ APP: {
+ mode: 'app'
+ }
+}
+
+/**
+ * 鏀粯绫诲瀷鏋氫妇
+ */
+export const PayType = {
+ WECHAT: 'WECHAT',
+ ALIPAY: 'ALIPAY',
+ MOCK: 'MOCK'
+}
+
+/**
+ * 鏀粯璁㈠崟鐘舵�佹灇涓�
+ */
+export const PayOrderStatusEnum = {
+ WAITING: {
+ status: 0,
+ name: '鏈敮浠�'
+ },
+ SUCCESS: {
+ status: 10,
+ name: '宸叉敮浠�'
+ },
+ CLOSED: {
+ status: 20,
+ name: '鏈敮浠�'
+ }
+}
+
+// ========== MALL - 鍟嗗搧妯″潡 ==========
+/**
+ * 鍟嗗搧 SPU 鐘舵��
+ */
+export const ProductSpuStatusEnum = {
+ RECYCLE: {
+ status: -1,
+ name: '鍥炴敹绔�'
+ },
+ DISABLE: {
+ status: 0,
+ name: '涓嬫灦'
+ },
+ ENABLE: {
+ status: 1,
+ name: '涓婃灦'
+ }
+}
+
+// ========== MALL - 钀ラ攢妯″潡 ==========
+/**
+ * 浼樻儬鍔垫ā鏉跨殑鏈夐檺鏈熺被鍨嬬殑鏋氫妇
+ */
+export const CouponTemplateValidityTypeEnum = {
+ DATE: {
+ type: 1,
+ name: '鍥哄畾鏃ユ湡鍙敤'
+ },
+ TERM: {
+ type: 2,
+ name: '棰嗗彇涔嬪悗鍙敤'
+ }
+}
+
+/**
+ * 浼樻儬鍔垫ā鏉跨殑棰嗗彇鏂瑰紡鐨勬灇涓�
+ */
+export const CouponTemplateTakeTypeEnum = {
+ USER: {
+ type: 1,
+ name: '鐩存帴棰嗗彇'
+ },
+ ADMIN: {
+ type: 2,
+ name: '鎸囧畾鍙戞斁'
+ },
+ REGISTER: {
+ type: 3,
+ name: '鏂颁汉鍒�'
+ }
+}
+
+/**
+ * 钀ラ攢鐨勫晢鍝佽寖鍥存灇涓�
+ */
+export const PromotionProductScopeEnum = {
+ ALL: {
+ scope: 1,
+ name: '閫氱敤鍔�'
+ },
+ SPU: {
+ scope: 2,
+ name: '鍟嗗搧鍔�'
+ },
+ CATEGORY: {
+ scope: 3,
+ name: '鍝佺被鍔�'
+ }
+}
+
+/**
+ * 钀ラ攢鐨勬潯浠剁被鍨嬫灇涓�
+ */
+export const PromotionConditionTypeEnum = {
+ PRICE: {
+ type: 10,
+ name: '婊� N 鍏�'
+ },
+ COUNT: {
+ type: 20,
+ name: '婊� N 浠�'
+ }
+}
+
+/**
+ * 浼樻儬绫诲瀷鏋氫妇
+ */
+export const PromotionDiscountTypeEnum = {
+ PRICE: {
+ type: 1,
+ name: '婊″噺'
+ },
+ PERCENT: {
+ type: 2,
+ name: '鎶樻墸'
+ }
+}
+
+// ========== MALL - 浜ゆ槗妯″潡 ==========
+/**
+ * 鍒嗛攢鍏崇郴缁戝畾妯″紡鏋氫妇
+ */
+export const BrokerageBindModeEnum = {
+ ANYTIME: {
+ mode: 1,
+ name: '棣栨缁戝畾'
+ },
+ REGISTER: {
+ mode: 2,
+ name: '娉ㄥ唽缁戝畾'
+ },
+ OVERRIDE: {
+ mode: 3,
+ name: '瑕嗙洊缁戝畾'
+ }
+}
+/**
+ * 鍒嗕剑妯″紡鏋氫妇
+ */
+export const BrokerageEnabledConditionEnum = {
+ ALL: {
+ condition: 1,
+ name: '浜轰汉鍒嗛攢'
+ },
+ ADMIN: {
+ condition: 2,
+ name: '鎸囧畾鍒嗛攢'
+ }
+}
+/**
+ * 浣i噾璁板綍涓氬姟绫诲瀷鏋氫妇
+ */
+export const BrokerageRecordBizTypeEnum = {
+ ORDER: {
+ type: 1,
+ name: '鑾峰緱鎺ㄥ箍浣i噾'
+ },
+ WITHDRAW: {
+ type: 2,
+ name: '鎻愮幇鐢宠'
+ }
+}
+/**
+ * 浣i噾鎻愮幇鐘舵�佹灇涓�
+ */
+export const BrokerageWithdrawStatusEnum = {
+ AUDITING: {
+ status: 0,
+ name: '瀹℃牳涓�'
+ },
+ AUDIT_SUCCESS: {
+ status: 10,
+ name: '瀹℃牳閫氳繃'
+ },
+ AUDIT_FAIL: {
+ status: 20,
+ name: '瀹℃牳涓嶉�氳繃'
+ },
+ WITHDRAW_SUCCESS: {
+ status: 11,
+ name: '鎻愮幇鎴愬姛'
+ },
+ WITHDRAW_FAIL: {
+ status: 21,
+ name: '鎻愮幇澶辫触'
+ }
+}
+/**
+ * 浣i噾鎻愮幇绫诲瀷鏋氫妇
+ */
+export const BrokerageWithdrawTypeEnum = {
+ WALLET: {
+ type: 1,
+ name: '閽卞寘'
+ },
+ BANK: {
+ type: 2,
+ name: '閾惰鍗�'
+ },
+ WECHAT: {
+ type: 3,
+ name: '寰俊'
+ },
+ ALIPAY: {
+ type: 4,
+ name: '鏀粯瀹�'
+ }
+}
+
+/**
+ * 閰嶉�佹柟寮忔灇涓�
+ */
+export const DeliveryTypeEnum = {
+ EXPRESS: {
+ type: 1,
+ name: '蹇�掑彂璐�'
+ },
+ PICK_UP: {
+ type: 2,
+ name: '鍒板簵鑷彁'
+ }
+}
+/**
+ * 浜ゆ槗璁㈠崟 - 鐘舵��
+ */
+export const TradeOrderStatusEnum = {
+ UNPAID: {
+ status: 0,
+ name: '寰呮敮浠�'
+ },
+ UNDELIVERED: {
+ status: 10,
+ name: '寰呭彂璐�'
+ },
+ DELIVERED: {
+ status: 20,
+ name: '宸插彂璐�'
+ },
+ COMPLETED: {
+ status: 30,
+ name: '宸插畬鎴�'
+ },
+ CANCELED: {
+ status: 40,
+ name: '宸插彇娑�'
+ }
+}
+
+// ========== ERP - 浼佷笟璧勬簮璁″垝 ==========
+
+export const ErpBizType = {
+ PURCHASE_ORDER: 10,
+ PURCHASE_IN: 11,
+ PURCHASE_RETURN: 12,
+ SALE_ORDER: 20,
+ SALE_OUT: 21,
+ SALE_RETURN: 22
+}
+
+// ========== BPM 妯″潡 ==========
+
+export const BpmModelType = {
+ BPMN: 10, // BPMN 璁捐鍣�
+ SIMPLE: 20 // 绠�鏄撹璁″櫒
+}
+
+export const BpmModelFormType = {
+ NORMAL: 10, // 娴佺▼琛ㄥ崟
+ CUSTOM: 20 // 涓氬姟琛ㄥ崟
+}
+
+export const BpmProcessInstanceStatus = {
+ NOT_START: -1, // 鏈紑濮�
+ RUNNING: 1, // 瀹℃壒涓�
+ APPROVE: 2, // 瀹℃壒閫氳繃
+ REJECT: 3, // 瀹℃壒涓嶉�氳繃
+ CANCEL: 4 // 宸插彇娑�
+}
+
+export const BpmAutoApproveType = {
+ NONE: 0, // 涓嶈嚜鍔ㄩ�氳繃
+ APPROVE_ALL: 1, // 浠呭鎵逛竴娆★紝鍚庣画閲嶅鐨勫鎵硅妭鐐瑰潎鑷姩閫氳繃
+ APPROVE_SEQUENT: 2 // 浠呴拡瀵硅繛缁鎵圭殑鑺傜偣鑷姩閫氳繃
+}
diff --git a/src/utils/cron.ts b/src/utils/cron.ts
new file mode 100644
index 0000000..ee132f0
--- /dev/null
+++ b/src/utils/cron.ts
@@ -0,0 +1,471 @@
+/**
+ * CRON 琛ㄨ揪寮忓伐鍏风被
+ * 鎻愪緵 CRON 琛ㄨ揪寮忕殑瑙f瀽銆佹牸寮忓寲銆侀獙璇佺瓑鍔熻兘
+ */
+
+/** CRON 瀛楁绫诲瀷鏋氫妇 */
+export enum CronFieldType {
+ SECOND = 'second',
+ MINUTE = 'minute',
+ HOUR = 'hour',
+ DAY = 'day',
+ MONTH = 'month',
+ WEEK = 'week',
+ YEAR = 'year'
+}
+
+/** CRON 瀛楁閰嶇疆 */
+export interface CronFieldConfig {
+ key: CronFieldType
+ label: string
+ min: number
+ max: number
+ names?: Record<string, number> // 鍚嶇О鏄犲皠锛屽鏈堜唤鍚嶇О
+}
+
+/** CRON 瀛楁閰嶇疆甯搁噺 */
+export const CRON_FIELD_CONFIGS: Record<CronFieldType, CronFieldConfig> = {
+ [CronFieldType.SECOND]: { key: CronFieldType.SECOND, label: '绉�', min: 0, max: 59 },
+ [CronFieldType.MINUTE]: { key: CronFieldType.MINUTE, label: '鍒�', min: 0, max: 59 },
+ [CronFieldType.HOUR]: { key: CronFieldType.HOUR, label: '鏃�', min: 0, max: 23 },
+ [CronFieldType.DAY]: { key: CronFieldType.DAY, label: '鏃�', min: 1, max: 31 },
+ [CronFieldType.MONTH]: {
+ key: CronFieldType.MONTH,
+ label: '鏈�',
+ min: 1,
+ max: 12,
+ names: {
+ JAN: 1,
+ FEB: 2,
+ MAR: 3,
+ APR: 4,
+ MAY: 5,
+ JUN: 6,
+ JUL: 7,
+ AUG: 8,
+ SEP: 9,
+ OCT: 10,
+ NOV: 11,
+ DEC: 12
+ }
+ },
+ [CronFieldType.WEEK]: {
+ key: CronFieldType.WEEK,
+ label: '鍛�',
+ min: 0,
+ max: 7,
+ names: {
+ SUN: 0,
+ MON: 1,
+ TUE: 2,
+ WED: 3,
+ THU: 4,
+ FRI: 5,
+ SAT: 6
+ }
+ },
+ [CronFieldType.YEAR]: { key: CronFieldType.YEAR, label: '骞�', min: 1970, max: 2099 }
+}
+
+/** 瑙f瀽鍚庣殑 CRON 瀛楁 */
+export interface ParsedCronField {
+ type: 'any' | 'specific' | 'range' | 'step' | 'list' | 'last' | 'weekday' | 'nth'
+ values: number[]
+ original: string
+ description: string
+}
+
+/** 瑙f瀽鍚庣殑 CRON 琛ㄨ揪寮� */
+export interface ParsedCronExpression {
+ second: ParsedCronField
+ minute: ParsedCronField
+ hour: ParsedCronField
+ day: ParsedCronField
+ month: ParsedCronField
+ week: ParsedCronField
+ year?: ParsedCronField
+ isValid: boolean
+ description: string
+ nextExecutionTime?: Date
+}
+
+/** 甯哥敤 CRON 琛ㄨ揪寮忛璁� */
+export const CRON_PRESETS = {
+ EVERY_SECOND: '* * * * * ?',
+ EVERY_MINUTE: '0 * * * * ?',
+ EVERY_HOUR: '0 0 * * * ?',
+ EVERY_DAY: '0 0 0 * * ?',
+ EVERY_WEEK: '0 0 0 ? * 1',
+ EVERY_MONTH: '0 0 0 1 * ?',
+ EVERY_YEAR: '0 0 0 1 1 ?',
+ WORKDAY_9AM: '0 0 9 ? * 2-6', // 宸ヤ綔鏃ヤ笂鍗�9鐐�
+ WORKDAY_6PM: '0 0 18 ? * 2-6', // 宸ヤ綔鏃ヤ笅鍗�6鐐�
+ WEEKEND_10AM: '0 0 10 ? * 1,7' // 鍛ㄦ湯涓婂崍10鐐�
+} as const
+
+/** CRON 琛ㄨ揪寮忓伐鍏风被 */
+export class CronUtils {
+ /** 楠岃瘉 CRON 琛ㄨ揪寮忔牸寮� */
+ static validate(cronExpression: string): boolean {
+ if (!cronExpression || typeof cronExpression !== 'string') {
+ return false
+ }
+
+ const parts = cronExpression.trim().split(/\s+/)
+
+ // 鏀寔 5-7 涓瓧娈电殑 CRON 琛ㄨ揪寮�
+ if (parts.length < 5 || parts.length > 7) {
+ return false
+ }
+
+ // 鍩烘湰鏍煎紡楠岃瘉
+ const cronRegex = /^[0-9*\/\-,?LW#]+$/
+ return parts.every((part) => cronRegex.test(part))
+ }
+
+ /** 瑙f瀽鍗曚釜 CRON 瀛楁 */
+ static parseField(
+ fieldValue: string,
+ fieldType: CronFieldType,
+ config: CronFieldConfig
+ ): ParsedCronField {
+ const field: ParsedCronField = {
+ type: 'any',
+ values: [],
+ original: fieldValue,
+ description: ''
+ }
+
+ // 澶勭悊鐗规畩瀛楃
+ if (fieldValue === '*' || fieldValue === '?') {
+ field.type = 'any'
+ field.description = `姣�${config.label}`
+ return field
+ }
+
+ // 澶勭悊鏈�鍚庝竴澶� (L)
+ if (fieldValue === 'L' && fieldType === CronFieldType.DAY) {
+ field.type = 'last'
+ field.description = '姣忔湀鏈�鍚庝竴澶�'
+ return field
+ }
+
+ // 澶勭悊鑼冨洿 (-)
+ if (fieldValue.includes('-')) {
+ const [start, end] = fieldValue.split('-').map(Number)
+ if (!isNaN(start) && !isNaN(end) && start >= config.min && end <= config.max) {
+ field.type = 'range'
+ field.values = Array.from({ length: end - start + 1 }, (_, i) => start + i)
+ field.description = `${config.label} ${start}-${end}`
+ }
+ return field
+ }
+
+ // 澶勭悊姝ラ暱 (/)
+ if (fieldValue.includes('/')) {
+ const [base, step] = fieldValue.split('/')
+ const stepNum = Number(step)
+ if (!isNaN(stepNum) && stepNum > 0) {
+ field.type = 'step'
+ if (base === '*') {
+ field.description = `姣�${stepNum}${config.label}`
+ } else {
+ const startNum = Number(base)
+ field.description = `浠�${startNum}寮�濮嬫瘡${stepNum}${config.label}`
+ }
+ }
+ return field
+ }
+
+ // 澶勭悊鍒楄〃 (,)
+ if (fieldValue.includes(',')) {
+ const values = fieldValue
+ .split(',')
+ .map(Number)
+ .filter((n) => !isNaN(n))
+ if (values.length > 0) {
+ field.type = 'list'
+ field.values = values
+ field.description = `${config.label} ${values.join(',')}`
+ }
+ return field
+ }
+
+ // 澶勭悊鍏蜂綋鏁板��
+ const numValue = Number(fieldValue)
+ if (!isNaN(numValue) && numValue >= config.min && numValue <= config.max) {
+ field.type = 'specific'
+ field.values = [numValue]
+ field.description = `${config.label} ${numValue}`
+ }
+
+ return field
+ }
+
+ /** 瑙f瀽瀹屾暣鐨� CRON 琛ㄨ揪寮� */
+ static parse(cronExpression: string): ParsedCronExpression {
+ const result: ParsedCronExpression = {
+ second: { type: 'any', values: [], original: '*', description: '姣忕' },
+ minute: { type: 'any', values: [], original: '*', description: '姣忓垎' },
+ hour: { type: 'any', values: [], original: '*', description: '姣忔椂' },
+ day: { type: 'any', values: [], original: '*', description: '姣忔棩' },
+ month: { type: 'any', values: [], original: '*', description: '姣忔湀' },
+ week: { type: 'any', values: [], original: '?', description: '浠绘剰鍛�' },
+ isValid: false,
+ description: ''
+ }
+
+ if (!this.validate(cronExpression)) {
+ result.description = '鏃犳晥鐨� CRON 琛ㄨ揪寮�'
+ return result
+ }
+
+ const parts = cronExpression.trim().split(/\s+/)
+ const fieldTypes = [
+ CronFieldType.SECOND,
+ CronFieldType.MINUTE,
+ CronFieldType.HOUR,
+ CronFieldType.DAY,
+ CronFieldType.MONTH,
+ CronFieldType.WEEK
+ ]
+
+ // 濡傛灉鍙湁5涓瓧娈碉紝鍒欑涓�涓瓧娈垫槸鍒嗛挓
+ const startIndex = parts.length === 5 ? 1 : 0
+
+ for (let i = 0; i < parts.length; i++) {
+ const fieldType = fieldTypes[i + startIndex]
+ if (fieldType && CRON_FIELD_CONFIGS[fieldType]) {
+ const config = CRON_FIELD_CONFIGS[fieldType]
+ result[fieldType] = this.parseField(parts[i], fieldType, config)
+ }
+ }
+
+ // 澶勭悊骞翠唤瀛楁锛堝鏋滃瓨鍦級
+ if (parts.length === 7) {
+ const yearConfig = CRON_FIELD_CONFIGS[CronFieldType.YEAR]
+ result.year = this.parseField(parts[6], CronFieldType.YEAR, yearConfig)
+ }
+
+ result.isValid = true
+ result.description = this.generateDescription(result)
+
+ return result
+ }
+
+ /** 鐢熸垚 CRON 琛ㄨ揪寮忕殑鍙鎻忚堪 */
+ static generateDescription(parsed: ParsedCronExpression): string {
+ const parts: string[] = []
+
+ // 鏋勫缓鏃堕棿閮ㄥ垎鎻忚堪
+ if (parsed.hour.type === 'specific' && parsed.minute.type === 'specific') {
+ const hour = parsed.hour.values[0]
+ const minute = parsed.minute.values[0]
+ parts.push(`${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`)
+ } else if (parsed.hour.type === 'specific') {
+ parts.push(`姣忓ぉ${parsed.hour.values[0]}鐐筦)
+ } else if (parsed.minute.type === 'specific' && parsed.minute.values[0] === 0) {
+ if (parsed.hour.type === 'any') {
+ parts.push('姣忓皬鏃舵暣鐐�')
+ }
+ } else if (parsed.minute.type === 'step') {
+ const step = parsed.minute.original.split('/')[1]
+ parts.push(`姣�${step}鍒嗛挓`)
+ } else if (parsed.hour.type === 'step') {
+ const step = parsed.hour.original.split('/')[1]
+ parts.push(`姣�${step}灏忔椂`)
+ }
+
+ // 鏋勫缓鏃ユ湡閮ㄥ垎鎻忚堪
+ if (parsed.day.type === 'specific') {
+ parts.push(`姣忔湀${parsed.day.values[0]}鏃)
+ } else if (parsed.week.type === 'specific') {
+ const weekNames = ['鍛ㄦ棩', '鍛ㄤ竴', '鍛ㄤ簩', '鍛ㄤ笁', '鍛ㄥ洓', '鍛ㄤ簲', '鍛ㄥ叚']
+ const weekDay = parsed.week.values[0]
+ if (weekDay >= 0 && weekDay <= 6) {
+ parts.push(`姣�${weekNames[weekDay]}`)
+ }
+ } else if (parsed.week.type === 'range') {
+ parts.push('宸ヤ綔鏃�')
+ }
+
+ // 鏋勫缓鏈堜唤閮ㄥ垎鎻忚堪
+ if (parsed.month.type === 'specific') {
+ parts.push(`${parsed.month.values[0]}鏈坄)
+ }
+
+ return parts.length > 0 ? parts.join(' ') : '鑷畾涔夋椂闂磋鍒�'
+ }
+
+ /** 鏍煎紡鍖� CRON 琛ㄨ揪寮忎负鍙鏂囨湰 */
+ static format(cronExpression: string): string {
+ if (!cronExpression) return ''
+
+ const parsed = this.parse(cronExpression)
+ return parsed.isValid ? parsed.description : cronExpression
+ }
+
+ /** 鑾峰彇棰勮鐨� CRON 琛ㄨ揪寮忓垪琛� */
+ static getPresets() {
+ return Object.entries(CRON_PRESETS).map(([key, value]) => ({
+ label: this.format(value),
+ value,
+ key
+ }))
+ }
+
+ /** 璁$畻 CRON 琛ㄨ揪寮忕殑涓嬫鎵ц鏃堕棿 */
+ static getNextExecutionTime(cronExpression: string, fromDate?: Date): Date | null {
+ const parsed = this.parse(cronExpression)
+ if (!parsed.isValid) {
+ return null
+ }
+
+ const now = fromDate || new Date()
+ // eslint-disable-next-line prefer-const
+ let nextTime = new Date(now.getTime() + 1000) // 浠庝笅涓�绉掑紑濮�
+
+ // 绠�鍖栫増鏈細澶勭悊甯歌鐨� CRON 琛ㄨ揪寮忔ā寮�
+ // 瀵逛簬澶嶆潅鐨� CRON 琛ㄨ揪寮忥紝寤鸿浣跨敤涓撻棬鐨勫簱濡� node-cron 鎴� cron-parser
+
+ // 澶勭悊姣忓垎閽熸墽琛�
+ if (parsed.second.type === 'specific' && parsed.minute.type === 'any') {
+ const targetSecond = parsed.second.values[0]
+ nextTime.setSeconds(targetSecond, 0)
+ if (nextTime <= now) {
+ nextTime.setMinutes(nextTime.getMinutes() + 1)
+ }
+ return nextTime
+ }
+
+ // 澶勭悊姣忓皬鏃舵墽琛�
+ if (
+ parsed.second.type === 'specific' &&
+ parsed.minute.type === 'specific' &&
+ parsed.hour.type === 'any'
+ ) {
+ const targetSecond = parsed.second.values[0]
+ const targetMinute = parsed.minute.values[0]
+ nextTime.setMinutes(targetMinute, targetSecond, 0)
+ if (nextTime <= now) {
+ nextTime.setHours(nextTime.getHours() + 1)
+ }
+ return nextTime
+ }
+
+ // 澶勭悊姣忓ぉ鎵ц
+ if (
+ parsed.second.type === 'specific' &&
+ parsed.minute.type === 'specific' &&
+ parsed.hour.type === 'specific'
+ ) {
+ const targetSecond = parsed.second.values[0]
+ const targetMinute = parsed.minute.values[0]
+ const targetHour = parsed.hour.values[0]
+
+ nextTime.setHours(targetHour, targetMinute, targetSecond, 0)
+ if (nextTime <= now) {
+ nextTime.setDate(nextTime.getDate() + 1)
+ }
+ return nextTime
+ }
+
+ // 澶勭悊姝ラ暱鎵ц
+ if (parsed.minute.type === 'step') {
+ const step = parseInt(parsed.minute.original.split('/')[1])
+ const currentMinute = nextTime.getMinutes()
+ const nextMinute = Math.ceil(currentMinute / step) * step
+
+ if (nextMinute >= 60) {
+ nextTime.setHours(nextTime.getHours() + 1, 0, 0, 0)
+ } else {
+ nextTime.setMinutes(nextMinute, 0, 0)
+ }
+ return nextTime
+ }
+
+ // 瀵逛簬鍏朵粬澶嶆潅鎯呭喌锛岃繑鍥炰竴涓及绠楁椂闂�
+ return new Date(now.getTime() + 60000) // 1鍒嗛挓鍚�
+ }
+
+ /** 鑾峰彇 CRON 琛ㄨ揪寮忕殑鎵ц棰戠巼鎻忚堪 */
+ static getFrequencyDescription(cronExpression: string): string {
+ const parsed = this.parse(cronExpression)
+ if (!parsed.isValid) {
+ return '鏃犳晥琛ㄨ揪寮�'
+ }
+
+ // 璁$畻澶ф鐨勬墽琛岄鐜�
+ if (parsed.second.type === 'any' && parsed.minute.type === 'any') {
+ return '姣忕鎵ц'
+ }
+
+ if (parsed.minute.type === 'any' && parsed.hour.type === 'any') {
+ return '姣忓垎閽熸墽琛�'
+ }
+
+ if (parsed.hour.type === 'any' && parsed.day.type === 'any') {
+ return '姣忓皬鏃舵墽琛�'
+ }
+
+ if (parsed.day.type === 'any' && parsed.month.type === 'any') {
+ return '姣忓ぉ鎵ц'
+ }
+
+ if (parsed.month.type === 'any') {
+ return '姣忔湀鎵ц'
+ }
+
+ return '鎸夎鍒掓墽琛�'
+ }
+
+ /** 妫�鏌� CRON 琛ㄨ揪寮忔槸鍚︿細鍦ㄦ寚瀹氭椂闂存墽琛� */
+ static willExecuteAt(cronExpression: string, targetDate: Date): boolean {
+ const parsed = this.parse(cronExpression)
+ if (!parsed.isValid) {
+ return false
+ }
+
+ // 妫�鏌ュ悇涓瓧娈垫槸鍚﹀尮閰�
+ const second = targetDate.getSeconds()
+ const minute = targetDate.getMinutes()
+ const hour = targetDate.getHours()
+ const day = targetDate.getDate()
+ const month = targetDate.getMonth() + 1
+ const weekDay = targetDate.getDay()
+
+ return (
+ this.fieldMatches(parsed.second, second) &&
+ this.fieldMatches(parsed.minute, minute) &&
+ this.fieldMatches(parsed.hour, hour) &&
+ this.fieldMatches(parsed.day, day) &&
+ this.fieldMatches(parsed.month, month) &&
+ (parsed.week.type === 'any' || this.fieldMatches(parsed.week, weekDay))
+ )
+ }
+
+ /** 妫�鏌ュ瓧娈靛�兼槸鍚﹀尮閰� */
+ private static fieldMatches(field: ParsedCronField, value: number): boolean {
+ if (field.type === 'any') {
+ return true
+ }
+
+ if (field.type === 'specific' || field.type === 'list') {
+ return field.values.includes(value)
+ }
+
+ if (field.type === 'range') {
+ return value >= field.values[0] && value <= field.values[field.values.length - 1]
+ }
+
+ if (field.type === 'step') {
+ const [base, step] = field.original.split('/').map(Number)
+ if (base === 0 || field.original.startsWith('*')) {
+ return value % step === 0
+ }
+ return value >= base && (value - base) % step === 0
+ }
+
+ return false
+ }
+}
diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts
new file mode 100644
index 0000000..316b870
--- /dev/null
+++ b/src/utils/dateUtil.ts
@@ -0,0 +1,18 @@
+/**
+ * Independent time operation tool to facilitate subsequent switch to dayjs
+ */
+// TODO 鑺嬭壙锛氥�愰攣灞忋�戝彲鑳藉悗闈㈠垹闄ゆ帀
+import dayjs from 'dayjs'
+
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
+const DATE_FORMAT = 'YYYY-MM-DD'
+
+export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string {
+ return dayjs(date).format(format)
+}
+
+export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string {
+ return dayjs(date).format(format)
+}
+
+export const dateUtil = dayjs
diff --git a/src/utils/dict.ts b/src/utils/dict.ts
new file mode 100644
index 0000000..b4f0c58
--- /dev/null
+++ b/src/utils/dict.ts
@@ -0,0 +1,251 @@
+/**
+ * 鏁版嵁瀛楀吀宸ュ叿绫�
+ */
+import { useDictStoreWithOut } from '@/store/modules/dict'
+import { ElementPlusInfoType } from '@/types/elementPlus'
+
+const dictStore = useDictStoreWithOut()
+
+/**
+ * 鑾峰彇 dictType 瀵瑰簲鐨勬暟鎹瓧鍏告暟缁�
+ *
+ * @param dictType 鏁版嵁绫诲瀷
+ * @returns {*|Array} 鏁版嵁瀛楀吀鏁扮粍
+ */
+export interface DictDataType {
+ dictType: string
+ label: string
+ value: string | number | boolean
+ colorType: ElementPlusInfoType | ''
+ cssClass: string
+}
+
+export interface NumberDictDataType extends DictDataType {
+ value: number
+}
+
+export interface StringDictDataType extends DictDataType {
+ value: string
+}
+
+export const getDictOptions = (dictType: string) => {
+ return dictStore.getDictByType(dictType) || []
+}
+
+export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
+ // 鑾峰緱閫氱敤鐨� DictDataType 鍒楄〃
+ const dictOptions: DictDataType[] = getDictOptions(dictType)
+ // 杞崲鎴� number 绫诲瀷鐨� NumberDictDataType 绫诲瀷
+ // why 闇�瑕佺壒娈婅浆鎹細閬垮厤 IDEA 鍦� v-for="dict in getIntDictOptions(...)" 鏃讹紝el-option 鐨� key 浼氬憡璀�
+ const dictOption: NumberDictDataType[] = []
+ dictOptions.forEach((dict: DictDataType) => {
+ dictOption.push({
+ ...dict,
+ value: parseInt(dict.value + '')
+ })
+ })
+ return dictOption
+}
+
+export const getStrDictOptions = (dictType: string) => {
+ // 鑾峰緱閫氱敤鐨� DictDataType 鍒楄〃
+ const dictOptions: DictDataType[] = getDictOptions(dictType)
+ // 杞崲鎴� string 绫诲瀷鐨� StringDictDataType 绫诲瀷
+ // why 闇�瑕佺壒娈婅浆鎹細閬垮厤 IDEA 鍦� v-for="dict in getStrDictOptions(...)" 鏃讹紝el-option 鐨� key 浼氬憡璀�
+ const dictOption: StringDictDataType[] = []
+ dictOptions.forEach((dict: DictDataType) => {
+ dictOption.push({
+ ...dict,
+ value: dict.value + ''
+ })
+ })
+ return dictOption
+}
+
+export const getBoolDictOptions = (dictType: string) => {
+ const dictOption: DictDataType[] = []
+ const dictOptions: DictDataType[] = getDictOptions(dictType)
+ dictOptions.forEach((dict: DictDataType) => {
+ dictOption.push({
+ ...dict,
+ value: dict.value + '' === 'true'
+ })
+ })
+ return dictOption
+}
+
+/**
+ * 鑾峰彇鎸囧畾瀛楀吀绫诲瀷鐨勬寚瀹氬�煎搴旂殑瀛楀吀瀵硅薄
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param value 瀛楀吀鍊�
+ * @return DictDataType 瀛楀吀瀵硅薄
+ */
+export const getDictObj = (dictType: string, value: any): DictDataType | undefined => {
+ const dictOptions: DictDataType[] = getDictOptions(dictType)
+ for (const dict of dictOptions) {
+ if (dict.value === value + '') {
+ return dict
+ }
+ }
+}
+
+/**
+ * 鑾峰緱瀛楀吀鏁版嵁鐨勬枃鏈睍绀�
+ *
+ * @param dictType 瀛楀吀绫诲瀷
+ * @param value 瀛楀吀鏁版嵁鐨勫��
+ * @return 瀛楀吀鍚嶇О
+ */
+export const getDictLabel = (dictType: string, value: any): string => {
+ const dictOptions: DictDataType[] = getDictOptions(dictType)
+ const dictLabel = ref('')
+ dictOptions.forEach((dict: DictDataType) => {
+ if (dict.value === value + '') {
+ dictLabel.value = dict.label
+ }
+ })
+ return dictLabel.value
+}
+
+export enum DICT_TYPE {
+ USER_TYPE = 'user_type',
+ COMMON_STATUS = 'common_status',
+ TERMINAL = 'terminal', // 缁堢
+ DATE_INTERVAL = 'date_interval', // 鏁版嵁闂撮殧
+
+ // ========== SYSTEM 妯″潡 ==========
+ SYSTEM_USER_SEX = 'system_user_sex',
+ SYSTEM_MENU_TYPE = 'system_menu_type',
+ SYSTEM_ROLE_TYPE = 'system_role_type',
+ SYSTEM_DATA_SCOPE = 'system_data_scope',
+ SYSTEM_NOTICE_TYPE = 'system_notice_type',
+ SYSTEM_LOGIN_TYPE = 'system_login_type',
+ SYSTEM_LOGIN_RESULT = 'system_login_result',
+ SYSTEM_SMS_CHANNEL_CODE = 'system_sms_channel_code',
+ SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
+ SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
+ SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
+ SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
+ SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
+ SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
+ SYSTEM_SOCIAL_TYPE = 'system_social_type',
+
+ // ========== INFRA 妯″潡 ==========
+ INFRA_BOOLEAN_STRING = 'infra_boolean_string',
+ INFRA_JOB_STATUS = 'infra_job_status',
+ INFRA_JOB_LOG_STATUS = 'infra_job_log_status',
+ INFRA_API_ERROR_LOG_PROCESS_STATUS = 'infra_api_error_log_process_status',
+ INFRA_CONFIG_TYPE = 'infra_config_type',
+ INFRA_CODEGEN_TEMPLATE_TYPE = 'infra_codegen_template_type',
+ INFRA_CODEGEN_FRONT_TYPE = 'infra_codegen_front_type',
+ INFRA_CODEGEN_SCENE = 'infra_codegen_scene',
+ INFRA_FILE_STORAGE = 'infra_file_storage',
+ INFRA_OPERATE_TYPE = 'infra_operate_type',
+
+ // ========== BPM 妯″潡 ==========
+ BPM_MODEL_TYPE = 'bpm_model_type',
+ BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
+ BPM_TASK_CANDIDATE_STRATEGY = 'bpm_task_candidate_strategy',
+ BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
+ BPM_TASK_STATUS = 'bpm_task_status',
+ BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
+ BPM_PROCESS_LISTENER_TYPE = 'bpm_process_listener_type',
+ BPM_PROCESS_LISTENER_VALUE_TYPE = 'bpm_process_listener_value_type',
+
+ // ========== PAY 妯″潡 ==========
+ PAY_CHANNEL_CODE = 'pay_channel_code', // 鏀粯娓犻亾缂栫爜绫诲瀷
+ PAY_ORDER_STATUS = 'pay_order_status', // 鍟嗘埛鏀粯璁㈠崟鐘舵��
+ PAY_REFUND_STATUS = 'pay_refund_status', // 閫�娆捐鍗曠姸鎬�
+ PAY_NOTIFY_STATUS = 'pay_notify_status', // 鍟嗘埛鏀粯鍥炶皟鐘舵��
+ PAY_NOTIFY_TYPE = 'pay_notify_type', // 鍟嗘埛鏀粯鍥炶皟鐘舵��
+ PAY_TRANSFER_STATUS = 'pay_transfer_status', // 杞处璁㈠崟鐘舵��
+
+ // ========== MP 妯″潡 ==========
+ MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 鑷姩鍥炲璇锋眰鍖归厤绫诲瀷
+ MP_MESSAGE_TYPE = 'mp_message_type', // 娑堟伅绫诲瀷
+
+ // ========== Member 浼氬憳妯″潡 ==========
+ MEMBER_POINT_BIZ_TYPE = 'member_point_biz_type', // 绉垎鐨勪笟鍔$被鍨�
+ MEMBER_EXPERIENCE_BIZ_TYPE = 'member_experience_biz_type', // 浼氬憳缁忛獙涓氬姟绫诲瀷
+
+ // ========== MALL - 鍟嗗搧妯″潡 ==========
+ PRODUCT_SPU_STATUS = 'product_spu_status', //鍟嗗搧鐘舵��
+
+ // ========== MALL - 浜ゆ槗妯″潡 ==========
+ EXPRESS_CHARGE_MODE = 'trade_delivery_express_charge_mode', //蹇�掔殑璁¤垂鏂瑰紡
+ TRADE_AFTER_SALE_STATUS = 'trade_after_sale_status', // 鍞悗 - 鐘舵��
+ TRADE_AFTER_SALE_WAY = 'trade_after_sale_way', // 鍞悗 - 鏂瑰紡
+ TRADE_AFTER_SALE_TYPE = 'trade_after_sale_type', // 鍞悗 - 绫诲瀷
+ TRADE_ORDER_TYPE = 'trade_order_type', // 璁㈠崟 - 绫诲瀷
+ TRADE_ORDER_STATUS = 'trade_order_status', // 璁㈠崟 - 鐘舵��
+ TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 璁㈠崟椤� - 鍞悗鐘舵��
+ TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 閰嶉�佹柟寮�
+ BROKERAGE_ENABLED_CONDITION = 'brokerage_enabled_condition', // 鍒嗕剑妯″紡
+ BROKERAGE_BIND_MODE = 'brokerage_bind_mode', // 鍒嗛攢鍏崇郴缁戝畾妯″紡
+ BROKERAGE_BANK_NAME = 'brokerage_bank_name', // 浣i噾鎻愮幇閾惰
+ BROKERAGE_WITHDRAW_TYPE = 'brokerage_withdraw_type', // 浣i噾鎻愮幇绫诲瀷
+ BROKERAGE_RECORD_BIZ_TYPE = 'brokerage_record_biz_type', // 浣i噾涓氬姟绫诲瀷
+ BROKERAGE_RECORD_STATUS = 'brokerage_record_status', // 浣i噾鐘舵��
+ BROKERAGE_WITHDRAW_STATUS = 'brokerage_withdraw_status', // 浣i噾鎻愮幇鐘舵��
+
+ // ========== MALL - 钀ラ攢妯″潡 ==========
+ PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 浼樻儬绫诲瀷
+ PROMOTION_PRODUCT_SCOPE = 'promotion_product_scope', // 钀ラ攢鐨勫晢鍝佽寖鍥�
+ PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE = 'promotion_coupon_template_validity_type', // 浼樻儬鍔垫ā鏉跨殑鏈夐檺鏈熺被鍨�
+ PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 浼樻儬鍔电殑鐘舵��
+ PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 浼樻儬鍔电殑棰嗗彇鏂瑰紡
+ PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 钀ラ攢鐨勬潯浠剁被鍨嬫灇涓�
+ PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 鐮嶄环璁板綍鐨勭姸鎬�
+ PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status', // 鎷煎洟璁板綍鐨勭姸鎬�
+ PROMOTION_BANNER_POSITION = 'promotion_banner_position', // banner 瀹氫綅
+
+ // ========== CRM - 瀹㈡埛绠$悊妯″潡 ==========
+ CRM_AUDIT_STATUS = 'crm_audit_status', // CRM 瀹℃壒鐘舵��
+ CRM_BIZ_TYPE = 'crm_biz_type', // CRM 涓氬姟绫诲瀷
+ CRM_BUSINESS_END_STATUS_TYPE = 'crm_business_end_status_type', // CRM 鍟嗘満缁撴潫鐘舵�佺被鍨�
+ CRM_RECEIVABLE_RETURN_TYPE = 'crm_receivable_return_type', // CRM 鍥炴鐨勮繕娆炬柟寮�
+ CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry', // CRM 瀹㈡埛鎵�灞炶涓�
+ CRM_CUSTOMER_LEVEL = 'crm_customer_level', // CRM 瀹㈡埛绾у埆
+ CRM_CUSTOMER_SOURCE = 'crm_customer_source', // CRM 瀹㈡埛鏉ユ簮
+ CRM_PRODUCT_STATUS = 'crm_product_status', // CRM 鍟嗗搧鐘舵��
+ CRM_PERMISSION_LEVEL = 'crm_permission_level', // CRM 鏁版嵁鏉冮檺鐨勭骇鍒�
+ CRM_PRODUCT_UNIT = 'crm_product_unit', // CRM 浜у搧鍗曚綅
+ CRM_FOLLOW_UP_TYPE = 'crm_follow_up_type', // CRM 璺熻繘鏂瑰紡
+
+ // ========== ERP - 浼佷笟璧勬簮璁″垝妯″潡 ==========
+ ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 瀹℃壒鐘舵��
+ ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 搴撳瓨鏄庣粏鐨勪笟鍔$被鍨�
+
+ // ========== AI - 浜哄伐鏅鸿兘妯″潡 ==========
+ AI_PLATFORM = 'ai_platform', // AI 骞冲彴
+ AI_MODEL_TYPE = 'ai_model_type', // AI 妯″瀷绫诲瀷
+ AI_IMAGE_STATUS = 'ai_image_status', // AI 鍥剧墖鐘舵��
+ AI_MUSIC_STATUS = 'ai_music_status', // AI 闊充箰鐘舵��
+ AI_GENERATE_MODE = 'ai_generate_mode', // AI 鐢熸垚妯″紡
+ AI_WRITE_TYPE = 'ai_write_type', // AI 鍐欎綔绫诲瀷
+ AI_WRITE_LENGTH = 'ai_write_length', // AI 鍐欎綔闀垮害
+ AI_WRITE_FORMAT = 'ai_write_format', // AI 鍐欎綔鏍煎紡
+ AI_WRITE_TONE = 'ai_write_tone', // AI 鍐欎綔璇皵
+ AI_WRITE_LANGUAGE = 'ai_write_language', // AI 鍐欎綔璇█
+ AI_MCP_CLIENT_NAME = 'ai_mcp_client_name', // AI MCP Client 鍚嶅瓧
+
+ // ========== IOT - 鐗╄仈缃戞ā鍧� ==========
+ IOT_NET_TYPE = 'iot_net_type', // IOT 鑱旂綉鏂瑰紡
+ IOT_PRODUCT_STATUS = 'iot_product_status', // IOT 浜у搧鐘舵��
+ IOT_PRODUCT_DEVICE_TYPE = 'iot_product_device_type', // IOT 浜у搧璁惧绫诲瀷
+ IOT_CODEC_TYPE = 'iot_codec_type', // IOT 鏁版嵁鏍煎紡锛堢紪瑙g爜鍣ㄧ被鍨嬶級
+ IOT_LOCATION_TYPE = 'iot_location_type', // IOT 瀹氫綅绫诲瀷
+ IOT_DEVICE_STATE = 'iot_device_state', // IOT 璁惧鐘舵��
+ IOT_THING_MODEL_TYPE = 'iot_thing_model_type', // IOT 浜у搧鍔熻兘绫诲瀷
+ IOT_THING_MODEL_UNIT = 'iot_thing_model_unit', // IOT 鐗╂ā鍨嬪崟浣�
+ IOT_RW_TYPE = 'iot_rw_type', // IOT 璇诲啓绫诲瀷
+ // TODO @鑺嬭壙锛氳矊浼艰繖鍑犱釜澶氫簡 _enum 鍚庣紑
+ IOT_DATA_SINK_TYPE_ENUM = 'iot_data_sink_type_enum', // IoT 鏁版嵁娴佽浆鐩殑绫诲瀷
+ IOT_RULE_SCENE_TRIGGER_TYPE_ENUM = 'iot_rule_scene_trigger_type_enum', // IoT 鍦烘櫙娴佽浆鐨勮Е鍙戠被鍨嬫灇涓�
+ IOT_RULE_SCENE_ACTION_TYPE_ENUM = 'iot_rule_scene_action_type_enum', // IoT 瑙勫垯鍦烘櫙鐨勮Е鍙戠被鍨嬫灇涓�
+ IOT_ALERT_LEVEL = 'iot_alert_level', // IoT 鍛婅绾у埆
+ IOT_ALERT_RECEIVE_TYPE = 'iot_alert_receive_type', // IoT 鍛婅鎺ユ敹绫诲瀷
+ IOT_OTA_TASK_DEVICE_SCOPE = 'iot_ota_task_device_scope', // IoT OTA浠诲姟璁惧鑼冨洿
+ IOT_OTA_TASK_STATUS = 'iot_ota_task_status', // IoT OTA 浠诲姟鐘舵��
+ IOT_OTA_TASK_RECORD_STATUS = 'iot_ota_task_record_status' // IoT OTA 璁板綍鐘舵��
+}
diff --git a/src/utils/domUtils.ts b/src/utils/domUtils.ts
new file mode 100644
index 0000000..dbc1989
--- /dev/null
+++ b/src/utils/domUtils.ts
@@ -0,0 +1,289 @@
+import { isServer } from './is'
+const ieVersion = isServer ? 0 : Number((document as any).documentMode)
+const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
+const MOZ_HACK_REGEXP = /^moz([A-Z])/
+
+export interface ViewportOffsetResult {
+ left: number
+ top: number
+ right: number
+ bottom: number
+ rightIncludeBody: number
+ bottomIncludeBody: number
+}
+
+/* istanbul ignore next */
+const trim = function (string: string) {
+ return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
+}
+
+/* istanbul ignore next */
+const camelCase = function (name: string) {
+ return name
+ .replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) {
+ return offset ? letter.toUpperCase() : letter
+ })
+ .replace(MOZ_HACK_REGEXP, 'Moz$1')
+}
+
+/* istanbul ignore next */
+export function hasClass(el: Element, cls: string) {
+ if (!el || !cls) return false
+ if (cls.indexOf(' ') !== -1) {
+ throw new Error('className should not contain space.')
+ }
+ if (el.classList) {
+ return el.classList.contains(cls)
+ } else {
+ return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
+ }
+}
+
+/* istanbul ignore next */
+export function addClass(el: Element, cls: string) {
+ if (!el) return
+ let curClass = el.className
+ const classes = (cls || '').split(' ')
+
+ for (let i = 0, j = classes.length; i < j; i++) {
+ const clsName = classes[i]
+ if (!clsName) continue
+
+ if (el.classList) {
+ el.classList.add(clsName)
+ } else if (!hasClass(el, clsName)) {
+ curClass += ' ' + clsName
+ }
+ }
+ if (!el.classList) {
+ el.className = curClass
+ }
+}
+
+/* istanbul ignore next */
+export function removeClass(el: Element, cls: string) {
+ if (!el || !cls) return
+ const classes = cls.split(' ')
+ let curClass = ' ' + el.className + ' '
+
+ for (let i = 0, j = classes.length; i < j; i++) {
+ const clsName = classes[i]
+ if (!clsName) continue
+
+ if (el.classList) {
+ el.classList.remove(clsName)
+ } else if (hasClass(el, clsName)) {
+ curClass = curClass.replace(' ' + clsName + ' ', ' ')
+ }
+ }
+ if (!el.classList) {
+ el.className = trim(curClass)
+ }
+}
+
+export function getBoundingClientRect(element: Element): DOMRect | number {
+ if (!element || !element.getBoundingClientRect) {
+ return 0
+ }
+ return element.getBoundingClientRect()
+}
+
+/**
+ * 鑾峰彇褰撳墠鍏冪礌鐨刲eft銆乼op鍋忕Щ
+ * left锛氬厓绱犳渶宸︿晶璺濈鏂囨。宸︿晶鐨勮窛绂�
+ * top:鍏冪礌鏈�椤剁璺濈鏂囨。椤剁鐨勮窛绂�
+ * right:鍏冪礌鏈�鍙充晶璺濈鏂囨。鍙充晶鐨勮窛绂�
+ * bottom锛氬厓绱犳渶搴曠璺濈鏂囨。搴曠鐨勮窛绂�
+ * rightIncludeBody锛氬厓绱犳渶宸︿晶璺濈鏂囨。鍙充晶鐨勮窛绂�
+ * bottomIncludeBody锛氬厓绱犳渶搴曠璺濈鏂囨。鏈�搴曢儴鐨勮窛绂�
+ *
+ * @description:
+ */
+export function getViewportOffset(element: Element): ViewportOffsetResult {
+ const doc = document.documentElement
+
+ const docScrollLeft = doc.scrollLeft
+ const docScrollTop = doc.scrollTop
+ const docClientLeft = doc.clientLeft
+ const docClientTop = doc.clientTop
+
+ const pageXOffset = window.pageXOffset
+ const pageYOffset = window.pageYOffset
+
+ const box = getBoundingClientRect(element)
+
+ const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect
+
+ const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
+ const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
+ const offsetLeft = retLeft + pageXOffset
+ const offsetTop = rectTop + pageYOffset
+
+ const left = offsetLeft - scrollLeft
+ const top = offsetTop - scrollTop
+
+ const clientWidth = window.document.documentElement.clientWidth
+ const clientHeight = window.document.documentElement.clientHeight
+ return {
+ left: left,
+ top: top,
+ right: clientWidth - rectWidth - left,
+ bottom: clientHeight - rectHeight - top,
+ rightIncludeBody: clientWidth - left,
+ bottomIncludeBody: clientHeight - top
+ }
+}
+
+/* istanbul ignore next */
+export const on = function (
+ element: HTMLElement | Document | Window,
+ event: string,
+ handler: EventListenerOrEventListenerObject
+): void {
+ if (element && event && handler) {
+ element.addEventListener(event, handler, false)
+ }
+}
+
+/* istanbul ignore next */
+export const off = function (
+ element: HTMLElement | Document | Window,
+ event: string,
+ handler: any
+): void {
+ if (element && event && handler) {
+ element.removeEventListener(event, handler, false)
+ }
+}
+
+/* istanbul ignore next */
+export const once = function (el: HTMLElement, event: string, fn: EventListener): void {
+ const listener = function (this: any, ...args: unknown[]) {
+ if (fn) {
+ // @ts-ignore
+ fn.apply(this, args)
+ }
+ off(el, event, listener)
+ }
+ on(el, event, listener)
+}
+
+/* istanbul ignore next */
+export const getStyle =
+ ieVersion < 9
+ ? function (element: Element | any, styleName: string) {
+ if (isServer) return
+ if (!element || !styleName) return null
+ styleName = camelCase(styleName)
+ if (styleName === 'float') {
+ styleName = 'styleFloat'
+ }
+ try {
+ switch (styleName) {
+ case 'opacity':
+ try {
+ return element.filters.item('alpha').opacity / 100
+ } catch (e) {
+ return 1.0
+ }
+ default:
+ return element.style[styleName] || element.currentStyle
+ ? element.currentStyle[styleName]
+ : null
+ }
+ } catch (e) {
+ return element.style[styleName]
+ }
+ }
+ : function (element: Element | any, styleName: string) {
+ if (isServer) return
+ if (!element || !styleName) return null
+ styleName = camelCase(styleName)
+ if (styleName === 'float') {
+ styleName = 'cssFloat'
+ }
+ try {
+ const computed = (document as any).defaultView.getComputedStyle(element, '')
+ return element.style[styleName] || computed ? computed[styleName] : null
+ } catch (e) {
+ return element.style[styleName]
+ }
+ }
+
+/* istanbul ignore next */
+export function setStyle(element: Element | any, styleName: any, value: any) {
+ if (!element || !styleName) return
+
+ if (typeof styleName === 'object') {
+ for (const prop in styleName) {
+ if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
+ setStyle(element, prop, styleName[prop])
+ }
+ }
+ } else {
+ styleName = camelCase(styleName)
+ if (styleName === 'opacity' && ieVersion < 9) {
+ element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
+ } else {
+ element.style[styleName] = value
+ }
+ }
+}
+
+/* istanbul ignore next */
+export const isScroll = (el: Element, vertical: any) => {
+ if (isServer) return
+
+ const determinedDirection = vertical !== null || vertical !== undefined
+ const overflow = determinedDirection
+ ? vertical
+ ? getStyle(el, 'overflow-y')
+ : getStyle(el, 'overflow-x')
+ : getStyle(el, 'overflow')
+
+ return overflow.match(/(scroll|auto)/)
+}
+
+/* istanbul ignore next */
+export const getScrollContainer = (el: Element, vertical?: any) => {
+ if (isServer) return
+
+ let parent: any = el
+ while (parent) {
+ if ([window, document, document.documentElement].includes(parent)) {
+ return window
+ }
+ if (isScroll(parent, vertical)) {
+ return parent
+ }
+ parent = parent.parentNode
+ }
+
+ return parent
+}
+
+/* istanbul ignore next */
+export const isInContainer = (el: Element, container: any) => {
+ if (isServer || !el || !container) return false
+
+ const elRect = el.getBoundingClientRect()
+ let containerRect
+
+ if ([window, document, document.documentElement, null, undefined].includes(container)) {
+ containerRect = {
+ top: 0,
+ right: window.innerWidth,
+ bottom: window.innerHeight,
+ left: 0
+ }
+ } else {
+ containerRect = container.getBoundingClientRect()
+ }
+
+ return (
+ elRect.top < containerRect.bottom &&
+ elRect.bottom > containerRect.top &&
+ elRect.right > containerRect.left &&
+ elRect.left < containerRect.right
+ )
+}
diff --git a/src/utils/download.ts b/src/utils/download.ts
new file mode 100644
index 0000000..32fc624
--- /dev/null
+++ b/src/utils/download.ts
@@ -0,0 +1,100 @@
+const download0 = (data: Blob, fileName: string, mineType: string) => {
+ // 鍒涘缓 blob
+ const blob = new Blob([data], { type: mineType })
+ // 鍒涘缓 href 瓒呴摼鎺ワ紝鐐瑰嚮杩涜涓嬭浇
+ window.URL = window.URL || window.webkitURL
+ const href = URL.createObjectURL(blob)
+ const downA = document.createElement('a')
+ downA.href = href
+ downA.download = fileName
+ downA.click()
+ // 閿�姣佽秴杩炴帴
+ window.URL.revokeObjectURL(href)
+}
+
+const download = {
+ // 涓嬭浇 Excel 鏂规硶
+ excel: (data: Blob, fileName: string) => {
+ download0(data, fileName, 'application/vnd.ms-excel')
+ },
+ // 涓嬭浇 Word 鏂规硶
+ word: (data: Blob, fileName: string) => {
+ download0(data, fileName, 'application/msword')
+ },
+ // 涓嬭浇 Zip 鏂规硶
+ zip: (data: Blob, fileName: string) => {
+ download0(data, fileName, 'application/zip')
+ },
+ // 涓嬭浇 Html 鏂规硶
+ html: (data: Blob, fileName: string) => {
+ download0(data, fileName, 'text/html')
+ },
+ // 涓嬭浇 Markdown 鏂规硶
+ markdown: (data: Blob, fileName: string) => {
+ download0(data, fileName, 'text/markdown')
+ },
+ // 涓嬭浇 Json 鏂规硶
+ json: (data: Blob, fileName: string) => {
+ download0(data, fileName, 'application/json')
+ },
+ // 涓嬭浇鍥剧墖锛堝厑璁歌法鍩燂級
+ image: ({
+ url,
+ canvasWidth,
+ canvasHeight,
+ drawWithImageSize = true
+ }: {
+ url: string
+ canvasWidth?: number // 鎸囧畾鐢诲竷瀹藉害
+ canvasHeight?: number // 鎸囧畾鐢诲竷楂樺害
+ drawWithImageSize?: boolean // 灏嗗浘鐗囩粯鍒跺湪鐢诲竷涓婃椂甯︿笂鍥剧墖鐨勫楂樺��, 榛樿鏄甯︿笂鐨�
+ }) => {
+ const image = new Image()
+ // image.setAttribute('crossOrigin', 'anonymous')
+ image.src = url
+ image.onload = () => {
+ const canvas = document.createElement('canvas')
+ canvas.width = canvasWidth || image.width
+ canvas.height = canvasHeight || image.height
+ const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
+ ctx?.clearRect(0, 0, canvas.width, canvas.height)
+ if (drawWithImageSize) {
+ ctx.drawImage(image, 0, 0, image.width, image.height)
+ } else {
+ ctx.drawImage(image, 0, 0)
+ }
+ const url = canvas.toDataURL('image/png')
+ const a = document.createElement('a')
+ a.href = url
+ a.download = 'image.png'
+ a.click()
+ }
+ },
+ base64ToFile: (base64: any, fileName: string) => {
+ // 灏哹ase64鎸夌収 , 杩涜鍒嗗壊 灏嗗墠缂� 涓庡悗缁唴瀹瑰垎闅斿紑
+ const data = base64.split(',')
+ // 鍒╃敤姝e垯琛ㄨ揪寮� 浠庡墠缂�涓幏鍙栧浘鐗囩殑绫诲瀷淇℃伅锛坕mage/png銆乮mage/jpeg銆乮mage/webp绛夛級
+ const type = data[0].match(/:(.*?);/)[1]
+ // 浠庡浘鐗囩殑绫诲瀷淇℃伅涓� 鑾峰彇鍏蜂綋鐨勬枃浠舵牸寮忓悗缂�锛坧ng銆乯peg銆亀ebp锛�
+ const suffix = type.split('/')[1]
+ // 浣跨敤atob()瀵筨ase64鏁版嵁杩涜瑙g爜 缁撴灉鏄竴涓枃浠舵暟鎹祦 浠ュ瓧绗︿覆鐨勬牸寮忚緭鍑�
+ const bstr = window.atob(data[1])
+ // 鑾峰彇瑙g爜缁撴灉瀛楃涓茬殑闀垮害
+ let n = bstr.length
+ // 鏍规嵁瑙g爜缁撴灉瀛楃涓茬殑闀垮害鍒涘缓涓�涓瓑闀跨殑鏁村舰鏁板瓧鏁扮粍
+ // 浣嗗湪鍒涘缓鏃� 鎵�鏈夊厓绱犲垵濮嬪�奸兘涓� 0
+ const u8arr = new Uint8Array(n)
+ // 灏嗘暣褰㈡暟缁勭殑姣忎釜鍏冪礌濉厖涓鸿В鐮佺粨鏋滃瓧绗︿覆瀵瑰簲浣嶇疆瀛楃鐨刄TF-16 缂栫爜鍗曞厓
+ while (n--) {
+ // charCodeAt()锛氳幏鍙栫粰瀹氱储寮曞瀛楃瀵瑰簲鐨� UTF-16 浠g爜鍗曞厓
+ u8arr[n] = bstr.charCodeAt(n)
+ }
+
+ // 灏咶ile鏂囦欢瀵硅薄杩斿洖缁欐柟娉曠殑璋冪敤鑰�
+ return new File([u8arr], `${fileName}.${suffix}`, {
+ type: type
+ })
+ }
+}
+
+export default download
diff --git a/src/utils/encrypt.ts b/src/utils/encrypt.ts
new file mode 100644
index 0000000..aee289e
--- /dev/null
+++ b/src/utils/encrypt.ts
@@ -0,0 +1,231 @@
+import CryptoJS from 'crypto-js'
+import { JSEncrypt } from 'jsencrypt'
+
+/**
+ * API 鍔犺В瀵嗗伐鍏风被
+ * 鏀寔 AES 鍜� RSA 鍔犲瘑绠楁硶
+ */
+
+// 浠庣幆澧冨彉閲忚幏鍙栭厤缃�
+const API_ENCRYPT_ENABLE = import.meta.env.VITE_APP_API_ENCRYPT_ENABLE === 'true'
+const API_ENCRYPT_HEADER = import.meta.env.VITE_APP_API_ENCRYPT_HEADER || 'X-Api-Encrypt'
+const API_ENCRYPT_ALGORITHM = import.meta.env.VITE_APP_API_ENCRYPT_ALGORITHM || 'AES'
+const API_ENCRYPT_REQUEST_KEY = import.meta.env.VITE_APP_API_ENCRYPT_REQUEST_KEY || '' // AES瀵嗛挜 鎴� RSA鍏挜
+const API_ENCRYPT_RESPONSE_KEY = import.meta.env.VITE_APP_API_ENCRYPT_RESPONSE_KEY || '' // AES瀵嗛挜 鎴� RSA绉侀挜
+
+/**
+ * AES 鍔犲瘑宸ュ叿绫�
+ */
+export class AES {
+ /**
+ * AES 鍔犲瘑
+ * @param data 瑕佸姞瀵嗙殑鏁版嵁
+ * @param key 鍔犲瘑瀵嗛挜
+ * @returns 鍔犲瘑鍚庣殑瀛楃涓�
+ */
+ static encrypt(data: string, key: string): string {
+ try {
+ if (!key) {
+ throw new Error('AES 鍔犲瘑瀵嗛挜涓嶈兘涓虹┖')
+ }
+ if (key.length !== 32) {
+ throw new Error(`AES 鍔犲瘑瀵嗛挜闀垮害蹇呴』涓� 32 浣嶏紝褰撳墠闀垮害: ${key.length}`)
+ }
+
+ const keyUtf8 = CryptoJS.enc.Utf8.parse(key)
+ const encrypted = CryptoJS.AES.encrypt(data, keyUtf8, {
+ mode: CryptoJS.mode.ECB,
+ padding: CryptoJS.pad.Pkcs7
+ })
+ return encrypted.toString()
+ } catch (error) {
+ console.error('AES 鍔犲瘑澶辫触:', error)
+ throw error
+ }
+ }
+
+ /**
+ * AES 瑙e瘑
+ * @param encryptedData 鍔犲瘑鐨勬暟鎹�
+ * @param key 瑙e瘑瀵嗛挜
+ * @returns 瑙e瘑鍚庣殑瀛楃涓�
+ */
+ static decrypt(encryptedData: string, key: string): string {
+ try {
+ if (!key) {
+ throw new Error('AES 瑙e瘑瀵嗛挜涓嶈兘涓虹┖')
+ }
+ if (key.length !== 32) {
+ throw new Error(`AES 瑙e瘑瀵嗛挜闀垮害蹇呴』涓� 32 浣嶏紝褰撳墠闀垮害: ${key.length}`)
+ }
+ if (!encryptedData) {
+ throw new Error('AES 瑙e瘑鏁版嵁涓嶈兘涓虹┖')
+ }
+
+ const keyUtf8 = CryptoJS.enc.Utf8.parse(key)
+ const decrypted = CryptoJS.AES.decrypt(encryptedData, keyUtf8, {
+ mode: CryptoJS.mode.ECB,
+ padding: CryptoJS.pad.Pkcs7
+ })
+ const result = decrypted.toString(CryptoJS.enc.Utf8)
+ if (!result) {
+ throw new Error('AES 瑙e瘑缁撴灉涓虹┖锛屽彲鑳芥槸瀵嗛挜閿欒鎴栨暟鎹崯鍧�')
+ }
+ return result
+ } catch (error) {
+ console.error('AES 瑙e瘑澶辫触:', error)
+ throw error
+ }
+ }
+}
+
+/**
+ * RSA 鍔犲瘑宸ュ叿绫�
+ */
+export class RSA {
+ /**
+ * RSA 鍔犲瘑
+ * @param data 瑕佸姞瀵嗙殑鏁版嵁
+ * @param publicKey 鍏挜锛堝繀闇�锛�
+ * @returns 鍔犲瘑鍚庣殑瀛楃涓�
+ */
+ static encrypt(data: string, publicKey: string): string | false {
+ try {
+ if (!publicKey) {
+ throw new Error('RSA 鍏挜涓嶈兘涓虹┖')
+ }
+
+ const encryptor = new JSEncrypt()
+ encryptor.setPublicKey(publicKey)
+ const result = encryptor.encrypt(data)
+ if (result === false) {
+ throw new Error('RSA 鍔犲瘑澶辫触锛屽彲鑳芥槸鍏挜鏍煎紡閿欒鎴栨暟鎹繃闀�')
+ }
+ return result
+ } catch (error) {
+ console.error('RSA 鍔犲瘑澶辫触:', error)
+ throw error
+ }
+ }
+
+ /**
+ * RSA 瑙e瘑
+ * @param encryptedData 鍔犲瘑鐨勬暟鎹�
+ * @param privateKey 绉侀挜锛堝繀闇�锛�
+ * @returns 瑙e瘑鍚庣殑瀛楃涓�
+ */
+ static decrypt(encryptedData: string, privateKey: string): string | false {
+ try {
+ if (!privateKey) {
+ throw new Error('RSA 绉侀挜涓嶈兘涓虹┖')
+ }
+ if (!encryptedData) {
+ throw new Error('RSA 瑙e瘑鏁版嵁涓嶈兘涓虹┖')
+ }
+
+ const encryptor = new JSEncrypt()
+ encryptor.setPrivateKey(privateKey)
+ const result = encryptor.decrypt(encryptedData)
+ if (result === false) {
+ throw new Error('RSA 瑙e瘑澶辫触锛屽彲鑳芥槸绉侀挜閿欒鎴栨暟鎹崯鍧�')
+ }
+ return result
+ } catch (error) {
+ console.error('RSA 瑙e瘑澶辫触:', error)
+ throw error
+ }
+ }
+}
+
+/**
+ * API 鍔犺В瀵嗕富绫�
+ */
+export class ApiEncrypt {
+ /**
+ * 鑾峰彇鍔犲瘑澶村悕绉�
+ */
+ static getEncryptHeader(): string {
+ return API_ENCRYPT_HEADER
+ }
+
+ /**
+ * 鍔犲瘑璇锋眰鏁版嵁
+ * @param data 瑕佸姞瀵嗙殑鏁版嵁
+ * @returns 鍔犲瘑鍚庣殑鏁版嵁
+ */
+ static encryptRequest(data: any): string {
+ if (!API_ENCRYPT_ENABLE) {
+ return data
+ }
+
+ try {
+ const jsonData = typeof data === 'string' ? data : JSON.stringify(data)
+
+ if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'AES') {
+ if (!API_ENCRYPT_REQUEST_KEY) {
+ throw new Error('AES 璇锋眰鍔犲瘑瀵嗛挜鏈厤缃�')
+ }
+ return AES.encrypt(jsonData, API_ENCRYPT_REQUEST_KEY)
+ } else if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'RSA') {
+ if (!API_ENCRYPT_REQUEST_KEY) {
+ throw new Error('RSA 鍏挜鏈厤缃�')
+ }
+ const result = RSA.encrypt(jsonData, API_ENCRYPT_REQUEST_KEY)
+ if (result === false) {
+ throw new Error('RSA 鍔犲瘑澶辫触')
+ }
+ return result
+ } else {
+ throw new Error(`涓嶆敮鎸佺殑鍔犲瘑绠楁硶: ${API_ENCRYPT_ALGORITHM}`)
+ }
+ } catch (error) {
+ console.error('璇锋眰鏁版嵁鍔犲瘑澶辫触:', error)
+ throw error
+ }
+ }
+
+ /**
+ * 瑙e瘑鍝嶅簲鏁版嵁
+ * @param encryptedData 鍔犲瘑鐨勫搷搴旀暟鎹�
+ * @returns 瑙e瘑鍚庣殑鏁版嵁
+ */
+ static decryptResponse(encryptedData: string): any {
+ if (!API_ENCRYPT_ENABLE) {
+ return encryptedData
+ }
+
+ try {
+ let decryptedData: string | false = ''
+ if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'AES') {
+ if (!API_ENCRYPT_RESPONSE_KEY) {
+ throw new Error('AES 鍝嶅簲瑙e瘑瀵嗛挜鏈厤缃�')
+ }
+ decryptedData = AES.decrypt(encryptedData, API_ENCRYPT_RESPONSE_KEY)
+ } else if (API_ENCRYPT_ALGORITHM.toUpperCase() === 'RSA') {
+ if (!API_ENCRYPT_RESPONSE_KEY) {
+ throw new Error('RSA 绉侀挜鏈厤缃�')
+ }
+ decryptedData = RSA.decrypt(encryptedData, API_ENCRYPT_RESPONSE_KEY)
+ if (decryptedData === false) {
+ throw new Error('RSA 瑙e瘑澶辫触')
+ }
+ } else {
+ throw new Error(`涓嶆敮鎸佺殑瑙e瘑绠楁硶: ${API_ENCRYPT_ALGORITHM}`)
+ }
+
+ if (!decryptedData) {
+ throw new Error('瑙e瘑缁撴灉涓虹┖')
+ }
+
+ // 灏濊瘯瑙f瀽涓� JSON锛屽鏋滃け璐ュ垯杩斿洖鍘熷瓧绗︿覆
+ try {
+ return JSON.parse(decryptedData)
+ } catch {
+ return decryptedData
+ }
+ } catch (error) {
+ console.error('鍝嶅簲鏁版嵁瑙e瘑澶辫触:', error)
+ throw error
+ }
+ }
+}
diff --git a/src/utils/file.ts b/src/utils/file.ts
new file mode 100644
index 0000000..e406519
--- /dev/null
+++ b/src/utils/file.ts
@@ -0,0 +1,37 @@
+/** 浠� URL 涓彁鍙栨枃浠跺悕 */
+export const getFileNameFromUrl = (url: string): string => {
+ try {
+ const urlObj = new URL(url)
+ const pathname = urlObj.pathname
+ const fileName = pathname.split('/').pop() || 'unknown'
+ return decodeURIComponent(fileName)
+ } catch {
+ // 濡傛灉 URL 瑙f瀽澶辫触锛屽皾璇曚粠瀛楃涓蹭腑鎻愬彇
+ const parts = url.split('/')
+ return parts[parts.length - 1] || 'unknown'
+ }
+}
+
+/** 鍒ゆ柇鏄惁涓哄浘鐗� */
+export const isImage = (filename: string): boolean => {
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
+ return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext)
+}
+
+/** 鏍煎紡鍖栨枃浠跺ぇ灏� */
+export const formatFileSize = (bytes: number): string => {
+ if (bytes === 0) return '0 B'
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+/** 鑾峰彇鏂囦欢鍥炬爣 */
+export const getFileIcon = (filename: string): string => {
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
+ if (isImage(ext)) {
+ return 'ep:picture'
+ }
+ return 'ep:document'
+}
diff --git a/src/utils/filt.ts b/src/utils/filt.ts
new file mode 100644
index 0000000..b1a7b2c
--- /dev/null
+++ b/src/utils/filt.ts
@@ -0,0 +1,157 @@
+export const openWindow = (
+ url: string,
+ opt?: {
+ target?: '_self' | '_blank' | string
+ noopener?: boolean
+ noreferrer?: boolean
+ }
+) => {
+ const { target = '__blank', noopener = true, noreferrer = true } = opt || {}
+ const feature: string[] = []
+
+ noopener && feature.push('noopener=yes')
+ noreferrer && feature.push('noreferrer=yes')
+
+ window.open(url, target, feature.join(','))
+}
+
+/**
+ * @description: base64 to blob
+ */
+export const dataURLtoBlob = (base64Buf: string): Blob => {
+ const arr = base64Buf.split(',')
+ const typeItem = arr[0]
+ const mime = typeItem.match(/:(.*?);/)![1]
+ const bstr = window.atob(arr[1])
+ let n = bstr.length
+ const u8arr = new Uint8Array(n)
+ while (n--) {
+ u8arr[n] = bstr.charCodeAt(n)
+ }
+ return new Blob([u8arr], { type: mime })
+}
+
+/**
+ * img url to base64
+ * @param url
+ */
+export const urlToBase64 = (url: string, mineType?: string): Promise<string> => {
+ return new Promise((resolve, reject) => {
+ let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>
+ const ctx = canvas!.getContext('2d')
+
+ const img = new Image()
+ img.crossOrigin = ''
+ img.onload = function () {
+ if (!canvas || !ctx) {
+ return reject()
+ }
+ canvas.height = img.height
+ canvas.width = img.width
+ ctx.drawImage(img, 0, 0)
+ const dataURL = canvas.toDataURL(mineType || 'image/png')
+ canvas = null
+ resolve(dataURL)
+ }
+ img.src = url
+ })
+}
+
+/**
+ * Download online pictures
+ * @param url
+ * @param filename
+ * @param mime
+ * @param bom
+ */
+export const downloadByOnlineUrl = (
+ url: string,
+ filename: string,
+ mime?: string,
+ bom?: BlobPart
+) => {
+ urlToBase64(url).then((base64) => {
+ downloadByBase64(base64, filename, mime, bom)
+ })
+}
+
+/**
+ * Download pictures based on base64
+ * @param buf
+ * @param filename
+ * @param mime
+ * @param bom
+ */
+export const downloadByBase64 = (buf: string, filename: string, mime?: string, bom?: BlobPart) => {
+ const base64Buf = dataURLtoBlob(buf)
+ downloadByData(base64Buf, filename, mime, bom)
+}
+
+/**
+ * Download according to the background interface file stream
+ * @param {*} data
+ * @param {*} filename
+ * @param {*} mime
+ * @param {*} bom
+ */
+export const downloadByData = (data: BlobPart, filename: string, mime?: string, bom?: BlobPart) => {
+ const blobData = typeof bom !== 'undefined' ? [bom, data] : [data]
+ const blob = new Blob(blobData, { type: mime || 'application/octet-stream' })
+
+ const blobURL = window.URL.createObjectURL(blob)
+ const tempLink = document.createElement('a')
+ tempLink.style.display = 'none'
+ tempLink.href = blobURL
+ tempLink.setAttribute('download', filename)
+ if (typeof tempLink.download === 'undefined') {
+ tempLink.setAttribute('target', '_blank')
+ }
+ document.body.appendChild(tempLink)
+ tempLink.click()
+ document.body.removeChild(tempLink)
+ window.URL.revokeObjectURL(blobURL)
+}
+
+/**
+ * Download file according to file address
+ * @param {*} sUrl
+ */
+export const downloadByUrl = ({
+ url,
+ target = '_blank',
+ fileName
+}: {
+ url: string
+ target?: '_self' | '_blank'
+ fileName?: string
+}): boolean => {
+ const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1
+ const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1
+
+ if (/(iP)/g.test(window.navigator.userAgent)) {
+ console.error('Your browser does not support download!')
+ return false
+ }
+ if (isChrome || isSafari) {
+ const link = document.createElement('a')
+ link.href = url
+ link.target = target
+
+ if (link.download !== undefined) {
+ link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length)
+ }
+
+ if (document.createEvent) {
+ const e = document.createEvent('MouseEvents')
+ e.initEvent('click', true, true)
+ link.dispatchEvent(e)
+ return true
+ }
+ }
+ if (url.indexOf('?') === -1) {
+ url += '?download'
+ }
+
+ openWindow(url, { target })
+ return true
+}
diff --git a/src/utils/formCreate.ts b/src/utils/formCreate.ts
new file mode 100644
index 0000000..e4e0dc3
--- /dev/null
+++ b/src/utils/formCreate.ts
@@ -0,0 +1,68 @@
+/**
+ * 閽堝 https://github.com/xaboy/form-create-designer 灏佽鐨勫伐鍏风被
+ */
+import { isRef } from 'vue'
+import formCreate from '@form-create/element-ui'
+
+/** 缂栫爜琛ㄥ崟 Conf */
+export const encodeConf = (designerRef: object) => {
+ // @ts-ignore
+ // 鍏宠仈妗堜緥锛歨ttps://gitee.com/yudaocode/yudao-ui-admin-vue3/pulls/834/
+ return formCreate.toJson(designerRef.value.getOption())
+}
+
+/** 瑙g爜琛ㄥ崟 Conf */
+export const decodeConf = (conf: string) => {
+ return formCreate.parseJson(conf)
+}
+
+/** 缂栫爜琛ㄥ崟 Fields */
+export const encodeFields = (designerRef: object) => {
+ // @ts-ignore
+ const rule = designerRef.value.getRule()
+ const fields: string[] = []
+ rule.forEach((item: any) => {
+ fields.push(formCreate.toJson(item))
+ })
+ return fields
+}
+
+/** 瑙g爜琛ㄥ崟 Fields */
+export const decodeFields = (fields: string[]) => {
+ const rule: object[] = []
+ fields.forEach((item) => {
+ rule.push(formCreate.parseJson(item))
+ })
+ return rule
+}
+
+/** 璁剧疆琛ㄥ崟鐨� Conf 鍜� Fields锛岄�傜敤 FcDesigner 鍦烘櫙 */
+export const setConfAndFields = (designerRef: object, conf: string, fields: string[]) => {
+ // @ts-ignore
+ designerRef.value.setOption(decodeConf(conf))
+ // @ts-ignore
+ designerRef.value.setRule(decodeFields(fields))
+}
+
+/** 璁剧疆琛ㄥ崟鐨� Conf 鍜� Fields锛岄�傜敤 form-create 鍦烘櫙 */
+export const setConfAndFields2 = (
+ detailPreview: object,
+ conf: string,
+ fields: string[],
+ value?: object
+) => {
+ if (isRef(detailPreview)) {
+ // @ts-ignore
+ detailPreview = detailPreview.value
+ }
+
+ // @ts-ignore
+ detailPreview.option = decodeConf(conf)
+ // @ts-ignore
+ detailPreview.rule = decodeFields(fields)
+
+ if (value) {
+ // @ts-ignore
+ detailPreview.value = value
+ }
+}
diff --git a/src/utils/formRules.ts b/src/utils/formRules.ts
new file mode 100644
index 0000000..2989867
--- /dev/null
+++ b/src/utils/formRules.ts
@@ -0,0 +1,7 @@
+const { t } = useI18n()
+
+// 蹇呭~椤�
+export const required = {
+ required: true,
+ message: t('common.required')
+}
diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts
new file mode 100644
index 0000000..99eb428
--- /dev/null
+++ b/src/utils/formatTime.ts
@@ -0,0 +1,332 @@
+import dayjs from 'dayjs'
+import type { TableColumnCtx } from 'element-plus'
+
+/**
+ * 鏃ユ湡蹇嵎閫夐」閫傜敤浜� el-date-picker
+ */
+export const defaultShortcuts = [
+ {
+ text: '浠婂ぉ',
+ value: () => {
+ return new Date()
+ }
+ },
+ {
+ text: '鏄ㄥぉ',
+ value: () => {
+ const date = new Date()
+ date.setTime(date.getTime() - 3600 * 1000 * 24)
+ return [date, date]
+ }
+ },
+ {
+ text: '鏈�杩戜竷澶�',
+ value: () => {
+ const date = new Date()
+ date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
+ return [date, new Date()]
+ }
+ },
+ {
+ text: '鏈�杩� 30 澶�',
+ value: () => {
+ const date = new Date()
+ date.setTime(date.getTime() - 3600 * 1000 * 24 * 30)
+ return [date, new Date()]
+ }
+ },
+ {
+ text: '鏈湀',
+ value: () => {
+ const date = new Date()
+ date.setDate(1) // 璁剧疆涓哄綋鍓嶆湀鐨勭涓�澶�
+ return [date, new Date()]
+ }
+ },
+ {
+ text: '浠婂勾',
+ value: () => {
+ const date = new Date()
+ return [new Date(`${date.getFullYear()}-01-01`), date]
+ }
+ }
+]
+
+/**
+ * 鏃堕棿鏃ユ湡杞崲
+ * @param date 褰撳墠鏃堕棿锛宯ew Date() 鏍煎紡
+ * @param format 闇�瑕佽浆鎹㈢殑鏃堕棿鏍煎紡瀛楃涓�
+ * @description format 瀛楃涓查殢鎰忥紝濡� `YYYY-MM銆乊YYY-MM-DD`
+ * @description format 瀛e害锛�"YYYY-MM-DD HH:mm:ss QQQQ"
+ * @description format 鏄熸湡锛�"YYYY-MM-DD HH:mm:ss WWW"
+ * @description format 鍑犲懆锛�"YYYY-MM-DD HH:mm:ss ZZZ"
+ * @description format 瀛e害 + 鏄熸湡 + 鍑犲懆锛�"YYYY-MM-DD HH:mm:ss WWW QQQQ ZZZ"
+ * @returns 杩斿洖鎷兼帴鍚庣殑鏃堕棿瀛楃涓�
+ */
+export function formatDate(date: Date, format?: string): string {
+ // 鏃ユ湡涓嶅瓨鍦紝鍒欒繑鍥炵┖
+ if (!date) {
+ return ''
+ }
+ // 鏃ユ湡瀛樺湪锛屽垯杩涜鏍煎紡鍖�
+ return date ? dayjs(date).format(format ?? 'YYYY-MM-DD HH:mm:ss') : ''
+}
+
+/**
+ * 鑾峰彇褰撳墠鐨勬棩鏈�+鏃堕棿
+ */
+export function getNowDateTime() {
+ return dayjs()
+}
+
+/**
+ * 鑾峰彇褰撳墠鏃ユ湡鏄鍑犲懆
+ * @param dateTime 褰撳墠浼犲叆鐨勬棩鏈熷��
+ * @returns 杩斿洖绗嚑鍛ㄦ暟瀛楀��
+ */
+export function getWeek(dateTime: Date): number {
+ const temptTime = new Date(dateTime.getTime())
+ // 鍛ㄥ嚑
+ const weekday = temptTime.getDay() || 7
+ // 鍛�1+5澶�=鍛ㄥ叚
+ temptTime.setDate(temptTime.getDate() - weekday + 1 + 5)
+ let firstDay = new Date(temptTime.getFullYear(), 0, 1)
+ const dayOfWeek = firstDay.getDay()
+ let spendDay = 1
+ if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1
+ firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay)
+ const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000)
+ return Math.ceil(d / 7)
+}
+
+/**
+ * 灏嗘椂闂磋浆鎹负 `鍑犵鍓峘銆乣鍑犲垎閽熷墠`銆乣鍑犲皬鏃跺墠`銆乣鍑犲ぉ鍓峘
+ * @param param 褰撳墠鏃堕棿锛宯ew Date() 鏍煎紡鎴栬�呭瓧绗︿覆鏃堕棿鏍煎紡
+ * @param format 闇�瑕佽浆鎹㈢殑鏃堕棿鏍煎紡瀛楃涓�
+ * @description param 10绉掞細 10 * 1000
+ * @description param 1鍒嗭細 60 * 1000
+ * @description param 1灏忔椂锛� 60 * 60 * 1000
+ * @description param 24灏忔椂锛�60 * 60 * 24 * 1000
+ * @description param 3澶╋細 60 * 60* 24 * 1000 * 3
+ * @returns 杩斿洖鎷兼帴鍚庣殑鏃堕棿瀛楃涓�
+ */
+export function formatPast(param: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string {
+ // 浼犲叆鏍煎紡澶勭悊銆佸瓨鍌ㄨ浆鎹㈠��
+ let t: any, s: number
+ // 鑾峰彇js 鏃堕棿鎴�
+ let time: number = new Date().getTime()
+ // 鏄惁鏄璞�
+ typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param)
+ // 褰撳墠鏃堕棿鎴� - 浼犲叆鏃堕棿鎴�
+ time = Number.parseInt(`${time - t}`)
+ if (time < 10000) {
+ // 10绉掑唴
+ return '鍒氬垰'
+ } else if (time < 60000 && time >= 10000) {
+ // 瓒呰繃10绉掑皯浜�1鍒嗛挓鍐�
+ s = Math.floor(time / 1000)
+ return `${s}绉掑墠`
+ } else if (time < 3600000 && time >= 60000) {
+ // 瓒呰繃1鍒嗛挓灏戜簬1灏忔椂
+ s = Math.floor(time / 60000)
+ return `${s}鍒嗛挓鍓峘
+ } else if (time < 86400000 && time >= 3600000) {
+ // 瓒呰繃1灏忔椂灏戜簬24灏忔椂
+ s = Math.floor(time / 3600000)
+ return `${s}灏忔椂鍓峘
+ } else if (time < 259200000 && time >= 86400000) {
+ // 瓒呰繃1澶╁皯浜�3澶╁唴
+ s = Math.floor(time / 86400000)
+ return `${s}澶╁墠`
+ } else {
+ // 瓒呰繃3澶�
+ const date = typeof param === 'string' || 'object' ? new Date(param) : param
+ return formatDate(date, format)
+ }
+}
+
+/**
+ * 鏃堕棿闂�欒
+ * @param param 褰撳墠鏃堕棿锛宯ew Date() 鏍煎紡
+ * @description param 璋冪敤 `formatAxis(new Date())` 杈撳嚭 `涓婂崍濂絗
+ * @returns 杩斿洖鎷兼帴鍚庣殑鏃堕棿瀛楃涓�
+ */
+export function formatAxis(param: Date): string {
+ const hour: number = new Date(param).getHours()
+ if (hour < 6) return '鍑屾櫒濂�'
+ else if (hour < 9) return '鏃╀笂濂�'
+ else if (hour < 12) return '涓婂崍濂�'
+ else if (hour < 14) return '涓崍濂�'
+ else if (hour < 17) return '涓嬪崍濂�'
+ else if (hour < 19) return '鍌嶆櫄濂�'
+ else if (hour < 22) return '鏅氫笂濂�'
+ else return '澶滈噷濂�'
+}
+
+/**
+ * 灏嗘绉掞紝杞崲鎴愭椂闂村瓧绗︿覆銆備緥濡傝锛寈x 鍒嗛挓
+ *
+ * @param ms 姣
+ * @returns {string} 瀛楃涓�
+ */
+export function formatPast2(ms: number): string {
+ const day = Math.floor(ms / (24 * 60 * 60 * 1000))
+ const hour = Math.floor(ms / (60 * 60 * 1000) - day * 24)
+ const minute = Math.floor(ms / (60 * 1000) - day * 24 * 60 - hour * 60)
+ const second = Math.floor(ms / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60)
+ if (day > 0) {
+ return day + ' 澶�' + hour + ' 灏忔椂 ' + minute + ' 鍒嗛挓'
+ }
+ if (hour > 0) {
+ return hour + ' 灏忔椂 ' + minute + ' 鍒嗛挓'
+ }
+ if (minute > 0) {
+ return minute + ' 鍒嗛挓'
+ }
+ if (second > 0) {
+ return second + ' 绉�'
+ } else {
+ return 0 + ' 绉�'
+ }
+}
+
+/**
+ * element plus 鐨勬椂闂� Formatter 瀹炵幇锛屼娇鐢� YYYY-MM-DD HH:mm:ss 鏍煎紡
+ *
+ * @param row 琛屾暟鎹�
+ * @param column 瀛楁
+ * @param cellValue 瀛楁鍊�
+ */
+export function dateFormatter(_row: any, _column: TableColumnCtx<any>, cellValue: any): string {
+ return cellValue ? formatDate(cellValue) : ''
+}
+
+/**
+ * element plus 鐨勬椂闂� Formatter 瀹炵幇锛屼娇鐢� YYYY-MM-DD 鏍煎紡
+ *
+ * @param row 琛屾暟鎹�
+ * @param column 瀛楁
+ * @param cellValue 瀛楁鍊�
+ */
+export function dateFormatter2(_row: any, _column: TableColumnCtx<any>, cellValue: any): string {
+ return cellValue ? formatDate(cellValue, 'YYYY-MM-DD') : ''
+}
+
+/**
+ * 璁剧疆璧峰鏃ユ湡锛屾椂闂翠负00:00:00
+ * @param param 浼犲叆鏃ユ湡
+ * @returns 甯︽椂闂�00:00:00鐨勬棩鏈�
+ */
+export function beginOfDay(param: Date): Date {
+ return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 0, 0, 0)
+}
+
+/**
+ * 璁剧疆缁撴潫鏃ユ湡锛屾椂闂翠负23:59:59
+ * @param param 浼犲叆鏃ユ湡
+ * @returns 甯︽椂闂�23:59:59鐨勬棩鏈�
+ */
+export function endOfDay(param: Date): Date {
+ return new Date(param.getFullYear(), param.getMonth(), param.getDate(), 23, 59, 59)
+}
+
+/**
+ * 璁$畻涓や釜鏃ユ湡闂撮殧澶╂暟
+ * @param param1 鏃ユ湡1
+ * @param param2 鏃ユ湡2
+ */
+export function betweenDay(param1: Date, param2: Date): number {
+ param1 = convertDate(param1)
+ param2 = convertDate(param2)
+ // 璁$畻宸��
+ return Math.floor((param2.getTime() - param1.getTime()) / (24 * 3600 * 1000))
+}
+
+/**
+ * 鏃ユ湡璁$畻
+ * @param param1 鏃ユ湡
+ * @param param2 娣诲姞鐨勬椂闂�
+ */
+export function addTime(param1: Date, param2: number): Date {
+ param1 = convertDate(param1)
+ return new Date(param1.getTime() + param2)
+}
+
+/**
+ * 鏃ユ湡杞崲
+ * @param param 鏃ユ湡
+ */
+export function convertDate(param: Date | string): Date {
+ if (typeof param === 'string') {
+ return new Date(param)
+ }
+ return param
+}
+
+/**
+ * 鎸囧畾鐨勪袱涓棩鏈�, 鏄惁涓哄悓涓�澶�
+ * @param a 鏃ユ湡 A
+ * @param b 鏃ユ湡 B
+ */
+export function isSameDay(a: dayjs.ConfigType, b: dayjs.ConfigType): boolean {
+ if (!a || !b) return false
+
+ const aa = dayjs(a)
+ const bb = dayjs(b)
+ return aa.year() == bb.year() && aa.month() == bb.month() && aa.day() == bb.day()
+}
+
+/**
+ * 鑾峰彇涓�澶╃殑寮�濮嬫椂闂淬�佹埅姝㈡椂闂�
+ * @param date 鏃ユ湡
+ * @param days 澶╂暟
+ */
+export function getDayRange(
+ date: dayjs.ConfigType,
+ days: number
+): [dayjs.ConfigType, dayjs.ConfigType] {
+ const day = dayjs(date).add(days, 'd')
+ return getDateRange(day, day)
+}
+
+/**
+ * 鑾峰彇鏈�杩�7澶╃殑寮�濮嬫椂闂淬�佹埅姝㈡椂闂�
+ */
+export function getLast7Days(): [dayjs.ConfigType, dayjs.ConfigType] {
+ const lastWeekDay = dayjs().subtract(7, 'd')
+ const yesterday = dayjs().subtract(1, 'd')
+ return getDateRange(lastWeekDay, yesterday)
+}
+
+/**
+ * 鑾峰彇鏈�杩�30澶╃殑寮�濮嬫椂闂淬�佹埅姝㈡椂闂�
+ */
+export function getLast30Days(): [dayjs.ConfigType, dayjs.ConfigType] {
+ const lastMonthDay = dayjs().subtract(30, 'd')
+ const yesterday = dayjs().subtract(1, 'd')
+ return getDateRange(lastMonthDay, yesterday)
+}
+
+/**
+ * 鑾峰彇鏈�杩�1骞寸殑寮�濮嬫椂闂淬�佹埅姝㈡椂闂�
+ */
+export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
+ const lastYearDay = dayjs().subtract(1, 'y')
+ const yesterday = dayjs().subtract(1, 'd')
+ return getDateRange(lastYearDay, yesterday)
+}
+
+/**
+ * 鑾峰彇鎸囧畾鏃ユ湡鐨勫紑濮嬫椂闂淬�佹埅姝㈡椂闂�
+ * @param beginDate 寮�濮嬫棩鏈�
+ * @param endDate 鎴鏃ユ湡
+ */
+export function getDateRange(
+ beginDate: dayjs.ConfigType,
+ endDate: dayjs.ConfigType
+): [string, string] {
+ return [
+ dayjs(beginDate).startOf('d').format('YYYY-MM-DD HH:mm:ss'),
+ dayjs(endDate).endOf('d').format('YYYY-MM-DD HH:mm:ss')
+ ]
+}
diff --git a/src/utils/formatter.ts b/src/utils/formatter.ts
new file mode 100644
index 0000000..8777f32
--- /dev/null
+++ b/src/utils/formatter.ts
@@ -0,0 +1,7 @@
+import { floatToFixed2 } from '@/utils'
+
+// 鏍煎紡鍖栭噾棰濄�愬垎杞厓銆�
+// @ts-ignore
+export const fenToYuanFormat = (_, __, cellValue: any, ___) => {
+ return `锟�${floatToFixed2(cellValue)}`
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 0000000..0bcedb4
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,537 @@
+import { toNumber } from 'lodash-es'
+
+/**
+ *
+ * @param component 闇�瑕佹敞鍐岀殑缁勪欢
+ * @param alias 缁勪欢鍒悕
+ * @returns any
+ */
+export const withInstall = <T>(component: T, alias?: string) => {
+ const comp = component as any
+ comp.install = (app: any) => {
+ app.component(comp.name || comp.displayName, component)
+ if (alias) {
+ app.config.globalProperties[alias] = component
+ }
+ }
+ return component as T & Plugin
+}
+
+/**
+ * @param str 闇�瑕佽浆涓嬪垝绾跨殑椹煎嘲瀛楃涓�
+ * @returns 瀛楃涓蹭笅鍒掔嚎
+ */
+export const humpToUnderline = (str: string): string => {
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase()
+}
+
+/**
+ * @param str 闇�瑕佽浆椹煎嘲鐨勪笅鍒掔嚎瀛楃涓�
+ * @returns 瀛楃涓查┘宄�
+ */
+export const underlineToHump = (str: string): string => {
+ if (!str) return ''
+ return str.replace(/\-(\w)/g, (_, letter: string) => {
+ return letter.toUpperCase()
+ })
+}
+
+/**
+ * 椹煎嘲杞í鏉�
+ */
+export const humpToDash = (str: string): string => {
+ return str.replace(/([A-Z])/g, '-$1').toLowerCase()
+}
+
+export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
+ dom.style.setProperty(prop, val)
+}
+
+/**
+ * 鏌ユ壘鏁扮粍瀵硅薄鐨勬煇涓笅鏍�
+ * @param {Array} ary 鏌ユ壘鐨勬暟缁�
+ * @param {Functon} fn 鍒ゆ柇鐨勬柟娉�
+ */
+// eslint-disable-next-line
+export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {
+ if (ary.findIndex) {
+ return ary.findIndex(fn)
+ }
+ let index = -1
+ ary.some((item: T, i: number, ary: Array<T>) => {
+ const ret: T = fn(item, i, ary)
+ if (ret) {
+ index = i
+ return ret
+ }
+ })
+ return index
+}
+
+export const trim = (str: string) => {
+ return str.replace(/(^\s*)|(\s*$)/g, '')
+}
+
+/**
+ * @param {Date | number | string} time 闇�瑕佽浆鎹㈢殑鏃堕棿
+ * @param {String} fmt 闇�瑕佽浆鎹㈢殑鏍煎紡 濡� yyyy-MM-dd銆亂yyy-MM-dd HH:mm:ss
+ */
+export function formatTime(time: Date | number | string, fmt: string) {
+ if (!time) return ''
+ else {
+ const date = new Date(time)
+ const o = {
+ 'M+': date.getMonth() + 1,
+ 'd+': date.getDate(),
+ 'H+': date.getHours(),
+ 'm+': date.getMinutes(),
+ 's+': date.getSeconds(),
+ 'q+': Math.floor((date.getMonth() + 3) / 3),
+ S: date.getMilliseconds()
+ }
+ if (/(y+)/.test(fmt)) {
+ fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+ }
+ for (const k in o) {
+ if (new RegExp('(' + k + ')').test(fmt)) {
+ fmt = fmt.replace(
+ RegExp.$1,
+ RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
+ )
+ }
+ }
+ return fmt
+ }
+}
+
+/**
+ * 鐢熸垚闅忔満瀛楃涓�
+ */
+export function toAnyString() {
+ const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
+ const r: number = (Math.random() * 16) | 0
+ const v: number = c === 'x' ? r : (r & 0x3) | 0x8
+ return v.toString()
+ })
+ return str
+}
+
+/**
+ * 鐢熸垚鎸囧畾闀垮害鐨勯殢鏈哄瓧绗︿覆
+ *
+ * @param length 瀛楃涓查暱搴�
+ */
+export function generateRandomStr(length: number): string {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+ let result = ''
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
+ }
+ return result
+}
+
+/**
+ * 鏍规嵁鏀寔鐨勬枃浠剁被鍨嬬敓鎴� accept 灞炴�у��
+ *
+ * @param supportedFileTypes 鏀寔鐨勬枃浠剁被鍨嬫暟缁勶紝濡� ['PDF', 'DOC', 'DOCX']
+ * @returns 鐢ㄤ簬鏂囦欢涓婁紶缁勪欢 accept 灞炴�х殑瀛楃涓�
+ */
+export const generateAcceptedFileTypes = (supportedFileTypes: string[]): string => {
+ const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase())
+ const mimeTypes: string[] = []
+
+ // 娣诲姞甯歌鐨� MIME 绫诲瀷鏄犲皠
+ if (allowedExtensions.includes('txt')) {
+ mimeTypes.push('text/plain')
+ }
+ if (allowedExtensions.includes('pdf')) {
+ mimeTypes.push('application/pdf')
+ }
+ if (allowedExtensions.includes('html') || allowedExtensions.includes('htm')) {
+ mimeTypes.push('text/html')
+ }
+ if (allowedExtensions.includes('csv')) {
+ mimeTypes.push('text/csv')
+ }
+ if (allowedExtensions.includes('xlsx') || allowedExtensions.includes('xls')) {
+ mimeTypes.push('application/vnd.ms-excel')
+ mimeTypes.push('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
+ }
+ if (allowedExtensions.includes('docx') || allowedExtensions.includes('doc')) {
+ mimeTypes.push('application/msword')
+ mimeTypes.push('application/vnd.openxmlformats-officedocument.wordprocessingml.document')
+ }
+ if (allowedExtensions.includes('pptx') || allowedExtensions.includes('ppt')) {
+ mimeTypes.push('application/vnd.ms-powerpoint')
+ mimeTypes.push('application/vnd.openxmlformats-officedocument.presentationml.presentation')
+ }
+ if (allowedExtensions.includes('xml')) {
+ mimeTypes.push('application/xml')
+ mimeTypes.push('text/xml')
+ }
+ if (allowedExtensions.includes('md') || allowedExtensions.includes('markdown')) {
+ mimeTypes.push('text/markdown')
+ }
+ if (allowedExtensions.includes('epub')) {
+ mimeTypes.push('application/epub+zip')
+ }
+ if (allowedExtensions.includes('eml')) {
+ mimeTypes.push('message/rfc822')
+ }
+ if (allowedExtensions.includes('msg')) {
+ mimeTypes.push('application/vnd.ms-outlook')
+ }
+
+ // 娣诲姞鏂囦欢鎵╁睍鍚�
+ const extensions = allowedExtensions.map((ext) => `.${ext}`)
+
+ return [...mimeTypes, ...extensions].join(',')
+}
+
+/**
+ * 棣栧瓧姣嶅ぇ鍐�
+ */
+export function firstUpperCase(str: string) {
+ return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
+}
+
+export const generateUUID = () => {
+ if (typeof crypto === 'object') {
+ if (typeof crypto.randomUUID === 'function') {
+ return crypto.randomUUID()
+ }
+ if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
+ const callback = (c: any) => {
+ const num = Number(c)
+ return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(
+ 16
+ )
+ }
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback)
+ }
+ }
+ let timestamp = new Date().getTime()
+ let performanceNow =
+ (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ let random = Math.random() * 16
+ if (timestamp > 0) {
+ random = (timestamp + random) % 16 | 0
+ timestamp = Math.floor(timestamp / 16)
+ } else {
+ random = (performanceNow + random) % 16 | 0
+ performanceNow = Math.floor(performanceNow / 16)
+ }
+ return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16)
+ })
+}
+
+/**
+ * element plus 鐨勬枃浠跺ぇ灏� Formatter 瀹炵幇
+ *
+ * @param row 琛屾暟鎹�
+ * @param column 瀛楁
+ * @param cellValue 瀛楁鍊�
+ */
+// @ts-ignore
+export const fileSizeFormatter = (row, column, cellValue) => {
+ const fileSize = cellValue
+ const unitArr = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+ const srcSize = parseFloat(fileSize)
+ const index = Math.floor(Math.log(srcSize) / Math.log(1024))
+ const size = srcSize / Math.pow(1024, index)
+ const sizeStr = size.toFixed(2) //淇濈暀鐨勫皬鏁颁綅鏁�
+ return sizeStr + ' ' + unitArr[index]
+}
+
+/**
+ * 灏嗗�煎鍒跺埌鐩爣瀵硅薄锛屼笖浠ョ洰鏍囧璞″睘鎬т负鍑嗭紝渚嬶細target: {a:1} source:{a:2,b:3} 缁撴灉涓猴細{a:2}
+ * @param target 鐩爣瀵硅薄
+ * @param source 婧愬璞�
+ */
+export const copyValueToTarget = (target: any, source: any) => {
+ const newObj = Object.assign({}, target, source)
+ // 鍒犻櫎澶氫綑灞炴��
+ Object.keys(newObj).forEach((key) => {
+ // 濡傛灉涓嶆槸target涓殑灞炴�у垯鍒犻櫎
+ if (Object.keys(target).indexOf(key) === -1) {
+ delete newObj[key]
+ }
+ })
+ // 鏇存柊鐩爣瀵硅薄鍊�
+ Object.assign(target, newObj)
+}
+
+/**
+ * 鑾峰彇閾炬帴鐨勫弬鏁板��
+ * @param key 鍙傛暟閿悕
+ * @param urlStr 閾炬帴鍦板潃锛岄粯璁や负褰撳墠娴忚鍣ㄧ殑鍦板潃
+ */
+export const getUrlValue = (key: string, urlStr: string = location.href): string => {
+ if (!urlStr || !key) return ''
+ const url = new URL(decodeURIComponent(urlStr))
+ return url.searchParams.get(key) ?? ''
+}
+
+/**
+ * 鑾峰彇閾炬帴鐨勫弬鏁板�硷紙鍊肩被鍨嬶級
+ * @param key 鍙傛暟閿悕
+ * @param urlStr 閾炬帴鍦板潃锛岄粯璁や负褰撳墠娴忚鍣ㄧ殑鍦板潃
+ */
+export const getUrlNumberValue = (key: string, urlStr: string = location.href): number => {
+ return toNumber(getUrlValue(key, urlStr))
+}
+
+/**
+ * 鏋勫缓鎺掑簭瀛楁
+ * @param prop 瀛楁鍚嶇О
+ * @param order 椤哄簭
+ */
+export const buildSortingField = ({ prop, order }) => {
+ return { field: prop, order: order === 'ascending' ? 'asc' : 'desc' }
+}
+
+// ========== NumberUtils 鏁板瓧鏂规硶 ==========
+
+/**
+ * 鏁扮粍姹傚拰
+ *
+ * @param values 鏁板瓧鏁扮粍
+ * @return 姹傚拰缁撴灉锛岄粯璁や负 0
+ */
+export const getSumValue = (values: number[]): number => {
+ return values.reduce((prev, curr) => {
+ const value = Number(curr)
+ if (!Number.isNaN(value)) {
+ return prev + curr
+ } else {
+ return prev
+ }
+ }, 0)
+}
+
+// ========== 閫氱敤閲戦鏂规硶 ==========
+
+/**
+ * 灏嗕竴涓暣鏁拌浆鎹负鍒嗘暟淇濈暀涓や綅灏忔暟
+ * @param num
+ */
+export const formatToFraction = (num: number | string | undefined): string => {
+ if (typeof num === 'undefined') return '0.00'
+ const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
+ return (parsedNumber / 100.0).toFixed(2)
+}
+
+/**
+ * 灏嗕竴涓暟杞崲涓� 1.00 杩欐牱
+ * 鏁版嵁鍛堢幇鐨勬椂鍊欎娇鐢�
+ *
+ * @param num 鏁存暟
+ */
+// TODO @鑺嬭壙锛氱湅鐪嬫�庝箞铻嶅悎鎺�
+export const floatToFixed2 = (num: number | string | undefined): string => {
+ let str = '0.00'
+ if (typeof num === 'undefined') {
+ return str
+ }
+ const f = formatToFraction(num)
+ const decimalPart = f.toString().split('.')[1]
+ const len = decimalPart ? decimalPart.length : 0
+ switch (len) {
+ case 0:
+ str = f.toString() + '.00'
+ break
+ case 1:
+ str = f.toString() + '0'
+ break
+ case 2:
+ str = f.toString()
+ break
+ }
+ return str
+}
+
+/**
+ * 灏嗕竴涓垎鏁拌浆鎹负鏁存暟
+ * @param num
+ */
+// TODO @鑺嬭壙锛氱湅鐪嬫�庝箞铻嶅悎鎺�
+export const convertToInteger = (num: number | string | undefined): number => {
+ if (typeof num === 'undefined') return 0
+ const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
+ // TODO 鍒嗚浆鍏冨悗杩樻湁灏忔暟鍒欏洓鑸嶄簲鍏�
+ return Math.round(parsedNumber * 100)
+}
+
+/**
+ * 鍏冭浆鍒�
+ */
+export const yuanToFen = (amount: string | number): number => {
+ return convertToInteger(amount)
+}
+
+/**
+ * 鍒嗚浆鍏�
+ */
+export const fenToYuan = (price: string | number): string => {
+ return formatToFraction(price)
+}
+
+/**
+ * 璁$畻鐜瘮
+ *
+ * @param value 褰撳墠鏁板��
+ * @param reference 瀵规瘮鏁板��
+ */
+export const calculateRelativeRate = (value?: number, reference?: number) => {
+ // 闃叉闄�0
+ if (!reference || reference == 0) return 0
+
+ return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
+}
+
+// ========== ERP 涓撳睘鏂规硶 ==========
+
+const ERP_COUNT_DIGIT = 3
+const ERP_PRICE_DIGIT = 2
+
+/**
+ * 銆怑RP銆戞牸寮忓寲 Input 鏁板瓧
+ *
+ * 渚嬪璇达細搴撳瓨鏁伴噺
+ *
+ * @param num 鏁伴噺
+ * @package digit 淇濈暀鐨勫皬鏁颁綅鏁�
+ * @return 鏍煎紡鍖栧悗鐨勬暟閲�
+ */
+export const erpNumberFormatter = (num: number | string | undefined, digit: number) => {
+ if (num == null) {
+ return ''
+ }
+ if (typeof num === 'string') {
+ num = parseFloat(num)
+ }
+ // 濡傛灉闈� number锛屽垯鐩存帴杩斿洖绌轰覆
+ if (isNaN(num)) {
+ return ''
+ }
+ return num.toFixed(digit)
+}
+
+/**
+ * 銆怑RP銆戞牸寮忓寲鏁伴噺锛屼繚鐣欎笁浣嶅皬鏁�
+ *
+ * 渚嬪璇达細搴撳瓨鏁伴噺
+ *
+ * @param num 鏁伴噺
+ * @return 鏍煎紡鍖栧悗鐨勬暟閲�
+ */
+export const erpCountInputFormatter = (num: number | string | undefined) => {
+ return erpNumberFormatter(num, ERP_COUNT_DIGIT)
+}
+
+// noinspection JSCommentMatchesSignature
+/**
+ * 銆怑RP銆戞牸寮忓寲鏁伴噺锛屼繚鐣欎笁浣嶅皬鏁�
+ *
+ * @param cellValue 鏁伴噺
+ * @return 鏍煎紡鍖栧悗鐨勬暟閲�
+ */
+export const erpCountTableColumnFormatter = (_, __, cellValue: any, ___) => {
+ return erpNumberFormatter(cellValue, ERP_COUNT_DIGIT)
+}
+
+/**
+ * 銆怑RP銆戞牸寮忓寲閲戦锛屼繚鐣欎簩浣嶅皬鏁�
+ *
+ * 渚嬪璇达細搴撳瓨鏁伴噺
+ *
+ * @param num 鏁伴噺
+ * @return 鏍煎紡鍖栧悗鐨勬暟閲�
+ */
+export const erpPriceInputFormatter = (num: number | string | undefined) => {
+ return erpNumberFormatter(num, ERP_PRICE_DIGIT)
+}
+
+// noinspection JSCommentMatchesSignature
+/**
+ * 銆怑RP銆戞牸寮忓寲閲戦锛屼繚鐣欎簩浣嶅皬鏁�
+ *
+ * @param cellValue 鏁伴噺
+ * @return 鏍煎紡鍖栧悗鐨勬暟閲�
+ */
+export const erpPriceTableColumnFormatter = (_, __, cellValue: any, ___) => {
+ return erpNumberFormatter(cellValue, ERP_PRICE_DIGIT)
+}
+
+/**
+ * 銆怑RP銆戜环鏍艰绠楋紝鍥涜垗浜斿叆淇濈暀涓や綅灏忔暟
+ *
+ * @param price 浠锋牸
+ * @param count 鏁伴噺
+ * @return 鎬讳环鏍笺�傚鏋滄湁浠讳竴涓虹┖锛屽垯杩斿洖 undefined
+ */
+export const erpPriceMultiply = (price: number, count: number) => {
+ if (price == null || count == null) {
+ return undefined
+ }
+ return parseFloat((price * count).toFixed(ERP_PRICE_DIGIT))
+}
+
+/**
+ * 銆怑RP銆戠櫨鍒嗘瘮璁$畻锛屽洓鑸嶄簲鍏ヤ繚鐣欎袱浣嶅皬鏁�
+ *
+ * 濡傛灉 total 涓� 0锛屽垯杩斿洖 0
+ *
+ * @param value 褰撳墠鍊�
+ * @param total 鎬诲��
+ */
+export const erpCalculatePercentage = (value: number, total: number) => {
+ if (total === 0) return 0
+ return ((value / total) * 100).toFixed(2)
+}
+
+/**
+ * 閫傞厤 echarts map 鐨勫湴鍚�
+ *
+ * @param areaName 鍦板尯鍚嶇О
+ */
+export const areaReplace = (areaName: string) => {
+ if (!areaName) {
+ return areaName
+ }
+ return areaName
+ .replace('缁村惥灏旇嚜娌诲尯', '')
+ .replace('澹棌鑷不鍖�', '')
+ .replace('鍥炴棌鑷不鍖�', '')
+ .replace('鑷不鍖�', '')
+ .replace('鐪�', '')
+}
+
+/**
+ * 瑙f瀽 JSON 瀛楃涓�
+ *
+ * @param str
+ */
+export function jsonParse(str: string) {
+ try {
+ return JSON.parse(str)
+ } catch (e) {
+ console.warn(`str[${str}] 涓嶆槸涓�涓� JSON 瀛楃涓瞏)
+ return str
+ }
+}
+
+/**
+ * 鎴彇瀛楃涓�
+ *
+ * @param str 瀛楃涓�
+ * @param start 寮�濮嬩綅缃�
+ * @param end 缁撴潫浣嶇疆
+ */
+export const subString = (str: string, start: number, end: number) => {
+ if (str.length > end) {
+ return str.slice(start, end)
+ }
+ return str
+}
diff --git a/src/utils/is.ts b/src/utils/is.ts
new file mode 100644
index 0000000..24d7191
--- /dev/null
+++ b/src/utils/is.ts
@@ -0,0 +1,118 @@
+// copy to vben-admin
+
+const toString = Object.prototype.toString
+
+export const is = (val: unknown, type: string) => {
+ return toString.call(val) === `[object ${type}]`
+}
+
+export const isDef = <T = unknown>(val?: T): val is T => {
+ return typeof val !== 'undefined'
+}
+
+export const isUnDef = <T = unknown>(val?: T): val is T => {
+ return !isDef(val)
+}
+
+export const isObject = (val: any): val is Record<any, any> => {
+ return val !== null && is(val, 'Object')
+}
+
+export const isEmpty = (val: any): boolean => {
+ if (val === null || val === undefined || typeof val === 'undefined') {
+ return true
+ }
+ if (isArray(val) || isString(val)) {
+ return val.length === 0
+ }
+
+ if (val instanceof Map || val instanceof Set) {
+ return val.size === 0
+ }
+
+ if (isObject(val)) {
+ return Object.keys(val).length === 0
+ }
+
+ return false
+}
+
+export const isDate = (val: unknown): val is Date => {
+ return is(val, 'Date')
+}
+
+export const isNull = (val: unknown): val is null => {
+ return val === null
+}
+
+export const isNullAndUnDef = (val: unknown): val is null | undefined => {
+ return isUnDef(val) && isNull(val)
+}
+
+export const isNullOrUnDef = (val: unknown): val is null | undefined => {
+ return isUnDef(val) || isNull(val)
+}
+
+export const isNumber = (val: unknown): val is number => {
+ return is(val, 'Number')
+}
+
+export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
+ return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
+}
+
+export const isString = (val: unknown): val is string => {
+ return is(val, 'String')
+}
+
+export const isFunction = (val: unknown): val is Function => {
+ return typeof val === 'function'
+}
+
+export const isBoolean = (val: unknown): val is boolean => {
+ return is(val, 'Boolean')
+}
+
+export const isRegExp = (val: unknown): val is RegExp => {
+ return is(val, 'RegExp')
+}
+
+export const isArray = (val: any): val is Array<any> => {
+ return val && Array.isArray(val)
+}
+
+export const isWindow = (val: any): val is Window => {
+ return typeof window !== 'undefined' && is(val, 'Window')
+}
+
+export const isElement = (val: unknown): val is Element => {
+ return isObject(val) && !!val.tagName
+}
+
+export const isMap = (val: unknown): val is Map<any, any> => {
+ return is(val, 'Map')
+}
+
+export const isServer = typeof window === 'undefined'
+
+export const isClient = !isServer
+
+export const isUrl = (path: string): boolean => {
+ // fix:淇hash璺敱鏃犳硶璺宠浆鐨勯棶棰�
+ const reg =
+ /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%#\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
+ return reg.test(path)
+}
+
+export const isDark = (): boolean => {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+}
+
+// 鏄惁鏄浘鐗囬摼鎺�
+export const isImgPath = (path: string): boolean => {
+ return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path)
+}
+
+export const isEmptyVal = (val: any): boolean => {
+ return val === '' || val === null || val === undefined
+}
diff --git a/src/utils/jsencrypt.ts b/src/utils/jsencrypt.ts
new file mode 100644
index 0000000..374d5f6
--- /dev/null
+++ b/src/utils/jsencrypt.ts
@@ -0,0 +1,31 @@
+import { JSEncrypt } from 'jsencrypt'
+
+// 瀵嗛挜瀵圭敓鎴� http://web.chacuo.net/netrsakeypair
+
+const publicKey =
+ 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
+ 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+
+const privateKey =
+ 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
+ '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
+ 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
+ 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
+ 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
+ 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
+ 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
+ 'UP8iWi1Qw0Y='
+
+// 鍔犲瘑
+export const encrypt = (txt: string) => {
+ const encryptor = new JSEncrypt()
+ encryptor.setPublicKey(publicKey) // 璁剧疆鍏挜
+ return encryptor.encrypt(txt) // 瀵规暟鎹繘琛屽姞瀵�
+}
+
+// 瑙e瘑
+export const decrypt = (txt: string) => {
+ const encryptor = new JSEncrypt()
+ encryptor.setPrivateKey(privateKey) // 璁剧疆绉侀挜
+ return encryptor.decrypt(txt) // 瀵规暟鎹繘琛岃В瀵�
+}
diff --git a/src/utils/permission.ts b/src/utils/permission.ts
new file mode 100644
index 0000000..4138173
--- /dev/null
+++ b/src/utils/permission.ts
@@ -0,0 +1,36 @@
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import {hasPermission} from "@/directives/permission/hasPermi";
+
+
+const { t } = useI18n() // 鍥介檯鍖�
+
+/**
+ * 瀛楃鏉冮檺鏍¢獙
+ * @param {Array} value 鏍¢獙鍊�
+ * @returns {Boolean}
+ */
+export function checkPermi(permission: string[]) {
+ return hasPermission(permission)
+}
+
+/**
+ * 瑙掕壊鏉冮檺鏍¢獙
+ * @param {string[]} value 鏍¢獙鍊�
+ * @returns {Boolean}
+ */
+export function checkRole(value: string[]) {
+ if (value && value instanceof Array && value.length > 0) {
+ const { wsCache } = useCache()
+ const permissionRoles = value
+ const super_admin = 'super_admin'
+ const userInfo = wsCache.get(CACHE_KEY.USER)
+ const roles = userInfo?.roles || []
+ const hasRole = roles.some((role: string) => {
+ return super_admin === role || permissionRoles.includes(role)
+ })
+ return !!hasRole
+ } else {
+ console.error(t('permission.hasRole'))
+ return false
+ }
+}
diff --git a/src/utils/propTypes.ts b/src/utils/propTypes.ts
new file mode 100644
index 0000000..863f55c
--- /dev/null
+++ b/src/utils/propTypes.ts
@@ -0,0 +1,24 @@
+import { VueTypeValidableDef, VueTypesInterface, createTypes, toValidableType } from 'vue-types'
+import { CSSProperties } from 'vue'
+
+type PropTypes = VueTypesInterface & {
+ readonly style: VueTypeValidableDef<CSSProperties>
+}
+const newPropTypes = createTypes({
+ func: undefined,
+ bool: undefined,
+ string: undefined,
+ number: undefined,
+ object: undefined,
+ integer: undefined
+}) as PropTypes
+
+class propTypes extends newPropTypes {
+ static get style() {
+ return toValidableType('style', {
+ type: [String, Object]
+ })
+ }
+}
+
+export { propTypes }
diff --git a/src/utils/routerHelper.ts b/src/utils/routerHelper.ts
new file mode 100644
index 0000000..cf1b362
--- /dev/null
+++ b/src/utils/routerHelper.ts
@@ -0,0 +1,257 @@
+import type { RouteLocationNormalized, Router, RouteRecordNormalized } from 'vue-router'
+import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
+import { isUrl } from '@/utils/is'
+import { cloneDeep, omit } from 'lodash-es'
+import qs from 'qs'
+
+const modules = import.meta.glob('../views/**/*.{vue,tsx}')
+/**
+ * 娉ㄥ唽涓�涓紓姝ョ粍浠�
+ * @param componentPath 渚�:/bpm/oa/leave/detail
+ */
+export const registerComponent = (componentPath: string) => {
+ for (const item in modules) {
+ if (item.includes(componentPath)) {
+ // 浣跨敤寮傛缁勪欢鐨勬柟寮忔潵鍔ㄦ�佸姞杞界粍浠�
+ // @ts-ignore
+ return defineAsyncComponent(modules[item])
+ }
+ }
+}
+/* Layout */
+export const Layout = () => import('@/layout/Layout.vue')
+
+export const getParentLayout = () => {
+ return () =>
+ new Promise((resolve) => {
+ resolve({
+ name: 'ParentLayout'
+ })
+ })
+}
+
+// 鎸夌収璺敱涓璵eta涓嬬殑rank绛夌骇鍗囧簭鏉ユ帓搴忚矾鐢�
+export const ascending = (arr: any[]) => {
+ arr.forEach((v) => {
+ if (v?.meta?.rank === null) v.meta.rank = undefined
+ if (v?.meta?.rank === 0) {
+ if (v.name !== 'home' && v.path !== '/') {
+ console.warn('rank only the home page can be 0')
+ }
+ }
+ })
+ return arr.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
+ return a?.meta?.rank - b?.meta?.rank
+ })
+}
+
+export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => {
+ if (!route) return route
+ const { matched, ...opt } = route
+ return {
+ ...opt,
+ matched: (matched
+ ? matched.map((item) => ({
+ meta: item.meta,
+ name: item.name,
+ path: item.path
+ }))
+ : undefined) as RouteRecordNormalized[]
+ }
+}
+
+// 鍚庣鎺у埗璺敱鐢熸垚
+export const generateRoute = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
+ const res: AppRouteRecordRaw[] = []
+ const modulesRoutesKeys = Object.keys(modules)
+ for (const route of routes) {
+ // 1. 鐢熸垚 meta 鑿滃崟鍏冩暟鎹�
+ const meta = {
+ title: route.name,
+ icon: route.icon,
+ hidden: !route.visible,
+ noCache: !route.keepAlive,
+ alwaysShow:
+ route.children &&
+ route.children.length > 0 &&
+ (route.alwaysShow !== undefined ? route.alwaysShow : true)
+ } as any
+ // 鐗规畩閫昏緫锛氬鏋滃悗绔厤缃殑 MenuDO.component 鍖呭惈 ?锛屽垯琛ㄧず闇�瑕佷紶閫掑弬鏁�
+ // 姝ゆ椂锛屾垜浠渶瑕佽В鏋愬弬鏁帮紝骞朵笖灏嗗弬鏁版斁鍒� meta.query 涓�
+ // 杩欐牱锛屽悗缁湪 Vue 鏂囦欢涓紝鍙互閫氳繃 const { currentRoute } = useRouter() 涓紝閫氳繃 meta.query 鑾峰彇鍒板弬鏁�
+ if (route.component && route.component.indexOf('?') > -1) {
+ const query = route.component.split('?')[1]
+ route.component = route.component.split('?')[0]
+ meta.query = qs.parse(query)
+ }
+
+ // 2. 鐢熸垚 data锛圓ppRouteRecordRaw锛�
+ // 璺敱鍦板潃杞瀛楁瘝澶у啓椹煎嘲锛屼綔涓鸿矾鐢卞悕绉帮紝閫傞厤keepAlive
+ let data: AppRouteRecordRaw = {
+ path:
+ route.path.indexOf('?') > -1 && !isUrl(route.path) ? route.path.split('?')[0] : route.path, // 娉ㄦ剰锛岄渶瑕佹帓闄� http 杩欑 url锛岄伩鍏嶅畠甯� ? 鍙傛暟琚埅鍙栨帀
+ name:
+ route.componentName && route.componentName.length > 0
+ ? route.componentName
+ : toCamelCase(route.path, true),
+ redirect: route.redirect,
+ meta: meta
+ }
+ //澶勭悊椤剁骇闈炵洰褰曡矾鐢�
+ if (!route.children && route.parentId == 0 && route.component) {
+ data.component = Layout
+ data.meta = {
+ hidden: meta.hidden
+ }
+ data.name = toCamelCase(route.path, true) + 'Parent'
+ data.redirect = ''
+ meta.alwaysShow = true
+ const childrenData: AppRouteRecordRaw = {
+ path: '',
+ name:
+ route.componentName && route.componentName.length > 0
+ ? route.componentName
+ : toCamelCase(route.path, true),
+ redirect: route.redirect,
+ meta: meta
+ }
+ const index = route?.component
+ ? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
+ : modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
+ childrenData.component = modules[modulesRoutesKeys[index]]
+ data.children = [childrenData]
+ } else {
+ // 鐩綍
+ if (route.children?.length) {
+ data.component = Layout
+ data.redirect = getRedirect(route.path, route.children)
+ // 澶栭摼
+ } else if (isUrl(route.path)) {
+ data = {
+ path: '/external-link',
+ component: Layout,
+ meta: {
+ name: route.name
+ },
+ children: [data]
+ } as AppRouteRecordRaw
+ // 鑿滃崟
+ } else {
+ // 瀵瑰悗绔紶component缁勪欢璺緞鍜屼笉浼犲仛鍏煎锛堝鏋滃悗绔紶component缁勪欢璺緞锛岄偅涔坧ath鍙互闅忎究鍐欙紝濡傛灉涓嶄紶锛宑omponent缁勪欢璺緞浼氭牴path淇濇寔涓�鑷达級
+ const index = route?.component
+ ? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
+ : modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
+ data.component = modules[modulesRoutesKeys[index]]
+ }
+ if (route.children) {
+ data.children = generateRoute(route.children)
+ }
+ }
+ res.push(data as AppRouteRecordRaw)
+ }
+ return res
+}
+export const getRedirect = (parentPath: string, children: AppCustomRouteRecordRaw[]) => {
+ if (!children || children.length == 0) {
+ return parentPath
+ }
+ const path = generateRoutePath(parentPath, children[0].path)
+ // 閫掑綊瀛愯妭鐐�
+ if (children[0].children) return getRedirect(path, children[0].children)
+}
+const generateRoutePath = (parentPath: string, path: string) => {
+ if (parentPath.endsWith('/')) {
+ parentPath = parentPath.slice(0, -1) // 绉婚櫎榛樿鐨� /
+ }
+ if (!path.startsWith('/')) {
+ path = '/' + path
+ }
+ return parentPath + path
+}
+export const pathResolve = (parentPath: string, path: string) => {
+ if (isUrl(path)) return path
+ if (!path) return parentPath // 淇 path 涓虹┖鏃惰繑鍥� parentPath锛岄伩鍏嶆嫾鎺ュ嚭閿� https://t.zsxq.com/QVr6b
+ const childPath = path.startsWith('/') ? path : `/${path}`
+ return `${parentPath}${childPath}`.replace(/\/+/g, '/')
+}
+
+// 璺敱闄嶇骇
+export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => {
+ const modules: AppRouteRecordRaw[] = cloneDeep(routes)
+ for (let index = 0; index < modules.length; index++) {
+ const route = modules[index]
+ if (!isMultipleRoute(route)) {
+ continue
+ }
+ promoteRouteLevel(route)
+ }
+ return modules
+}
+
+// 灞傜骇鏄惁澶т簬2
+const isMultipleRoute = (route: AppRouteRecordRaw) => {
+ if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
+ return false
+ }
+
+ const children = route.children
+
+ let flag = false
+ for (let index = 0; index < children.length; index++) {
+ const child = children[index]
+ if (child.children?.length) {
+ flag = true
+ break
+ }
+ }
+ return flag
+}
+
+// 鐢熸垚浜岀骇璺敱
+const promoteRouteLevel = (route: AppRouteRecordRaw) => {
+ let router: Router | null = createRouter({
+ routes: [route as RouteRecordRaw],
+ history: createWebHashHistory()
+ })
+
+ const routes = router.getRoutes()
+ addToChildren(routes, route.children || [], route)
+ router = null
+
+ route.children = route.children?.map((item) => omit(item, 'children'))
+}
+
+// 娣诲姞鎵�鏈夊瓙鑿滃崟
+const addToChildren = (
+ routes: RouteRecordNormalized[],
+ children: AppRouteRecordRaw[],
+ routeModule: AppRouteRecordRaw
+) => {
+ for (let index = 0; index < children.length; index++) {
+ const child = children[index]
+ const route = routes.find((item) => item.name === child.name)
+ if (!route) {
+ continue
+ }
+ routeModule.children = routeModule.children || []
+ if (!routeModule.children.find((item) => item.name === route.name)) {
+ routeModule.children?.push(route as unknown as AppRouteRecordRaw)
+ }
+ if (child.children?.length) {
+ addToChildren(routes, child.children, routeModule)
+ }
+ }
+}
+const toCamelCase = (str: string, upperCaseFirst: boolean) => {
+ str = (str || '')
+ .replace(/-(.)/g, function (group1: string) {
+ return group1.toUpperCase()
+ })
+ .replaceAll('-', '')
+
+ if (upperCaseFirst && str) {
+ str = str.charAt(0).toUpperCase() + str.slice(1)
+ }
+
+ return str
+}
diff --git a/src/utils/tree.ts b/src/utils/tree.ts
new file mode 100644
index 0000000..e5db503
--- /dev/null
+++ b/src/utils/tree.ts
@@ -0,0 +1,403 @@
+interface TreeHelperConfig {
+ id: string
+ children: string
+ pid: string
+}
+
+const DEFAULT_CONFIG: TreeHelperConfig = {
+ id: 'id',
+ children: 'children',
+ pid: 'pid'
+}
+export const defaultProps = {
+ children: 'children',
+ label: 'name',
+ value: 'id',
+ isLeaf: 'leaf',
+ emitPath: false // 鐢ㄤ簬 cascader 缁勪欢锛氬湪閫変腑鑺傜偣鏀瑰彉鏃讹紝鏄惁杩斿洖鐢辫鑺傜偣鎵�鍦ㄧ殑鍚勭骇鑿滃崟鐨勫�兼墍缁勬垚鐨勬暟缁勶紝鑻ヨ缃� false锛屽垯鍙繑鍥炶鑺傜偣鐨勫��
+}
+
+const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
+
+// tree from list
+export const listToTree = <T = any>(list: any[], config: Partial<TreeHelperConfig> = {}): T[] => {
+ const conf = getConfig(config) as TreeHelperConfig
+ const nodeMap = new Map()
+ const result: T[] = []
+ const { id, children, pid } = conf
+
+ for (const node of list) {
+ node[children] = node[children] || []
+ nodeMap.set(node[id], node)
+ }
+ for (const node of list) {
+ const parent = nodeMap.get(node[pid])
+ ;(parent ? parent.children : result).push(node)
+ }
+ return result
+}
+
+export const treeToList = <T = any>(tree: any, config: Partial<TreeHelperConfig> = {}): T => {
+ config = getConfig(config)
+ const { children } = config
+ const result: any = [...tree]
+ for (let i = 0; i < result.length; i++) {
+ if (!result[i][children!]) continue
+ result.splice(i + 1, 0, ...result[i][children!])
+ }
+ return result
+}
+
+export const findNode = <T = any>(
+ tree: any,
+ func: Fn,
+ config: Partial<TreeHelperConfig> = {}
+): T | null => {
+ config = getConfig(config)
+ const { children } = config
+ const list = [...tree]
+ for (const node of list) {
+ if (func(node)) return node
+ node[children!] && list.push(...node[children!])
+ }
+ return null
+}
+
+export const findNodeAll = <T = any>(
+ tree: any,
+ func: Fn,
+ config: Partial<TreeHelperConfig> = {}
+): T[] => {
+ config = getConfig(config)
+ const { children } = config
+ const list = [...tree]
+ const result: T[] = []
+ for (const node of list) {
+ func(node) && result.push(node)
+ node[children!] && list.push(...node[children!])
+ }
+ return result
+}
+
+export const findPath = <T = any>(
+ tree: any,
+ func: Fn,
+ config: Partial<TreeHelperConfig> = {}
+): T | T[] | null => {
+ config = getConfig(config)
+ const path: T[] = []
+ const list = [...tree]
+ const visitedSet = new Set()
+ const { children } = config
+ while (list.length) {
+ const node = list[0]
+ if (visitedSet.has(node)) {
+ path.pop()
+ list.shift()
+ } else {
+ visitedSet.add(node)
+ node[children!] && list.unshift(...node[children!])
+ path.push(node)
+ if (func(node)) {
+ return path
+ }
+ }
+ }
+ return null
+}
+
+export const findPathAll = (tree: any, func: Fn, config: Partial<TreeHelperConfig> = {}) => {
+ config = getConfig(config)
+ const path: any[] = []
+ const list = [...tree]
+ const result: any[] = []
+ const visitedSet = new Set(),
+ { children } = config
+ while (list.length) {
+ const node = list[0]
+ if (visitedSet.has(node)) {
+ path.pop()
+ list.shift()
+ } else {
+ visitedSet.add(node)
+ node[children!] && list.unshift(...node[children!])
+ path.push(node)
+ func(node) && result.push([...path])
+ }
+ }
+ return result
+}
+
+export const filter = <T = any>(
+ tree: T[],
+ func: (n: T) => boolean,
+ config: Partial<TreeHelperConfig> = {}
+): T[] => {
+ config = getConfig(config)
+ const children = config.children as string
+
+ function listFilter(list: T[]) {
+ return list
+ .map((node: any) => ({ ...node }))
+ .filter((node) => {
+ node[children] = node[children] && listFilter(node[children])
+ return func(node) || (node[children] && node[children].length)
+ })
+ }
+
+ return listFilter(tree)
+}
+
+export const forEach = <T = any>(
+ tree: T[],
+ func: (n: T) => any,
+ config: Partial<TreeHelperConfig> = {}
+): void => {
+ config = getConfig(config)
+ const list: any[] = [...tree]
+ const { children } = config
+ for (let i = 0; i < list.length; i++) {
+ // func 杩斿洖true灏辩粓姝㈤亶鍘嗭紝閬垮厤澶ч噺鑺傜偣鍦烘櫙涓嬫棤鎰忎箟寰幆锛屽紩璧锋祻瑙堝櫒鍗¢】
+ if (func(list[i])) {
+ return
+ }
+ children && list[i][children] && list.splice(i + 1, 0, ...list[i][children])
+ }
+}
+
+/**
+ * @description: Extract tree specified structure
+ */
+export const treeMap = <T = any>(
+ treeData: T[],
+ opt: { children?: string; conversion: Fn }
+): T[] => {
+ return treeData.map((item) => treeMapEach(item, opt))
+}
+
+/**
+ * @description: Extract tree specified structure
+ */
+export const treeMapEach = (
+ data: any,
+ { children = 'children', conversion }: { children?: string; conversion: Fn }
+) => {
+ const haveChildren = Array.isArray(data[children]) && data[children].length > 0
+ const conversionData = conversion(data) || {}
+ if (haveChildren) {
+ return {
+ ...conversionData,
+ [children]: data[children].map((i: number) =>
+ treeMapEach(i, {
+ children,
+ conversion
+ })
+ )
+ }
+ } else {
+ return {
+ ...conversionData
+ }
+ }
+}
+
+/**
+ * 閫掑綊閬嶅巻鏍戠粨鏋�
+ * @param treeDatas 鏍�
+ * @param callBack 鍥炶皟
+ * @param parentNode 鐖惰妭鐐�
+ */
+export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => {
+ treeDatas.forEach((element) => {
+ const newNode = callBack(element, parentNode) || element
+ if (element.children) {
+ eachTree(element.children, callBack, newNode)
+ }
+ })
+}
+
+/**
+ * 鏋勯�犳爲鍨嬬粨鏋勬暟鎹�
+ * @param {*} data 鏁版嵁婧�
+ * @param {*} id id瀛楁 榛樿 'id'
+ * @param {*} parentId 鐖惰妭鐐瑰瓧娈� 榛樿 'parentId'
+ * @param {*} children 瀛╁瓙鑺傜偣瀛楁 榛樿 'children'
+ */
+export const handleTree = (data: any[], id?: string, parentId?: string, children?: string) => {
+ if (!Array.isArray(data)) {
+ console.warn('data must be an array')
+ return []
+ }
+ const config = {
+ id: id || 'id',
+ parentId: parentId || 'parentId',
+ childrenList: children || 'children'
+ }
+
+ const childrenListMap = {}
+ const nodeIds = {}
+ const tree: any[] = []
+
+ for (const d of data) {
+ const parentId = d[config.parentId]
+ if (childrenListMap[parentId] == null) {
+ childrenListMap[parentId] = []
+ }
+ nodeIds[d[config.id]] = d
+ childrenListMap[parentId].push(d)
+ }
+
+ for (const d of data) {
+ const parentId = d[config.parentId]
+ if (nodeIds[parentId] == null) {
+ tree.push(d)
+ }
+ }
+
+ for (const t of tree) {
+ adaptToChildrenList(t)
+ }
+
+ function adaptToChildrenList(o) {
+ if (childrenListMap[o[config.id]] !== null) {
+ o[config.childrenList] = childrenListMap[o[config.id]]
+ }
+ if (o[config.childrenList]) {
+ for (const c of o[config.childrenList]) {
+ adaptToChildrenList(c)
+ }
+ }
+ }
+
+ return tree
+}
+
+/**
+ * 鏋勯�犳爲鍨嬬粨鏋勬暟鎹�
+ * @param {*} data 鏁版嵁婧�
+ * @param {*} id id瀛楁 榛樿 'id'
+ * @param {*} parentId 鐖惰妭鐐瑰瓧娈� 榛樿 'parentId'
+ * @param {*} children 瀛╁瓙鑺傜偣瀛楁 榛樿 'children'
+ * @param {*} rootId 鏍笽d 榛樿 0
+ */
+// @ts-ignore
+export const handleTree2 = (data, id, parentId, children, rootId) => {
+ id = id || 'id'
+ parentId = parentId || 'parentId'
+ // children = children || 'children'
+ rootId =
+ rootId ||
+ Math.min(
+ ...data.map((item) => {
+ return item[parentId]
+ })
+ ) ||
+ 0
+ // 瀵规簮鏁版嵁娣卞害鍏嬮殕
+ const cloneData = JSON.parse(JSON.stringify(data))
+ // 寰幆鎵�鏈夐」
+ const treeData = cloneData.filter((father) => {
+ const branchArr = cloneData.filter((child) => {
+ // 杩斿洖姣忎竴椤圭殑瀛愮骇鏁扮粍
+ return father[id] === child[parentId]
+ })
+ branchArr.length > 0 ? (father.children = branchArr) : ''
+ // 杩斿洖绗竴灞�
+ return father[parentId] === rootId
+ })
+ return treeData !== '' ? treeData : data
+}
+
+/**
+ * 鏍¢獙閫変腑鐨勮妭鐐癸紝鏄惁涓烘寚瀹� level
+ *
+ * @param tree 瑕佹搷浣滅殑鏍戠粨鏋勬暟鎹�
+ * @param nodeId 闇�瑕佸垽鏂湪浠�涔堝眰绾х殑鏁版嵁
+ * @param level 妫�鏌ョ殑绾у埆, 榛樿妫�鏌ュ埌浜岀骇
+ * @return true 鏄紱false 鍚�
+ */
+export const checkSelectedNode = (tree: any[], nodeId: any, level = 2): boolean => {
+ if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
+ console.warn('tree must be an array')
+ return false
+ }
+
+ // 鏍¢獙鏄惁鏄竴绾ц妭鐐�
+ if (tree.some((item) => item.id === nodeId)) {
+ return false
+ }
+
+ // 閫掑綊璁℃暟
+ let count = 1
+
+ // 娣卞眰娆℃牎楠�
+ function performAThoroughValidation(arr: any[]): boolean {
+ count += 1
+ for (const item of arr) {
+ if (item.id === nodeId) {
+ return true
+ } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
+ if (performAThoroughValidation(item.children)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ for (const item of tree) {
+ count = 1
+ if (performAThoroughValidation(item.children)) {
+ // 鎵惧埌鍚庡姣旀槸鍚︽槸鏈熸湜鐨勫眰绾�
+ if (count >= level) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+/**
+ * 鑾峰彇鑺傜偣鐨勫畬鏁寸粨鏋�
+ * @param tree 鏍戞暟鎹�
+ * @param nodeId 鑺傜偣 id
+ */
+export const treeToString = (tree: any[], nodeId) => {
+ if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
+ console.warn('tree must be an array')
+ return ''
+ }
+ // 鏍¢獙鏄惁鏄竴绾ц妭鐐�
+ const node = tree.find((item) => item.id === nodeId)
+ if (typeof node !== 'undefined') {
+ return node.name
+ }
+ let str = ''
+
+ function performAThoroughValidation(arr) {
+ if (typeof arr === 'undefined' || !Array.isArray(arr) || arr.length === 0) {
+ return false
+ }
+ for (const item of arr) {
+ if (item.id === nodeId) {
+ str += ` / ${item.name}`
+ return true
+ } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
+ str += ` / ${item.name}`
+ if (performAThoroughValidation(item.children)) {
+ return true
+ }
+ }
+ }
+ return false
+ }
+
+ for (const item of tree) {
+ str = `${item.name}`
+ if (performAThoroughValidation(item.children)) {
+ break
+ }
+ }
+ return str
+}
diff --git a/src/utils/tsxHelper.ts b/src/utils/tsxHelper.ts
new file mode 100644
index 0000000..6087fa3
--- /dev/null
+++ b/src/utils/tsxHelper.ts
@@ -0,0 +1,16 @@
+import { Slots } from 'vue'
+import { isFunction } from '@/utils/is'
+
+export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => {
+ // Reflect.has 鍒ゆ柇涓�涓璞℃槸鍚﹀瓨鍦ㄦ煇涓睘鎬�
+ if (!slots || !Reflect.has(slots, slot)) {
+ return null
+ }
+ if (!isFunction(slots[slot])) {
+ console.error(`${slot} is not a function!`)
+ return null
+ }
+ const slotFn = slots[slot]
+ if (!slotFn) return null
+ return slotFn(data)
+}
diff --git a/src/views/Error/403.vue b/src/views/Error/403.vue
new file mode 100644
index 0000000..a3ec487
--- /dev/null
+++ b/src/views/Error/403.vue
@@ -0,0 +1,8 @@
+<template>
+ <Error type="403" @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error403' })
+
+const { push } = useRouter()
+</script>
diff --git a/src/views/Error/404.vue b/src/views/Error/404.vue
new file mode 100644
index 0000000..f6a08de
--- /dev/null
+++ b/src/views/Error/404.vue
@@ -0,0 +1,7 @@
+<template>
+ <Error @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error404' })
+const { push } = useRouter()
+</script>
diff --git a/src/views/Error/500.vue b/src/views/Error/500.vue
new file mode 100644
index 0000000..998487d
--- /dev/null
+++ b/src/views/Error/500.vue
@@ -0,0 +1,7 @@
+<template>
+ <Error type="500" @error-click="push('/')" />
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Error500' })
+const { push } = useRouter()
+</script>
diff --git a/src/views/Home/Index.vue b/src/views/Home/Index.vue
new file mode 100644
index 0000000..7d4c673
--- /dev/null
+++ b/src/views/Home/Index.vue
@@ -0,0 +1,422 @@
+<template>
+ <div>
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <el-row :gutter="16" justify="space-between">
+ <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+ <div class="flex items-center">
+ <el-avatar :src="avatar" :size="70" class="mr-16px">
+ <img src="@/assets/imgs/avatar.gif" alt="" />
+ </el-avatar>
+ <div>
+ <div class="text-20px">
+ {{ t('workplace.welcome') }} {{ username }} {{ t('workplace.happyDay') }}
+ </div>
+ <div class="mt-10px text-14px text-gray-500">
+ {{ t('workplace.toady') }}锛�20鈩� - 32鈩冿紒
+ </div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
+ <div class="h-70px flex items-center justify-end lt-sm:mt-10px">
+ <div class="px-8px text-right">
+ <div class="mb-16px text-14px text-gray-400">{{ t('workplace.project') }}</div>
+ <CountTo
+ class="text-20px"
+ :start-val="0"
+ :end-val="totalSate.project"
+ :duration="2600"
+ />
+ </div>
+ <el-divider direction="vertical" />
+ <div class="px-8px text-right">
+ <div class="mb-16px text-14px text-gray-400">{{ t('workplace.toDo') }}</div>
+ <CountTo
+ class="text-20px"
+ :start-val="0"
+ :end-val="totalSate.todo"
+ :duration="2600"
+ />
+ </div>
+ <el-divider direction="vertical" border-style="dashed" />
+ <div class="px-8px text-right">
+ <div class="mb-16px text-14px text-gray-400">{{ t('workplace.access') }}</div>
+ <CountTo
+ class="text-20px"
+ :start-val="0"
+ :end-val="totalSate.access"
+ :duration="2600"
+ />
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ </el-skeleton>
+ </el-card>
+ </div>
+
+ <el-row class="mt-8px" :gutter="8" justify="space-between">
+ <el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
+ <el-card shadow="never">
+ <template #header>
+ <div class="h-3 flex justify-between">
+ <span>{{ t('workplace.project') }}</span>
+ <el-link
+ type="primary"
+ :underline="false"
+ href="https://github.com/yudaocode"
+ target="_blank"
+ >
+ {{ t('action.more') }}
+ </el-link>
+ </div>
+ </template>
+ <el-skeleton :loading="loading" animated>
+ <el-row>
+ <el-col
+ v-for="(item, index) in projects"
+ :key="`card-${index}`"
+ :xl="8"
+ :lg="8"
+ :md="8"
+ :sm="24"
+ :xs="24"
+ >
+ <el-card
+ shadow="hover"
+ class="mr-5px mt-5px cursor-pointer"
+ @click="handleProjectClick(item.message)"
+ >
+ <div class="flex items-center">
+ <Icon
+ :icon="item.icon"
+ :size="25"
+ class="mr-8px"
+ :style="{ color: item.color }"
+ />
+ <span class="text-16px">{{ item.name }}</span>
+ </div>
+ <div class="mt-12px text-12px text-gray-400">{{ t(item.message) }}</div>
+ <div class="mt-12px flex justify-between text-12px text-gray-400">
+ <span>{{ item.personal }}</span>
+ <span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </el-skeleton>
+ </el-card>
+
+ <el-card shadow="never" class="mt-8px">
+ <el-skeleton :loading="loading" animated>
+ <el-row :gutter="20" justify="space-between">
+ <el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
+ <el-card shadow="hover" class="mb-8px">
+ <el-skeleton :loading="loading" animated>
+ <Echart :options="pieOptionsData" :height="280" />
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ <el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
+ <el-card shadow="hover" class="mb-8px">
+ <el-skeleton :loading="loading" animated>
+ <Echart :options="barOptionsData" :height="280" />
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ </el-row>
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ <el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
+ <el-card shadow="never">
+ <template #header>
+ <div class="h-3 flex justify-between">
+ <span>{{ t('workplace.shortcutOperation') }}</span>
+ </div>
+ </template>
+ <el-skeleton :loading="loading" animated>
+ <el-row>
+ <el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
+ <div class="flex items-center">
+ <Icon :icon="item.icon" class="mr-8px" :style="{ color: item.color }" />
+ <el-link type="default" :underline="false" @click="handleShortcutClick(item.url)">
+ {{ item.name }}
+ </el-link>
+ </div>
+ </el-col>
+ </el-row>
+ </el-skeleton>
+ </el-card>
+ <el-card shadow="never" class="mt-8px">
+ <template #header>
+ <div class="h-3 flex justify-between">
+ <span>{{ t('workplace.notice') }}</span>
+ <el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
+ </div>
+ </template>
+ <el-skeleton :loading="loading" animated>
+ <div v-for="(item, index) in notice" :key="`dynamics-${index}`">
+ <div class="flex items-center">
+ <el-avatar :src="avatar" :size="35" class="mr-16px">
+ <img src="@/assets/imgs/avatar.gif" alt="" />
+ </el-avatar>
+ <div>
+ <div class="text-14px">
+ <Highlight :keys="item.keys.map((v) => t(v))">
+ {{ item.type }} : {{ item.title }}
+ </Highlight>
+ </div>
+ <div class="mt-16px text-12px text-gray-400">
+ {{ formatTime(item.date, 'yyyy-MM-dd') }}
+ </div>
+ </div>
+ </div>
+ <el-divider />
+ </div>
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ </el-row>
+</template>
+<script lang="ts" setup>
+import { set } from 'lodash-es'
+import { EChartsOption } from 'echarts'
+import { formatTime } from '@/utils'
+
+import { useUserStore } from '@/store/modules/user'
+// import { useWatermark } from '@/hooks/web/useWatermark'
+import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
+import { pieOptions, barOptions } from './echarts-data'
+import { useRouter } from 'vue-router'
+
+defineOptions({ name: 'Index' })
+
+const { t } = useI18n()
+const router = useRouter()
+const userStore = useUserStore()
+// const { setWatermark } = useWatermark()
+const loading = ref(true)
+const avatar = userStore.getUser.avatar
+const username = userStore.getUser.nickname
+const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
+// 鑾峰彇缁熻鏁�
+let totalSate = reactive<WorkplaceTotal>({
+ project: 0,
+ access: 0,
+ todo: 0
+})
+
+const getCount = async () => {
+ const data = {
+ project: 40,
+ access: 2340,
+ todo: 10
+ }
+ totalSate = Object.assign(totalSate, data)
+}
+
+// 鑾峰彇椤圭洰鏁�
+let projects = reactive<Project[]>([])
+const getProject = async () => {
+ const data = [
+ {
+ name: 'ruoyi-vue-pro',
+ icon: 'simple-icons:springboot',
+ message: 'github.com/YunaiV/ruoyi-vue-pro',
+ personal: 'Spring Boot 鍗曚綋鏋舵瀯',
+ time: new Date('2025-01-02'),
+ color: '#6DB33F'
+ },
+ {
+ name: 'yudao-ui-admin-vue3',
+ icon: 'ep:element-plus',
+ message: 'github.com/yudaocode/yudao-ui-admin-vue3',
+ personal: 'Vue3 + element-plus 绠$悊鍚庡彴',
+ time: new Date('2025-02-03'),
+ color: '#409EFF'
+ },
+ {
+ name: 'yudao-ui-mall-uniapp',
+ icon: 'icon-park-outline:mall-bag',
+ message: 'github.com/yudaocode/yudao-ui-mall-uniapp',
+ personal: 'Vue3 + uniapp 鍟嗗煄鎵嬫満绔�',
+ time: new Date('2025-03-04'),
+ color: '#ff4d4f'
+ },
+ {
+ name: 'yudao-cloud',
+ icon: 'material-symbols:cloud-outline',
+ message: 'github.com/YunaiV/yudao-cloud',
+ personal: 'Spring Cloud 寰湇鍔℃灦鏋�',
+ time: new Date('2025-04-05'),
+ color: '#1890ff'
+ },
+ {
+ name: 'yudao-ui-admin-vben',
+ icon: 'devicon:antdesign',
+ message: 'github.com/yudaocode/yudao-ui-admin-vben',
+ personal: 'Vue3 + vben5(antd) 绠$悊鍚庡彴',
+ time: new Date('2025-05-06'),
+ color: '#e18525'
+ },
+ {
+ name: 'yudao-ui-admin-uniapp',
+ icon: 'ant-design:mobile',
+ message: 'github.com/yudaocode/yudao-ui-admin-uniapp',
+ personal: 'Vue3 + uniapp 绠$悊鎵嬫満绔�',
+ time: new Date('2025-06-01'),
+ color: '#2979ff'
+ }
+ ]
+ projects = Object.assign(projects, data)
+}
+
+// 鑾峰彇閫氱煡鍏憡
+let notice = reactive<Notice[]>([])
+const getNotice = async () => {
+ const data = [
+ {
+ title: '绯荤粺鏀寔 JDK 8/17/21锛孷ue 2/3',
+ type: '鎶�鏈吋瀹规��',
+ keys: ['JDK', 'Vue'],
+ date: new Date()
+ },
+ {
+ title: '鍚庣鎻愪緵 Spring Boot 2.7/3.2 + Cloud 鍙屾灦鏋�',
+ type: '鏋舵瀯鐏垫椿鎬�',
+ keys: ['Boot', 'Cloud'],
+ date: new Date()
+ },
+ {
+ title: '鍏ㄩ儴寮�婧愶紝涓汉涓庝紒涓氬彲 100% 鐩存帴浣跨敤锛屾棤闇�鎺堟潈',
+ type: '寮�婧愬厤鎺堟潈',
+ keys: ['鏃犻渶鎺堟潈'],
+ date: new Date()
+ },
+ {
+ title: '鍥藉唴浣跨敤鏈�骞挎硾鐨勫揩閫熷紑鍙戝钩鍙帮紝杩滆秴 10w+ 浼佷笟浣跨敤',
+ type: '骞挎硾浼佷笟璁ゅ彲',
+ keys: ['鏈�骞挎硾', '10w+'],
+ date: new Date()
+ }
+ ]
+ notice = Object.assign(notice, data)
+}
+
+// 鑾峰彇蹇嵎鍏ュ彛
+let shortcut = reactive<Shortcut[]>([])
+
+const getShortcut = async () => {
+ const data = [
+ {
+ name: '棣栭〉',
+ icon: 'ion:home-outline',
+ url: '/',
+ color: '#1fdaca'
+ },
+ {
+ name: '鍟嗗煄涓績',
+ icon: 'ep:shop',
+ url: '/mall/home',
+ color: '#ff6b6b'
+ },
+ {
+ name: 'AI 澶фā鍨�',
+ icon: 'tabler:ai',
+ url: '/ai/chat',
+ color: '#7c3aed'
+ },
+ {
+ name: 'ERP 绯荤粺',
+ icon: 'simple-icons:erpnext',
+ url: '/erp/home',
+ color: '#3fb27f'
+ },
+ {
+ name: 'CRM 绯荤粺',
+ icon: 'simple-icons:civicrm',
+ url: '/crm/backlog',
+ color: '#4daf1bc9'
+ },
+ {
+ name: 'IoT 鐗╄仈缃�',
+ icon: 'fa-solid:hdd',
+ url: '/iot/home',
+ color: '#1a73e8'
+ }
+ ]
+ shortcut = Object.assign(shortcut, data)
+}
+
+// 鐢ㄦ埛鏉ユ簮
+const getUserAccessSource = async () => {
+ const data = [
+ { value: 335, name: 'analysis.directAccess' },
+ { value: 310, name: 'analysis.mailMarketing' },
+ { value: 234, name: 'analysis.allianceAdvertising' },
+ { value: 135, name: 'analysis.videoAdvertising' },
+ { value: 1548, name: 'analysis.searchEngines' }
+ ]
+ set(
+ pieOptionsData,
+ 'legend.data',
+ data.map((v) => t(v.name))
+ )
+ pieOptionsData!.series![0].data = data.map((v) => {
+ return {
+ name: t(v.name),
+ value: v.value
+ }
+ })
+}
+const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+
+// 鍛ㄦ椿璺冮噺
+const getWeeklyUserActivity = async () => {
+ const data = [
+ { value: 13253, name: 'analysis.monday' },
+ { value: 34235, name: 'analysis.tuesday' },
+ { value: 26321, name: 'analysis.wednesday' },
+ { value: 12340, name: 'analysis.thursday' },
+ { value: 24643, name: 'analysis.friday' },
+ { value: 1322, name: 'analysis.saturday' },
+ { value: 1324, name: 'analysis.sunday' }
+ ]
+ set(
+ barOptionsData,
+ 'xAxis.data',
+ data.map((v) => t(v.name))
+ )
+ set(barOptionsData, 'series', [
+ {
+ name: t('analysis.activeQuantity'),
+ data: data.map((v) => v.value),
+ type: 'bar'
+ }
+ ])
+}
+
+const getAllApi = async () => {
+ await Promise.all([
+ getCount(),
+ getProject(),
+ getNotice(),
+ getShortcut(),
+ getUserAccessSource(),
+ getWeeklyUserActivity()
+ ])
+ loading.value = false
+}
+
+const handleProjectClick = (message: string) => {
+ window.open(`https://${message}`, '_blank')
+}
+
+const handleShortcutClick = (url: string) => {
+ router.push(url)
+}
+
+getAllApi()
+</script>
diff --git a/src/views/Home/Index2.vue b/src/views/Home/Index2.vue
new file mode 100644
index 0000000..c9429ab
--- /dev/null
+++ b/src/views/Home/Index2.vue
@@ -0,0 +1,319 @@
+<template>
+ <el-row :class="prefixCls" :gutter="20" justify="space-between">
+ <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" :rows="2" animated>
+ <template #default>
+ <div :class="`${prefixCls}__item flex justify-between`">
+ <div>
+ <div
+ :class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
+ >
+ <Icon :size="40" icon="svg-icon:peoples" />
+ </div>
+ </div>
+ <div class="flex flex-col justify-between">
+ <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+ >{{ t('analysis.newUser') }}
+ </div>
+ <CountTo
+ :duration="2600"
+ :end-val="102400"
+ :start-val="0"
+ class="text-right text-20px font-700"
+ />
+ </div>
+ </div>
+ </template>
+ </el-skeleton>
+ </el-card>
+ </el-col>
+
+ <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" :rows="2" animated>
+ <template #default>
+ <div :class="`${prefixCls}__item flex justify-between`">
+ <div>
+ <div
+ :class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
+ >
+ <Icon :size="40" icon="svg-icon:message" />
+ </div>
+ </div>
+ <div class="flex flex-col justify-between">
+ <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+ >{{ t('analysis.unreadInformation') }}
+ </div>
+ <CountTo
+ :duration="2600"
+ :end-val="81212"
+ :start-val="0"
+ class="text-right text-20px font-700"
+ />
+ </div>
+ </div>
+ </template>
+ </el-skeleton>
+ </el-card>
+ </el-col>
+
+ <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" :rows="2" animated>
+ <template #default>
+ <div :class="`${prefixCls}__item flex justify-between`">
+ <div>
+ <div
+ :class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
+ >
+ <Icon :size="40" icon="svg-icon:money" />
+ </div>
+ </div>
+ <div class="flex flex-col justify-between">
+ <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+ >{{ t('analysis.transactionAmount') }}
+ </div>
+ <CountTo
+ :duration="2600"
+ :end-val="9280"
+ :start-val="0"
+ class="text-right text-20px font-700"
+ />
+ </div>
+ </div>
+ </template>
+ </el-skeleton>
+ </el-card>
+ </el-col>
+
+ <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" :rows="2" animated>
+ <template #default>
+ <div :class="`${prefixCls}__item flex justify-between`">
+ <div>
+ <div
+ :class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
+ >
+ <Icon :size="40" icon="svg-icon:shopping" />
+ </div>
+ </div>
+ <div class="flex flex-col justify-between">
+ <div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
+ >{{ t('analysis.totalShopping') }}
+ </div>
+ <CountTo
+ :duration="2600"
+ :end-val="13600"
+ :start-val="0"
+ class="text-right text-20px font-700"
+ />
+ </div>
+ </div>
+ </template>
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20" justify="space-between">
+ <el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="300" :options="pieOptionsData" />
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ <el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="300" :options="barOptionsData" />
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ <el-col :span="24">
+ <el-card class="mb-20px" shadow="hover">
+ <el-skeleton :loading="loading" :rows="4" animated>
+ <Echart :height="350" :options="lineOptionsData" />
+ </el-skeleton>
+ </el-card>
+ </el-col>
+ </el-row>
+</template>
+<script lang="ts" setup>
+import { set } from 'lodash-es'
+import { EChartsOption } from 'echarts'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import type { AnalysisTotalTypes } from './types'
+import { barOptions, lineOptions, pieOptions } from './echarts-data'
+
+defineOptions({ name: 'Home2' })
+
+const { t } = useI18n()
+const loading = ref(true)
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('panel')
+const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
+
+let totalState = reactive<AnalysisTotalTypes>({
+ users: 0,
+ messages: 0,
+ moneys: 0,
+ shoppings: 0
+})
+
+const getCount = async () => {
+ const data = {
+ users: 102400,
+ messages: 81212,
+ moneys: 9280,
+ shoppings: 13600
+ }
+ totalState = Object.assign(totalState, data)
+}
+
+// 鐢ㄦ埛鏉ユ簮
+const getUserAccessSource = async () => {
+ const data = [
+ { value: 335, name: 'analysis.directAccess' },
+ { value: 310, name: 'analysis.mailMarketing' },
+ { value: 234, name: 'analysis.allianceAdvertising' },
+ { value: 135, name: 'analysis.videoAdvertising' },
+ { value: 1548, name: 'analysis.searchEngines' }
+ ]
+ set(
+ pieOptionsData,
+ 'legend.data',
+ data.map((v) => t(v.name))
+ )
+ set(pieOptionsData, 'series.data', data)
+}
+const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
+
+// 鍛ㄦ椿璺冮噺
+const getWeeklyUserActivity = async () => {
+ const data = [
+ { value: 13253, name: 'analysis.monday' },
+ { value: 34235, name: 'analysis.tuesday' },
+ { value: 26321, name: 'analysis.wednesday' },
+ { value: 12340, name: 'analysis.thursday' },
+ { value: 24643, name: 'analysis.friday' },
+ { value: 1322, name: 'analysis.saturday' },
+ { value: 1324, name: 'analysis.sunday' }
+ ]
+ set(
+ barOptionsData,
+ 'xAxis.data',
+ data.map((v) => t(v.name))
+ )
+ set(barOptionsData, 'series', [
+ {
+ name: t('analysis.activeQuantity'),
+ data: data.map((v) => v.value),
+ type: 'bar'
+ }
+ ])
+}
+
+const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
+
+// 姣忔湀閿�鍞�婚
+const getMonthlySales = async () => {
+ const data = [
+ { estimate: 100, actual: 120, name: 'analysis.january' },
+ { estimate: 120, actual: 82, name: 'analysis.february' },
+ { estimate: 161, actual: 91, name: 'analysis.march' },
+ { estimate: 134, actual: 154, name: 'analysis.april' },
+ { estimate: 105, actual: 162, name: 'analysis.may' },
+ { estimate: 160, actual: 140, name: 'analysis.june' },
+ { estimate: 165, actual: 145, name: 'analysis.july' },
+ { estimate: 114, actual: 250, name: 'analysis.august' },
+ { estimate: 163, actual: 134, name: 'analysis.september' },
+ { estimate: 185, actual: 56, name: 'analysis.october' },
+ { estimate: 118, actual: 99, name: 'analysis.november' },
+ { estimate: 123, actual: 123, name: 'analysis.december' }
+ ]
+ set(
+ lineOptionsData,
+ 'xAxis.data',
+ data.map((v) => t(v.name))
+ )
+ set(lineOptionsData, 'series', [
+ {
+ name: t('analysis.estimate'),
+ smooth: true,
+ type: 'line',
+ data: data.map((v) => v.estimate),
+ animationDuration: 2800,
+ animationEasing: 'cubicInOut'
+ },
+ {
+ name: t('analysis.actual'),
+ smooth: true,
+ type: 'line',
+ itemStyle: {},
+ data: data.map((v) => v.actual),
+ animationDuration: 2800,
+ animationEasing: 'quadraticOut'
+ }
+ ])
+}
+
+const getAllApi = async () => {
+ await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
+ loading.value = false
+}
+
+getAllApi()
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-panel;
+
+.#{$prefix-cls} {
+ &__item {
+ &--peoples {
+ color: #40c9c6;
+ }
+
+ &--message {
+ color: #36a3f7;
+ }
+
+ &--money {
+ color: #f4516c;
+ }
+
+ &--shopping {
+ color: #34bfa3;
+ }
+
+ &:hover {
+ :deep(.#{$namespace}-icon) {
+ color: #fff !important;
+ }
+
+ .#{$prefix-cls}__item--icon {
+ transition: all 0.38s ease-out;
+ }
+
+ .#{$prefix-cls}__item--peoples {
+ background: #40c9c6;
+ }
+
+ .#{$prefix-cls}__item--message {
+ background: #36a3f7;
+ }
+
+ .#{$prefix-cls}__item--money {
+ background: #f4516c;
+ }
+
+ .#{$prefix-cls}__item--shopping {
+ background: #34bfa3;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/Home/echarts-data.ts b/src/views/Home/echarts-data.ts
new file mode 100644
index 0000000..56093f4
--- /dev/null
+++ b/src/views/Home/echarts-data.ts
@@ -0,0 +1,308 @@
+import { EChartsOption } from 'echarts'
+
+const { t } = useI18n()
+
+export const lineOptions: EChartsOption = {
+ title: {
+ text: t('analysis.monthlySales'),
+ left: 'center'
+ },
+ xAxis: {
+ data: [
+ t('analysis.january'),
+ t('analysis.february'),
+ t('analysis.march'),
+ t('analysis.april'),
+ t('analysis.may'),
+ t('analysis.june'),
+ t('analysis.july'),
+ t('analysis.august'),
+ t('analysis.september'),
+ t('analysis.october'),
+ t('analysis.november'),
+ t('analysis.december')
+ ],
+ boundaryGap: false,
+ axisTick: {
+ show: false
+ }
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ },
+ padding: [5, 10]
+ },
+ yAxis: {
+ axisTick: {
+ show: false
+ }
+ },
+ legend: {
+ data: [t('analysis.estimate'), t('analysis.actual')],
+ top: 50
+ },
+ series: [
+ {
+ name: t('analysis.estimate'),
+ smooth: true,
+ type: 'line',
+ data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
+ animationDuration: 2800,
+ animationEasing: 'cubicInOut'
+ },
+ {
+ name: t('analysis.actual'),
+ smooth: true,
+ type: 'line',
+ itemStyle: {},
+ data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
+ animationDuration: 2800,
+ animationEasing: 'quadraticOut'
+ }
+ ]
+}
+
+export const pieOptions: EChartsOption = {
+ title: {
+ text: t('analysis.userAccessSource'),
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left',
+ data: [
+ t('analysis.directAccess'),
+ t('analysis.mailMarketing'),
+ t('analysis.allianceAdvertising'),
+ t('analysis.videoAdvertising'),
+ t('analysis.searchEngines')
+ ]
+ },
+ series: [
+ {
+ name: t('analysis.userAccessSource'),
+ type: 'pie',
+ radius: '55%',
+ center: ['50%', '60%'],
+ data: [
+ { value: 335, name: t('analysis.directAccess') },
+ { value: 310, name: t('analysis.mailMarketing') },
+ { value: 234, name: t('analysis.allianceAdvertising') },
+ { value: 135, name: t('analysis.videoAdvertising') },
+ { value: 1548, name: t('analysis.searchEngines') }
+ ]
+ }
+ ]
+}
+
+export const barOptions: EChartsOption = {
+ title: {
+ text: t('analysis.weeklyUserActivity'),
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ grid: {
+ left: 50,
+ right: 20,
+ bottom: 20
+ },
+ xAxis: {
+ type: 'category',
+ data: [
+ t('analysis.monday'),
+ t('analysis.tuesday'),
+ t('analysis.wednesday'),
+ t('analysis.thursday'),
+ t('analysis.friday'),
+ t('analysis.saturday'),
+ t('analysis.sunday')
+ ],
+ axisTick: {
+ alignWithLabel: true
+ }
+ },
+ yAxis: {
+ type: 'value'
+ },
+ series: [
+ {
+ name: t('analysis.activeQuantity'),
+ data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
+ type: 'bar'
+ }
+ ]
+}
+
+export const radarOption: EChartsOption = {
+ legend: {
+ data: [t('workplace.personal'), t('workplace.team')]
+ },
+ radar: {
+ // shape: 'circle',
+ indicator: [
+ { name: t('workplace.quote'), max: 65 },
+ { name: t('workplace.contribution'), max: 160 },
+ { name: t('workplace.hot'), max: 300 },
+ { name: t('workplace.yield'), max: 130 },
+ { name: t('workplace.follow'), max: 100 }
+ ]
+ },
+ series: [
+ {
+ name: `xxx${t('workplace.index')}`,
+ type: 'radar',
+ data: [
+ {
+ value: [42, 30, 20, 35, 80],
+ name: t('workplace.personal')
+ },
+ {
+ value: [50, 140, 290, 100, 90],
+ name: t('workplace.team')
+ }
+ ]
+ }
+ ]
+}
+
+export const wordOptions = {
+ series: [
+ {
+ type: 'wordCloud',
+ gridSize: 2,
+ sizeRange: [12, 50],
+ rotationRange: [-90, 90],
+ shape: 'pentagon',
+ width: 600,
+ height: 400,
+ drawOutOfBound: true,
+ textStyle: {
+ color: function () {
+ return (
+ 'rgb(' +
+ [
+ Math.round(Math.random() * 160),
+ Math.round(Math.random() * 160),
+ Math.round(Math.random() * 160)
+ ].join(',') +
+ ')'
+ )
+ }
+ },
+ emphasis: {
+ textStyle: {
+ shadowBlur: 10,
+ shadowColor: '#333'
+ }
+ },
+ data: [
+ {
+ name: 'Sam S Club',
+ value: 10000,
+ textStyle: {
+ color: 'black'
+ },
+ emphasis: {
+ textStyle: {
+ color: 'red'
+ }
+ }
+ },
+ {
+ name: 'Macys',
+ value: 6181
+ },
+ {
+ name: 'Amy Schumer',
+ value: 4386
+ },
+ {
+ name: 'Jurassic World',
+ value: 4055
+ },
+ {
+ name: 'Charter Communications',
+ value: 2467
+ },
+ {
+ name: 'Chick Fil A',
+ value: 2244
+ },
+ {
+ name: 'Planet Fitness',
+ value: 1898
+ },
+ {
+ name: 'Pitch Perfect',
+ value: 1484
+ },
+ {
+ name: 'Express',
+ value: 1112
+ },
+ {
+ name: 'Home',
+ value: 965
+ },
+ {
+ name: 'Johnny Depp',
+ value: 847
+ },
+ {
+ name: 'Lena Dunham',
+ value: 582
+ },
+ {
+ name: 'Lewis Hamilton',
+ value: 555
+ },
+ {
+ name: 'KXAN',
+ value: 550
+ },
+ {
+ name: 'Mary Ellen Mark',
+ value: 462
+ },
+ {
+ name: 'Farrah Abraham',
+ value: 366
+ },
+ {
+ name: 'Rita Ora',
+ value: 360
+ },
+ {
+ name: 'Serena Williams',
+ value: 282
+ },
+ {
+ name: 'NCAA baseball tournament',
+ value: 273
+ },
+ {
+ name: 'Point Break',
+ value: 265
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/views/Home/types.ts b/src/views/Home/types.ts
new file mode 100644
index 0000000..956636a
--- /dev/null
+++ b/src/views/Home/types.ts
@@ -0,0 +1,57 @@
+export type WorkplaceTotal = {
+ project: number
+ access: number
+ todo: number
+}
+
+export type Project = {
+ name: string
+ icon: string
+ message: string
+ personal: string
+ time: Date | number | string
+ color: string
+}
+
+export type Notice = {
+ title: string
+ type: string
+ keys: string[]
+ date: Date | number | string
+}
+
+export type Shortcut = {
+ name: string
+ icon: string
+ url: string
+ color: string
+}
+
+export type RadarData = {
+ personal: number
+ team: number
+ max: number
+ name: string
+}
+export type AnalysisTotalTypes = {
+ users: number
+ messages: number
+ moneys: number
+ shoppings: number
+}
+
+export type UserAccessSource = {
+ value: number
+ name: string
+}
+
+export type WeeklyUserActivity = {
+ value: number
+ name: string
+}
+
+export type MonthlySales = {
+ name: string
+ estimate: number
+ actual: number
+}
diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue
new file mode 100644
index 0000000..30af14f
--- /dev/null
+++ b/src/views/Login/Login.vue
@@ -0,0 +1,121 @@
+<template>
+ <div
+ :class="prefixCls"
+ class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
+ >
+ <div class="relative mx-auto h-full flex">
+ <div
+ :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
+ >
+ <!-- 宸︿笂瑙掔殑 logo + 绯荤粺鏍囬 -->
+ <div class="relative flex items-center text-white">
+ <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+ <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+ </div>
+ <!-- 宸﹁竟鐨勮儗鏅浘 + 娆㈣繋璇� -->
+ <div class="h-[calc(100%-60px)] flex items-center justify-center">
+ <TransitionGroup
+ appear
+ enter-active-class="animate__animated animate__bounceInLeft"
+ tag="div"
+ >
+ <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
+ <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
+ <div key="3" class="mt-5 text-14px font-normal text-white">
+ {{ t('login.message') }}
+ </div>
+ </TransitionGroup>
+ </div>
+ </div>
+ <div
+ class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
+ >
+ <!-- 鍙充笂瑙掔殑涓婚銆佽瑷�閫夋嫨 -->
+ <div
+ class="flex items-center justify-between at-2xl:justify-end at-xl:justify-end"
+ style="color: var(--el-text-color-primary);"
+ >
+ <div class="flex items-center at-2xl:hidden at-xl:hidden">
+ <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+ <span class="text-20px font-bold" >{{ underlineToHump(appStore.getTitle) }}</span>
+ </div>
+ <div class="flex items-center justify-end space-x-10px h-48px">
+ <ThemeSwitch />
+ <LocaleDropdown />
+ </div>
+ </div>
+ <!-- 鍙宠竟鐨勭櫥褰曠晫闈� -->
+ <Transition appear enter-active-class="animate__animated animate__bounceInRight">
+ <div
+ class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
+ >
+ <!-- 璐﹀彿鐧诲綍 -->
+ <LoginForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 鎵嬫満鐧诲綍 -->
+ <MobileForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 浜岀淮鐮佺櫥褰� -->
+ <QrCodeForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 娉ㄥ唽 -->
+ <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 涓夋柟鐧诲綍 -->
+ <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ <!-- 蹇樿瀵嗙爜 -->
+ <ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+ </div>
+ </Transition>
+ </div>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { underlineToHump } from '@/utils'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import { useAppStore } from '@/store/modules/app'
+import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
+import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+
+import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
+
+defineOptions({ name: 'Login' })
+
+const { t } = useI18n()
+const appStore = useAppStore()
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('login')
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-login;
+
+.#{$prefix-cls} {
+ overflow: auto;
+
+ &__left {
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -1;
+ width: 100%;
+ height: 100%;
+ background-image: url('@/assets/svgs/login-bg.svg');
+ background-position: center;
+ background-repeat: no-repeat;
+ content: '';
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+.dark .login-form {
+ .el-divider__text {
+ background-color: var(--login-bg-color);
+ }
+
+ .el-card {
+ background-color: var(--login-bg-color);
+ }
+}
+</style>
diff --git a/src/views/Login/SocialLogin.vue b/src/views/Login/SocialLogin.vue
new file mode 100644
index 0000000..961f4dd
--- /dev/null
+++ b/src/views/Login/SocialLogin.vue
@@ -0,0 +1,347 @@
+<template>
+ <div
+ :class="prefixCls"
+ class="relative h-[100%] lt-md:px-10px lt-sm:px-10px lt-xl:px-10px lt-xl:px-10px"
+ >
+ <div class="relative mx-auto h-full flex">
+ <div
+ :class="`${prefixCls}__left flex-1 bg-gray-500 bg-opacity-20 relative p-30px lt-xl:hidden overflow-x-hidden overflow-y-auto`"
+ >
+ <!-- 宸︿笂瑙掔殑 logo + 绯荤粺鏍囬 -->
+ <div class="relative flex items-center text-white">
+ <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+ <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+ </div>
+ <!-- 宸﹁竟鐨勮儗鏅浘 + 娆㈣繋璇� -->
+ <div class="h-[calc(100%-60px)] flex items-center justify-center">
+ <TransitionGroup
+ appear
+ enter-active-class="animate__animated animate__bounceInLeft"
+ tag="div"
+ >
+ <img key="1" alt="" class="w-350px" src="@/assets/svgs/login-box-bg.svg" />
+ <div key="2" class="text-3xl text-white">{{ t('login.welcome') }}</div>
+ <div key="3" class="mt-5 text-14px font-normal text-white">
+ {{ t('login.message') }}
+ </div>
+ </TransitionGroup>
+ </div>
+ </div>
+ <div
+ class="relative flex-1 p-30px dark:bg-[var(--login-bg-color)] lt-sm:p-10px overflow-x-hidden overflow-y-auto"
+ >
+ <!-- 鍙充笂瑙掔殑涓婚銆佽瑷�閫夋嫨 -->
+ <div
+ class="flex items-center justify-between text-white at-2xl:justify-end at-xl:justify-end"
+ >
+ <div class="flex items-center at-2xl:hidden at-xl:hidden">
+ <img alt="" class="mr-10px h-48px w-48px" src="@/assets/imgs/logo.png" />
+ <span class="text-20px font-bold">{{ underlineToHump(appStore.getTitle) }}</span>
+ </div>
+ <div class="flex items-center justify-end space-x-10px h-48px">
+ <ThemeSwitch />
+ <LocaleDropdown class="dark:text-white lt-xl:text-white" />
+ </div>
+ </div>
+ <!-- 鍙宠竟鐨勭櫥褰曠晫闈� -->
+ <Transition appear enter-active-class="animate__animated animate__bounceInRight">
+ <div
+ class="m-auto h-[calc(100%-60px)] w-[100%] flex items-center at-2xl:max-w-500px at-lg:max-w-500px at-md:max-w-500px at-xl:max-w-500px"
+ >
+ <!-- 璐﹀彿鐧诲綍 -->
+ <el-form
+ v-show="getShow"
+ ref="formLogin"
+ :model="loginData.loginForm"
+ :rules="LoginRules"
+ class="login-form"
+ label-position="top"
+ label-width="120px"
+ size="large"
+ >
+ <el-row style="margin-right: -10px; margin-left: -10px">
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <el-form-item>
+ <LoginFormTitle style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <el-form-item v-if="loginData.tenantEnable" prop="tenantName">
+ <el-input
+ v-model="loginData.loginForm.tenantName"
+ :placeholder="t('login.tenantNamePlaceholder')"
+ :prefix-icon="iconHouse"
+ link
+ type="primary"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <el-form-item prop="username">
+ <el-input
+ v-model="loginData.loginForm.username"
+ :placeholder="t('login.usernamePlaceholder')"
+ :prefix-icon="iconAvatar"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <el-form-item prop="password">
+ <el-input
+ v-model="loginData.loginForm.password"
+ :placeholder="t('login.passwordPlaceholder')"
+ :prefix-icon="iconLock"
+ show-password
+ type="password"
+ @keyup.enter="getCode()"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col
+ :span="24"
+ style="
+ padding-right: 10px;
+ padding-left: 10px;
+ margin-top: -20px;
+ margin-bottom: -20px;
+ "
+ >
+ <el-form-item>
+ <el-row justify="space-between" style="width: 100%">
+ <el-col :span="6">
+ <el-checkbox v-model="loginData.loginForm.rememberMe">
+ {{ t('login.remember') }}
+ </el-checkbox>
+ </el-col>
+ <el-col :offset="6" :span="12">
+ <el-link style="float: right" type="primary"
+ >{{ t('login.forgetPassword') }}
+ </el-link>
+ </el-col>
+ </el-row>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.login')"
+ class="w-[100%]"
+ type="primary"
+ @click="getCode()"
+ />
+ </el-form-item>
+ </el-col>
+ <Verify
+ v-if="loginData.captchaEnable === 'true'"
+ ref="verify"
+ :captchaType="captchaType"
+ :imgSize="{ width: '400px', height: '200px' }"
+ mode="pop"
+ @success="handleLogin"
+ />
+ </el-row>
+ </el-form>
+ </div>
+ </Transition>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { underlineToHump } from '@/utils'
+
+import { ElLoading } from 'element-plus'
+
+import { useDesign } from '@/hooks/web/useDesign'
+import { useAppStore } from '@/store/modules/app'
+import { useIcon } from '@/hooks/web/useIcon'
+import { usePermissionStore } from '@/store/modules/permission'
+
+import * as LoginApi from '@/api/login'
+import * as authUtil from '@/utils/auth'
+import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
+import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
+import { LoginStateEnum, useFormValid, useLoginState } from './components/useLogin'
+import LoginFormTitle from './components/LoginFormTitle.vue'
+import router from '@/router'
+
+defineOptions({ name: 'SocialLogin' })
+
+const { t } = useI18n()
+const route = useRoute()
+
+const appStore = useAppStore()
+const { getPrefixCls } = useDesign()
+const prefixCls = getPrefixCls('login')
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconAvatar = useIcon({ icon: 'ep:avatar' })
+const iconLock = useIcon({ icon: 'ep:lock' })
+const formLogin = ref<any>()
+const { validForm } = useFormValid(formLogin)
+const { getLoginState } = useLoginState()
+const { push } = useRouter()
+const permissionStore = usePermissionStore()
+const loginLoading = ref(false)
+const verify = ref()
+const captchaType = ref('blockPuzzle') // blockPuzzle 婊戝潡 clickWord 鐐瑰嚮鏂囧瓧 pictureWord 鏂囧瓧楠岃瘉鐮�
+
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
+
+const LoginRules = {
+ tenantName: [required],
+ username: [required],
+ password: [required]
+}
+const loginData = reactive({
+ isShowPassword: false,
+ captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
+ tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
+ loginForm: {
+ tenantName: '鑺嬮亾婧愮爜',
+ username: 'admin',
+ password: 'admin123',
+ captchaVerification: '',
+ rememberMe: false
+ }
+})
+
+// 鑾峰彇楠岃瘉鐮�
+const getCode = async () => {
+ // 鎯呭喌涓�锛屾湭寮�鍚細鍒欑洿鎺ョ櫥褰�
+ if (!loginData.captchaEnable) {
+ await handleLogin({})
+ } else {
+ // 鎯呭喌浜岋紝宸插紑鍚細鍒欏睍绀洪獙璇佺爜锛涘彧鏈夊畬鎴愰獙璇佺爜鐨勬儏鍐碉紝鎵嶈繘琛岀櫥褰�
+ // 寮瑰嚭楠岃瘉鐮�
+ verify.value.show()
+ }
+}
+//鑾峰彇绉熸埛ID
+const getTenantId = async () => {
+ if (loginData.tenantEnable) {
+ const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
+ authUtil.setTenantId(res)
+ }
+}
+// 璁颁綇鎴�
+const getCookie = () => {
+ const loginForm = authUtil.getLoginForm()
+ if (loginForm) {
+ loginData.loginForm = {
+ ...loginData.loginForm,
+ username: loginForm.username ? loginForm.username : loginData.loginForm.username,
+ password: loginForm.password ? loginForm.password : loginData.loginForm.password,
+ rememberMe: loginForm.rememberMe ? true : false,
+ tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
+ }
+ }
+}
+const loading = ref() // ElLoading.service 杩斿洖鐨勫疄渚�
+
+// tricky: 閰嶅悎LoginForm.vue涓璻edirectUri闇�瑕佸鍙傛暟杩涜encode锛岄渶瑕佸湪鍥炶皟鍚庤繘琛宒ecode
+function getUrlValue(key: string): string {
+ const url = new URL(decodeURIComponent(location.href))
+ return url.searchParams.get(key) ?? ''
+}
+
+// 灏濊瘯鐧诲綍: 褰撹处鍙峰凡缁忕粦瀹氾紝socialLogin浼氱洿鎺ヨ幏寰梩oken
+const tryLogin = async () => {
+ try {
+ const type = getUrlValue('type')
+ const redirect = getUrlValue('redirect')
+ const code = route?.query?.code as string
+ const state = route?.query?.state as string
+
+ const res = await LoginApi.socialLogin(type, code, state)
+ authUtil.setToken(res)
+
+ router.push({ path: redirect || '/' })
+ } catch (err) {}
+}
+
+// 鐧诲綍
+const handleLogin = async (params) => {
+ loginLoading.value = true
+ try {
+ await getTenantId()
+ const data = await validForm()
+ if (!data) {
+ return
+ }
+
+ let redirect = getUrlValue('redirect')
+
+ const type = getUrlValue('type')
+ const code = route?.query?.code as string
+ const state = route?.query?.state as string
+
+ const loginDataLoginForm = { ...loginData.loginForm }
+ const res = await LoginApi.login({
+ // 璐﹀彿瀵嗙爜鐧诲綍
+ username: loginDataLoginForm.username,
+ password: loginDataLoginForm.password,
+ captchaVerification: params.captchaVerification,
+ // 绀句氦鐧诲綍
+ socialCode: code,
+ socialState: state,
+ socialType: type
+ })
+ if (!res) {
+ return
+ }
+ loading.value = ElLoading.service({
+ lock: true,
+ text: '姝e湪鍔犺浇绯荤粺涓�...',
+ background: 'rgba(0, 0, 0, 0.7)'
+ })
+ if (loginDataLoginForm.rememberMe) {
+ authUtil.setLoginForm(loginDataLoginForm)
+ } else {
+ authUtil.removeLoginForm()
+ }
+ authUtil.setToken(res)
+ if (!redirect) {
+ redirect = '/'
+ }
+ // 鍒ゆ柇鏄惁涓篠SO鐧诲綍
+ if (redirect.indexOf('sso') !== -1) {
+ window.location.href = window.location.href.replace('/login?redirect=', '')
+ } else {
+ push({ path: redirect || permissionStore.addRouters[0].path })
+ }
+ } finally {
+ loginLoading.value = false
+ loading.value.close()
+ }
+}
+
+onMounted(() => {
+ getCookie()
+ tryLogin()
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-login;
+
+.#{$prefix-cls} {
+ overflow: auto;
+
+ &__left {
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: -1;
+ width: 100%;
+ height: 100%;
+ background-image: url('@/assets/svgs/login-bg.svg');
+ background-position: center;
+ background-repeat: no-repeat;
+ content: '';
+ }
+ }
+}
+</style>
diff --git a/src/views/Login/components/ForgetPasswordForm.vue b/src/views/Login/components/ForgetPasswordForm.vue
new file mode 100644
index 0000000..f47b229
--- /dev/null
+++ b/src/views/Login/components/ForgetPasswordForm.vue
@@ -0,0 +1,278 @@
+<template>
+ <el-form
+ v-show="getShow"
+ ref="formSmsResetPassword"
+ :model="resetPasswordData"
+ :rules="rules"
+ class="login-form"
+ label-position="top"
+ label-width="120px"
+ size="large"
+ >
+ <el-row class="mx-[-10px]">
+ <!-- 绉熸埛鍚� -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <LoginFormTitle class="w-full" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
+ <el-input
+ v-model="resetPasswordData.tenantName"
+ :placeholder="t('login.tenantNamePlaceholder')"
+ :prefix-icon="iconHouse"
+ type="primary"
+ link
+ />
+ </el-form-item>
+ </el-col>
+ <!-- 鎵嬫満鍙� -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="mobile">
+ <el-input
+ v-model="resetPasswordData.mobile"
+ :placeholder="t('login.mobileNumberPlaceholder')"
+ :prefix-icon="iconCellphone"
+ />
+ </el-form-item>
+ </el-col>
+ <Verify
+ ref="verify"
+ :captchaType="captchaType"
+ :imgSize="{ width: '400px', height: '200px' }"
+ mode="pop"
+ @success="getSmsCode"
+ />
+ <!-- 楠岃瘉鐮� -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="code">
+ <el-row :gutter="5" justify="space-between" style="width: 100%">
+ <el-col :span="24">
+ <el-input
+ v-model="resetPasswordData.code"
+ :placeholder="t('login.codePlaceholder')"
+ :prefix-icon="iconCircleCheck"
+ >
+ <template #append>
+ <span
+ v-if="mobileCodeTimer <= 0"
+ class="getMobileCode"
+ style="cursor: pointer"
+ @click="getCode"
+ >
+ {{ t('login.getSmsCode') }}
+ </span>
+ <span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
+ {{ mobileCodeTimer }}绉掑悗鍙噸鏂拌幏鍙�
+ </span>
+ </template>
+ </el-input>
+ <!-- </el-button> -->
+ </el-col>
+ </el-row>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="password">
+ <InputPassword
+ v-model="resetPasswordData.password"
+ :placeholder="t('login.passwordPlaceholder')"
+ class="w-full"
+ :strength="true"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="check_password">
+ <InputPassword
+ v-model="resetPasswordData.check_password"
+ :placeholder="t('login.checkPassword')"
+ class="w-full"
+ :strength="true"
+ />
+ </el-form-item>
+ </el-col>
+ <!-- 鐧诲綍鎸夐挳 / 杩斿洖鎸夐挳 -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.resetPassword')"
+ class="w-full"
+ type="primary"
+ @click="resetPassword()"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.backLogin')"
+ class="w-full"
+ @click="handleBackLogin()"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import { sendSmsCode, smsResetPassword } from '@/api/login'
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+import { ElLoading } from 'element-plus'
+import * as authUtil from '@/utils/auth'
+import * as LoginApi from '@/api/login'
+defineOptions({ name: 'ForgetPasswordForm' })
+const verify = ref()
+
+const { t } = useI18n()
+const message = useMessage()
+const { currentRoute } = useRouter()
+const formSmsResetPassword = ref()
+const loginLoading = ref(false)
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconCellphone = useIcon({ icon: 'ep:cellphone' })
+const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
+const { validForm } = useFormValid(formSmsResetPassword)
+const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
+const captchaType = ref('blockPuzzle') // blockPuzzle 婊戝潡 clickWord 鐐瑰嚮鏂囧瓧 pictureWord 鏂囧瓧楠岃瘉鐮�
+
+const validatePass2 = (_rule, value, callback) => {
+ if (value === '') {
+ callback(new Error('璇峰啀娆¤緭鍏ュ瘑鐮�'))
+ } else if (value !== resetPasswordData.password) {
+ callback(new Error('涓ゆ杈撳叆瀵嗙爜涓嶄竴鑷�!'))
+ } else {
+ callback()
+ }
+}
+
+const rules = {
+ tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '闀垮害涓�4鍒�16浣�' }],
+ mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '鎵嬫満鍙烽暱搴︿负11浣�' }],
+ password: [
+ {
+ required: true,
+ min: 4,
+ max: 16,
+ validator: validatePass2,
+ trigger: 'blur',
+ message: '瀵嗙爜闀垮害涓�4鍒�16浣�'
+ }
+ ],
+ check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
+ code: [required]
+}
+
+const resetPasswordData = reactive({
+ captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+ tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+ tenantName: '',
+ username: '',
+ password: '',
+ check_password: '',
+ mobile: '',
+ code: ''
+})
+
+const smsVO = reactive({
+ tenantName: '',
+ mobile: '',
+ captchaVerification: '',
+ scene: 23
+})
+const mobileCodeTimer = ref(0)
+const redirect = ref<string>('')
+
+// 鑾峰彇楠岃瘉鐮�
+const getCode = async () => {
+ // 鎯呭喌涓�锛屾湭寮�鍚細鍒欑洿鎺ュ彂閫侀獙璇佺爜
+ if (resetPasswordData.captchaEnable === 'false') {
+ await getSmsCode({})
+ } else {
+ // 鎯呭喌浜岋紝宸插紑鍚細鍒欏睍绀洪獙璇佺爜锛涘彧鏈夊畬鎴愰獙璇佺爜鐨勬儏鍐碉紝鎵嶈繘琛屽彂閫侀獙璇佺爜
+ // 寮瑰嚭楠岃瘉鐮�
+ verify.value.show()
+ }
+}
+
+const getSmsCode = async (params) => {
+ if (resetPasswordData.tenantEnable === 'true') {
+ await getTenantId()
+ }
+ smsVO.captchaVerification = params.captchaVerification
+ smsVO.mobile = resetPasswordData.mobile
+ await sendSmsCode(smsVO).then(async () => {
+ message.success(t('login.SmsSendMsg'))
+ // 璁剧疆鍊掕鏃�
+ mobileCodeTimer.value = 60
+ let msgTimer = setInterval(() => {
+ mobileCodeTimer.value = mobileCodeTimer.value - 1
+ if (mobileCodeTimer.value <= 0) {
+ clearInterval(msgTimer)
+ }
+ }, 1000)
+ })
+}
+watch(
+ () => currentRoute.value,
+ (route: RouteLocationNormalizedLoaded) => {
+ redirect.value = route?.query?.redirect as string
+ },
+ {
+ immediate: true
+ }
+)
+
+const getTenantId = async () => {
+ if (resetPasswordData.tenantEnable === 'true') {
+ const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
+ if (res == null) {
+ message.error(t('login.invalidTenantName'))
+ throw t('login.invalidTenantName')
+ }
+ authUtil.setTenantId(res)
+ }
+}
+
+// 閲嶇疆瀵嗙爜
+const resetPassword = async () => {
+ const data = await validForm()
+ if (!data) return
+ await getTenantId()
+ loginLoading.value = true
+ await smsResetPassword(resetPasswordData)
+ .then(async () => {
+ message.success(t('login.resetPasswordSuccess'))
+ setLoginState(LoginStateEnum.LOGIN)
+ })
+ .catch(() => {})
+ .finally(() => {
+ loginLoading.value = false
+ setTimeout(() => {
+ const loadingInstance = ElLoading.service()
+ loadingInstance.close()
+ }, 400)
+ })
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+ &:hover {
+ color: var(--el-color-primary) !important;
+ }
+}
+
+.smsbtn {
+ margin-top: 33px;
+}
+</style>
diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue
new file mode 100644
index 0000000..1bb5173
--- /dev/null
+++ b/src/views/Login/components/LoginForm.vue
@@ -0,0 +1,360 @@
+<template>
+ <el-form
+ v-show="getShow"
+ ref="formLogin"
+ :model="loginData.loginForm"
+ :rules="LoginRules"
+ class="login-form"
+ label-position="top"
+ label-width="120px"
+ size="large"
+ >
+ <el-row class="mx-[-10px]">
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <LoginFormTitle class="w-full" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
+ <el-input
+ v-model="loginData.loginForm.tenantName"
+ :placeholder="t('login.tenantNamePlaceholder')"
+ :prefix-icon="iconHouse"
+ link
+ type="primary"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="username">
+ <el-input
+ v-model="loginData.loginForm.username"
+ :placeholder="t('login.usernamePlaceholder')"
+ :prefix-icon="iconAvatar"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="password">
+ <el-input
+ v-model="loginData.loginForm.password"
+ :placeholder="t('login.passwordPlaceholder')"
+ :prefix-icon="iconLock"
+ show-password
+ type="password"
+ @keyup.enter="getCode()"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px mt-[-20px] mb-[-20px]">
+ <el-form-item>
+ <el-row justify="space-between" style="width: 100%">
+ <el-col :span="6">
+ <el-checkbox v-model="loginData.loginForm.rememberMe">
+ {{ t('login.remember') }}
+ </el-checkbox>
+ </el-col>
+ <el-col :offset="6" :span="12">
+ <el-link
+ class="float-right"
+ type="primary"
+ @click="setLoginState(LoginStateEnum.RESET_PASSWORD)"
+ >
+ {{ t('login.forgetPassword') }}
+ </el-link>
+ </el-col>
+ </el-row>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.login')"
+ class="w-full"
+ type="primary"
+ @click="getCode()"
+ />
+ </el-form-item>
+ </el-col>
+ <Verify
+ v-if="loginData.captchaEnable === 'true'"
+ ref="verify"
+ :captchaType="captchaType"
+ :imgSize="{ width: '400px', height: '200px' }"
+ mode="pop"
+ @success="handleLogin"
+ />
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <el-row :gutter="5" justify="space-between" style="width: 100%">
+ <el-col :span="8">
+ <XButton
+ :title="t('login.btnMobile')"
+ class="w-full"
+ @click="setLoginState(LoginStateEnum.MOBILE)"
+ />
+ </el-col>
+ <el-col :span="8">
+ <XButton
+ :title="t('login.btnQRCode')"
+ class="w-full"
+ @click="setLoginState(LoginStateEnum.QR_CODE)"
+ />
+ </el-col>
+ <el-col :span="8">
+ <XButton
+ :title="t('login.btnRegister')"
+ class="w-full"
+ @click="setLoginState(LoginStateEnum.REGISTER)"
+ />
+ </el-col>
+ </el-row>
+ </el-form-item>
+ </el-col>
+ <el-divider content-position="center">{{ t('login.otherLogin') }}</el-divider>
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <div class="w-full flex justify-between">
+ <Icon
+ v-for="(item, key) in socialList"
+ :key="key"
+ :icon="item.icon"
+ :size="30"
+ class="anticon cursor-pointer"
+ color="#999"
+ @click="doSocialLogin(item.type)"
+ />
+ </div>
+ </el-form-item>
+ </el-col>
+ <el-divider content-position="center">钀屾柊蹇呰</el-divider>
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <div class="w-full flex justify-between">
+ <el-link href="https://doc.iocoder.cn/" target="_blank">馃摎寮�鍙戞寚鍗�</el-link>
+ <el-link href="https://doc.iocoder.cn/video/" target="_blank">馃敟瑙嗛鏁欑▼</el-link>
+ <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
+ 鈿¢潰璇曟墜鍐�
+ </el-link>
+ <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
+ 馃澶栧寘鍜ㄨ
+ </el-link>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import { ElLoading } from 'element-plus'
+import LoginFormTitle from './LoginFormTitle.vue'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import * as authUtil from '@/utils/auth'
+import { usePermissionStore } from '@/store/modules/permission'
+import * as LoginApi from '@/api/login'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+
+defineOptions({ name: 'LoginForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconAvatar = useIcon({ icon: 'ep:avatar' })
+const iconLock = useIcon({ icon: 'ep:lock' })
+const formLogin = ref()
+const { validForm } = useFormValid(formLogin)
+const { setLoginState, getLoginState } = useLoginState()
+const { currentRoute, push } = useRouter()
+const permissionStore = usePermissionStore()
+const redirect = ref<string>('')
+const loginLoading = ref(false)
+const verify = ref()
+const captchaType = ref('blockPuzzle') // blockPuzzle 婊戝潡 clickWord 鐐瑰嚮鏂囧瓧 pictureWord 鏂囧瓧楠岃瘉鐮�
+
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN)
+
+const LoginRules = {
+ tenantName: [required],
+ username: [required],
+ password: [required]
+}
+const loginData = reactive({
+ isShowPassword: false,
+ captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+ tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+ loginForm: {
+ tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
+ username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
+ password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
+ captchaVerification: '',
+ rememberMe: true // 榛樿璁板綍鎴戙�傚鏋滀笉闇�瑕侊紝鍙墜鍔ㄤ慨鏀�
+ }
+})
+
+const socialList = [
+ { icon: 'ant-design:wechat-filled', type: 30 },
+ { icon: 'ant-design:dingtalk-circle-filled', type: 20 },
+ { icon: 'ant-design:github-filled', type: 0 },
+ { icon: 'ant-design:alipay-circle-filled', type: 0 }
+]
+
+// 鑾峰彇楠岃瘉鐮�
+const getCode = async () => {
+ // 鎯呭喌涓�锛屾湭寮�鍚細鍒欑洿鎺ョ櫥褰�
+ if (loginData.captchaEnable === 'false') {
+ await handleLogin({})
+ } else {
+ // 鎯呭喌浜岋紝宸插紑鍚細鍒欏睍绀洪獙璇佺爜锛涘彧鏈夊畬鎴愰獙璇佺爜鐨勬儏鍐碉紝鎵嶈繘琛岀櫥褰�
+ // 寮瑰嚭楠岃瘉鐮�
+ verify.value.show()
+ }
+}
+// 鑾峰彇绉熸埛 ID
+const getTenantId = async () => {
+ if (loginData.tenantEnable === 'true') {
+ const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
+ authUtil.setTenantId(res)
+ }
+}
+// 璁颁綇鎴�
+const getLoginFormCache = () => {
+ const loginForm = authUtil.getLoginForm()
+ if (loginForm) {
+ loginData.loginForm = {
+ ...loginData.loginForm,
+ username: loginForm.username ? loginForm.username : loginData.loginForm.username,
+ password: loginForm.password ? loginForm.password : loginData.loginForm.password,
+ rememberMe: loginForm.rememberMe,
+ tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
+ }
+ }
+}
+// 鏍规嵁鍩熷悕锛岃幏寰楃鎴蜂俊鎭�
+const getTenantByWebsite = async () => {
+ if (loginData.tenantEnable === 'true') {
+ const website = location.host
+ const res = await LoginApi.getTenantByWebsite(website)
+ if (res) {
+ loginData.loginForm.tenantName = res.name
+ authUtil.setTenantId(res.id)
+ }
+ }
+}
+const loading = ref() // ElLoading.service 杩斿洖鐨勫疄渚�
+// 鐧诲綍
+const handleLogin = async (params: any) => {
+ loginLoading.value = true
+ try {
+ await getTenantId()
+ const data = await validForm()
+ if (!data) {
+ return
+ }
+ const loginDataLoginForm = { ...loginData.loginForm }
+ loginDataLoginForm.captchaVerification = params.captchaVerification
+ const res = await LoginApi.login(loginDataLoginForm)
+ if (!res) {
+ return
+ }
+ loading.value = ElLoading.service({
+ lock: true,
+ text: '姝e湪鍔犺浇绯荤粺涓�...',
+ background: 'rgba(0, 0, 0, 0.7)'
+ })
+ if (loginDataLoginForm.rememberMe) {
+ authUtil.setLoginForm(loginDataLoginForm)
+ } else {
+ authUtil.removeLoginForm()
+ }
+ authUtil.setToken(res)
+ if (!redirect.value) {
+ redirect.value = '/'
+ }
+ // 鍒ゆ柇鏄惁涓篠SO鐧诲綍
+ if (redirect.value.indexOf('sso') !== -1) {
+ window.location.href = window.location.href.replace('/login?redirect=', '')
+ } else {
+ await push({ path: redirect.value || permissionStore.addRouters[0].path })
+ }
+ } finally {
+ loginLoading.value = false
+ loading.value.close()
+ }
+}
+
+// 绀句氦鐧诲綍
+const doSocialLogin = async (type: number) => {
+ if (type === 0) {
+ message.error('姝ゆ柟寮忔湭閰嶇疆')
+ } else {
+ loginLoading.value = true
+ if (loginData.tenantEnable === 'true') {
+ // 灏濊瘯鍏堥�氳繃 tenantName 鑾峰彇绉熸埛
+ await getTenantId()
+ // 濡傛灉鑾峰彇涓嶅埌锛屽垯闇�瑕佸脊鍑烘彁绀猴紝杩涜澶勭悊
+ if (!authUtil.getTenantId()) {
+ try {
+ const data = await message.prompt('璇疯緭鍏ョ鎴峰悕绉�', t('common.reminder'))
+ if (data?.action !== 'confirm') throw 'cancel'
+ const res = await LoginApi.getTenantIdByName(data.value)
+ authUtil.setTenantId(res)
+ } catch (error) {
+ if (error === 'cancel') return
+ } finally {
+ loginLoading.value = false
+ }
+ }
+ }
+ // 璁$畻 redirectUri
+ // 娉ㄦ剰: type銆乺edirect 闇�瑕佸厛 encode 涓�娆★紝鍚﹀垯閽夐拤鍥炶皟浼氫涪澶便��
+ // 閰嶅悎 social-login.vue#getUrlValue() 浣跨敤
+ const redirectUri =
+ location.origin +
+ '/social-login?' +
+ encodeURIComponent(`type=${type}&redirect=${redirect.value || '/'}`)
+
+ // 杩涜璺宠浆
+ window.location.href = await LoginApi.socialAuthRedirect(type, encodeURIComponent(redirectUri))
+ }
+}
+watch(
+ () => currentRoute.value,
+ (route: RouteLocationNormalizedLoaded) => {
+ redirect.value = route?.query?.redirect as string
+ },
+ {
+ immediate: true
+ }
+)
+onMounted(() => {
+ getLoginFormCache()
+ getTenantByWebsite()
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+ &:hover {
+ color: var(--el-color-primary) !important;
+ }
+}
+
+.login-code {
+ float: right;
+ width: 100%;
+ height: 38px;
+
+ img {
+ width: 100%;
+ height: auto;
+ max-width: 100px;
+ vertical-align: middle;
+ cursor: pointer;
+ }
+}
+</style>
diff --git a/src/views/Login/components/LoginFormTitle.vue b/src/views/Login/components/LoginFormTitle.vue
new file mode 100644
index 0000000..cdf4fac
--- /dev/null
+++ b/src/views/Login/components/LoginFormTitle.vue
@@ -0,0 +1,26 @@
+<template>
+ <h2 class="enter-x mb-3 text-center text-2xl font-bold xl:text-center xl:text-3xl">
+ {{ getFormTitle }}
+ </h2>
+</template>
+<script lang="ts" setup>
+import { LoginStateEnum, useLoginState } from './useLogin'
+
+defineOptions({ name: 'LoginFormTitle' })
+
+const { t } = useI18n()
+
+const { getLoginState } = useLoginState()
+
+const getFormTitle = computed(() => {
+ const titleObj = {
+ [LoginStateEnum.RESET_PASSWORD]: t('sys.login.forgetFormTitle'),
+ [LoginStateEnum.LOGIN]: t('sys.login.signInFormTitle'),
+ [LoginStateEnum.REGISTER]: t('sys.login.signUpFormTitle'),
+ [LoginStateEnum.MOBILE]: t('sys.login.mobileSignInFormTitle'),
+ [LoginStateEnum.QR_CODE]: t('sys.login.qrSignInFormTitle'),
+ [LoginStateEnum.SSO]: t('sys.login.ssoFormTitle')
+ }
+ return titleObj[unref(getLoginState)]
+})
+</script>
diff --git a/src/views/Login/components/MobileForm.vue b/src/views/Login/components/MobileForm.vue
new file mode 100644
index 0000000..bb4f1a6
--- /dev/null
+++ b/src/views/Login/components/MobileForm.vue
@@ -0,0 +1,226 @@
+<template>
+ <el-form
+ v-show="getShow"
+ ref="formSmsLogin"
+ :model="loginData.loginForm"
+ :rules="rules"
+ class="login-form"
+ label-position="top"
+ label-width="120px"
+ size="large"
+ >
+ <el-row class="mx-[-10px]">
+ <!-- 绉熸埛鍚� -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <LoginFormTitle class="w-full" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
+ <el-input
+ v-model="loginData.loginForm.tenantName"
+ :placeholder="t('login.tenantNamePlaceholder')"
+ :prefix-icon="iconHouse"
+ type="primary"
+ link
+ />
+ </el-form-item>
+ </el-col>
+ <!-- 鎵嬫満鍙� -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="mobileNumber">
+ <el-input
+ v-model="loginData.loginForm.mobileNumber"
+ :placeholder="t('login.mobileNumberPlaceholder')"
+ :prefix-icon="iconCellphone"
+ />
+ </el-form-item>
+ </el-col>
+ <!-- 楠岃瘉鐮� -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="code">
+ <el-row :gutter="5" justify="space-between" style="width: 100%">
+ <el-col :span="24">
+ <el-input
+ v-model="loginData.loginForm.code"
+ :placeholder="t('login.codePlaceholder')"
+ :prefix-icon="iconCircleCheck"
+ >
+ <!-- <el-button class="w-[100%]"> -->
+ <template #append>
+ <span
+ v-if="mobileCodeTimer <= 0"
+ class="getMobileCode"
+ style="cursor: pointer"
+ @click="getSmsCode"
+ >
+ {{ t('login.getSmsCode') }}
+ </span>
+ <span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
+ {{ mobileCodeTimer }}绉掑悗鍙噸鏂拌幏鍙�
+ </span>
+ </template>
+ </el-input>
+ <!-- </el-button> -->
+ </el-col>
+ </el-row>
+ </el-form-item>
+ </el-col>
+ <!-- 鐧诲綍鎸夐挳 / 杩斿洖鎸夐挳 -->
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.login')"
+ class="w-full"
+ type="primary"
+ @click="signIn()"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.backLogin')"
+ class="w-full"
+ @click="handleBackLogin()"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import { setTenantId, setToken } from '@/utils/auth'
+import { usePermissionStore } from '@/store/modules/permission'
+import { getTenantIdByName, sendSmsCode, smsLogin } from '@/api/login'
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+import { ElLoading } from 'element-plus'
+
+defineOptions({ name: 'MobileForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+const permissionStore = usePermissionStore()
+const { currentRoute, push } = useRouter()
+const formSmsLogin = ref()
+const loginLoading = ref(false)
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconCellphone = useIcon({ icon: 'ep:cellphone' })
+const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
+const { validForm } = useFormValid(formSmsLogin)
+const { handleBackLogin, getLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.MOBILE)
+
+const rules = {
+ tenantName: [required],
+ mobileNumber: [required],
+ code: [required]
+}
+const loginData = reactive({
+ codeImg: '',
+ tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+ token: '',
+ loading: {
+ signIn: false
+ },
+ loginForm: {
+ uuid: '',
+ tenantName: '鑺嬮亾婧愮爜',
+ mobileNumber: '',
+ code: ''
+ }
+})
+const smsVO = reactive({
+ smsCode: {
+ mobile: '',
+ scene: 21
+ },
+ loginSms: {
+ mobile: '',
+ code: ''
+ }
+})
+const mobileCodeTimer = ref(0)
+const redirect = ref<string>('')
+const getSmsCode = async () => {
+ await getTenantId()
+ smsVO.smsCode.mobile = loginData.loginForm.mobileNumber
+ await sendSmsCode(smsVO.smsCode).then(async () => {
+ message.success(t('login.SmsSendMsg'))
+ // 璁剧疆鍊掕鏃�
+ mobileCodeTimer.value = 60
+ let msgTimer = setInterval(() => {
+ mobileCodeTimer.value = mobileCodeTimer.value - 1
+ if (mobileCodeTimer.value <= 0) {
+ clearInterval(msgTimer)
+ }
+ }, 1000)
+ })
+}
+watch(
+ () => currentRoute.value,
+ (route: RouteLocationNormalizedLoaded) => {
+ redirect.value = route?.query?.redirect as string
+ },
+ {
+ immediate: true
+ }
+)
+// 鑾峰彇绉熸埛 ID
+const getTenantId = async () => {
+ if (loginData.tenantEnable === 'true') {
+ const res = await getTenantIdByName(loginData.loginForm.tenantName)
+ setTenantId(res)
+ }
+}
+// 鐧诲綍
+const signIn = async () => {
+ await getTenantId()
+ const data = await validForm()
+ if (!data) return
+ ElLoading.service({
+ lock: true,
+ text: '姝e湪鍔犺浇绯荤粺涓�...',
+ background: 'rgba(0, 0, 0, 0.7)'
+ })
+ loginLoading.value = true
+ smsVO.loginSms.mobile = loginData.loginForm.mobileNumber
+ smsVO.loginSms.code = loginData.loginForm.code
+ await smsLogin(smsVO.loginSms)
+ .then(async (res) => {
+ setToken(res)
+ if (!redirect.value) {
+ redirect.value = '/'
+ }
+ push({ path: redirect.value || permissionStore.addRouters[0].path })
+ })
+ .catch(() => {})
+ .finally(() => {
+ loginLoading.value = false
+ setTimeout(() => {
+ const loadingInstance = ElLoading.service()
+ loadingInstance.close()
+ }, 400)
+ })
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+ &:hover {
+ color: var(--el-color-primary) !important;
+ }
+}
+
+.smsbtn {
+ margin-top: 33px;
+}
+</style>
diff --git a/src/views/Login/components/QrCodeForm.vue b/src/views/Login/components/QrCodeForm.vue
new file mode 100644
index 0000000..601052d
--- /dev/null
+++ b/src/views/Login/components/QrCodeForm.vue
@@ -0,0 +1,30 @@
+<template>
+ <el-row v-show="getShow" class="login-form mx-[-10px]">
+ <el-col :span="24" class="px-10px">
+ <LoginFormTitle class="w-full" />
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-card class="mb-10px text-center" shadow="hover">
+ <Qrcode :logo="logoImg" />
+ </el-card>
+ </el-col>
+ <el-divider class="enter-x">{{ t('login.qrcode') }}</el-divider>
+ <el-col :span="24" class="px-10px">
+ <div class="mt-4 w-full">
+ <XButton :title="t('login.backLogin')" class="w-full" @click="handleBackLogin()" />
+ </div>
+ </el-col>
+ </el-row>
+</template>
+<script lang="ts" setup>
+import logoImg from '@/assets/imgs/logo.png'
+
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useLoginState } from './useLogin'
+
+defineOptions({ name: 'QrCodeForm' })
+
+const { t } = useI18n()
+const { handleBackLogin, getLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.QR_CODE)
+</script>
diff --git a/src/views/Login/components/RegisterForm.vue b/src/views/Login/components/RegisterForm.vue
new file mode 100644
index 0000000..514dd0d
--- /dev/null
+++ b/src/views/Login/components/RegisterForm.vue
@@ -0,0 +1,288 @@
+<template>
+ <el-form
+ v-show="getShow"
+ ref="formLogin"
+ :model="registerData.registerForm"
+ :rules="registerRules"
+ class="login-form"
+ label-position="top"
+ label-width="120px"
+ size="large"
+ >
+ <el-row class="mx-[-10px]">
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <LoginFormTitle class="w-full" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item v-if="registerData.tenantEnable === 'true'" prop="tenantName">
+ <el-input
+ v-model="registerData.registerForm.tenantName"
+ :placeholder="t('login.tenantname')"
+ :prefix-icon="iconHouse"
+ link
+ type="primary"
+ size="large"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="username">
+ <el-input
+ v-model="registerData.registerForm.username"
+ :placeholder="t('login.username')"
+ size="large"
+ :prefix-icon="iconAvatar"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="nickname">
+ <el-input
+ v-model="registerData.registerForm.nickname"
+ placeholder="鏄电О"
+ size="large"
+ :prefix-icon="iconAvatar"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="password">
+ <el-input
+ v-model="registerData.registerForm.password"
+ type="password"
+ auto-complete="off"
+ :placeholder="t('login.password')"
+ size="large"
+ :prefix-icon="iconLock"
+ show-password
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item prop="confirmPassword">
+ <el-input
+ v-model="registerData.registerForm.confirmPassword"
+ type="password"
+ size="large"
+ auto-complete="off"
+ :placeholder="t('login.checkPassword')"
+ :prefix-icon="iconLock"
+ show-password
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" class="px-10px">
+ <el-form-item>
+ <XButton
+ :loading="loginLoading"
+ :title="t('login.register')"
+ class="w-full"
+ type="primary"
+ @click="getCode()"
+ />
+ </el-form-item>
+ </el-col>
+ <Verify
+ v-if="registerData.captchaEnable === 'true'"
+ ref="verify"
+ :captchaType="captchaType"
+ :imgSize="{ width: '400px', height: '200px' }"
+ mode="pop"
+ @success="handleRegister"
+ />
+ </el-row>
+ <XButton :title="t('login.hasUser')" class="w-full" @click="handleBackLogin()" />
+ </el-form>
+</template>
+<script lang="ts" setup>
+import { ElLoading } from 'element-plus'
+import LoginFormTitle from './LoginFormTitle.vue'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+import { useIcon } from '@/hooks/web/useIcon'
+import * as authUtil from '@/utils/auth'
+import { usePermissionStore } from '@/store/modules/permission'
+import * as LoginApi from '@/api/login'
+import { LoginStateEnum, useLoginState, useFormValid } from './useLogin'
+
+defineOptions({ name: 'RegisterForm' })
+
+const { t } = useI18n()
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconAvatar = useIcon({ icon: 'ep:avatar' })
+const iconLock = useIcon({ icon: 'ep:lock' })
+const formLogin = ref()
+const {validForm} = useFormValid(formLogin)
+const { handleBackLogin, getLoginState } = useLoginState()
+const { currentRoute, push } = useRouter()
+const permissionStore = usePermissionStore()
+const redirect = ref<string>('')
+const loginLoading = ref(false)
+const verify = ref()
+const captchaType = ref('blockPuzzle') // blockPuzzle 婊戝潡 clickWord 鐐瑰嚮鏂囧瓧 pictureWord 鏂囧瓧楠岃瘉鐮�
+
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.REGISTER)
+
+const equalToPassword = (_rule, value, callback) => {
+ if (registerData.registerForm.password !== value) {
+ callback(new Error('涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�'))
+ } else {
+ callback()
+ }
+}
+
+const registerRules = {
+ tenantName: [
+ { required: true, trigger: 'blur', message: '璇疯緭鍏ユ偍鎵�灞炵殑绉熸埛' },
+ { min: 2, max: 20, message: '绉熸埛璐﹀彿闀垮害蹇呴』浠嬩簬 2 鍜� 20 涔嬮棿', trigger: 'blur' }
+ ],
+ username: [
+ { required: true, trigger: 'blur', message: '璇疯緭鍏ユ偍鐨勮处鍙�' },
+ { min: 4, max: 30, message: '鐢ㄦ埛璐﹀彿闀垮害蹇呴』浠嬩簬 4 鍜� 30 涔嬮棿', trigger: 'blur' }
+ ],
+ nickname: [
+ { required: true, trigger: 'blur', message: '璇疯緭鍏ユ偍鐨勬樀绉�' },
+ { min: 0, max: 30, message: '鏄电О闀垮害蹇呴』浠嬩簬 0 鍜� 30 涔嬮棿', trigger: 'blur' }
+ ],
+ password: [
+ { required: true, trigger: 'blur', message: '璇疯緭鍏ユ偍鐨勫瘑鐮�' },
+ { min: 5, max: 20, message: '鐢ㄦ埛瀵嗙爜闀垮害蹇呴』浠嬩簬 5 鍜� 20 涔嬮棿', trigger: 'blur' },
+ { pattern: /^[^<>"'|\\]+$/, message: '涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > " \' \\\ |', trigger: 'blur' }
+ ],
+ confirmPassword: [
+ { required: true, trigger: 'blur', message: '璇峰啀娆¤緭鍏ユ偍鐨勫瘑鐮�' },
+ { required: true, validator: equalToPassword, trigger: 'blur' }
+ ]
+}
+
+const registerData = reactive({
+ isShowPassword: false,
+ captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+ tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+ registerForm: {
+ tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
+ nickname: '',
+ tenantId: 0,
+ username: '',
+ password: '',
+ confirmPassword: '',
+ captchaVerification: ''
+ }
+})
+
+const loading = ref() // ElLoading.service 杩斿洖鐨勫疄渚�
+// 鎻愪氦娉ㄥ唽
+const handleRegister = async (params: any) => {
+ loading.value = true
+ try {
+ if (registerData.tenantEnable) {
+ await getTenantId()
+ registerData.registerForm.tenantId = authUtil.getTenantId()
+ }
+
+ if (registerData.captchaEnable) {
+ registerData.registerForm.captchaVerification = params.captchaVerification
+ }
+
+ const data = await validForm()
+ if (!data) {
+ return
+ }
+
+ const res = await LoginApi.register(registerData.registerForm)
+ if (!res) {
+ return
+ }
+ loading.value = ElLoading.service({
+ lock: true,
+ text: '姝e湪鍔犺浇绯荤粺涓�...',
+ background: 'rgba(0, 0, 0, 0.7)'
+ })
+
+ authUtil.removeLoginForm()
+
+ authUtil.setToken(res)
+ if (!redirect.value) {
+ redirect.value = '/'
+ }
+ // 鍒ゆ柇鏄惁涓篠SO鐧诲綍
+ if (redirect.value.indexOf('sso') !== -1) {
+ window.location.href = window.location.href.replace('/login?redirect=', '')
+ } else {
+ push({ path: redirect.value || permissionStore.addRouters[0].path })
+ }
+ } finally {
+ loginLoading.value = false
+ loading.value.close()
+ }
+}
+
+// 鑾峰彇楠岃瘉鐮�
+const getCode = async () => {
+ // 鎯呭喌涓�锛屾湭寮�鍚細鍒欑洿鎺ユ敞鍐�
+ if (registerData.captchaEnable === 'false') {
+ await handleRegister({})
+ } else {
+ // 鎯呭喌浜岋紝宸插紑鍚細鍒欏睍绀洪獙璇佺爜锛涘彧鏈夊畬鎴愰獙璇佺爜鐨勬儏鍐碉紝鎵嶈繘琛屾敞鍐�
+ // 寮瑰嚭楠岃瘉鐮�
+ verify.value.show()
+ }
+}
+
+// 鑾峰彇绉熸埛 ID
+const getTenantId = async () => {
+ if (registerData.tenantEnable === 'true') {
+ const res = await LoginApi.getTenantIdByName(registerData.registerForm.tenantName)
+ authUtil.setTenantId(res)
+ }
+}
+
+// 鏍规嵁鍩熷悕锛岃幏寰楃鎴蜂俊鎭�
+const getTenantByWebsite = async () => {
+ if (registerData.tenantEnable === 'true') {
+ const website = location.host
+ const res = await LoginApi.getTenantByWebsite(website)
+ if (res) {
+ registerData.registerForm.tenantName = res.name
+ authUtil.setTenantId(res.id)
+ }
+ }
+}
+
+watch(
+ () => currentRoute.value,
+ (route: RouteLocationNormalizedLoaded) => {
+ redirect.value = route?.query?.redirect as string
+ },
+ {
+ immediate: true
+ }
+)
+onMounted(() => {
+ // getCookie()
+ getTenantByWebsite()
+})
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+ &:hover {
+ color: var(--el-color-primary) !important;
+ }
+}
+
+.login-code {
+ float: right;
+ width: 100%;
+ height: 38px;
+
+ img {
+ width: 100%;
+ height: auto;
+ max-width: 100px;
+ vertical-align: middle;
+ cursor: pointer;
+ }
+}
+</style>
diff --git a/src/views/Login/components/SSOLogin.vue b/src/views/Login/components/SSOLogin.vue
new file mode 100644
index 0000000..99e359e
--- /dev/null
+++ b/src/views/Login/components/SSOLogin.vue
@@ -0,0 +1,199 @@
+<template>
+ <div v-show="ssoVisible" class="form-cont">
+ <!-- 搴旂敤鍚� -->
+ <LoginFormTitle class="w-full" />
+ <el-tabs class="form" style="float: none" value="uname">
+ <el-tab-pane :label="client.name" name="uname" />
+ </el-tabs>
+ <div>
+ <el-form :model="formData" class="login-form">
+ <!-- 鎺堟潈鑼冨洿鐨勯�夋嫨 -->
+ 姝ょ涓夋柟搴旂敤璇锋眰鑾峰緱浠ヤ笅鏉冮檺锛�
+ <el-form-item prop="scopes">
+ <el-checkbox-group v-model="formData.scopes">
+ <el-checkbox
+ v-for="scope in queryParams.scopes"
+ :key="scope"
+ :value="scope"
+ class="block mb-[-10px]"
+ >
+ {{ formatScope(scope) }}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ <!-- 涓嬫柟鐨勭櫥褰曟寜閽� -->
+ <el-form-item class="w-full">
+ <el-button
+ :loading="formLoading"
+ class="w-3/5"
+ type="primary"
+ @click.prevent="handleAuthorize(true)"
+ >
+ <span v-if="!formLoading">鍚屾剰鎺堟潈</span>
+ <span v-else>鎺� 鏉� 涓�...</span>
+ </el-button>
+ <el-button class="w-3/10" @click.prevent="handleAuthorize(false)">鎷掔粷</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import LoginFormTitle from './LoginFormTitle.vue'
+import * as OAuth2Api from '@/api/login/oauth2'
+import { LoginStateEnum, useLoginState } from './useLogin'
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+defineOptions({ name: 'SSOLogin' })
+
+const route = useRoute() // 璺敱
+const { currentRoute } = useRouter() // 璺敱
+const { getLoginState, setLoginState } = useLoginState()
+
+const client = ref({
+ // 瀹㈡埛绔俊鎭�
+ name: '',
+ logo: ''
+})
+interface queryType {
+ responseType: string
+ clientId: string
+ redirectUri: string
+ state: string
+ scopes: string[]
+}
+const queryParams = reactive<queryType>({
+ // URL 涓婄殑 client_id銆乻cope 绛夊弬鏁�
+ responseType: '',
+ clientId: '',
+ redirectUri: '',
+ state: '',
+ scopes: [] // 浼樺厛浠� query 鍙傛暟鑾峰彇锛涘鏋滄湭浼犻�掞紝浠庡悗绔幏鍙�
+})
+const ssoVisible = computed(() => unref(getLoginState) === LoginStateEnum.SSO) // 鏄惁灞曠ず SSO 鐧诲綍鐨勮〃鍗�
+interface formType {
+ scopes: string[]
+}
+const formData = reactive<formType>({
+ scopes: [] // 宸查�変腑鐨� scope 鏁扮粍
+})
+const formLoading = ref(false) // 琛ㄥ崟鏄惁鎻愪氦涓�
+
+/** 鍒濆鍖栨巿鏉冧俊鎭� */
+const init = async () => {
+ // 闃叉鍦ㄦ病鏈夌櫥褰曠殑鎯呭喌涓嬪惊鐜脊绐�
+ if (typeof route.query.client_id === 'undefined') return
+ // 瑙f瀽鍙傛暟
+ // 渚嬪璇淬�愯嚜鍔ㄦ巿鏉冧笉閫氳繃銆戯細client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read%20user.write
+ // 渚嬪璇淬�愯嚜鍔ㄦ巿鏉冮�氳繃銆戯細client_id=default&redirect_uri=https%3A%2F%2Fwww.iocoder.cn&response_type=code&scope=user.read
+ queryParams.responseType = route.query.response_type as string
+ queryParams.clientId = route.query.client_id as string
+ queryParams.redirectUri = route.query.redirect_uri as string
+ queryParams.state = route.query.state as string
+ if (route.query.scope) {
+ queryParams.scopes = (route.query.scope as string).split(' ')
+ }
+
+ // 濡傛灉鏈� scope 鍙傛暟锛屽厛鎵ц涓�娆¤嚜鍔ㄦ巿鏉冿紝鐪嬬湅鏄惁涔嬪墠閮芥巿鏉冭繃浜嗐��
+ if (queryParams.scopes.length > 0) {
+ const data = await doAuthorize(true, queryParams.scopes, [])
+ if (data) {
+ location.href = data
+ return
+ }
+ }
+
+ // 鑾峰彇鎺堟潈椤电殑鍩烘湰淇℃伅
+ const data = await OAuth2Api.getAuthorize(queryParams.clientId)
+ client.value = data.client
+ // 瑙f瀽 scope
+ let scopes
+ // 1.1 濡傛灉 params.scope 闈炵┖锛屽垯杩囨护涓嬭繑鍥炵殑 scopes
+ if (queryParams.scopes.length > 0) {
+ scopes = []
+ for (const scope of data.scopes) {
+ if (queryParams.scopes.indexOf(scope.key) >= 0) {
+ scopes.push(scope)
+ }
+ }
+ // 1.2 濡傛灉 params.scope 涓虹┖锛屽垯浣跨敤杩斿洖鐨� scopes 璁剧疆瀹�
+ } else {
+ scopes = data.scopes
+ for (const scope of scopes) {
+ queryParams.scopes.push(scope.key)
+ }
+ }
+ // 鐢熸垚宸查�変腑鐨� checkedScopes
+ for (const scope of scopes) {
+ if (scope.value) {
+ formData.scopes.push(scope.key)
+ }
+ }
+}
+
+/** 澶勭悊鎺堟潈鐨勬彁浜� */
+const handleAuthorize = async (approved) => {
+ // 璁$畻 checkedScopes + uncheckedScopes
+ let checkedScopes
+ let uncheckedScopes
+ if (approved) {
+ // 鍚屾剰鎺堟潈锛屾寜鐓х敤鎴风殑閫夋嫨
+ checkedScopes = formData.scopes
+ uncheckedScopes = queryParams.scopes.filter((item) => checkedScopes.indexOf(item) === -1)
+ } else {
+ // 鎷掔粷锛屽垯閮芥槸鍙栨秷
+ checkedScopes = []
+ uncheckedScopes = queryParams.scopes
+ }
+ // 鎻愪氦鎺堟潈鐨勮姹�
+ formLoading.value = true
+ try {
+ const data = await doAuthorize(false, checkedScopes, uncheckedScopes)
+ if (!data) {
+ return
+ }
+ location.href = data
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 璋冪敤鎺堟潈 API 鎺ュ彛 */
+const doAuthorize = (autoApprove, checkedScopes, uncheckedScopes) => {
+ return OAuth2Api.authorize(
+ queryParams.responseType,
+ queryParams.clientId,
+ queryParams.redirectUri,
+ queryParams.state,
+ autoApprove,
+ checkedScopes,
+ uncheckedScopes
+ )
+}
+
+/** 鏍煎紡鍖� scope 鏂囨湰 */
+const formatScope = (scope) => {
+ // 鏍煎紡鍖� scope 鎺堟潈鑼冨洿锛屾柟渚跨敤鎴风悊瑙c��
+ // 杩欓噷浠呬粎鏄竴涓� demo锛屽彲浠ヨ�冭檻褰曞叆鍒板瓧鍏告暟鎹腑锛屼緥濡傝瀛楀吀绫诲瀷 "system_oauth2_scope"锛屽畠鐨勬瘡涓� scope 閮芥槸涓�鏉″瓧鍏告暟鎹��
+ switch (scope) {
+ case 'user.read':
+ return '璁块棶浣犵殑涓汉淇℃伅'
+ case 'user.write':
+ return '淇敼浣犵殑涓汉淇℃伅'
+ default:
+ return scope
+ }
+}
+
+/** 鐩戝惉褰撳墠璺敱涓� SSOLogin 鏃讹紝杩涜鏁版嵁鐨勫垵濮嬪寲 */
+watch(
+ () => currentRoute.value,
+ (route: RouteLocationNormalizedLoaded) => {
+ if (route.name === 'SSOLogin') {
+ setLoginState(LoginStateEnum.SSO)
+ init()
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/views/Login/components/index.ts b/src/views/Login/components/index.ts
new file mode 100644
index 0000000..7c42415
--- /dev/null
+++ b/src/views/Login/components/index.ts
@@ -0,0 +1,9 @@
+import LoginForm from './LoginForm.vue'
+import MobileForm from './MobileForm.vue'
+import LoginFormTitle from './LoginFormTitle.vue'
+import RegisterForm from './RegisterForm.vue'
+import QrCodeForm from './QrCodeForm.vue'
+import SSOLoginVue from './SSOLogin.vue'
+import ForgetPasswordForm from './ForgetPasswordForm.vue'
+
+export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }
diff --git a/src/views/Login/components/useLogin.ts b/src/views/Login/components/useLogin.ts
new file mode 100644
index 0000000..b4a02f8
--- /dev/null
+++ b/src/views/Login/components/useLogin.ts
@@ -0,0 +1,42 @@
+import { Ref } from 'vue'
+
+export enum LoginStateEnum {
+ LOGIN,
+ REGISTER,
+ RESET_PASSWORD,
+ MOBILE,
+ QR_CODE,
+ SSO
+}
+
+const currentState = ref(LoginStateEnum.LOGIN)
+
+export function useLoginState() {
+ function setLoginState(state: LoginStateEnum) {
+ currentState.value = state
+ }
+ const getLoginState = computed(() => currentState.value)
+
+ function handleBackLogin() {
+ setLoginState(LoginStateEnum.LOGIN)
+ }
+
+ return {
+ setLoginState,
+ getLoginState,
+ handleBackLogin
+ }
+}
+
+export function useFormValid<T extends Object = any>(formRef: Ref<any>) {
+ async function validForm() {
+ const form = unref(formRef)
+ if (!form) return
+ const data = await form.validate()
+ return data as T
+ }
+
+ return {
+ validForm
+ }
+}
diff --git a/src/views/Profile/Index.vue b/src/views/Profile/Index.vue
new file mode 100644
index 0000000..29b2694
--- /dev/null
+++ b/src/views/Profile/Index.vue
@@ -0,0 +1,67 @@
+<template>
+ <!-- TODO @鑺嬭壙锛氬彲浼樺寲锛屽鏍� vben 鐗堟湰 -->
+ <div class="flex">
+ <el-card class="user w-1/3" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span>{{ t('profile.user.title') }}</span>
+ </div>
+ </template>
+ <ProfileUser ref="profileUserRef" />
+ </el-card>
+ <el-card class="user ml-3 w-2/3" shadow="hover">
+ <div>
+ <el-tabs v-model="activeName" class="profile-tabs" style="height: 400px" tab-position="top">
+ <el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
+ <BasicInfo @success="handleBasicInfoSuccess" />
+ </el-tab-pane>
+ <el-tab-pane :label="t('profile.info.resetPwd')" name="resetPwd">
+ <ResetPwd />
+ </el-tab-pane>
+ <el-tab-pane :label="t('profile.info.userSocial')" name="userSocial">
+ <UserSocial v-model:activeName="activeName" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+ </el-card>
+ </div>
+</template>
+<script lang="ts" setup>
+import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components'
+
+const { t } = useI18n()
+defineOptions({ name: 'Profile' })
+const activeName = ref('basicInfo')
+const profileUserRef = ref()
+
+// 澶勭悊鍩烘湰淇℃伅鏇存柊鎴愬姛
+const handleBasicInfoSuccess = async () => {
+ await profileUserRef.value?.refresh()
+}
+</script>
+<style scoped>
+.user {
+ max-height: 960px;
+ padding: 15px 20px 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+:deep(.el-card .el-card__header, .el-card .el-card__body) {
+ padding: 15px !important;
+}
+
+.profile-tabs > .el-tabs__content {
+ padding: 32px;
+ font-weight: 600;
+ color: #6b778c;
+}
+
+.el-tabs--left .el-tabs__content {
+ height: 100%;
+}
+</style>
diff --git a/src/views/Profile/components/BasicInfo.vue b/src/views/Profile/components/BasicInfo.vue
new file mode 100644
index 0000000..094df40
--- /dev/null
+++ b/src/views/Profile/components/BasicInfo.vue
@@ -0,0 +1,121 @@
+<template>
+ <Form ref="formRef" :labelWidth="200" :rules="rules" :schema="schema">
+ <template #sex="form">
+ <el-radio-group v-model="form['sex']">
+ <el-radio :value="1">{{ t('profile.user.man') }}</el-radio>
+ <el-radio :value="2">{{ t('profile.user.woman') }}</el-radio>
+ </el-radio-group>
+ </template>
+ </Form>
+ <div style="text-align: center">
+ <XButton :title="t('common.save')" type="primary" @click="submit()" />
+ <XButton :title="t('common.reset')" type="danger" @click="init()" />
+ </div>
+</template>
+<script lang="ts" setup>
+import type { FormRules } from 'element-plus'
+import { FormSchema } from '@/types/form'
+import type { FormExpose } from '@/components/Form'
+import {
+ getUserProfile,
+ updateUserProfile,
+ UserProfileUpdateReqVO
+} from '@/api/system/user/profile'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'BasicInfo' })
+
+const { t } = useI18n()
+const message = useMessage() // 娑堟伅寮圭獥
+const userStore = useUserStore()
+
+// 瀹氫箟浜嬩欢
+const emit = defineEmits<{
+ (e: 'success'): void
+}>()
+
+// 琛ㄥ崟鏍¢獙
+const rules = reactive<FormRules>({
+ nickname: [{ required: true, message: t('profile.rules.nickname'), trigger: 'blur' }],
+ email: [
+ { required: true, message: t('profile.rules.mail'), trigger: 'blur' },
+ {
+ type: 'email',
+ message: t('profile.rules.truemail'),
+ trigger: ['blur', 'change']
+ }
+ ],
+ mobile: [
+ { required: true, message: t('profile.rules.phone'), trigger: 'blur' },
+ {
+ pattern: /^1[3-9]\d{9}$/,
+ message: t('profile.rules.truephone'),
+ trigger: 'blur'
+ }
+ ]
+})
+const schema = reactive<FormSchema[]>([
+ {
+ field: 'nickname',
+ label: t('profile.user.nickname'),
+ component: 'Input'
+ },
+ {
+ field: 'mobile',
+ label: t('profile.user.mobile'),
+ component: 'Input'
+ },
+ {
+ field: 'email',
+ label: t('profile.user.email'),
+ component: 'Input'
+ },
+ {
+ field: 'sex',
+ label: t('profile.user.sex'),
+ component: 'InputNumber',
+ value: 0
+ }
+])
+const formRef = ref<FormExpose>() // 琛ㄥ崟 Ref
+
+// 鐩戝惉 userStore 涓ご鍍忕殑鍙樺寲锛屽悓姝ユ洿鏂拌〃鍗曟暟鎹�
+watch(
+ () => userStore.getUser.avatar,
+ (newAvatar) => {
+ if (newAvatar && formRef.value) {
+ // 鐩存帴鏇存柊琛ㄥ崟妯″瀷涓殑澶村儚瀛楁
+ const formModel = formRef.value.formModel
+ if (formModel) {
+ formModel.avatar = newAvatar
+ }
+ }
+ }
+)
+
+const submit = () => {
+ const elForm = unref(formRef)?.getElFormRef()
+ if (!elForm) return
+ elForm.validate(async (valid) => {
+ if (valid) {
+ const data = unref(formRef)?.formModel as UserProfileUpdateReqVO
+ await updateUserProfile(data)
+ message.success(t('common.updateSuccess'))
+ const profile = await init()
+ await userStore.setUserNicknameAction(profile.nickname)
+ // 鍙戦�佹垚鍔熶簨浠�
+ emit('success')
+ }
+ })
+}
+
+const init = async () => {
+ const res = await getUserProfile()
+ unref(formRef)?.setValues(res)
+ return res
+}
+
+onMounted(async () => {
+ await init()
+})
+</script>
diff --git a/src/views/Profile/components/ProfileUser.vue b/src/views/Profile/components/ProfileUser.vue
new file mode 100644
index 0000000..e226af0
--- /dev/null
+++ b/src/views/Profile/components/ProfileUser.vue
@@ -0,0 +1,118 @@
+<template>
+ <div>
+ <div class="text-center">
+ <UserAvatar :img="userInfo?.avatar" />
+ </div>
+ <ul class="list-group list-group-striped">
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="ep:user" />
+ {{ t('profile.user.username') }}
+ <div class="pull-right">{{ userInfo?.username }}</div>
+ </li>
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="ep:phone" />
+ {{ t('profile.user.mobile') }}
+ <div class="pull-right">{{ userInfo?.mobile }}</div>
+ </li>
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="fontisto:email" />
+ {{ t('profile.user.email') }}
+ <div class="pull-right">{{ userInfo?.email }}</div>
+ </li>
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="carbon:tree-view-alt" />
+ {{ t('profile.user.dept') }}
+ <div v-if="userInfo?.dept" class="pull-right">{{ userInfo?.dept.name }}</div>
+ </li>
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="ep:suitcase" />
+ {{ t('profile.user.posts') }}
+ <div v-if="userInfo?.posts" class="pull-right">
+ {{ userInfo?.posts.map((post) => post.name).join(',') }}
+ </div>
+ </li>
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="icon-park-outline:peoples" />
+ {{ t('profile.user.roles') }}
+ <div v-if="userInfo?.roles" class="pull-right">
+ {{ userInfo?.roles.map((role) => role.name).join(',') }}
+ </div>
+ </li>
+ <li class="list-group-item">
+ <Icon class="mr-5px" icon="ep:calendar" />
+ {{ t('profile.user.createTime') }}
+ <div class="pull-right">{{ formatDate(userInfo.createTime) }}</div>
+ </li>
+ </ul>
+ </div>
+</template>
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import UserAvatar from './UserAvatar.vue'
+import { useUserStore } from '@/store/modules/user'
+
+import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
+
+defineOptions({ name: 'ProfileUser' })
+
+const { t } = useI18n()
+const userStore = useUserStore()
+const userInfo = ref({} as ProfileVO)
+
+const getUserInfo = async () => {
+ const users = await getUserProfile()
+ userInfo.value = users
+}
+
+// 鐩戝惉 userStore 涓ご鍍忕殑鍙樺寲锛屽悓姝ユ洿鏂版湰鍦� userInfo
+watch(
+ () => userStore.getUser.avatar,
+ (newAvatar) => {
+ if (newAvatar && userInfo.value) {
+ userInfo.value.avatar = newAvatar
+ }
+ }
+)
+
+// 鏆撮湶鍒锋柊鏂规硶
+defineExpose({
+ refresh: getUserInfo
+})
+
+onMounted(async () => {
+ await getUserInfo()
+})
+</script>
+
+<style scoped>
+.text-center {
+ position: relative;
+ height: 120px;
+ text-align: center;
+}
+
+.list-group-striped > .list-group-item {
+ padding-right: 0;
+ padding-left: 0;
+ border-right: 0;
+ border-left: 0;
+ border-radius: 0;
+}
+
+.list-group {
+ padding-left: 0;
+ list-style: none;
+}
+
+.list-group-item {
+ padding: 11px 0;
+ margin-bottom: -1px;
+ font-size: 13px;
+ border-top: 1px solid #e7eaec;
+ border-bottom: 1px solid #e7eaec;
+}
+
+.pull-right {
+ float: right !important;
+}
+</style>
diff --git a/src/views/Profile/components/ResetPwd.vue b/src/views/Profile/components/ResetPwd.vue
new file mode 100644
index 0000000..d9e0de1
--- /dev/null
+++ b/src/views/Profile/components/ResetPwd.vue
@@ -0,0 +1,73 @@
+<template>
+ <el-form ref="formRef" :model="password" :rules="rules" :label-width="200">
+ <el-form-item :label="t('profile.password.oldPassword')" prop="oldPassword">
+ <InputPassword v-model="password.oldPassword" />
+ </el-form-item>
+ <el-form-item :label="t('profile.password.newPassword')" prop="newPassword">
+ <InputPassword v-model="password.newPassword" strength />
+ </el-form-item>
+ <el-form-item :label="t('profile.password.confirmPassword')" prop="confirmPassword">
+ <InputPassword v-model="password.confirmPassword" strength />
+ </el-form-item>
+ <el-form-item>
+ <XButton :title="t('common.save')" type="primary" @click="submit(formRef)" />
+ <XButton :title="t('common.reset')" type="danger" @click="reset(formRef)" />
+ </el-form-item>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import type { FormInstance, FormRules } from 'element-plus'
+
+import { InputPassword } from '@/components/InputPassword'
+import { updateUserPassword } from '@/api/system/user/profile'
+
+defineOptions({ name: 'ResetPwd' })
+
+const { t } = useI18n()
+const message = useMessage()
+const formRef = ref<FormInstance>()
+const password = reactive({
+ oldPassword: '',
+ newPassword: '',
+ confirmPassword: ''
+})
+
+// 琛ㄥ崟鏍¢獙
+const equalToPassword = (_rule, value, callback) => {
+ if (password.newPassword !== value) {
+ callback(new Error(t('profile.password.diffPwd')))
+ } else {
+ callback()
+ }
+}
+
+const rules = reactive<FormRules>({
+ oldPassword: [
+ { required: true, message: t('profile.password.oldPwdMsg'), trigger: 'blur' },
+ { min: 4, max: 16, message: t('profile.password.pwdRules'), trigger: 'blur' }
+ ],
+ newPassword: [
+ { required: true, message: t('profile.password.newPwdMsg'), trigger: 'blur' },
+ { min: 4, max: 16, message: t('profile.password.pwdRules'), trigger: 'blur' }
+ ],
+ confirmPassword: [
+ { required: true, message: t('profile.password.cfPwdMsg'), trigger: 'blur' },
+ { required: true, validator: equalToPassword, trigger: 'blur' }
+ ]
+})
+
+const submit = (formEl: FormInstance | undefined) => {
+ if (!formEl) return
+ formEl.validate(async (valid) => {
+ if (valid) {
+ await updateUserPassword(password.oldPassword, password.newPassword)
+ message.success(t('common.updateSuccess'))
+ }
+ })
+}
+
+const reset = (formEl: FormInstance | undefined) => {
+ if (!formEl) return
+ formEl.resetFields()
+}
+</script>
diff --git a/src/views/Profile/components/UserAvatar.vue b/src/views/Profile/components/UserAvatar.vue
new file mode 100644
index 0000000..23ecc3a
--- /dev/null
+++ b/src/views/Profile/components/UserAvatar.vue
@@ -0,0 +1,54 @@
+<template>
+ <div class="change-avatar">
+ <CropperAvatar
+ ref="cropperRef"
+ :btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
+ :showBtn="false"
+ :value="img"
+ width="120px"
+ @change="handelUpload"
+ />
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { updateUserProfile } from '@/api/system/user/profile'
+import { CropperAvatar } from '@/components/Cropper'
+import { useUserStore } from '@/store/modules/user'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
+
+defineOptions({ name: 'UserAvatar' })
+
+defineProps({
+ img: propTypes.string.def('')
+})
+
+const userStore = useUserStore()
+
+const cropperRef = ref()
+const handelUpload = async ({ data }) => {
+ const { httpRequest } = useUpload()
+ const avatar = (
+ (await httpRequest({
+ file: data,
+ filename: 'avatar.png'
+ } as UploadRequestOptions)) as unknown as { data: string }
+ ).data
+ await updateUserProfile({ avatar })
+
+ // 鍏抽棴寮圭獥锛屽苟鏇存柊 userStore
+ cropperRef.value.close()
+ await userStore.setUserAvatarAction(avatar)
+}
+</script>
+
+<style lang="scss" scoped>
+.change-avatar {
+ img {
+ display: block;
+ margin-bottom: 15px;
+ border-radius: 50%;
+ }
+}
+</style>
diff --git a/src/views/Profile/components/UserSocial.vue b/src/views/Profile/components/UserSocial.vue
new file mode 100644
index 0000000..8d25354
--- /dev/null
+++ b/src/views/Profile/components/UserSocial.vue
@@ -0,0 +1,107 @@
+<template>
+ <el-table :data="socialUsers" :show-header="false">
+ <el-table-column fixed="left" title="搴忓彿" type="seq" width="60" />
+ <el-table-column align="left" label="绀句氦骞冲彴" width="120">
+ <template #default="{ row }">
+ <img :src="row.img" alt="" class="h-5 align-middle" />
+ <p class="mr-5">{{ row.title }}</p>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="{ row }">
+ <template v-if="row.openid">
+ 宸茬粦瀹�
+ <XTextButton class="mr-5" title="(瑙g粦)" type="primary" @click="unbind(row)" />
+ </template>
+ <template v-else>
+ 鏈粦瀹�
+ <XTextButton class="mr-5" title="(缁戝畾)" type="primary" @click="bind(row)" />
+ </template>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+<script lang="ts" setup>
+import { SystemUserSocialTypeEnum } from '@/utils/constants'
+import { getBindSocialUserList } from '@/api/system/social/user'
+import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
+
+defineOptions({ name: 'UserSocial' })
+defineProps<{
+ activeName: string
+}>()
+const message = useMessage()
+const socialUsers = ref<any[]>([])
+
+const initSocial = async () => {
+ socialUsers.value = [] // 閲嶇疆閬垮厤鏃犻檺澧為暱
+ // 鑾峰彇宸茬粦瀹氱殑绀句氦鐢ㄦ埛鍒楄〃
+ const bindSocialUserList = await getBindSocialUserList()
+ // 妫�鏌ヨ绀句氦骞冲彴鏄惁宸茬粦瀹�
+ for (const i in SystemUserSocialTypeEnum) {
+ const socialUser = { ...SystemUserSocialTypeEnum[i] }
+ socialUsers.value.push(socialUser)
+ if (bindSocialUserList && bindSocialUserList.length > 0) {
+ for (const bindUser of bindSocialUserList) {
+ if (socialUser.type === bindUser.type) {
+ socialUser.openid = bindUser.openid
+ break
+ }
+ }
+ }
+ }
+}
+const route = useRoute()
+const emit = defineEmits<{
+ (e: 'update:activeName', v: string): void
+}>()
+const bindSocial = () => {
+ // 绀句氦缁戝畾
+ const type = getUrlValue('type')
+ const code = route.query.code
+ const state = route.query.state
+ if (!code) {
+ return
+ }
+ socialBind(type, code, state).then(() => {
+ message.success('缁戝畾鎴愬姛')
+ emit('update:activeName', 'userSocial')
+ })
+}
+
+// 鍙屽眰 encode 闇�瑕佸湪鍥炶皟鍚庤繘琛� decode
+function getUrlValue(key: string): string {
+ const url = new URL(decodeURIComponent(location.href))
+ return url.searchParams.get(key) ?? ''
+}
+
+const bind = (row) => {
+ // 鍙屽眰 encode 瑙e喅閽夐拤鍥炶皟 type 鍙傛暟涓㈠け鐨勯棶棰�
+ const redirectUri = location.origin + '/user/profile?' + encodeURIComponent(`type=${row.type}`)
+ // 杩涜璺宠浆
+ socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
+ window.location.href = res
+ })
+}
+const unbind = async (row) => {
+ const res = await socialUnbind(row.type, row.openid)
+ if (res) {
+ row.openid = undefined
+ }
+ message.success('瑙g粦鎴愬姛')
+}
+
+onMounted(async () => {
+ await initSocial()
+})
+
+watch(
+ () => route,
+ () => {
+ bindSocial()
+ },
+ {
+ immediate: true
+ }
+)
+</script>
diff --git a/src/views/Profile/components/index.ts b/src/views/Profile/components/index.ts
new file mode 100644
index 0000000..9e1883c
--- /dev/null
+++ b/src/views/Profile/components/index.ts
@@ -0,0 +1,7 @@
+import BasicInfo from './BasicInfo.vue'
+import ProfileUser from './ProfileUser.vue'
+import ResetPwd from './ResetPwd.vue'
+import UserAvatarVue from './UserAvatar.vue'
+import UserSocial from './UserSocial.vue'
+
+export { BasicInfo, ProfileUser, ResetPwd, UserAvatarVue, UserSocial }
diff --git a/src/views/Redirect/Redirect.vue b/src/views/Redirect/Redirect.vue
new file mode 100644
index 0000000..f7717ce
--- /dev/null
+++ b/src/views/Redirect/Redirect.vue
@@ -0,0 +1,28 @@
+<template>
+ <div></div>
+</template>
+<script lang="ts" setup>
+defineOptions({ name: 'Redirect' })
+
+const { currentRoute, replace } = useRouter()
+const { params, query } = unref(currentRoute)
+const { path, _redirect_type = 'path' } = params
+
+Reflect.deleteProperty(params, '_redirect_type')
+Reflect.deleteProperty(params, 'path')
+
+const _path = Array.isArray(path) ? path.join('/') : path
+
+if (_redirect_type === 'name') {
+ replace({
+ name: _path,
+ query,
+ params
+ })
+} else {
+ replace({
+ path: _path.startsWith('/') ? _path : '/' + _path,
+ query
+ })
+}
+</script>
diff --git a/src/views/ai/chat/index/components/conversation/ConversationList.vue b/src/views/ai/chat/index/components/conversation/ConversationList.vue
new file mode 100644
index 0000000..f73fb27
--- /dev/null
+++ b/src/views/ai/chat/index/components/conversation/ConversationList.vue
@@ -0,0 +1,391 @@
+<!-- AI 瀵硅瘽 -->
+<template>
+ <el-aside
+ width="260px"
+ class="h-100% relative flex flex-col justify-between px-2.5 pt-2.5 pb-0 overflow-hidden"
+ >
+ <!-- 宸﹂《閮細瀵硅瘽 -->
+ <div class="h-100%">
+ <el-button class="w-1/1 py-4.5" type="primary" @click="createConversation">
+ <Icon icon="ep:plus" class="mr-5px" />
+ 鏂板缓瀵硅瘽
+ </el-button>
+
+ <!-- 宸﹂《閮細鎼滅储瀵硅瘽 -->
+ <el-input
+ v-model="searchName"
+ size="large"
+ class="mt-5"
+ placeholder="鎼滅储鍘嗗彶璁板綍"
+ @keyup="searchConversation"
+ >
+ <template #prefix>
+ <Icon icon="ep:search" />
+ </template>
+ </el-input>
+
+ <!-- 宸︿腑闂达細瀵硅瘽鍒楄〃 -->
+ <div class="overflow-auto h-full">
+ <!-- 鎯呭喌涓�锛氬姞杞戒腑 -->
+ <el-empty v-if="loading" description="." :v-loading="loading" />
+ <!-- 鎯呭喌浜岋細鎸夌収 group 鍒嗙粍锛屽睍绀鸿亰澶╀細璇� list 鍒楄〃 -->
+ <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
+ <div class="mt-1.25 pt-2.5" v-if="conversationMap[conversationKey].length">
+ <el-text class="mx-1" size="small" tag="b">
+ {{ conversationKey }}
+ </el-text>
+ </div>
+ <div
+ class="mt-1.25"
+ v-for="conversation in conversationMap[conversationKey]"
+ :key="conversation.id"
+ @click="handleConversationClick(conversation.id)"
+ @mouseover="hoverConversationId = conversation.id"
+ @mouseout="hoverConversationId = ''"
+ >
+ <div
+ class="flex flex-row justify-between flex-1 px-1.25 cursor-pointer rounded-1.25 items-center leading-7.5"
+ :style="
+ conversation.id === activeConversationId
+ ? 'background-color: var(--el-color-primary-light-9); border: 1px solid var(--el-color-primary-light-7);'
+ : ''
+ "
+ >
+ <div class="flex flex-row items-center">
+ <img
+ class="w-6.25 h-6.25 rounded-1.25 flex flex-row justify-center"
+ :src="conversation.roleAvatar || roleAvatarDefaultImg"
+ />
+ <span
+ class="py-0.5 px-2.5"
+ style="
+ max-width: 220px;
+ font-size: 14px;
+ font-weight: 400;
+ color: var(--el-text-color-regular);
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ "
+ >
+ {{ conversation.title }}
+ </span>
+ </div>
+ <div
+ class="right-0.5 flex flex-row justify-center"
+ style="color: var(--el-text-color-regular)"
+ v-show="hoverConversationId === conversation.id"
+ >
+ <el-button class="m-0" link @click.stop="handleTop(conversation)">
+ <el-icon title="缃《" v-if="!conversation.pinned"><Top /></el-icon>
+ <el-icon title="缃《" v-if="conversation.pinned"><Bottom /></el-icon>
+ </el-button>
+ <el-button class="m-0" link @click.stop="updateConversationTitle(conversation)">
+ <el-icon title="缂栬緫">
+ <Icon icon="ep:edit" />
+ </el-icon>
+ </el-button>
+ <el-button class="m-0" link @click.stop="deleteChatConversation(conversation)">
+ <el-icon title="鍒犻櫎瀵硅瘽">
+ <Icon icon="ep:delete" />
+ </el-icon>
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 搴曢儴鍗犱綅 -->
+ <div class="h-160px w-100%"></div>
+ </div>
+ </div>
+
+ <!-- 宸﹀簳閮細宸ュ叿鏍� -->
+ <div
+ class="absolute bottom-0 left-0 right-0 px-5 leading-8.75 flex justify-between items-center"
+ style="
+ background-color: var(--el-fill-color-extra-light);
+ box-shadow: 0 0 1px 1px var(--el-border-color-lighter);
+ color: var(--el-text-color);
+ "
+ >
+ <div
+ class="flex items-center p-0 m-0 cursor-pointer"
+ style="color: var(--el-text-color-regular)"
+ @click="handleRoleRepository"
+ >
+ <Icon icon="ep:user" />
+ <el-text class="ml-1.25" size="small">瑙掕壊浠撳簱</el-text>
+ </div>
+ <div
+ class="flex items-center p-0 m-0 cursor-pointer"
+ style="color: var(--el-text-color-regular)"
+ @click="handleClearConversation"
+ >
+ <Icon icon="ep:delete" />
+ <el-text class="ml-1.25" size="small">娓呯┖鏈疆椤跺璇�</el-text>
+ </div>
+ </div>
+
+ <!-- 瑙掕壊浠撳簱鎶藉眽 -->
+ <el-drawer v-model="roleRepositoryOpen" title="瑙掕壊浠撳簱" size="754px">
+ <RoleRepository />
+ </el-drawer>
+ </el-aside>
+</template>
+
+<script setup lang="ts">
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import RoleRepository from '../role/RoleRepository.vue'
+import { Bottom, Top } from '@element-plus/icons-vue'
+import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 瀹氫箟灞炴��
+const searchName = ref<string>('') // 瀵硅瘽鎼滅储
+const activeConversationId = ref<number | null>(null) // 閫変腑鐨勫璇濓紝榛樿涓� null
+const hoverConversationId = ref<number | null>(null) // 鎮诞涓婂幓鐨勫璇�
+const conversationList = ref([] as ChatConversationVO[]) // 瀵硅瘽鍒楄〃
+const conversationMap = ref<any>({}) // 瀵硅瘽鍒嗙粍 (缃《銆佷粖澶┿�佷笁澶╁墠銆佷竴鏄熸湡鍓嶃�佷竴涓湀鍓�)
+const loading = ref<boolean>(false) // 鍔犺浇涓�
+const loadingTime = ref<any>() // 鍔犺浇涓畾鏃跺櫒
+
+// 瀹氫箟缁勪欢 props
+const props = defineProps({
+ activeId: {
+ type: String || null,
+ required: true
+ }
+})
+
+// 瀹氫箟閽╁瓙
+const emits = defineEmits([
+ 'onConversationCreate',
+ 'onConversationClick',
+ 'onConversationClear',
+ 'onConversationDelete'
+])
+
+/** 鎼滅储瀵硅瘽 */
+const searchConversation = async (e) => {
+ // 鎭㈠鏁版嵁
+ if (!searchName.value.trim().length) {
+ conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
+ } else {
+ // 杩囨护
+ const filterValues = conversationList.value.filter((item) => {
+ return item.title.includes(searchName.value.trim())
+ })
+ conversationMap.value = await getConversationGroupByCreateTime(filterValues)
+ }
+}
+
+/** 鐐瑰嚮瀵硅瘽 */
+const handleConversationClick = async (id: number) => {
+ // 杩囨护鍑洪�変腑鐨勫璇�
+ const filterConversation = conversationList.value.filter((item) => {
+ return item.id === id
+ })
+ // 鍥炶皟 onConversationClick
+ // noinspection JSVoidFunctionReturnValueUsed
+ const success = emits('onConversationClick', filterConversation[0])
+ // 鍒囨崲瀵硅瘽
+ if (success) {
+ activeConversationId.value = id
+ }
+}
+
+/** 鑾峰彇瀵硅瘽鍒楄〃 */
+const getChatConversationList = async () => {
+ try {
+ // 鍔犺浇涓�
+ loadingTime.value = setTimeout(() => {
+ loading.value = true
+ }, 50)
+
+ // 1.1 鑾峰彇 瀵硅瘽鏁版嵁
+ conversationList.value = await ChatConversationApi.getChatConversationMyList()
+ // 1.2 鎺掑簭
+ conversationList.value.sort((a, b) => {
+ return b.createTime - a.createTime
+ })
+ // 1.3 娌℃湁浠讳綍瀵硅瘽鎯呭喌
+ if (conversationList.value.length === 0) {
+ activeConversationId.value = null
+ conversationMap.value = {}
+ return
+ }
+
+ // 2. 瀵硅瘽鏍规嵁鏃堕棿鍒嗙粍(缃《銆佷粖澶┿�佷竴澶╁墠銆佷笁澶╁墠銆佷竷澶╁墠銆�30 澶╁墠)
+ conversationMap.value = await getConversationGroupByCreateTime(conversationList.value)
+ } finally {
+ // 娓呯悊瀹氭椂鍣�
+ if (loadingTime.value) {
+ clearTimeout(loadingTime.value)
+ }
+ // 鍔犺浇瀹屾垚
+ loading.value = false
+ }
+}
+
+/** 鎸夌収 creteTime 鍒涘缓鏃堕棿锛岃繘琛屽垎缁� */
+const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => {
+ // 鎺掑簭銆佹寚瀹氥�佹椂闂村垎缁�(浠婂ぉ銆佷竴澶╁墠銆佷笁澶╁墠銆佷竷澶╁墠銆�30澶╁墠)
+ // noinspection NonAsciiCharacters
+ const groupMap = {
+ 缃《: [] as ChatConversationVO[],
+ 浠婂ぉ: [] as ChatConversationVO[],
+ 涓�澶╁墠: [] as ChatConversationVO[],
+ 涓夊ぉ鍓�: [] as ChatConversationVO[],
+ 涓冨ぉ鍓�: [] as ChatConversationVO[],
+ 涓夊崄澶╁墠: [] as ChatConversationVO[]
+ }
+ // 褰撳墠鏃堕棿鐨勬椂闂存埑
+ const now = Date.now()
+ // 瀹氫箟鏃堕棿闂撮殧甯搁噺锛堝崟浣嶏細姣锛�
+ const oneDay = 24 * 60 * 60 * 1000
+ const threeDays = 3 * oneDay
+ const sevenDays = 7 * oneDay
+ const thirtyDays = 30 * oneDay
+ for (const conversation of list) {
+ // 缃《
+ if (conversation.pinned) {
+ groupMap['缃《'].push(conversation)
+ continue
+ }
+ // 璁$畻鏃堕棿宸紙鍗曚綅锛氭绉掞級
+ const diff = now - conversation.createTime
+ // 鏍规嵁鏃堕棿闂撮殧鍒ゆ柇
+ if (diff < oneDay) {
+ groupMap['浠婂ぉ'].push(conversation)
+ } else if (diff < threeDays) {
+ groupMap['涓�澶╁墠'].push(conversation)
+ } else if (diff < sevenDays) {
+ groupMap['涓夊ぉ鍓�'].push(conversation)
+ } else if (diff < thirtyDays) {
+ groupMap['涓冨ぉ鍓�'].push(conversation)
+ } else {
+ groupMap['涓夊崄澶╁墠'].push(conversation)
+ }
+ }
+ return groupMap
+}
+
+/** 鏂板缓瀵硅瘽 */
+const createConversation = async () => {
+ // 1. 鏂板缓瀵硅瘽
+ const conversationId = await ChatConversationApi.createChatConversationMy(
+ {} as unknown as ChatConversationVO
+ )
+ // 2. 鑾峰彇瀵硅瘽鍐呭
+ await getChatConversationList()
+ // 3. 閫変腑瀵硅瘽
+ await handleConversationClick(conversationId)
+ // 4. 鍥炶皟
+ emits('onConversationCreate')
+}
+
+/** 淇敼瀵硅瘽鐨勬爣棰� */
+const updateConversationTitle = async (conversation: ChatConversationVO) => {
+ // 1. 浜屾纭
+ const { value } = await ElMessageBox.prompt('淇敼鏍囬', {
+ inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 鍒ゆ柇闈炵┖锛屼笖闈炵┖鏍�
+ inputErrorMessage: '鏍囬涓嶈兘涓虹┖',
+ inputValue: conversation.title
+ })
+ // 2. 鍙戣捣淇敼
+ await ChatConversationApi.updateChatConversationMy({
+ id: conversation.id,
+ title: value
+ } as ChatConversationVO)
+ message.success('閲嶅懡鍚嶆垚鍔�')
+ // 3. 鍒锋柊鍒楄〃
+ await getChatConversationList()
+ // 4. 杩囨护褰撳墠鍒囨崲鐨�
+ const filterConversationList = conversationList.value.filter((item) => {
+ return item.id === conversation.id
+ })
+ if (filterConversationList.length > 0) {
+ // tip锛氶伩鍏嶅垏鎹㈠璇�
+ if (activeConversationId.value === filterConversationList[0].id) {
+ emits('onConversationClick', filterConversationList[0])
+ }
+ }
+}
+
+/** 鍒犻櫎鑱婂ぉ瀵硅瘽 */
+const deleteChatConversation = async (conversation: ChatConversationVO) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm(`鏄惁纭鍒犻櫎瀵硅瘽 - ${conversation.title}?`)
+ // 鍙戣捣鍒犻櫎
+ await ChatConversationApi.deleteChatConversationMy(conversation.id)
+ message.success('瀵硅瘽宸插垹闄�')
+ // 鍒锋柊鍒楄〃
+ await getChatConversationList()
+ // 鍥炶皟
+ emits('onConversationDelete', conversation)
+ } catch {}
+}
+
+/** 娓呯┖瀵硅瘽 */
+const handleClearConversation = async () => {
+ try {
+ await message.confirm('纭鍚庡璇濅細鍏ㄩ儴娓呯┖锛岀疆椤剁殑瀵硅瘽闄ゅ銆�')
+ await ChatConversationApi.deleteChatConversationMyByUnpinned()
+ ElMessage({
+ message: '鎿嶄綔鎴愬姛!',
+ type: 'success'
+ })
+ // 娓呯┖ 瀵硅瘽 鍜� 瀵硅瘽鍐呭
+ activeConversationId.value = null
+ // 鑾峰彇 瀵硅瘽鍒楄〃
+ await getChatConversationList()
+ // 鍥炶皟 鏂规硶
+ emits('onConversationClear')
+ } catch {}
+}
+
+/** 瀵硅瘽缃《 */
+const handleTop = async (conversation: ChatConversationVO) => {
+ // 鏇存柊瀵硅瘽缃《
+ conversation.pinned = !conversation.pinned
+ await ChatConversationApi.updateChatConversationMy(conversation)
+ // 鍒锋柊瀵硅瘽
+ await getChatConversationList()
+}
+
+// ============ 瑙掕壊浠撳簱 ============
+
+/** 瑙掕壊浠撳簱鎶藉眽 */
+const roleRepositoryOpen = ref<boolean>(false) // 瑙掕壊浠撳簱鏄惁鎵撳紑
+const handleRoleRepository = async () => {
+ roleRepositoryOpen.value = !roleRepositoryOpen.value
+}
+
+/** 鐩戝惉閫変腑鐨勫璇� */
+const { activeId } = toRefs(props)
+watch(activeId, async (newValue, oldValue) => {
+ activeConversationId.value = newValue as string
+})
+
+// 瀹氫箟 public 鏂规硶
+defineExpose({ createConversation })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ // 鑾峰彇 瀵硅瘽鍒楄〃
+ await getChatConversationList()
+ // 榛樿閫変腑
+ if (props.activeId) {
+ activeConversationId.value = props.activeId
+ } else {
+ // 棣栨榛樿閫変腑绗竴涓�
+ if (conversationList.value.length) {
+ activeConversationId.value = conversationList.value[0].id
+ // 鍥炶皟 onConversationClick
+ await emits('onConversationClick', conversationList.value[0])
+ }
+ }
+})
+</script>
diff --git a/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue b/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue
new file mode 100644
index 0000000..90f68c6
--- /dev/null
+++ b/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue
@@ -0,0 +1,148 @@
+<template>
+ <Dialog title="璁惧畾" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="130px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="瑙掕壊璁惧畾" prop="systemMessage">
+ <el-input
+ type="textarea"
+ v-model="formData.systemMessage"
+ :rows="4"
+ placeholder="璇疯緭鍏ヨ鑹茶瀹�"
+ />
+ </el-form-item>
+ <el-form-item label="妯″瀷" prop="modelId">
+ <el-select v-model="formData.modelId" placeholder="璇烽�夋嫨妯″瀷">
+ <el-option
+ v-for="model in models"
+ :key="model.id"
+ :label="model.name"
+ :value="model.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娓╁害鍙傛暟" prop="temperature">
+ <el-input-number
+ v-model="formData.temperature"
+ placeholder="璇疯緭鍏ユ俯搴﹀弬鏁�"
+ :min="0"
+ :max="2"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="鍥炲鏁� Token 鏁�" prop="maxTokens">
+ <el-input-number
+ v-model="formData.maxTokens"
+ placeholder="璇疯緭鍏ュ洖澶嶆暟 Token 鏁�"
+ :min="0"
+ :max="8192"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="涓婁笅鏂囨暟閲�" prop="maxContexts">
+ <el-input-number
+ v-model="formData.maxContexts"
+ placeholder="璇疯緭鍏ヤ笂涓嬫枃鏁伴噺"
+ :min="0"
+ :max="20"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+
+/** AI 鑱婂ぉ瀵硅瘽鐨勬洿鏂拌〃鍗� */
+defineOptions({ name: 'ChatConversationUpdateForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ systemMessage: undefined,
+ modelId: undefined,
+ temperature: undefined,
+ maxTokens: undefined,
+ maxContexts: undefined
+})
+const formRules = reactive({
+ modelId: [{ required: true, message: '妯″瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ temperature: [{ required: true, message: '娓╁害鍙傛暟涓嶈兘涓虹┖', trigger: 'blur' }],
+ maxTokens: [{ required: true, message: '鍥炲鏁� Token 鏁颁笉鑳戒负绌�', trigger: 'blur' }],
+ maxContexts: [{ required: true, message: '涓婁笅鏂囨暟閲忎笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const models = ref([] as ModelVO[]) // 鑱婂ぉ妯″瀷鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = await ChatConversationApi.getChatConversationMy(id)
+ formData.value = Object.keys(formData.value).reduce((obj, key) => {
+ if (data.hasOwnProperty(key)) {
+ obj[key] = data[key]
+ }
+ return obj
+ }, {})
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱涓嬫媺鏁版嵁
+ models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ChatConversationVO
+ await ChatConversationApi.updateChatConversationMy(data)
+ message.success('瀵硅瘽閰嶇疆宸叉洿鏂�')
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ systemMessage: undefined,
+ modelId: undefined,
+ temperature: undefined,
+ maxTokens: undefined,
+ maxContexts: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/chat/index/components/message/MessageFileUpload.vue b/src/views/ai/chat/index/components/message/MessageFileUpload.vue
new file mode 100644
index 0000000..8d7dc0f
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageFileUpload.vue
@@ -0,0 +1,394 @@
+<template>
+ <div
+ class="relative inline-block"
+ @mouseenter="showTooltipHandler"
+ @mouseleave="hideTooltipHandler"
+ >
+ <!-- 鏂囦欢涓婁紶鎸夐挳 -->
+ <el-button
+ v-if="!disabled"
+ circle
+ size="small"
+ class="upload-btn relative transition-all-200ms"
+ :class="{ 'has-files': fileList.length > 0 }"
+ @click="triggerFileInput"
+ :disabled="fileList.length >= limit"
+ >
+ <Icon icon="ep:paperclip" :size="16" />
+ <!-- 鏂囦欢鏁伴噺寰界珷 -->
+ <span
+ v-if="fileList.length > 0"
+ class="absolute -top-1 -right-1 bg-red-500 text-white text-10px px-1 rounded-8px min-w-4 h-4 flex items-center justify-center leading-none font-medium"
+ >
+ {{ fileList.length }}
+ </span>
+ </el-button>
+
+ <!-- 闅愯棌鐨勬枃浠惰緭鍏ユ -->
+ <input
+ ref="fileInputRef"
+ type="file"
+ multiple
+ style="display: none"
+ :accept="acceptTypes"
+ @change="handleFileSelect"
+ />
+
+ <!-- Hover 鏄剧ず鐨勬枃浠跺垪琛� -->
+ <div
+ v-if="fileList.length > 0 && showTooltip"
+ class="file-tooltip"
+ @mouseenter="showTooltipHandler"
+ @mouseleave="hideTooltipHandler"
+ >
+ <div class="tooltip-arrow"></div>
+ <div class="max-h-200px overflow-y-auto file-list">
+ <div
+ v-for="(file, index) in fileList"
+ :key="index"
+ class="flex items-center justify-between p-2 mb-1 bg-gray-50 rounded-6px text-12px transition-all-200ms last:mb-0 hover:bg-gray-100"
+ :class="{ 'opacity-70': file.uploading }"
+ >
+ <div class="flex items-center flex-1 min-w-0">
+ <Icon :icon="getFileIcon(file.name)" class="text-blue-500 mr-2 flex-shrink-0" />
+ <span
+ class="font-medium text-gray-900 mr-1 overflow-hidden text-ellipsis whitespace-nowrap flex-1"
+ >{{ file.name }}</span
+ >
+ <span class="text-gray-500 flex-shrink-0 text-11px"
+ >({{ formatFileSize(file.size) }})</span
+ >
+ </div>
+ <div class="flex items-center gap-1 flex-shrink-0 ml-2">
+ <el-progress
+ v-if="file.uploading"
+ :percentage="file.progress || 0"
+ :show-text="false"
+ size="small"
+ class="w-60px"
+ />
+ <el-button
+ v-else-if="!disabled"
+ link
+ type="danger"
+ size="small"
+ @click="removeFile(index)"
+ >
+ <Icon icon="ep:close" :size="12" />
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { formatFileSize, getFileIcon } from '@/utils/file'
+
+export interface FileItem {
+ name: string
+ size: number
+ url?: string
+ uploading?: boolean
+ progress?: number
+ raw?: File
+}
+
+defineOptions({ name: 'MessageFileUpload' })
+
+const props = defineProps({
+ modelValue: {
+ type: Array as PropType<string[]>,
+ default: () => []
+ },
+ limit: {
+ type: Number,
+ default: 5
+ },
+ maxSize: {
+ type: Number,
+ default: 10 // MB
+ },
+ acceptTypes: {
+ type: String,
+ default: '.jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.txt,.xls,.xlsx,.ppt,.pptx,.csv,.md'
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const emit = defineEmits(['update:modelValue', 'upload-success', 'upload-error'])
+
+const fileInputRef = ref<HTMLInputElement>()
+const fileList = ref<FileItem[]>([]) // 鍐呴儴绠$悊鏂囦欢鍒楄〃
+const uploadedUrls = ref<string[]>([]) // 宸蹭笂浼犵殑 URL 鍒楄〃
+const showTooltip = ref(false) // 鎺у埗 tooltip 鏄剧ず
+const hideTimer = ref<NodeJS.Timeout | null>(null) // 闅愯棌寤惰繜瀹氭椂鍣�
+const message = useMessage()
+const { httpRequest } = useUpload()
+
+/** 鐩戝惉 v-model 鍙樺寲 */
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ uploadedUrls.value = [...newVal]
+ // 濡傛灉澶栭儴娓呯┖浜� URLs锛屼篃娓呯┖鍐呴儴鏂囦欢鍒楄〃
+ if (newVal.length === 0) {
+ fileList.value = []
+ }
+ },
+ { immediate: true, deep: true }
+)
+
+/** 瑙﹀彂鏂囦欢閫夋嫨 */
+const triggerFileInput = () => {
+ fileInputRef.value?.click()
+}
+
+/** 鏄剧ず tooltip */
+const showTooltipHandler = () => {
+ if (hideTimer.value) {
+ clearTimeout(hideTimer.value)
+ hideTimer.value = null
+ }
+ showTooltip.value = true
+}
+
+/** 闅愯棌 tooltip */
+const hideTooltipHandler = () => {
+ hideTimer.value = setTimeout(() => {
+ showTooltip.value = false
+ hideTimer.value = null
+ }, 300) // 300ms 寤惰繜闅愯棌
+}
+
+/** 澶勭悊鏂囦欢閫夋嫨 */
+const handleFileSelect = (event: Event) => {
+ const target = event.target as HTMLInputElement
+ const files = Array.from(target.files || [])
+ if (files.length === 0) {
+ return
+ }
+ // 妫�鏌ユ�绘枃浠舵暟鏄惁瓒呰繃闄愬埗
+ if (files.length + fileList.value.length > props.limit) {
+ message.error(`鏈�澶氬彧鑳戒笂浼� ${props.limit} 涓枃浠禶)
+ target.value = '' // 娓呯┖杈撳叆
+ return
+ }
+ // 澶勭悊姣忎釜鏂囦欢
+ files.forEach((file) => {
+ if (file.size > props.maxSize * 1024 * 1024) {
+ message.error(`鏂囦欢 ${file.name} 澶у皬瓒呰繃 ${props.maxSize}MB`)
+ return
+ }
+ const fileItem: FileItem = {
+ name: file.name,
+ size: file.size,
+ uploading: true,
+ progress: 0,
+ raw: file
+ }
+ fileList.value.push(fileItem)
+ // 绔嬪嵆寮�濮嬩笂浼�
+ uploadFile(fileItem)
+ })
+
+ // 娓呯┖ input 鍊硷紝鍏佽閲嶅閫夋嫨鐩稿悓鏂囦欢
+ target.value = ''
+}
+
+/** 涓婁紶鏂囦欢 */
+const uploadFile = async (fileItem: FileItem) => {
+ try {
+ // 妯℃嫙涓婁紶杩涘害
+ const progressInterval = setInterval(() => {
+ if (fileItem.progress! < 90) {
+ fileItem.progress = (fileItem.progress || 0) + Math.random() * 10
+ }
+ }, 100)
+
+ // 璋冪敤涓婁紶鎺ュ彛
+ // const formData = new FormData()
+ // formData.append('file', fileItem.raw!)
+ const response = await httpRequest({
+ file: fileItem.raw!,
+ filename: fileItem.name
+ } as any)
+ fileItem.uploading = false
+ fileItem.progress = 100
+ fileItem.url = (response as any).data
+ // 娣诲姞鍒� URL 鍒楄〃
+ uploadedUrls.value.push(fileItem.url!)
+
+ clearInterval(progressInterval)
+
+ emit('upload-success', fileItem)
+ updateModelValue()
+ } catch (error) {
+ fileItem.uploading = false
+ message.error(`鏂囦欢 ${fileItem.name} 涓婁紶澶辫触`)
+ emit('upload-error', error)
+
+ // 绉婚櫎涓婁紶澶辫触鐨勬枃浠�
+ const index = fileList.value.indexOf(fileItem)
+ if (index > -1) {
+ removeFile(index)
+ }
+ }
+}
+
+/** 鍒犻櫎鏂囦欢 */
+const removeFile = (index: number) => {
+ // 浠� URL 鍒楄〃涓Щ闄�
+ const removedFile = fileList.value[index]
+ fileList.value.splice(index, 1)
+ if (removedFile.url) {
+ const urlIndex = uploadedUrls.value.indexOf(removedFile.url)
+ if (urlIndex > -1) {
+ uploadedUrls.value.splice(urlIndex, 1)
+ }
+ }
+
+ updateModelValue()
+}
+
+/** 鏇存柊 v-model */
+const updateModelValue = () => {
+ emit('update:modelValue', [...uploadedUrls.value])
+}
+
+// 鏆撮湶鏂规硶
+defineExpose({
+ triggerFileInput,
+ clearFiles: () => {
+ fileList.value = []
+ uploadedUrls.value = []
+ updateModelValue()
+ }
+})
+
+// 缁勪欢閿�姣佹椂娓呯悊瀹氭椂鍣�
+onUnmounted(() => {
+ if (hideTimer.value) {
+ clearTimeout(hideTimer.value)
+ }
+})
+</script>
+
+<style scoped>
+/* 涓婁紶鎸夐挳鏍峰紡 */
+.upload-btn {
+ --el-button-bg-color: transparent;
+ --el-button-border-color: transparent;
+ --el-button-hover-bg-color: var(--el-fill-color-light);
+ --el-button-hover-border-color: transparent;
+ color: var(--el-text-color-regular);
+}
+
+.upload-btn.has-files {
+ color: var(--el-color-primary);
+ --el-button-hover-bg-color: var(--el-color-primary-light-9);
+}
+
+.file-tooltip {
+ position: absolute;
+ bottom: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ background: white;
+ border: 1px solid var(--el-border-color-light);
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 1000;
+ min-width: 240px;
+ max-width: 320px;
+ padding: 8px;
+ animation: fadeInDown 0.2s ease;
+}
+
+.tooltip-arrow {
+ position: absolute;
+ bottom: -5px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 5px solid var(--el-border-color-light);
+}
+
+/* Tooltip 绠ご浼厓绱� */
+.tooltip-arrow::after {
+ content: '';
+ position: absolute;
+ bottom: 1px;
+ left: -4px;
+ width: 0;
+ height: 0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid white;
+}
+
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+@keyframes fadeInDown {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+/* 婊氬姩鏉℃牱寮� */
+.file-list::-webkit-scrollbar {
+ width: 4px;
+}
+
+.file-list::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.file-list::-webkit-scrollbar-thumb {
+ background: var(--el-border-color-light);
+ border-radius: 2px;
+}
+
+.file-list::-webkit-scrollbar-thumb:hover {
+ background: var(--el-border-color);
+}
+/* 婊氬姩鏉℃牱寮� */
+.file-list::-webkit-scrollbar {
+ width: 4px;
+}
+
+.file-list::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.file-list::-webkit-scrollbar-thumb {
+ background: var(--el-border-color-light);
+ border-radius: 2px;
+}
+
+.file-list::-webkit-scrollbar-thumb:hover {
+ background: var(--el-border-color);
+}
+</style>
diff --git a/src/views/ai/chat/index/components/message/MessageFiles.vue b/src/views/ai/chat/index/components/message/MessageFiles.vue
new file mode 100644
index 0000000..71969f1
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageFiles.vue
@@ -0,0 +1,66 @@
+<template>
+ <div v-if="attachmentUrls && attachmentUrls.length > 0" class="mt-2">
+ <div class="flex flex-wrap gap-2">
+ <div
+ v-for="(url, index) in attachmentUrls"
+ :key="index"
+ class="flex items-center p-3 bg-gray-1 rounded-2 cursor-pointer transition-all duration-200 min-w-40 max-w-70 border border-transparent hover:(bg-gray-2 -translate-y-1 shadow-lg)"
+ @click="handleFileClick(url)"
+ >
+ <div class="mr-3 flex-shrink-0">
+ <div
+ class="flex items-center justify-center w-8 h-8 rounded-1.5 text-white font-bold"
+ :class="getFileTypeClass(getFileNameFromUrl(url))"
+ >
+ <Icon :icon="getFileIcon(getFileNameFromUrl(url))" :size="20" />
+ </div>
+ </div>
+ <div class="flex-1 min-w-0">
+ <div
+ class="text-sm font-medium text-gray-8 leading-tight mb-1 overflow-hidden text-ellipsis whitespace-nowrap"
+ :title="getFileNameFromUrl(url)"
+ >
+ {{ getFileNameFromUrl(url) }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { getFileIcon, getFileNameFromUrl, isImage } from '@/utils/file'
+
+defineOptions({ name: 'MessageFiles' })
+
+defineProps<{
+ attachmentUrls?: string[]
+}>()
+
+/** 鑾峰彇鏂囦欢绫诲瀷鏍峰紡绫� */
+const getFileTypeClass = (filename: string): string => {
+ const ext = filename.split('.').pop()?.toLowerCase() || ''
+ if (isImage(ext)) {
+ return 'bg-gradient-to-br from-yellow-4 to-orange-5'
+ } else if (['pdf'].includes(ext)) {
+ return 'bg-gradient-to-br from-red-5 to-red-7'
+ } else if (['doc', 'docx'].includes(ext)) {
+ return 'bg-gradient-to-br from-blue-6 to-blue-8'
+ } else if (['xls', 'xlsx'].includes(ext)) {
+ return 'bg-gradient-to-br from-green-6 to-green-8'
+ } else if (['ppt', 'pptx'].includes(ext)) {
+ return 'bg-gradient-to-br from-orange-6 to-orange-8'
+ } else if (['mp3', 'wav', 'm4a', 'aac'].includes(ext)) {
+ return 'bg-gradient-to-br from-purple-5 to-purple-7'
+ } else if (['mp4', 'avi', 'mov', 'wmv'].includes(ext)) {
+ return 'bg-gradient-to-br from-red-5 to-red-7'
+ } else {
+ return 'bg-gradient-to-br from-gray-5 to-gray-7'
+ }
+}
+
+/** 鐐瑰嚮鏂囦欢 */
+const handleFileClick = (url: string) => {
+ window.open(url, '_blank')
+}
+</script>
diff --git a/src/views/ai/chat/index/components/message/MessageKnowledge.vue b/src/views/ai/chat/index/components/message/MessageKnowledge.vue
new file mode 100644
index 0000000..5ff3f63
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageKnowledge.vue
@@ -0,0 +1,104 @@
+<!-- 鐭ヨ瘑寮曠敤缁勪欢 -->
+<template>
+ <!-- 鐭ヨ瘑寮曠敤鍒楄〃 -->
+ <div v-if="segments && segments.length > 0" class="mt-10px p-10px rounded-8px bg-[#f5f5f5]">
+ <div class="text-14px text-[#666] mb-8px flex items-center">
+ <Icon icon="ep:document" class="mr-5px" /> 鐭ヨ瘑寮曠敤
+ </div>
+ <div class="flex flex-wrap gap-8px">
+ <div
+ v-for="(doc, index) in documentList"
+ :key="index"
+ class="p-8px px-12px bg-white rounded-6px cursor-pointer transition-all hover:bg-[#e6f4ff]"
+ @click="handleClick(doc)"
+ >
+ <div class="text-14px text-[#333] mb-4px">
+ {{ doc.title }}
+ <span class="text-12px text-[#999] ml-4px">锛坽{ doc.segments.length }} 鏉★級</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鐭ヨ瘑寮曠敤璇︽儏寮圭獥 -->
+ <el-popover
+ v-model:visible="dialogVisible"
+ :width="600"
+ trigger="click"
+ placement="top-start"
+ :offset="55"
+ popper-class="knowledge-popover"
+ >
+ <template #reference>
+ <div ref="documentRef"></div>
+ </template>
+ <template #default>
+ <div class="text-16px font-bold mb-12px">{{ document?.title }}</div>
+ <div class="max-h-[60vh] overflow-y-auto">
+ <div
+ v-for="(segment, index) in document?.segments"
+ :key="index"
+ class="p-12px border-b-solid border-b-[#eee] last:border-b-0"
+ >
+ <div
+ class="block mb-8px px-8px py-2px bg-[#f5f5f5] rounded-4px text-12px text-[#666] w-fit"
+ >
+ 鍒嗘 {{ segment.id }}
+ </div>
+ <div class="text-14px leading-[1.6] text-[#333] mt-[10px]">
+ {{ segment.content }}
+ </div>
+ </div>
+ </div>
+ </template>
+ </el-popover>
+</template>
+
+<script setup lang="ts">
+const props = defineProps<{
+ segments: {
+ id: number
+ documentId: number
+ documentName: string
+ content: string
+ }[]
+}>()
+
+const document = ref<{
+ id: number
+ title: string
+ segments: {
+ id: number
+ content: string
+ }[]
+} | null>(null) // 鐭ヨ瘑搴撴枃妗e垪琛�
+const dialogVisible = ref(false) // 鐭ヨ瘑寮曠敤璇︽儏寮圭獥
+const documentRef = ref<HTMLElement>() // 鐭ヨ瘑寮曠敤璇︽儏寮圭獥 Ref
+
+/** 鎸夌収 document 鑱氬悎 segments */
+const documentList = computed(() => {
+ if (!props.segments) return []
+
+ const docMap = new Map()
+ props.segments.forEach((segment) => {
+ if (!docMap.has(segment.documentId)) {
+ docMap.set(segment.documentId, {
+ id: segment.documentId,
+ title: segment.documentName,
+ segments: []
+ })
+ }
+ docMap.get(segment.documentId).segments.push({
+ id: segment.id,
+ content: segment.content
+ })
+ })
+ return Array.from(docMap.values())
+})
+
+/** 鐐瑰嚮 document 澶勭悊 */
+const handleClick = (doc: any) => {
+ document.value = doc
+ dialogVisible.value = true
+}
+</script>
diff --git a/src/views/ai/chat/index/components/message/MessageList.vue b/src/views/ai/chat/index/components/message/MessageList.vue
new file mode 100644
index 0000000..da5c338
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageList.vue
@@ -0,0 +1,226 @@
+<template>
+ <div ref="messageContainer" class="h-100% overflow-y-auto relative">
+ <div class="flex flex-col overflow-y-hidden px-20px" v-for="(item, index) in list" :key="index">
+ <!-- 闈犲乏 message锛歴ystem銆乤ssistant 绫诲瀷 -->
+ <div class="flex flex-row mt-50px" v-if="item.type !== 'user'">
+ <div class="avatar">
+ <el-avatar :src="roleAvatar" />
+ </div>
+ <div class="flex flex-col text-left mx-15px">
+ <div>
+ <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
+ </div>
+ <div
+ class="relative flex flex-col break-words bg-[var(--el-fill-color-light)] shadow-[0_0_0_1px_var(--el-border-color-light)] rounded-10px pt-10px px-10px pb-5px"
+ ref="markdownViewRef"
+ >
+ <MessageReasoning
+ :reasoning-content="item.reasoningContent || ''"
+ :content="item.content || ''"
+ />
+ <MarkdownView
+ class="text-[var(--el-text-color-primary)] text-[0.95rem]"
+ :content="item.content"
+ />
+ <MessageFiles :attachment-urls="item.attachmentUrls" />
+ <MessageKnowledge v-if="item.segments" :segments="item.segments" />
+ <MessageWebSearch v-if="item.webSearchPages" :web-search-pages="item.webSearchPages" />
+ </div>
+ <div class="flex flex-row mt-8px">
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="copyContent(item.content)"
+ >
+ <img class="h-20px" src="@/assets/ai/copy.svg" />
+ </el-button>
+ <el-button
+ v-if="item.id > 0"
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onDelete(item.id)"
+ >
+ <img class="h-17px" src="@/assets/ai/delete.svg" />
+ </el-button>
+ </div>
+ </div>
+ </div>
+ <!-- 闈犲彸 message锛歶ser 绫诲瀷 -->
+ <div class="flex flex-row-reverse justify-start mt-50px" v-if="item.type === 'user'">
+ <div class="avatar">
+ <el-avatar :src="userAvatar" />
+ </div>
+ <div class="flex flex-col text-left mx-15px">
+ <div>
+ <el-text class="text-left leading-30px">{{ formatDate(item.createTime) }}</el-text>
+ </div>
+ <!-- 闄勪欢鏄剧ず琛� -->
+ <div
+ v-if="item.attachmentUrls && item.attachmentUrls.length > 0"
+ class="flex flex-row-reverse mb-8px"
+ >
+ <MessageFiles :attachment-urls="item.attachmentUrls" />
+ </div>
+ <!-- 鏂囨湰鍐呭琛� -->
+ <div class="flex flex-row-reverse">
+ <div
+ v-if="item.content && item.content.trim()"
+ class="text-[0.95rem] text-[var(--el-color-white)] inline bg-[var(--el-color-primary)] shadow-[0_0_0_1px_var(--el-color-primary)] rounded-10px p-10px w-auto break-words whitespace-pre-wrap"
+ >
+ {{ item.content }}
+ </div>
+ </div>
+ <div class="flex flex-row-reverse mt-8px">
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="copyContent(item.content)"
+ >
+ <img class="h-20px" src="@/assets/ai/copy.svg" />
+ </el-button>
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onDelete(item.id)"
+ >
+ <img class="h-17px mr-12px" src="@/assets/ai/delete.svg" />
+ </el-button>
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onRefresh(item)"
+ >
+ <el-icon size="17"><RefreshRight /></el-icon>
+ </el-button>
+ <el-button
+ class="flex bg-transparent items-center hover:cursor-pointer hover:bg-[var(--el-fill-color-lighter)]"
+ link
+ @click="onEdit(item)"
+ >
+ <el-icon size="17"><Edit /></el-icon>
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 鍥炲埌搴曢儴 -->
+ <div v-if="isScrolling" class="absolute z-1000 bottom-0 right-50%" @click="handleGoBottom">
+ <el-button :icon="ArrowDownBold" circle />
+ </div>
+</template>
+<script setup lang="ts">
+import { PropType } from 'vue'
+import { formatDate } from '@/utils/formatTime'
+import MarkdownView from '@/components/MarkdownView/index.vue'
+import MessageKnowledge from './MessageKnowledge.vue'
+import MessageReasoning from './MessageReasoning.vue'
+import MessageFiles from './MessageFiles.vue'
+import MessageWebSearch from './MessageWebSearch.vue'
+import { useClipboard } from '@vueuse/core'
+import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
+import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
+import { ChatConversationVO } from '@/api/ai/chat/conversation'
+import { useUserStore } from '@/store/modules/user'
+import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
+import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { copy } = useClipboard({ legacy: true }) // 鍒濆鍖� copy 鍒扮矘璐存澘
+const userStore = useUserStore()
+
+// 鍒ゆ柇鈥滄秷鎭垪琛ㄢ�濇粴鍔ㄧ殑浣嶇疆(鐢ㄤ簬鍒ゆ柇鏄惁闇�瑕佹粴鍔ㄥ埌娑堟伅鏈�涓嬫柟)
+const messageContainer: any = ref(null)
+const isScrolling = ref(false) //鐢ㄤ簬鍒ゆ柇鐢ㄦ埛鏄惁鍦ㄦ粴鍔�
+
+const userAvatar = computed(() => userStore.user.avatar || userAvatarDefaultImg)
+const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
+
+// 瀹氫箟 props
+const props = defineProps({
+ conversation: {
+ type: Object as PropType<ChatConversationVO>,
+ required: true
+ },
+ list: {
+ type: Array as PropType<ChatMessageVO[]>,
+ required: true
+ }
+})
+
+const { list } = toRefs(props) // 娑堟伅鍒楄〃
+
+const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 瀹氫箟 emits
+
+// ============ 澶勭悊瀵硅瘽婊氬姩 ==============
+
+/** 婊氬姩鍒板簳閮� */
+const scrollToBottom = async (isIgnore?: boolean) => {
+ // 娉ㄦ剰瑕佷娇鐢� nextTick 浠ュ厤鑾峰彇涓嶅埌 dom
+ await nextTick()
+ if (isIgnore || !isScrolling.value) {
+ messageContainer.value.scrollTop =
+ messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
+ }
+}
+
+function handleScroll() {
+ const scrollContainer = messageContainer.value
+ const scrollTop = scrollContainer.scrollTop
+ const scrollHeight = scrollContainer.scrollHeight
+ const offsetHeight = scrollContainer.offsetHeight
+ if (scrollTop + offsetHeight < scrollHeight - 100) {
+ // 鐢ㄦ埛寮�濮嬫粴鍔ㄥ苟鍦ㄦ渶搴曢儴涔嬩笂锛屽彇娑堜繚鎸佸湪鏈�搴曢儴鐨勬晥鏋�
+ isScrolling.value = true
+ } else {
+ // 鐢ㄦ埛鍋滄婊氬姩骞舵粴鍔ㄥ埌鏈�搴曢儴锛屽紑鍚繚鎸佸埌鏈�搴曢儴鐨勬晥鏋�
+ isScrolling.value = false
+ }
+}
+
+/** 鍥炲埌搴曢儴 */
+const handleGoBottom = async () => {
+ const scrollContainer = messageContainer.value
+ scrollContainer.scrollTop = scrollContainer.scrollHeight
+}
+
+/** 鍥炲埌椤堕儴 */
+const handlerGoTop = async () => {
+ const scrollContainer = messageContainer.value
+ scrollContainer.scrollTop = 0
+}
+
+defineExpose({ scrollToBottom, handlerGoTop }) // 鎻愪緵鏂规硶缁� parent 璋冪敤
+
+// ============ 澶勭悊娑堟伅鎿嶄綔 ==============
+
+/** 澶嶅埗 */
+const copyContent = async (content: string) => {
+ await copy(content)
+ message.success('澶嶅埗鎴愬姛锛�')
+}
+
+/** 鍒犻櫎 */
+const onDelete = async (id) => {
+ // 鍒犻櫎 message
+ await ChatMessageApi.deleteChatMessage(id)
+ message.success('鍒犻櫎鎴愬姛锛�')
+ // 鍥炶皟
+ emits('onDeleteSuccess')
+}
+
+/** 鍒锋柊 */
+const onRefresh = async (message: ChatMessageVO) => {
+ emits('onRefresh', message)
+}
+
+/** 缂栬緫 */
+const onEdit = async (message: ChatMessageVO) => {
+ emits('onEdit', message)
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ messageContainer.value.addEventListener('scroll', handleScroll)
+})
+</script>
diff --git a/src/views/ai/chat/index/components/message/MessageListEmpty.vue b/src/views/ai/chat/index/components/message/MessageListEmpty.vue
new file mode 100644
index 0000000..11f7f68
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageListEmpty.vue
@@ -0,0 +1,36 @@
+<!-- 娑堟伅鍒楄〃涓虹┖鏃讹紝灞曠ず prompt 鍒楄〃 -->
+<template>
+ <div class="relative flex flex-row justify-center w-full h-full">
+ <!-- title -->
+ <div class="flex flex-col justify-center">
+ <div class="text-28px font-bold text-center">鑺嬮亾 AI</div>
+ <div class="flex flex-row flex-wrap items-center justify-center w-460px mt-20px">
+ <div
+ class="flex justify-center w-180px leading-50px border border-solid border-[#e4e4e4] rounded-10px m-10px cursor-pointer hover:bg-[rgba(243,243,243,0.73)]"
+ v-for="prompt in promptList"
+ :key="prompt.prompt"
+ @click="handlerPromptClick(prompt)"
+ >
+ {{ prompt.prompt }}
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+const promptList = [
+ {
+ prompt: '浠婂ぉ姘旀�庝箞鏍�?'
+ },
+ {
+ prompt: '鍐欎竴棣栧ソ鍚殑璇楁瓕?'
+ }
+] // prompt 鍒楄〃
+
+const emits = defineEmits(['onPrompt'])
+
+/** 閫変腑 prompt 鐐瑰嚮 */
+const handlerPromptClick = async ({ prompt }) => {
+ emits('onPrompt', prompt)
+}
+</script>
diff --git a/src/views/ai/chat/index/components/message/MessageLoading.vue b/src/views/ai/chat/index/components/message/MessageLoading.vue
new file mode 100644
index 0000000..ecf5773
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageLoading.vue
@@ -0,0 +1,6 @@
+<!-- message 鍔犺浇椤甸潰 -->
+<template>
+ <div class="p-30px">
+ <el-skeleton animated />
+ </div>
+</template>
diff --git a/src/views/ai/chat/index/components/message/MessageNewConversation.vue b/src/views/ai/chat/index/components/message/MessageNewConversation.vue
new file mode 100644
index 0000000..e9c67d6
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageNewConversation.vue
@@ -0,0 +1,19 @@
+<!-- 鏃犺亰澶╁璇濇椂锛屽湪 message 鍖哄煙锛屽彲浠ユ柊澧炲璇� -->
+<template>
+ <div class="flex flex-row justify-center w-100% h-100%">
+ <div class="flex flex-col justify-center">
+ <div class="text-14px text-#858585">鐐瑰嚮涓嬫柟鎸夐挳锛屽紑濮嬩綘鐨勫璇濆惂</div>
+ <div class="flex flex-row justify-center mt-20px">
+ <el-button type="primary" round @click="handlerNewChat">鏂板缓瀵硅瘽</el-button>
+ </div>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+const emits = defineEmits(['onNewConversation'])
+
+/** 鏂板缓 conversation 鑱婂ぉ瀵硅瘽 */
+const handlerNewChat = () => {
+ emits('onNewConversation')
+}
+</script>
diff --git a/src/views/ai/chat/index/components/message/MessageReasoning.vue b/src/views/ai/chat/index/components/message/MessageReasoning.vue
new file mode 100644
index 0000000..7cf615c
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageReasoning.vue
@@ -0,0 +1,89 @@
+<template>
+ <div v-if="shouldShowComponent" class="mt-10px">
+ <!-- 鎺ㄧ悊杩囩▼鏍囬鏍� -->
+ <div
+ class="flex items-center justify-between cursor-pointer p-8px rounded-t-8px bg-gradient-to-r from-blue-50 to-purple-50 border border-b-0 border-gray-200/60 hover:from-blue-100 hover:to-purple-100 transition-all duration-200"
+ @click="toggleExpanded"
+ >
+ <div class="flex items-center gap-6px text-14px font-medium text-gray-700">
+ <el-icon :size="16" class="text-blue-600">
+ <ChatDotSquare />
+ </el-icon>
+ <span>{{ titleText }}</span>
+ </div>
+ <el-icon
+ :size="14"
+ class="text-gray-500 transition-transform duration-200"
+ :class="{ 'transform rotate-180': isExpanded }"
+ >
+ <ArrowDown />
+ </el-icon>
+ </div>
+
+ <!-- 鎺ㄧ悊鍐呭鍖哄煙 -->
+ <div
+ v-show="isExpanded"
+ class="max-h-300px overflow-y-auto p-12px bg-white/70 backdrop-blur-sm border border-t-0 border-gray-200/60 rounded-b-8px shadow-sm"
+ >
+ <MarkdownView
+ v-if="props.reasoningContent"
+ class="text-gray-700 text-13px leading-relaxed"
+ :content="props.reasoningContent"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue'
+import { ArrowDown, ChatDotSquare } from '@element-plus/icons-vue'
+import MarkdownView from '@/components/MarkdownView/index.vue'
+
+// 瀹氫箟 props
+const props = defineProps<{
+ reasoningContent?: string
+ content?: string
+}>()
+
+const isExpanded = ref(true) // 榛樿灞曞紑
+
+/** 璁$畻灞炴�э細鍒ゆ柇鏄惁搴旇鏄剧ず缁勪欢锛堟湁鎬濊�冨唴瀹规椂锛屽垯灞曠ず锛� */
+const shouldShowComponent = computed(() => {
+ return !(!props.reasoningContent || props.reasoningContent.trim() === '')
+})
+
+/** 璁$畻灞炴�э細鏍囬鏂囨湰 */
+const titleText = computed(() => {
+ const hasReasoningContent = props.reasoningContent && props.reasoningContent.trim() !== ''
+ const hasContent = props.content && props.content.trim() !== ''
+ if (hasReasoningContent && !hasContent) {
+ return '娣卞害鎬濊�冧腑'
+ }
+ return '宸叉繁搴︽�濊��'
+})
+
+/** 鍒囨崲灞曞紑/鏀剁缉鐘舵�� */
+const toggleExpanded = () => {
+ isExpanded.value = !isExpanded.value
+}
+</script>
+
+<style scoped>
+/* 鑷畾涔夋粴鍔ㄦ潯鏍峰紡 */
+.max-h-300px::-webkit-scrollbar {
+ width: 4px;
+}
+
+.max-h-300px::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.max-h-300px::-webkit-scrollbar-thumb {
+ background: rgba(156, 163, 175, 0.4);
+ border-radius: 2px;
+}
+
+.max-h-300px::-webkit-scrollbar-thumb:hover {
+ background: rgba(156, 163, 175, 0.6);
+}
+</style>
diff --git a/src/views/ai/chat/index/components/message/MessageWebSearch.vue b/src/views/ai/chat/index/components/message/MessageWebSearch.vue
new file mode 100644
index 0000000..f77e1ca
--- /dev/null
+++ b/src/views/ai/chat/index/components/message/MessageWebSearch.vue
@@ -0,0 +1,190 @@
+<!-- 鑱旂綉鎼滅储缁撴灉缁勪欢 -->
+<template>
+ <!-- 鑱旂綉鎼滅储缁撴灉鍒楄〃 -->
+ <div
+ v-if="webSearchPages && webSearchPages.length > 0"
+ class="mt-10px p-10px rounded-8px bg-[#f5f5f5]"
+ >
+ <!-- 鏍囬鏍忥細鍙偣鍑诲睍寮�/鏀惰捣 -->
+ <div
+ class="text-14px text-[#666] mb-8px flex items-center justify-between cursor-pointer hover:text-[#409eff]"
+ @click="toggleExpanded"
+ >
+ <div class="flex items-center">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鑱旂綉鎼滅储缁撴灉 ({{ webSearchPages.length }} 鏉�)
+ </div>
+ <Icon
+ :icon="isExpanded ? 'ep:arrow-up' : 'ep:arrow-down'"
+ class="text-12px transition-transform duration-200"
+ />
+ </div>
+
+ <!-- 鍙睍寮�鐨勬悳绱㈢粨鏋滃垪琛� -->
+ <div v-show="isExpanded" class="flex flex-col gap-8px transition-all duration-200 ease-in-out">
+ <div
+ v-for="(result, index) in webSearchPages"
+ :key="index"
+ class="p-10px bg-white rounded-6px cursor-pointer transition-all hover:bg-[#e6f4ff]"
+ @click="handleClick(result)"
+ >
+ <div class="flex items-start gap-8px">
+ <!-- 缃戠珯鍥炬爣 -->
+ <div class="flex-shrink-0 w-16px h-16px mt-2px">
+ <img
+ v-if="result.icon"
+ :src="result.icon"
+ :alt="result.name"
+ class="w-full h-full object-contain rounded-2px"
+ @error="handleImageError"
+ />
+ <Icon v-else icon="ep:link" class="w-full h-full text-[#666]" />
+ </div>
+
+ <!-- 鍐呭鍖哄煙 -->
+ <div class="flex-1 min-w-0">
+ <!-- 鏍囬鍜屾潵婧� -->
+ <div class="flex items-center gap-4px mb-4px">
+ <span class="text-12px text-[#999] truncate">{{ result.name }}</span>
+ </div>
+
+ <!-- 涓绘爣棰� -->
+ <div class="text-14px text-[#1a73e8] font-medium mb-4px line-clamp-2 leading-[1.4]">
+ {{ result.title }}
+ </div>
+
+ <!-- 鎻忚堪 -->
+ <div class="text-13px text-[#666] line-clamp-2 leading-[1.4] mb-4px">
+ {{ result.snippet }}
+ </div>
+
+ <!-- URL -->
+ <div class="text-12px text-[#006621] truncate">
+ {{ result.url }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鑱旂綉鎼滅储璇︽儏寮圭獥 -->
+ <el-popover
+ v-model:visible="dialogVisible"
+ :width="600"
+ trigger="click"
+ placement="top-start"
+ :offset="55"
+ popper-class="web-search-popover"
+ >
+ <template #reference>
+ <div ref="resultRef"></div>
+ </template>
+ <template #default>
+ <div v-if="selectedResult">
+ <!-- 鏍囬鍖哄煙 -->
+ <div class="flex items-start gap-8px mb-12px">
+ <div class="flex-shrink-0 w-20px h-20px mt-2px">
+ <img
+ v-if="selectedResult.icon"
+ :src="selectedResult.icon"
+ :alt="selectedResult.name"
+ class="w-full h-full object-contain rounded-2px"
+ @error="handleImageError"
+ />
+ <Icon v-else icon="ep:link" class="w-full h-full text-[#666]" />
+ </div>
+ <div class="flex-1 min-w-0">
+ <div class="text-16px font-bold text-[#333] mb-4px line-clamp-2">
+ {{ selectedResult.title }}
+ </div>
+ <div class="text-12px text-[#999] mb-4px">{{ selectedResult.name }}</div>
+ <div class="text-12px text-[#006621] break-all">{{ selectedResult.url }}</div>
+ </div>
+ </div>
+
+ <!-- 鍐呭鍖哄煙 -->
+ <div class="max-h-[60vh] overflow-y-auto">
+ <!-- 绠�鐭弿杩� -->
+ <div class="mb-12px">
+ <div class="text-14px font-medium text-[#333] mb-6px">绠�鐭弿杩�</div>
+ <div class="text-14px leading-[1.6] text-[#666] bg-[#f8f9fa] p-10px rounded-6px">
+ {{ selectedResult.snippet }}
+ </div>
+ </div>
+
+ <!-- 鍐呭鎽樿 -->
+ <div v-if="selectedResult.summary">
+ <div class="text-14px font-medium text-[#333] mb-6px">鍐呭鎽樿</div>
+ <div
+ class="text-14px leading-[1.6] text-[#333] bg-[#f8f9fa] p-10px rounded-6px whitespace-pre-wrap"
+ >
+ {{ selectedResult.summary }}
+ </div>
+ </div>
+ </div>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="flex justify-end gap-8px mt-12px pt-12px border-t border-[#eee]">
+ <el-button size="small" @click="dialogVisible = false">鍏抽棴</el-button>
+ <el-button type="primary" size="small" @click="openUrl(selectedResult.url)">
+ 璁块棶鍘熸枃
+ </el-button>
+ </div>
+ </div>
+ </template>
+ </el-popover>
+</template>
+
+<script setup lang="ts">
+defineProps<{
+ webSearchPages: {
+ name: string // 鍚嶇О
+ icon: string // 鍥炬爣
+ title: string // 鏍囬
+ url: string // URL
+ snippet: string // 鍐呭鐨勭畝鐭弿杩�
+ summary: string // 鍐呭鐨勬枃鏈憳瑕�
+ }[]
+}>()
+
+const isExpanded = ref(false) // 鏄惁灞曞紑鎼滅储缁撴灉
+const selectedResult = ref<{
+ name: string
+ icon: string
+ title: string
+ url: string
+ snippet: string
+ summary: string
+} | null>(null) // 閫変腑鐨勬悳绱㈢粨鏋�
+const dialogVisible = ref(false) // 璇︽儏寮圭獥
+const resultRef = ref<HTMLElement>() // 璇︽儏寮圭獥 Ref
+
+/** 鍒囨崲灞曞紑/鏀惰捣鐘舵�� */
+const toggleExpanded = () => {
+ isExpanded.value = !isExpanded.value
+}
+
+/** 鐐瑰嚮鎼滅储缁撴灉澶勭悊 */
+const handleClick = (result: any) => {
+ selectedResult.value = result
+ dialogVisible.value = true
+}
+
+/** 澶勭悊鍥剧墖鍔犺浇閿欒 */
+const handleImageError = (event: Event) => {
+ const img = event.target as HTMLImageElement
+ img.style.display = 'none'
+}
+
+/** 鎵撳紑URL */
+const openUrl = (url: string) => {
+ window.open(url, '_blank')
+}
+</script>
+
+<style scoped>
+.web-search-popover {
+ max-width: 600px;
+}
+</style>
diff --git a/src/views/ai/chat/index/components/role/RoleCategoryList.vue b/src/views/ai/chat/index/components/role/RoleCategoryList.vue
new file mode 100644
index 0000000..2efc821
--- /dev/null
+++ b/src/views/ai/chat/index/components/role/RoleCategoryList.vue
@@ -0,0 +1,39 @@
+<template>
+ <div class="flex flex-row flex-wrap items-center">
+ <div class="flex flex-row mr-10px" v-for="category in categoryList" :key="category">
+ <el-button
+ plain
+ round
+ size="small"
+ :type="category === active ? 'primary' : ''"
+ @click="handleCategoryClick(category)"
+ >
+ {{ category }}
+ </el-button>
+ </div>
+ </div>
+</template>
+<script setup lang="ts">
+import { PropType } from 'vue'
+
+// 瀹氫箟灞炴��
+defineProps({
+ categoryList: {
+ type: Array as PropType<string[]>,
+ required: true
+ },
+ active: {
+ type: String,
+ required: false,
+ default: '鍏ㄩ儴'
+ }
+})
+
+// 瀹氫箟鍥炶皟
+const emits = defineEmits(['onCategoryClick'])
+
+/** 澶勭悊鍒嗙被鐐瑰嚮浜嬩欢 */
+const handleCategoryClick = async (category: string) => {
+ emits('onCategoryClick', category)
+}
+</script>
diff --git a/src/views/ai/chat/index/components/role/RoleList.vue b/src/views/ai/chat/index/components/role/RoleList.vue
new file mode 100644
index 0000000..a1ecb35
--- /dev/null
+++ b/src/views/ai/chat/index/components/role/RoleList.vue
@@ -0,0 +1,106 @@
+<template>
+ <div
+ class="flex flex-row flex-wrap relative h-full overflow-auto pb-140px items-start content-start justify-start"
+ ref="tabsRef"
+ @scroll="handleTabsScroll"
+ >
+ <div v-for="role in roleList" :key="role.id">
+ <el-card
+ class="inline-block mr-20px rounded-10px mb-20px relative"
+ body-class="max-w-240px w-240px pt-15px px-15px pb-10px flex flex-row justify-start relative"
+ >
+ <!-- 鏇村鎿嶄綔 -->
+ <div class="absolute top-0 right-12px" v-if="showMore">
+ <el-dropdown @command="handleMoreClick">
+ <span class="el-dropdown-link">
+ <el-button type="text">
+ <el-icon><More /></el-icon>
+ </el-button>
+ </span>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item :command="['edit', role]">
+ <Icon icon="ep:edit" color="var(--el-text-color-placeholder)" />缂栬緫
+ </el-dropdown-item>
+ <el-dropdown-item :command="['delete', role]" style="color: var(--el-color-danger)">
+ <Icon icon="ep:delete" color="var(--el-color-danger)" />鍒犻櫎
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ <!-- 瑙掕壊淇℃伅 -->
+ <div>
+ <img class="w-40px h-40px rounded-10px overflow-hidden" :src="role.avatar" />
+ </div>
+ <div class="ml-10px w-full">
+ <div class="h-85px">
+ <div class="text-18px font-bold" style="color: var(--el-text-color-primary)">
+ {{ role.name }}
+ </div>
+ <div class="mt-10px text-14px" style="color: var(--el-text-color-regular)">
+ {{ role.description }}
+ </div>
+ </div>
+ <div class="flex flex-row-reverse mt-2px">
+ <el-button type="primary" size="small" @click="handleUseClick(role)">浣跨敤</el-button>
+ </div>
+ </div>
+ </el-card>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { ChatRoleVO } from '@/api/ai/model/chatRole'
+import { PropType, ref } from 'vue'
+import { More } from '@element-plus/icons-vue'
+
+const tabsRef = ref<any>() // tabs ref
+
+// 瀹氫箟灞炴��
+const props = defineProps({
+ loading: {
+ type: Boolean,
+ required: true
+ },
+ roleList: {
+ type: Array as PropType<ChatRoleVO[]>,
+ required: true
+ },
+ showMore: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+})
+
+// 瀹氫箟閽╁瓙
+const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage'])
+
+/** 鎿嶄綔锛氱紪杈戙�佸垹闄� */
+const handleMoreClick = async (data) => {
+ const type = data[0]
+ const role = data[1]
+ if (type === 'delete') {
+ emits('onDelete', role)
+ } else {
+ emits('onEdit', role)
+ }
+}
+
+/** 閫変腑 */
+const handleUseClick = (role: any) => {
+ emits('onUse', role)
+}
+
+/** 婊氬姩 */
+const handleTabsScroll = async () => {
+ if (tabsRef.value) {
+ const { scrollTop, scrollHeight, clientHeight } = tabsRef.value
+ if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) {
+ emits('onPage')
+ }
+ }
+}
+</script>
diff --git a/src/views/ai/chat/index/components/role/RoleRepository.vue b/src/views/ai/chat/index/components/role/RoleRepository.vue
new file mode 100644
index 0000000..0411373
--- /dev/null
+++ b/src/views/ai/chat/index/components/role/RoleRepository.vue
@@ -0,0 +1,246 @@
+<!-- chat 瑙掕壊浠撳簱 -->
+<template>
+ <el-container class="bg-[var(--el-bg-color)] -mt-25px">
+ <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
+ <!-- main -->
+ <el-main class="flex-1 overflow-hidden m-0 !p-0 relative">
+ <div class="mx-3 mt-3 mb-0 absolute right-0 -top-1.25 z-100">
+ <!-- 鎼滅储鎸夐挳 -->
+ <el-input
+ :loading="loading"
+ v-model="search"
+ class="!w-60"
+ size="default"
+ placeholder="璇疯緭鍏ユ悳绱㈢殑鍐呭"
+ :suffix-icon="Search"
+ @change="getActiveTabsRole"
+ />
+ <el-button
+ v-if="activeTab == 'my-role'"
+ type="primary"
+ @click="handlerAddRole"
+ class="ml-20px"
+ >
+ <Icon icon="ep:user" class="mr-1.25" />
+ 娣诲姞瑙掕壊
+ </el-button>
+ </div>
+ <!-- tabs -->
+ <el-tabs v-model="activeTab" @tab-click="handleTabsClick" class="relative h-full">
+ <el-tab-pane label="鎴戠殑瑙掕壊" name="my-role" class="flex flex-col h-full overflow-y-auto">
+ <RoleList
+ :loading="loading"
+ :role-list="myRoleList"
+ :show-more="true"
+ @on-delete="handlerCardDelete"
+ @on-edit="handlerCardEdit"
+ @on-use="handlerCardUse"
+ @on-page="handlerCardPage('my')"
+ class="mt-3"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍏叡瑙掕壊" name="public-role" class="!pt-2">
+ <RoleCategoryList
+ class="mx-3"
+ :category-list="categoryList"
+ :active="activeCategory"
+ @on-category-click="handlerCategoryClick"
+ />
+ <RoleList
+ :role-list="publicRoleList"
+ @on-delete="handlerCardDelete"
+ @on-edit="handlerCardEdit"
+ @on-use="handlerCardUse"
+ @on-page="handlerCardPage('public')"
+ class="mt-3"
+ loading
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-main>
+ </el-container>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import RoleList from './RoleList.vue'
+import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
+import RoleCategoryList from './RoleCategoryList.vue'
+import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole'
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import { Search } from '@element-plus/icons-vue'
+import { TabsPaneContext } from 'element-plus'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+const router = useRouter() // 璺敱瀵硅薄
+const { currentRoute } = useRouter() // 璺敱
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+
+// 灞炴�у畾涔�
+const loading = ref<boolean>(false) // 鍔犺浇涓�
+const activeTab = ref<string>('my-role') // 閫変腑鐨勮鑹� Tab
+const search = ref<string>('') // 鍔犺浇涓�
+const myRoleParams = reactive({
+ pageNo: 1,
+ pageSize: 50
+})
+const myRoleList = ref<ChatRoleVO[]>([]) // my 鍒嗛〉澶у皬
+const publicRoleParams = reactive({
+ pageNo: 1,
+ pageSize: 50
+})
+const publicRoleList = ref<ChatRoleVO[]>([]) // public 鍒嗛〉澶у皬
+const activeCategory = ref<string>('鍏ㄩ儴') // 閫夋嫨涓殑鍒嗙被
+const categoryList = ref<string[]>([]) // 瑙掕壊鍒嗙被绫诲埆
+
+/** tabs 鐐瑰嚮 */
+const handleTabsClick = async (tab: TabsPaneContext) => {
+ // 璁剧疆鍒囨崲鐘舵��
+ activeTab.value = tab.paneName + ''
+ // 鍒囨崲鐨勬椂鍊欓噸鏂板姞杞芥暟鎹�
+ await getActiveTabsRole()
+}
+
+/** 鑾峰彇 my role 鎴戠殑瑙掕壊 */
+const getMyRole = async (append?: boolean) => {
+ const params: ChatRolePageReqVO = {
+ ...myRoleParams,
+ name: search.value,
+ publicStatus: false
+ }
+ const { list } = await ChatRoleApi.getMyPage(params)
+ if (append) {
+ myRoleList.value.push.apply(myRoleList.value, list)
+ } else {
+ myRoleList.value = list
+ }
+}
+
+/** 鑾峰彇 public role 鍏叡瑙掕壊 */
+const getPublicRole = async (append?: boolean) => {
+ const params: ChatRolePageReqVO = {
+ ...publicRoleParams,
+ category: activeCategory.value === '鍏ㄩ儴' ? '' : activeCategory.value,
+ name: search.value,
+ publicStatus: true
+ }
+ const { list } = await ChatRoleApi.getMyPage(params)
+ if (append) {
+ publicRoleList.value.push.apply(publicRoleList.value, list)
+ } else {
+ publicRoleList.value = list
+ }
+}
+
+/** 鑾峰彇閫変腑鐨� tabs 瑙掕壊 */
+const getActiveTabsRole = async () => {
+ if (activeTab.value === 'my-role') {
+ myRoleParams.pageNo = 1
+ await getMyRole()
+ } else {
+ publicRoleParams.pageNo = 1
+ await getPublicRole()
+ }
+}
+
+/** 鑾峰彇瑙掕壊鍒嗙被鍒楄〃 */
+const getRoleCategoryList = async () => {
+ categoryList.value = ['鍏ㄩ儴', ...(await ChatRoleApi.getCategoryList())]
+}
+
+/** 澶勭悊鍒嗙被鐐瑰嚮 */
+const handlerCategoryClick = async (category: string) => {
+ // 鍒囨崲閫夋嫨鐨勫垎绫�
+ activeCategory.value = category
+ // 绛涢��
+ await getActiveTabsRole()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const handlerAddRole = async () => {
+ formRef.value.open('my-create', null, '娣诲姞瑙掕壊')
+}
+/** 缂栬緫瑙掕壊 */
+const handlerCardEdit = async (role) => {
+ formRef.value.open('my-update', role.id, '缂栬緫瑙掕壊')
+}
+
+/** 娣诲姞瑙掕壊鎴愬姛 */
+const handlerAddRoleSuccess = async (e) => {
+ // 鍒锋柊鏁版嵁
+ await getActiveTabsRole()
+}
+
+/** 鍒犻櫎瑙掕壊 */
+const handlerCardDelete = async (role) => {
+ await ChatRoleApi.deleteMy(role.id)
+ // 鍒锋柊鏁版嵁
+ await getActiveTabsRole()
+}
+
+/** 瑙掕壊鍒嗛〉锛氳幏鍙栦笅涓�椤� */
+const handlerCardPage = async (type) => {
+ try {
+ loading.value = true
+ if (type === 'public') {
+ publicRoleParams.pageNo++
+ await getPublicRole(true)
+ } else {
+ myRoleParams.pageNo++
+ await getMyRole(true)
+ }
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閫夋嫨 card 瑙掕壊锛氭柊寤鸿亰澶╁璇� */
+const handlerCardUse = async (role) => {
+ // 1. 鍒涘缓瀵硅瘽
+ const data: ChatConversationVO = {
+ roleId: role.id
+ } as unknown as ChatConversationVO
+ const conversationId = await ChatConversationApi.createChatConversationMy(data)
+
+ // 2. 璺宠浆椤甸潰
+ delView(unref(currentRoute))
+ await router.replace({
+ name: 'AiChat',
+ query: {
+ conversationId: conversationId
+ }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 鑾峰彇鍒嗙被
+ await getRoleCategoryList()
+ // 鑾峰彇 role 鏁版嵁
+ await getActiveTabsRole()
+})
+</script>
+<!-- 瑕嗙洊 element plus css -->
+<style lang="scss">
+.el-tabs__nav-scroll {
+ margin: 2px 8px !important;
+}
+
+.el-tabs__header {
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+.el-tabs__nav-wrap {
+ margin-bottom: 0 !important;
+}
+
+.el-tabs__content {
+ padding: 0 !important;
+}
+
+.el-tab-pane {
+ padding: 8px 0 0 0 !important;
+}
+</style>
diff --git a/src/views/ai/chat/index/index.vue b/src/views/ai/chat/index/index.vue
new file mode 100644
index 0000000..8a4deee
--- /dev/null
+++ b/src/views/ai/chat/index/index.vue
@@ -0,0 +1,630 @@
+<template>
+ <el-container class="absolute flex-1 top-0 left-0 h-full w-full">
+ <!-- 宸︿晶锛氬璇濆垪琛� -->
+ <ConversationList
+ :active-id="activeConversationId?.toString() || ''"
+ ref="conversationListRef"
+ @on-conversation-create="handleConversationCreateSuccess"
+ @on-conversation-click="handleConversationClick"
+ @on-conversation-clear="handleConversationClear"
+ @on-conversation-delete="handlerConversationDelete"
+ />
+ <!-- 鍙充晶锛氬璇濊鎯� -->
+ <el-container class="bg-[var(--el-bg-color)]">
+ <el-header
+ class="flex flex-row items-center justify-between bg-[var(--el-bg-color-page)] shadow-[0_0_0_0_var(--el-border-color-light)]"
+ >
+ <div class="text-18px font-bold">
+ {{ activeConversation?.title ? activeConversation?.title : '瀵硅瘽' }}
+ <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
+ </div>
+ <div class="flex w-300px flex-row justify-end" v-if="activeConversation">
+ <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
+ <span v-html="activeConversation?.modelName"></span>
+ <Icon icon="ep:setting" class="ml-10px" />
+ </el-button>
+ <el-button size="small" class="p-10px" @click="handlerMessageClear">
+ <Icon
+ icon="heroicons-outline:archive-box-x-mark"
+ color="var(--el-text-color-placeholder)"
+ />
+ </el-button>
+ <el-button size="small" class="p-10px">
+ <Icon icon="ep:download" color="var(--el-text-color-placeholder)" />
+ </el-button>
+ <el-button size="small" class="p-10px" @click="handleGoTopMessage">
+ <Icon icon="ep:top" color="var(--el-text-color-placeholder)" />
+ </el-button>
+ </div>
+ </el-header>
+
+ <!-- main锛氭秷鎭垪琛� -->
+ <el-main class="m-0 p-0 relative h-full w-full">
+ <div>
+ <div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
+ <!-- 鎯呭喌涓�锛氭秷鎭姞杞戒腑 -->
+ <MessageLoading v-if="activeMessageListLoading" />
+ <!-- 鎯呭喌浜岋細鏃犺亰澶╁璇濇椂 -->
+ <MessageNewConversation
+ v-if="!activeConversation"
+ @on-new-conversation="handleConversationCreate"
+ />
+ <!-- 鎯呭喌涓夛細娑堟伅鍒楄〃涓虹┖ -->
+ <MessageListEmpty
+ v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation"
+ @on-prompt="doSendMessage"
+ />
+ <!-- 鎯呭喌鍥涳細娑堟伅鍒楄〃涓嶄负绌� -->
+ <MessageList
+ v-if="!activeMessageListLoading && messageList.length > 0 && activeConversation"
+ ref="messageRef"
+ :conversation="activeConversation"
+ :list="messageList"
+ @on-delete-success="handleMessageDelete"
+ @on-edit="handleMessageEdit"
+ @on-refresh="handleMessageRefresh"
+ />
+ </div>
+ </div>
+ </el-main>
+
+ <!-- 搴曢儴 -->
+ <el-footer class="flex flex-col !h-auto !p-0">
+ <!-- TODO @鑺嬭壙锛氳繖鍧楄鎯冲姙娉曡縼绉讳笅锛� -->
+ <form
+ class="mt-10px mx-20px mb-20px py-9px px-10px flex flex-col h-auto rounded-10px"
+ style="border: 1px solid var(--el-border-color)"
+ >
+ <textarea
+ class="h-80px border-none box-border resize-none py-0 px-2px overflow-auto focus:outline-none"
+ v-model="prompt"
+ @keydown="handleSendByKeydown"
+ @input="handlePromptInput"
+ @compositionstart="onCompositionstart"
+ @compositionend="onCompositionend"
+ placeholder="闂垜浠讳綍闂...锛圫hift+Enter 鎹㈣锛屾寜涓� Enter 鍙戦�侊級"
+ >
+ </textarea>
+ <div class="flex justify-between pb-0 pt-5px">
+ <div class="flex items-center">
+ <MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px" />
+ <el-switch v-model="enableContext" />
+ <span class="ml-5px mr-15px text-14px text-#8f8f8f">涓婁笅鏂�</span>
+ <el-switch v-model="enableWebSearch" />
+ <span class="ml-5px text-14px text-#8f8f8f">鑱旂綉鎼滅储</span>
+ </div>
+ <el-button
+ type="primary"
+ size="default"
+ @click="handleSendByButton"
+ :loading="conversationInProgress"
+ v-if="conversationInProgress == false"
+ >
+ {{ conversationInProgress ? '杩涜涓�' : '鍙戦��' }}
+ </el-button>
+ <el-button
+ type="danger"
+ size="default"
+ @click="stopStream()"
+ v-if="conversationInProgress == true"
+ >
+ 鍋滄
+ </el-button>
+ </div>
+ </form>
+ </el-footer>
+ </el-container>
+
+ <!-- 鏇存柊瀵硅瘽 Form -->
+ <ConversationUpdateForm
+ ref="conversationUpdateFormRef"
+ @success="handleConversationUpdateSuccess"
+ />
+ </el-container>
+</template>
+
+<script setup lang="ts">
+import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import ConversationList from './components/conversation/ConversationList.vue'
+import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue'
+import MessageList from './components/message/MessageList.vue'
+import MessageListEmpty from './components/message/MessageListEmpty.vue'
+import MessageLoading from './components/message/MessageLoading.vue'
+import MessageNewConversation from './components/message/MessageNewConversation.vue'
+import MessageFileUpload from './components/message/MessageFileUpload.vue'
+
+/** AI 鑱婂ぉ瀵硅瘽 鍒楄〃 */
+defineOptions({ name: 'AiChat' })
+
+const route = useRoute() // 璺敱
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 鑱婂ぉ瀵硅瘽
+const conversationListRef = ref()
+const activeConversationId = ref<number | null>(null) // 閫変腑鐨勫璇濈紪鍙�
+const activeConversation = ref<ChatConversationVO | null>(null) // 閫変腑鐨� Conversation
+const conversationInProgress = ref(false) // 瀵硅瘽鏄惁姝e湪杩涜涓�傜洰鍓嶅彧鏈夈�愬彂閫併�戞秷鎭椂锛屼細鏇存柊涓� true锛岄伩鍏嶅垏鎹㈠璇濄�佸垹闄ゅ璇濈瓑鎿嶄綔
+
+// 娑堟伅鍒楄〃
+const messageRef = ref()
+const activeMessageList = ref<ChatMessageVO[]>([]) // 閫変腑瀵硅瘽鐨勬秷鎭垪琛�
+const activeMessageListLoading = ref<boolean>(false) // activeMessageList 鏄惁姝e湪鍔犺浇涓�
+const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 瀹氭椂鍣ㄣ�傚鏋滃姞杞介�熷害寰堝揩锛屽氨涓嶈繘鍏ュ姞杞戒腑
+// 娑堟伅婊氬姩
+const textSpeed = ref<number>(50) // Typing speed in milliseconds
+const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
+
+// 鍙戦�佹秷鎭緭鍏ユ
+const isComposing = ref(false) // 鍒ゆ柇鐢ㄦ埛鏄惁鍦ㄨ緭鍏�
+const conversationInAbortController = ref<any>() // 瀵硅瘽杩涜涓� abort 鎺у埗鍣�(鎺у埗 stream 瀵硅瘽)
+const inputTimeout = ref<any>() // 澶勭悊杈撳叆涓洖杞︾殑瀹氭椂鍣�
+const prompt = ref<string>() // prompt
+const enableContext = ref<boolean>(true) // 鏄惁寮�鍚笂涓嬫枃
+const enableWebSearch = ref<boolean>(false) // 鏄惁寮�鍚仈缃戞悳绱�
+const uploadFiles = ref<string[]>([]) // 涓婁紶鐨勬枃浠� URL 鍒楄〃
+// 鎺ユ敹 Stream 娑堟伅
+const receiveMessageFullText = ref('')
+const receiveMessageDisplayedText = ref('')
+
+// =========== 銆愯亰澶╁璇濄�戠浉鍏� ===========
+
+/** 鑾峰彇瀵硅瘽淇℃伅 */
+const getConversation = async (id: number | null) => {
+ if (!id) {
+ return
+ }
+ const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id)
+ if (!conversation) {
+ return
+ }
+ activeConversation.value = conversation
+ activeConversationId.value = conversation.id
+}
+
+/**
+ * 鐐瑰嚮鏌愪釜瀵硅瘽
+ *
+ * @param conversation 閫変腑鐨勫璇�
+ * @return 鏄惁鍒囨崲鎴愬姛
+ */
+const handleConversationClick = async (conversation: ChatConversationVO) => {
+ // 瀵硅瘽杩涜涓紝涓嶅厑璁稿垏鎹�
+ if (conversationInProgress.value) {
+ message.alert('瀵硅瘽涓紝涓嶅厑璁稿垏鎹�!')
+ return false
+ }
+
+ // 鏇存柊閫変腑鐨勫璇� id
+ activeConversationId.value = conversation.id
+ activeConversation.value = conversation
+ // 鍒锋柊 message 鍒楄〃
+ await getMessageList()
+ // 婊氬姩搴曢儴
+ scrollToBottom(true)
+ // 娓呯┖杈撳叆妗�
+ prompt.value = ''
+ // 娓呯┖鏂囦欢鍒楄〃
+ uploadFiles.value = []
+ return true
+}
+
+/** 鍒犻櫎鏌愪釜瀵硅瘽*/
+const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
+ // 鍒犻櫎鐨勫璇濆鏋滄槸褰撳墠閫変腑鐨勶紝閭d箞灏遍噸缃�
+ if (activeConversationId.value === delConversation.id) {
+ await handleConversationClear()
+ }
+}
+/** 娓呯┖閫変腑鐨勫璇� */
+const handleConversationClear = async () => {
+ // 瀵硅瘽杩涜涓紝涓嶅厑璁稿垏鎹�
+ if (conversationInProgress.value) {
+ message.alert('瀵硅瘽涓紝涓嶅厑璁稿垏鎹�!')
+ return false
+ }
+ activeConversationId.value = null
+ activeConversation.value = null
+ activeMessageList.value = []
+}
+
+/** 淇敼鑱婂ぉ瀵硅瘽 */
+const conversationUpdateFormRef = ref()
+const openChatConversationUpdateForm = async () => {
+ conversationUpdateFormRef.value.open(activeConversationId.value)
+}
+const handleConversationUpdateSuccess = async () => {
+ // 瀵硅瘽鏇存柊鎴愬姛锛屽埛鏂版渶鏂颁俊鎭�
+ await getConversation(activeConversationId.value)
+}
+
+/** 澶勭悊鑱婂ぉ瀵硅瘽鐨勫垱寤烘垚鍔� */
+const handleConversationCreate = async () => {
+ // 鍒涘缓瀵硅瘽
+ await conversationListRef.value.createConversation()
+}
+/** 澶勭悊鑱婂ぉ瀵硅瘽鐨勫垱寤烘垚鍔� */
+const handleConversationCreateSuccess = async () => {
+ // 鍒涘缓鏂扮殑瀵硅瘽锛屾竻绌鸿緭鍏ユ
+ prompt.value = ''
+ // 娓呯┖鏂囦欢鍒楄〃
+ uploadFiles.value = []
+}
+
+// =========== 銆愭秷鎭垪琛ㄣ�戠浉鍏� ===========
+
+/** 鑾峰彇娑堟伅 message 鍒楄〃 */
+const getMessageList = async () => {
+ try {
+ if (activeConversationId.value === null) {
+ return
+ }
+ // Timer 瀹氭椂鍣紝濡傛灉鍔犺浇閫熷害寰堝揩锛屽氨涓嶈繘鍏ュ姞杞戒腑
+ activeMessageListLoadingTimer.value = setTimeout(() => {
+ activeMessageListLoading.value = true
+ }, 60)
+
+ // 鑾峰彇娑堟伅鍒楄〃
+ activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
+ activeConversationId.value
+ )
+
+ // 婊氬姩鍒版渶涓嬮潰
+ await nextTick()
+ await scrollToBottom()
+ } finally {
+ // time 瀹氭椂鍣紝濡傛灉鍔犺浇閫熷害寰堝揩锛屽氨涓嶈繘鍏ュ姞杞戒腑
+ if (activeMessageListLoadingTimer.value) {
+ clearTimeout(activeMessageListLoadingTimer.value)
+ }
+ // 鍔犺浇缁撴潫
+ activeMessageListLoading.value = false
+ }
+}
+
+/**
+ * 娑堟伅鍒楄〃
+ *
+ * 鍜� {@link #getMessageList()} 鐨勫樊寮傛槸锛屾妸 systemMessage 鑰冭檻杩涘幓
+ */
+const messageList = computed(() => {
+ if (activeMessageList.value.length > 0) {
+ return activeMessageList.value
+ }
+ // 娌℃湁娑堟伅鏃讹紝濡傛灉鏈� systemMessage 鍒欏睍绀哄畠
+ if (activeConversation.value?.systemMessage) {
+ return [
+ {
+ id: 0,
+ conversationId: activeConversation.value.id || 0,
+ type: 'system',
+ userId: '',
+ roleId: '',
+ model: 0,
+ modelId: 0,
+ content: activeConversation.value.systemMessage,
+ tokens: 0,
+ createTime: new Date(),
+ roleAvatar: '',
+ userAvatar: ''
+ } as ChatMessageVO
+ ]
+ }
+ return []
+})
+
+/** 澶勭悊鍒犻櫎 message 娑堟伅 */
+const handleMessageDelete = () => {
+ if (conversationInProgress.value) {
+ message.alert('鍥炵瓟涓紝涓嶈兘鍒犻櫎!')
+ return
+ }
+ // 鍒锋柊 message 鍒楄〃
+ getMessageList()
+}
+
+/** 澶勭悊 message 娓呯┖ */
+const handlerMessageClear = async () => {
+ if (!activeConversationId.value) {
+ return
+ }
+ try {
+ // 纭鎻愮ず
+ await message.delConfirm('纭娓呯┖瀵硅瘽娑堟伅锛�')
+ // 娓呯┖瀵硅瘽
+ await ChatMessageApi.deleteByConversationId(activeConversationId.value)
+ // 鍒锋柊 message 鍒楄〃
+ activeMessageList.value = []
+ } catch {}
+}
+
+/** 鍥炲埌 message 鍒楄〃鐨勯《閮� */
+const handleGoTopMessage = () => {
+ messageRef.value.handlerGoTop()
+}
+
+// =========== 銆愬彂閫佹秷鎭�戠浉鍏� ===========
+
+/** 澶勭悊鏉ヨ嚜 keydown 鐨勫彂閫佹秷鎭� */
+const handleSendByKeydown = async (event) => {
+ // 鍒ゆ柇鐢ㄦ埛鏄惁鍦ㄨ緭鍏�
+ if (isComposing.value) {
+ return
+ }
+ // 杩涜涓笉鍏佽鍙戦��
+ if (conversationInProgress.value) {
+ return
+ }
+ const content = prompt.value?.trim() as string
+ if (event.key === 'Enter') {
+ if (event.shiftKey) {
+ // 鎻掑叆鎹㈣
+ prompt.value += '\r\n'
+ event.preventDefault() // 闃叉榛樿鐨勬崲琛岃涓�
+ } else {
+ // 鍙戦�佹秷鎭�
+ await doSendMessage(content)
+ event.preventDefault() // 闃叉榛樿鐨勬彁浜よ涓�
+ }
+ }
+}
+
+/** 澶勭悊鏉ヨ嚜銆愬彂閫併�戞寜閽殑鍙戦�佹秷鎭� */
+const handleSendByButton = () => {
+ doSendMessage(prompt.value?.trim() as string)
+}
+
+/** 澶勭悊 prompt 杈撳叆鍙樺寲 */
+const handlePromptInput = (event) => {
+ // 闈炶緭鍏ユ硶 杈撳叆璁剧疆涓� true
+ if (!isComposing.value) {
+ // 鍥炶溅 event data 鏄� null
+ if (event.data == null) {
+ return
+ }
+ isComposing.value = true
+ }
+ // 娓呯悊瀹氭椂鍣�
+ if (inputTimeout.value) {
+ clearTimeout(inputTimeout.value)
+ }
+ // 閲嶇疆瀹氭椂鍣�
+ inputTimeout.value = setTimeout(() => {
+ isComposing.value = false
+ }, 400)
+}
+// TODO @鑺嬭壙锛氭槸涓嶆槸鍙互閫氳繃 @keydown.enter銆丂keydown.shift.enter 鏉ュ疄鐜帮紝鍥炶溅鍙戦�併�乻hift+鍥炶溅鎹㈣锛涗富瑕佺湅鐪嬶紝鏄笉鏄彲浠ョ畝鍖� isComposing 鐩稿叧鐨勯�昏緫
+const onCompositionstart = () => {
+ isComposing.value = true
+}
+const onCompositionend = () => {
+ // console.log('杈撳叆缁撴潫...')
+ setTimeout(() => {
+ isComposing.value = false
+ }, 200)
+}
+
+/** 鐪熸鎵ц銆愬彂閫併�戞秷鎭搷浣� */
+const doSendMessage = async (content: string) => {
+ // 鏍¢獙
+ if (content.length < 1) {
+ message.error('鍙戦�佸け璐ワ紝鍘熷洜锛氬唴瀹逛负绌猴紒')
+ return
+ }
+ if (activeConversationId.value == null) {
+ message.error('杩樻病鍒涘缓瀵硅瘽锛屼笉鑳藉彂閫�!')
+ return
+ }
+
+ // 鍑嗗闄勪欢 URL 鏁扮粍
+ const attachmentUrls = [...uploadFiles.value]
+
+ // 娓呯┖杈撳叆妗嗗拰鏂囦欢鍒楄〃
+ prompt.value = ''
+ uploadFiles.value = []
+
+ // 鎵ц鍙戦��
+ await doSendMessageStream({
+ conversationId: activeConversationId.value,
+ content: content,
+ attachmentUrls: attachmentUrls
+ } as ChatMessageVO)
+}
+
+/** 鐪熸鎵ц銆愬彂閫併�戞秷鎭搷浣� */
+const doSendMessageStream = async (userMessage: ChatMessageVO) => {
+ // 鍒涘缓 AbortController 瀹炰緥锛屼互渚夸腑姝㈣姹�
+ conversationInAbortController.value = new AbortController()
+ // 鏍囪瀵硅瘽杩涜涓�
+ conversationInProgress.value = true
+ // 璁剧疆涓虹┖
+ receiveMessageFullText.value = ''
+
+ try {
+ // 1.1 鍏堟坊鍔犱袱涓亣鏁版嵁锛岀瓑 stream 杩斿洖鍐嶆浛鎹�
+ activeMessageList.value.push({
+ id: -1,
+ conversationId: activeConversationId.value,
+ type: 'user',
+ content: userMessage.content,
+ attachmentUrls: userMessage.attachmentUrls || [],
+ createTime: new Date()
+ } as ChatMessageVO)
+ activeMessageList.value.push({
+ id: -2,
+ conversationId: activeConversationId.value,
+ type: 'assistant',
+ content: '鎬濊�冧腑...',
+ reasoningContent: '',
+ createTime: new Date()
+ } as ChatMessageVO)
+ // 1.2 婊氬姩鍒版渶涓嬮潰
+ await nextTick()
+ await scrollToBottom() // 搴曢儴
+ // 1.3 寮�濮嬫粴鍔�
+ textRoll()
+
+ // 2. 鍙戦�� event stream
+ let isFirstChunk = true // 鏄惁鏄涓�涓� chunk 娑堟伅娈�
+ await ChatMessageApi.sendChatMessageStream(
+ userMessage.conversationId,
+ userMessage.content,
+ conversationInAbortController.value,
+ enableContext.value,
+ enableWebSearch.value,
+ async (res) => {
+ const { code, data, msg } = JSON.parse(res.data)
+ if (code !== 0) {
+ message.alert(`瀵硅瘽寮傚父! ${msg}`)
+ // 濡傛灉鏈帴鏀跺埌娑堟伅锛屽垯杩涜鍒犻櫎
+ if (receiveMessageFullText.value === '') {
+ activeMessageList.value.pop()
+ }
+ return
+ }
+
+ // 濡傛灉鍐呭涓虹┖锛屽氨涓嶅鐞嗐��
+ if (data.receive.content === '' && !data.receive.reasoningContent) {
+ return
+ }
+
+ // 棣栨杩斿洖闇�瑕佹坊鍔犱竴涓� message 鍒伴〉闈紝鍚庨潰鐨勯兘鏄洿鏂�
+ if (isFirstChunk) {
+ isFirstChunk = false
+ // 寮瑰嚭涓や釜鍋囨暟鎹�
+ activeMessageList.value.pop()
+ activeMessageList.value.pop()
+ // 鏇存柊杩斿洖鐨勬暟鎹�
+ activeMessageList.value.push(data.send)
+ data.send.attachmentUrls = userMessage.attachmentUrls
+ activeMessageList.value.push(data.receive)
+ }
+
+ // 澶勭悊 reasoningContent
+ if (data.receive.reasoningContent) {
+ const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
+ lastMessage.reasoningContent =
+ lastMessage.reasoningContent + data.receive.reasoningContent
+ }
+
+ // 澶勭悊姝e父鍐呭
+ if (data.receive.content !== '') {
+ receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
+ }
+ // 婊氬姩鍒版渶涓嬮潰
+ await scrollToBottom()
+ },
+ (error: any) => {
+ // 寮傚父鎻愮ず锛屽苟鍋滄娴�
+ message.alert(`瀵硅瘽寮傚父锛乣)
+ stopStream()
+ // 闇�瑕佹姏鍑哄紓甯革紝绂佹閲嶈瘯
+ throw error
+ },
+ () => {
+ stopStream()
+ },
+ userMessage.attachmentUrls
+ )
+ } catch {}
+}
+
+/** 鍋滄 stream 娴佸紡璋冪敤 */
+const stopStream = async () => {
+ // tip锛氬鏋� stream 杩涜涓殑 message锛屽氨闇�瑕佽皟鐢� controller 缁撴潫
+ if (conversationInAbortController.value) {
+ conversationInAbortController.value.abort()
+ }
+ // 璁剧疆涓� false
+ conversationInProgress.value = false
+}
+
+/** 缂栬緫 message锛氳缃负 prompt锛屽彲浠ュ啀娆$紪杈� */
+const handleMessageEdit = (message: ChatMessageVO) => {
+ prompt.value = message.content
+}
+
+/** 鍒锋柊 message锛氬熀浜庢寚瀹氭秷鎭紝鍐嶆鍙戣捣瀵硅瘽 */
+const handleMessageRefresh = (message: ChatMessageVO) => {
+ doSendMessage(message.content)
+}
+
+// ============== 銆愭秷鎭粴鍔ㄣ�戠浉鍏� =============
+
+/** 婊氬姩鍒� message 搴曢儴 */
+const scrollToBottom = async (isIgnore?: boolean) => {
+ await nextTick()
+ if (messageRef.value) {
+ messageRef.value.scrollToBottom(isIgnore)
+ }
+}
+
+/** 鑷彁婊氬姩鏁堟灉 */
+const textRoll = async () => {
+ let index = 0
+ try {
+ // 鍙兘鎵ц涓�娆�
+ if (textRoleRunning.value) {
+ return
+ }
+ // 璁剧疆鐘舵��
+ textRoleRunning.value = true
+ receiveMessageDisplayedText.value = ''
+ const task = async () => {
+ // 璋冩暣閫熷害
+ const diff =
+ (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
+ if (diff > 5) {
+ textSpeed.value = 10
+ } else if (diff > 2) {
+ textSpeed.value = 30
+ } else if (diff > 1.5) {
+ textSpeed.value = 50
+ } else {
+ textSpeed.value = 100
+ }
+ // 瀵硅瘽缁撴潫锛屽氨鎸� 30 鐨勯�熷害
+ if (!conversationInProgress.value) {
+ textSpeed.value = 10
+ }
+
+ if (index < receiveMessageFullText.value.length) {
+ receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
+ index++
+
+ // 鏇存柊 message
+ const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
+ lastMessage.content = receiveMessageDisplayedText.value
+ // 婊氬姩鍒颁綇涓嬮潰
+ await scrollToBottom()
+ // 閲嶆柊璁剧疆浠诲姟
+ timer = setTimeout(task, textSpeed.value)
+ } else {
+ // 涓嶆槸瀵硅瘽涓彲浠ョ粨鏉�
+ if (!conversationInProgress.value) {
+ textRoleRunning.value = false
+ clearTimeout(timer)
+ } else {
+ // 閲嶆柊璁剧疆浠诲姟
+ timer = setTimeout(task, textSpeed.value)
+ }
+ }
+ }
+ let timer = setTimeout(task, textSpeed.value)
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 濡傛灉鏈� conversationId 鍙傛暟锛屽垯榛樿閫変腑
+ if (route.query.conversationId) {
+ const id = route.query.conversationId as unknown as number
+ activeConversationId.value = id
+ await getConversation(id)
+ }
+
+ // 鑾峰彇鍒楄〃鏁版嵁
+ activeMessageListLoading.value = true
+ await getMessageList()
+})
+</script>
diff --git a/src/views/ai/chat/manager/ChatConversationList.vue b/src/views/ai/chat/manager/ChatConversationList.vue
new file mode 100644
index 0000000..23933f0
--- /dev/null
+++ b/src/views/ai/chat/manager/ChatConversationList.vue
@@ -0,0 +1,163 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑱婂ぉ缂栧彿" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ヨ亰澶╃紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="瀵硅瘽缂栧彿" align="center" prop="id" width="180" fixed="left" />
+ <el-table-column label="瀵硅瘽鏍囬" align="center" prop="title" width="180" fixed="left" />
+ <el-table-column label="鐢ㄦ埛" align="center" prop="userId" width="180">
+ <template #default="scope">
+ <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙掕壊" align="center" prop="roleName" width="180" />
+ <el-table-column label="妯″瀷鏍囪瘑" align="center" prop="model" width="180" />
+ <el-table-column label="娑堟伅鏁�" align="center" prop="messageCount" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="娓╁害鍙傛暟" align="center" prop="temperature" />
+ <el-table-column label="鍥炲 Token 鏁�" align="center" prop="maxTokens" width="120" />
+ <el-table-column label="涓婁笅鏂囨暟閲�" align="center" prop="maxContexts" width="120" />
+ <el-table-column label="鎿嶄綔" align="center" width="180" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:chat-conversation:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import * as UserApi from '@/api/system/user'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ChatConversationVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: undefined,
+ title: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ChatConversationApi.getChatConversationPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ChatConversationApi.deleteChatConversationByAdmin(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/ai/chat/manager/ChatMessageList.vue b/src/views/ai/chat/manager/ChatMessageList.vue
new file mode 100644
index 0000000..0d84184
--- /dev/null
+++ b/src/views/ai/chat/manager/ChatMessageList.vue
@@ -0,0 +1,175 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="瀵硅瘽缂栧彿" prop="conversationId">
+ <el-input
+ v-model="queryParams.conversationId"
+ placeholder="璇疯緭鍏ュ璇濈紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="娑堟伅缂栧彿" align="center" prop="id" width="180" fixed="left" />
+ <el-table-column
+ label="瀵硅瘽缂栧彿"
+ align="center"
+ prop="conversationId"
+ width="180"
+ fixed="left"
+ />
+ <el-table-column label="鐢ㄦ埛" align="center" prop="userId" width="180">
+ <template #default="scope">
+ <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙掕壊" align="center" prop="roleName" width="180" />
+ <el-table-column label="娑堟伅绫诲瀷" align="center" prop="type" width="100" />
+ <el-table-column label="妯″瀷鏍囪瘑" align="center" prop="model" width="180" />
+ <el-table-column label="娑堟伅鍐呭" align="center" prop="content" width="300" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鍥炲娑堟伅缂栧彿" align="center" prop="replyId" width="180" />
+ <el-table-column label="鎼哄甫涓婁笅鏂�" align="center" prop="useContext" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.useContext" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:chat-message:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
+import * as UserApi from '@/api/system/user'
+import { DICT_TYPE } from '@/utils/dict'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ChatMessageVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ conversationId: undefined,
+ userId: undefined,
+ content: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ChatMessageApi.getChatMessagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ChatMessageApi.deleteChatMessageByAdmin(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/ai/chat/manager/index.vue b/src/views/ai/chat/manager/index.vue
new file mode 100644
index 0000000..9cb0006
--- /dev/null
+++ b/src/views/ai/chat/manager/index.vue
@@ -0,0 +1,22 @@
+<template>
+ <doc-alert title="AI 瀵硅瘽鑱婂ぉ" url="https://doc.iocoder.cn/ai/chat/" />
+
+ <ContentWrap>
+ <el-tabs>
+ <el-tab-pane label="瀵硅瘽鍒楄〃">
+ <ChatConversationList />
+ </el-tab-pane>
+ <el-tab-pane label="娑堟伅鍒楄〃">
+ <ChatMessageList />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import ChatConversationList from './ChatConversationList.vue'
+import ChatMessageList from './ChatMessageList.vue'
+
+/** AI 鑱婂ぉ瀵硅瘽 鍒楄〃 */
+defineOptions({ name: 'AiChatManager' })
+</script>
diff --git a/src/views/ai/image/index/components/ImageCard.vue b/src/views/ai/image/index/components/ImageCard.vue
new file mode 100644
index 0000000..9a5632e
--- /dev/null
+++ b/src/views/ai/image/index/components/ImageCard.vue
@@ -0,0 +1,131 @@
+<template>
+ <el-card
+ body-class=""
+ class="!w-80 !h-auto !rounded-10px !relative !flex !flex-col"
+ >
+ <div class="!flex !flex-row !justify-between">
+ <div>
+ <el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
+ 鐢熸垚涓�
+ </el-button>
+ <el-button text bg v-else-if="detail?.status === AiImageStatusEnum.SUCCESS">
+ 宸插畬鎴�
+ </el-button>
+ <el-button type="danger" text bg v-else-if="detail?.status === AiImageStatusEnum.FAIL">
+ 寮傚父
+ </el-button>
+ </div>
+ <!-- 鎿嶄綔鍖� -->
+ <div>
+ <el-button
+ class="!p-10px !m-0"
+ text
+ :icon="Download"
+ @click="handleButtonClick('download', detail)"
+ />
+ <el-button
+ class="!p-10px !m-0"
+ text
+ :icon="RefreshRight"
+ @click="handleButtonClick('regeneration', detail)"
+ />
+ <el-button
+ class="!p-10px !m-0"
+ text
+ :icon="Delete"
+ @click="handleButtonClick('delete', detail)"
+ />
+ <el-button
+ class="!p-10px !m-0"
+ text
+ :icon="More"
+ @click="handleButtonClick('more', detail)"
+ />
+ </div>
+ </div>
+ <div class="!overflow-hidden !mt-20px !h-280px !flex-1" ref="cardImageRef">
+ <el-image
+ class="!w-full !rounded-10px"
+ :src="detail?.picUrl"
+ :preview-src-list="[detail.picUrl]"
+ preview-teleported
+ />
+ <div v-if="detail?.status === AiImageStatusEnum.FAIL">
+ {{ detail?.errorMessage }}
+ </div>
+ </div>
+ <!-- Midjourney 涓撳睘鎿嶄綔 -->
+ <div class="!mt-5px !w-full !flex !flex-row !flex-wrap !justify-start">
+ <el-button
+ size="small"
+ v-for="button in detail?.buttons"
+ :key="button"
+ class="min-w-40px ml-0 mr-10px mt-5px"
+ @click="handleMidjourneyBtnClick(button)"
+ >
+ {{ button.label }}{{ button.emoji }}
+ </el-button>
+ </div>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { Delete, Download, More, RefreshRight } from '@element-plus/icons-vue'
+import { ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
+import { PropType } from 'vue'
+import { ElLoading, LoadingOptionsResolved } from 'element-plus'
+import { AiImageStatusEnum } from '@/views/ai/utils/constants'
+
+const message = useMessage() // 娑堟伅
+
+const props = defineProps({
+ detail: {
+ type: Object as PropType<ImageVO>,
+ require: true
+ }
+})
+
+const cardImageRef = ref<any>() // 鍗$墖 image ref
+const cardImageLoadingInstance = ref<any>() // 鍗$墖 image ref
+
+/** 澶勭悊鐐瑰嚮浜嬩欢 */
+const handleButtonClick = async (type, detail: ImageVO) => {
+ emits('onBtnClick', type, detail)
+}
+
+/** 澶勭悊 Midjourney 鎸夐挳鐐瑰嚮浜嬩欢 */
+const handleMidjourneyBtnClick = async (button: ImageMidjourneyButtonsVO) => {
+ // 纭绐椾綋
+ await message.confirm(`纭鎿嶄綔 "${button.label} ${button.emoji}" ?`)
+ emits('onMjBtnClick', button, props.detail)
+}
+
+const emits = defineEmits(['onBtnClick', 'onMjBtnClick']) // emits
+
+/** 鐩戝惉璇︽儏 */
+const { detail } = toRefs(props)
+watch(detail, async (newVal, oldVal) => {
+ await handleLoading(newVal.status as string)
+})
+
+/** 澶勭悊鍔犺浇鐘舵�� */
+const handleLoading = async (status: number) => {
+ // 鎯呭喌涓�锛氬鏋滄槸鐢熸垚涓紝鍒欒缃姞杞戒腑鐨� loading
+ if (status === AiImageStatusEnum.IN_PROGRESS) {
+ cardImageLoadingInstance.value = ElLoading.service({
+ target: cardImageRef.value,
+ text: '鐢熸垚涓�...'
+ } as LoadingOptionsResolved)
+ // 鎯呭喌浜岋細濡傛灉宸茬粡鐢熸垚缁撴潫锛屽垯绉婚櫎 loading
+ } else {
+ if (cardImageLoadingInstance.value) {
+ cardImageLoadingInstance.value.close()
+ cardImageLoadingInstance.value = null
+ }
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await handleLoading(props.detail.status as string)
+})
+</script>
diff --git a/src/views/ai/image/index/components/ImageDetail.vue b/src/views/ai/image/index/components/ImageDetail.vue
new file mode 100644
index 0000000..d4f8dc6
--- /dev/null
+++ b/src/views/ai/image/index/components/ImageDetail.vue
@@ -0,0 +1,187 @@
+<template>
+ <el-drawer
+ v-model="showDrawer"
+ title="鍥剧墖璇︾粏"
+ @close="handleDrawerClose"
+ custom-class="drawer-class"
+ >
+ <!-- 鍥剧墖棰勮 -->
+ <div class="mb-5">
+ <el-image
+ :src="detail?.picUrl"
+ :preview-src-list="[detail.picUrl]"
+ preview-teleported
+ class="w-full rounded-2"
+ fit="contain"
+ />
+ </div>
+
+ <!-- 鍩虹淇℃伅 -->
+ <el-descriptions title="鍩虹淇℃伅" :column="1" :label-width="100" border>
+ <el-descriptions-item label="鎻愪氦鏃堕棿">
+ {{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢熸垚鏃堕棿">
+ {{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯″瀷">
+ {{ detail.model }}({{ detail.height }}x{{ detail.width }})
+ </el-descriptions-item>
+ <el-descriptions-item label="鎻愮ず璇�">
+ <div class="break-words">{{ detail.prompt }}</div>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥剧墖鍦板潃">
+ <div class="break-all text-xs">{{ detail.picUrl }}</div>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- StableDiffusion 涓撳睘鍖哄煙 -->
+ <el-descriptions
+ v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && hasStableDiffusionOptions"
+ title="StableDiffusion 鍙傛暟"
+ :column="1"
+ :label-width="100"
+ border
+ class="mt-5"
+ >
+ <el-descriptions-item v-if="detail?.options?.sampler" label="閲囨牱鏂规硶">
+ {{
+ StableDiffusionSamplers.find(
+ (item: ImageModelVO) => item.key === detail?.options?.sampler
+ )?.name
+ }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detail?.options?.clipGuidancePreset" label="CLIP">
+ {{
+ StableDiffusionClipGuidancePresets.find(
+ (item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset
+ )?.name
+ }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detail?.options?.stylePreset" label="椋庢牸">
+ {{
+ StableDiffusionStylePresets.find(
+ (item: ImageModelVO) => item.key === detail?.options?.stylePreset
+ )?.name
+ }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detail?.options?.steps" label="杩唬姝ユ暟">
+ {{ detail?.options?.steps }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detail?.options?.scale" label="寮曞绯绘暟">
+ {{ detail?.options?.scale }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detail?.options?.seed" label="闅忔満鍥犲瓙">
+ {{ detail?.options?.seed }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- Dall3 涓撳睘鍖哄煙 -->
+ <el-descriptions
+ v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style"
+ title="DALL-E 3 鍙傛暟"
+ :column="1"
+ :label-width="100"
+ border
+ class="mt-5"
+ >
+ <el-descriptions-item label="椋庢牸閫夋嫨">
+ {{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- Midjourney 涓撳睘鍖哄煙 -->
+ <el-descriptions
+ v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && hasMidjourneyOptions"
+ title="Midjourney 鍙傛暟"
+ :column="1"
+ :label-width="100"
+ border
+ class="mt-5"
+ >
+ <el-descriptions-item v-if="detail?.options?.version" label="妯″瀷鐗堟湰">
+ {{ detail?.options?.version }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detail?.options?.referImageUrl" label="鍙傝�冨浘">
+ <el-image
+ :src="detail.options.referImageUrl"
+ class="max-w-[200px] rounded-2"
+ fit="contain"
+ />
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { ImageApi, ImageVO } from '@/api/ai/image'
+import {
+ AiPlatformEnum,
+ Dall3StyleList,
+ ImageModelVO,
+ StableDiffusionClipGuidancePresets,
+ StableDiffusionSamplers,
+ StableDiffusionStylePresets
+} from '@/views/ai/utils/constants'
+import { formatTime } from '@/utils'
+
+const showDrawer = ref<boolean>(false) // 鏄惁鏄剧ず
+const detail = ref<ImageVO>({} as ImageVO) // 鍥剧墖璇︾粏淇℃伅
+
+// 璁$畻灞炴�э細鍒ゆ柇鏄惁鏈� StableDiffusion 閫夐」
+const hasStableDiffusionOptions = computed(() => {
+ const options = detail.value?.options
+ return (
+ options?.sampler ||
+ options?.clipGuidancePreset ||
+ options?.stylePreset ||
+ options?.steps ||
+ options?.scale ||
+ options?.seed
+ )
+})
+
+// 璁$畻灞炴�э細鍒ゆ柇鏄惁鏈� Midjourney 閫夐」
+const hasMidjourneyOptions = computed(() => {
+ const options = detail.value?.options
+ return options?.version || options?.referImageUrl
+})
+
+const props = defineProps({
+ show: {
+ type: Boolean,
+ require: true,
+ default: false
+ },
+ id: {
+ type: Number,
+ required: true
+ }
+})
+
+/** 鍏抽棴鎶藉眽 */
+const handleDrawerClose = async () => {
+ emits('handleDrawerClose')
+}
+
+/** 鐩戝惉 drawer 鏄惁鎵撳紑 */
+const { show } = toRefs(props)
+watch(show, async (newValue, _oldValue) => {
+ showDrawer.value = newValue as boolean
+})
+
+/** 鑾峰彇鍥剧墖璇︽儏 */
+const getImageDetail = async (id: number) => {
+ detail.value = await ImageApi.getImageMy(id)
+}
+
+/** 鐩戝惉 id 鍙樺寲锛屽姞杞芥渶鏂板浘鐗囪鎯� */
+const { id } = toRefs(props)
+watch(id, async (newVal, _oldVal) => {
+ if (newVal) {
+ await getImageDetail(newVal)
+ }
+})
+
+const emits = defineEmits(['handleDrawerClose'])
+</script>
diff --git a/src/views/ai/image/index/components/ImageList.vue b/src/views/ai/image/index/components/ImageList.vue
new file mode 100644
index 0000000..5169e45
--- /dev/null
+++ b/src/views/ai/image/index/components/ImageList.vue
@@ -0,0 +1,208 @@
+<template>
+ <el-card
+ class="wh-full"
+ :body-style="{ margin: 0, padding: 0, height: '100%', position: 'relative' }"
+ shadow="never"
+ >
+ <template #header>
+ 缁樼敾浠诲姟
+ <!-- TODO @fan锛氱湅鐪嬶紝鎬庝箞浼樺寲涓嬭繖涓牱瀛愬搱銆� -->
+ <el-button @click="handleViewPublic">缁樼敾浣滃搧</el-button>
+ </template>
+ <!-- 鍥剧墖鍒楄〃 -->
+ <div
+ class="relative flex flex-row flex-wrap content-start h-full overflow-auto p-5 pb-[140px] box-border [&>div]:mr-5 [&>div]:mb-5"
+ ref="imageListRef"
+ >
+ <ImageCard
+ v-for="image in imageList"
+ :key="image.id"
+ :detail="image"
+ @on-btn-click="handleImageButtonClick"
+ @on-mj-btn-click="handleImageMidjourneyButtonClick"
+ />
+ </div>
+ <div
+ class="absolute bottom-[60px] h-[50px] leading-[90px] w-full z-[999] bg-white flex flex-row justify-center items-center"
+ >
+ <Pagination
+ :total="pageTotal"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getImageList"
+ />
+ </div>
+ </el-card>
+
+ <!-- 鍥剧墖璇︽儏 -->
+ <ImageDetail
+ :show="isShowImageDetail"
+ :id="showImageDetailId"
+ @handle-drawer-close="handleDetailClose"
+ />
+</template>
+<script setup lang="ts">
+import {
+ ImageApi,
+ ImageVO,
+ ImageMidjourneyActionVO,
+ ImageMidjourneyButtonsVO
+} from '@/api/ai/image'
+import ImageDetail from './ImageDetail.vue'
+import ImageCard from './ImageCard.vue'
+import { ElLoading, LoadingOptionsResolved } from 'element-plus'
+import { AiImageStatusEnum } from '@/views/ai/utils/constants'
+import download from '@/utils/download'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const router = useRouter() // 璺敱
+
+// 鍥剧墖鍒嗛〉鐩稿叧鐨勫弬鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+const pageTotal = ref<number>(0) // page size
+const imageList = ref<ImageVO[]>([]) // image 鍒楄〃
+const imageListLoadingInstance = ref<any>() // image 鍒楄〃鏄惁姝e湪鍔犺浇涓�
+const imageListRef = ref<any>() // ref
+// 鍥剧墖杞鐩稿叧鐨勫弬鏁帮紙姝e湪鐢熸垚涓殑锛�
+const inProgressImageMap = ref<{}>({}) // 鐩戝惉鐨� image 鏄犲皠锛屼竴鑸槸鐢熸垚涓紙闇�瑕佽疆璇級锛宬ey 涓� image 缂栧彿锛寁alue 涓� image
+const inProgressTimer = ref<any>() // 鐢熸垚涓殑 image 瀹氭椂鍣紝杞鐢熸垚杩涘睍
+// 鍥剧墖璇︽儏鐩稿叧鐨勫弬鏁�
+const isShowImageDetail = ref<boolean>(false) // 鍥剧墖璇︽儏鏄惁灞曠ず
+const showImageDetailId = ref<number>(0) // 鍥剧墖璇︽儏鐨勫浘鐗囩紪鍙�
+
+/** 澶勭悊鏌ョ湅缁樺浘浣滃搧 */
+const handleViewPublic = () => {
+ router.push({
+ name: 'AiImageSquare'
+ })
+}
+
+/** 鏌ョ湅鍥剧墖鐨勮鎯� */
+const handleDetailOpen = async () => {
+ isShowImageDetail.value = true
+}
+
+/** 鍏抽棴鍥剧墖鐨勮鎯� */
+const handleDetailClose = async () => {
+ isShowImageDetail.value = false
+}
+
+/** 鑾峰緱 image 鍥剧墖鍒楄〃 */
+const getImageList = async () => {
+ try {
+ // 1. 鍔犺浇鍥剧墖鍒楄〃
+ imageListLoadingInstance.value = ElLoading.service({
+ target: imageListRef.value,
+ text: '鍔犺浇涓�...'
+ } as LoadingOptionsResolved)
+ const { list, total } = await ImageApi.getImagePageMy(queryParams)
+ imageList.value = list
+ pageTotal.value = total
+
+ // 2. 璁$畻闇�瑕佽疆璇㈢殑鍥剧墖
+ const newWatImages = {}
+ imageList.value.forEach((item) => {
+ if (item.status === AiImageStatusEnum.IN_PROGRESS) {
+ newWatImages[item.id] = item
+ }
+ })
+ inProgressImageMap.value = newWatImages
+ } finally {
+ // 鍏抽棴姝e湪鈥滃姞杞戒腑鈥濈殑 Loading
+ if (imageListLoadingInstance.value) {
+ imageListLoadingInstance.value.close()
+ imageListLoadingInstance.value = null
+ }
+ }
+}
+
+/** 杞鐢熸垚涓殑 image 鍒楄〃 */
+const refreshWatchImages = async () => {
+ const imageIds = Object.keys(inProgressImageMap.value).map(Number)
+ if (imageIds.length == 0) {
+ return
+ }
+ const list = (await ImageApi.getImageListMyByIds(imageIds)) as ImageVO[]
+ const newWatchImages = {}
+ list.forEach((image) => {
+ if (image.status === AiImageStatusEnum.IN_PROGRESS) {
+ newWatchImages[image.id] = image
+ } else {
+ const index = imageList.value.findIndex((oldImage) => image.id === oldImage.id)
+ if (index >= 0) {
+ // 鏇存柊 imageList
+ imageList.value[index] = image
+ }
+ }
+ })
+ inProgressImageMap.value = newWatchImages
+}
+
+/** 鍥剧墖鐨勭偣鍑讳簨浠� */
+const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
+ // 璇︽儏
+ if (type === 'more') {
+ showImageDetailId.value = imageDetail.id
+ await handleDetailOpen()
+ return
+ }
+ // 鍒犻櫎
+ if (type === 'delete') {
+ await message.confirm(`鏄惁鍒犻櫎鐓х墖?`)
+ await ImageApi.deleteImageMy(imageDetail.id)
+ await getImageList()
+ message.success('鍒犻櫎鎴愬姛!')
+ return
+ }
+ // 涓嬭浇
+ if (type === 'download') {
+ download.image({ url: imageDetail.picUrl })
+ return
+ }
+ // 閲嶆柊鐢熸垚
+ if (type === 'regeneration') {
+ emits('onRegeneration', imageDetail)
+ return
+ }
+}
+
+/** 澶勭悊 Midjourney 鎸夐挳鐐瑰嚮浜嬩欢 */
+const handleImageMidjourneyButtonClick = async (
+ button: ImageMidjourneyButtonsVO,
+ imageDetail: ImageVO
+) => {
+ // 1. 鏋勫缓 params 鍙傛暟
+ const data = {
+ id: imageDetail.id,
+ customId: button.customId
+ } as ImageMidjourneyActionVO
+ // 2. 鍙戦�� action
+ await ImageApi.midjourneyAction(data)
+ // 3. 鍒锋柊鍒楄〃
+ await getImageList()
+}
+
+defineExpose({ getImageList }) // 鏆撮湶缁勪欢鏂规硶
+
+const emits = defineEmits(['onRegeneration'])
+
+/** 缁勪欢鎸傚湪鐨勬椂鍊� */
+onMounted(async () => {
+ // 鑾峰彇 image 鍒楄〃
+ await getImageList()
+ // 鑷姩鍒锋柊 image 鍒楄〃
+ inProgressTimer.value = setInterval(async () => {
+ await refreshWatchImages()
+ }, 1000 * 3)
+})
+
+/** 缁勪欢鍙栨秷鎸傚湪鐨勬椂鍊� */
+onUnmounted(async () => {
+ if (inProgressTimer.value) {
+ clearInterval(inProgressTimer.value)
+ }
+})
+</script>
diff --git a/src/views/ai/image/index/components/common/index.vue b/src/views/ai/image/index/components/common/index.vue
new file mode 100644
index 0000000..6af1caf
--- /dev/null
+++ b/src/views/ai/image/index/components/common/index.vue
@@ -0,0 +1,189 @@
+<!-- dall3 -->
+<template>
+ <div class="prompt">
+ <el-text tag="b">鐢婚潰鎻忚堪</el-text>
+ <el-text tag="p">寤鸿浣跨敤鈥滃舰瀹硅瘝 + 鍔ㄨ瘝 + 椋庢牸鈥濈殑鏍煎紡锛屼娇鐢ㄢ�滐紝鈥濋殧寮�</el-text>
+ <el-input
+ v-model="prompt"
+ maxlength="1024"
+ :rows="5"
+ class="w-100% mt-15px"
+ input-style="border-radius: 7px;"
+ placeholder="渚嬪锛氱璇濋噷鐨勫皬灞嬪簲璇ユ槸浠�涔堟牱瀛愶紵"
+ show-word-limit
+ type="textarea"
+ />
+ </div>
+ <div class="flex flex-col mt-30px">
+ <div>
+ <el-text tag="b">闅忔満鐑瘝</el-text>
+ </div>
+ <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
+ <el-button
+ round
+ class="m-0"
+ :type="selectHotWord === hotWord ? 'primary' : 'default'"
+ v-for="hotWord in ImageHotWords"
+ :key="hotWord"
+ @click="handleHotWordClick(hotWord)"
+ >
+ {{ hotWord }}
+ </el-button>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">骞冲彴</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-select
+ v-model="otherPlatform"
+ placeholder="Select"
+ size="large"
+ class="!w-350px"
+ @change="handlerPlatformChange"
+ >
+ <el-option
+ v-for="item in OtherPlatformEnum"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">妯″瀷</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-select v-model="modelId" placeholder="Select" size="large" class="!w-350px">
+ <el-option
+ v-for="item in platformModels"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">鍥剧墖灏哄</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-input v-model="width" type="number" class="w-170px" placeholder="鍥剧墖瀹藉害" />
+ <el-input v-model="height" type="number" class="w-170px" placeholder="鍥剧墖楂樺害" />
+ </el-space>
+ </div>
+ <div class="flex justify-center mt-50px">
+ <el-button
+ type="primary"
+ size="large"
+ round
+ :loading="drawIn"
+ :disabled="prompt.length === 0"
+ @click="handleGenerateImage"
+ >
+ {{ drawIn ? '鐢熸垚涓�' : '鐢熸垚鍐呭' }}
+ </el-button>
+ </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
+import { AiPlatformEnum, ImageHotWords, OtherPlatformEnum } from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 鎺ユ敹鐖剁粍浠朵紶鍏ョ殑妯″瀷鍒楄〃
+const props = defineProps({
+ models: {
+ type: Array<ModelVO>,
+ default: () => [] as ModelVO[]
+ }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 瀹氫箟 emits
+
+// 瀹氫箟灞炴��
+const drawIn = ref<boolean>(false) // 鐢熸垚涓�
+const selectHotWord = ref<string>('') // 閫変腑鐨勭儹璇�
+// 琛ㄥ崟
+const prompt = ref<string>('') // 鎻愮ず璇�
+const width = ref<number>(512) // 鍥剧墖瀹藉害
+const height = ref<number>(512) // 鍥剧墖楂樺害
+const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 骞冲彴
+const platformModels = ref<ModelVO[]>([]) // 妯″瀷鍒楄〃
+const modelId = ref<number>() // 閫変腑鐨勬ā鍨�
+
+/** 閫夋嫨鐑瘝 */
+const handleHotWordClick = async (hotWord: string) => {
+ // 鎯呭喌涓�锛氬彇娑堥�変腑
+ if (selectHotWord.value == hotWord) {
+ selectHotWord.value = ''
+ return
+ }
+
+ // 鎯呭喌浜岋細閫変腑
+ selectHotWord.value = hotWord // 閫変腑
+ prompt.value = hotWord // 鏇挎崲鎻愮ず璇�
+}
+
+/** 鍥剧墖鐢熸垚 */
+const handleGenerateImage = async () => {
+ // 浜屾纭
+ await message.confirm(`纭鐢熸垚鍐呭?`)
+ try {
+ // 鍔犺浇涓�
+ drawIn.value = true
+ // 鍥炶皟
+ emits('onDrawStart', otherPlatform.value)
+ // 鍙戦�佽姹�
+ const form = {
+ platform: otherPlatform.value,
+ modelId: modelId.value, // 妯″瀷
+ prompt: prompt.value, // 鎻愮ず璇�
+ width: width.value, // 鍥剧墖瀹藉害
+ height: height.value, // 鍥剧墖楂樺害
+ options: {}
+ } as unknown as ImageDrawReqVO
+ await ImageApi.drawImage(form)
+ } finally {
+ // 鍥炶皟
+ emits('onDrawComplete', otherPlatform.value)
+ // 鍔犺浇缁撴潫
+ drawIn.value = false
+ }
+}
+
+/** 濉厖鍊� */
+const settingValues = async (detail: ImageVO) => {
+ prompt.value = detail.prompt
+ width.value = detail.width
+ height.value = detail.height
+}
+
+/** 骞冲彴鍒囨崲 */
+const handlerPlatformChange = async (platform: string) => {
+ // 鏍规嵁閫夋嫨鐨勫钩鍙扮瓫閫夋ā鍨�
+ platformModels.value = props.models.filter((item: ModelVO) => item.platform === platform)
+
+ // 鍒囨崲骞冲彴锛岄粯璁ら�夋嫨涓�涓ā鍨�
+ if (platformModels.value.length > 0) {
+ modelId.value = platformModels.value[0].id // 浣跨敤 model 灞炴�т綔涓哄��
+ } else {
+ modelId.value = undefined
+ }
+}
+
+/** 鐩戝惉 models 鍙樺寲 */
+watch(
+ () => props.models,
+ () => {
+ handlerPlatformChange(otherPlatform.value)
+ },
+ { immediate: true, deep: true }
+)
+/** 鏆撮湶缁勪欢鏂规硶 */
+defineExpose({ settingValues })
+</script>
diff --git a/src/views/ai/image/index/components/dall3/index.vue b/src/views/ai/image/index/components/dall3/index.vue
new file mode 100644
index 0000000..07a35e8
--- /dev/null
+++ b/src/views/ai/image/index/components/dall3/index.vue
@@ -0,0 +1,232 @@
+<!-- dall3 -->
+<template>
+ <div class="prompt">
+ <el-text tag="b">鐢婚潰鎻忚堪</el-text>
+ <el-text tag="p">寤鸿浣跨敤"褰㈠璇� + 鍔ㄨ瘝 + 椋庢牸"鐨勬牸寮忥紝浣跨敤"锛�"闅斿紑</el-text>
+ <el-input
+ v-model="prompt"
+ maxlength="1024"
+ :rows="5"
+ class="w-100% mt-15px"
+ input-style="border-radius: 7px;"
+ placeholder="渚嬪锛氱璇濋噷鐨勫皬灞嬪簲璇ユ槸浠�涔堟牱瀛愶紵"
+ show-word-limit
+ type="textarea"
+ />
+ </div>
+ <div class="flex flex-col mt-30px">
+ <div>
+ <el-text tag="b">闅忔満鐑瘝</el-text>
+ </div>
+ <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
+ <el-button
+ round
+ class="m-0"
+ :type="selectHotWord === hotWord ? 'primary' : 'default'"
+ v-for="hotWord in ImageHotWords"
+ :key="hotWord"
+ @click="handleHotWordClick(hotWord)"
+ >
+ {{ hotWord }}
+ </el-button>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">妯″瀷閫夋嫨</el-text>
+ </div>
+ <el-space wrap class="mt-15px">
+ <div
+ :class="selectModel === model.key ? 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-transparent cursor-pointer'"
+ v-for="model in Dall3Models"
+ :key="model.key"
+ >
+ <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
+ <div class="text-14px color-#3e3e3e font-bold">{{ model.name }}</div>
+ </div>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">椋庢牸閫夋嫨</el-text>
+ </div>
+ <el-space wrap class="mt-15px">
+ <div
+ :class="style === imageStyle.key ? 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'w-110px overflow-hidden flex flex-col items-center border-3 border-solid border-transparent cursor-pointer'"
+ v-for="imageStyle in Dall3StyleList"
+ :key="imageStyle.key"
+ >
+ <el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" />
+ <div class="text-14px color-#3e3e3e font-bold">{{ imageStyle.name }}</div>
+ </div>
+ </el-space>
+ </div>
+ <div class="w-full mt-30px">
+ <div>
+ <el-text tag="b">鐢婚潰姣斾緥</el-text>
+ </div>
+ <el-space wrap class="flex flex-row justify-between w-full mt-20px">
+ <div
+ class="flex flex-col items-center cursor-pointer"
+ v-for="imageSize in Dall3SizeList"
+ :key="imageSize.key"
+ @click="handleSizeClick(imageSize)"
+ >
+ <div
+ :class="selectSize === imageSize.key ? 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-#1293ff' : 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-white'"
+ >
+ <div :style="imageSize.style"></div>
+ </div>
+ <div class="text-14px color-#3e3e3e font-bold">{{ imageSize.name }}</div>
+ </div>
+ </el-space>
+ </div>
+ <div class="flex justify-center mt-50px">
+ <el-button
+ type="primary"
+ size="large"
+ round
+ :loading="drawIn"
+ :disabled="prompt.length === 0"
+ @click="handleGenerateImage"
+ >
+ {{ drawIn ? '鐢熸垚涓�' : '鐢熸垚鍐呭' }}
+ </el-button>
+ </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
+import {
+ Dall3Models,
+ Dall3StyleList,
+ ImageHotWords,
+ Dall3SizeList,
+ ImageModelVO,
+ AiPlatformEnum,
+ ImageSizeVO
+} from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 鎺ユ敹鐖剁粍浠朵紶鍏ョ殑妯″瀷鍒楄〃
+const props = defineProps({
+ models: {
+ type: Array<ModelVO>,
+ default: () => [] as ModelVO[]
+ }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 瀹氫箟 emits
+
+// 瀹氫箟灞炴��
+const prompt = ref<string>('') // 鎻愮ず璇�
+const drawIn = ref<boolean>(false) // 鐢熸垚涓�
+const selectHotWord = ref<string>('') // 閫変腑鐨勭儹璇�
+const selectModel = ref<string>('dall-e-3') // 妯″瀷
+const selectSize = ref<string>('1024x1024') // 閫変腑 size
+const style = ref<string>('vivid') // style 鏍峰紡
+
+/** 閫夋嫨鐑瘝 */
+const handleHotWordClick = async (hotWord: string) => {
+ // 鎯呭喌涓�锛氬彇娑堥�変腑
+ if (selectHotWord.value == hotWord) {
+ selectHotWord.value = ''
+ return
+ }
+
+ // 鎯呭喌浜岋細閫変腑
+ selectHotWord.value = hotWord
+ prompt.value = hotWord
+}
+
+/** 閫夋嫨 model 妯″瀷 */
+const handleModelClick = async (model: ImageModelVO) => {
+ selectModel.value = model.key
+ // 鍙互鍦ㄨ繖閲屾坊鍔犳ā鍨嬬壒瀹氱殑澶勭悊閫昏緫
+ // 渚嬪锛屽鏋滄湭鏉ラ渶瑕佹牴鎹笉鍚屾ā鍨嬭缃笉鍚屽弬鏁�
+ if (model.key === 'dall-e-3') {
+ // DALL-E-3 妯″瀷鐗瑰畾鐨勫鐞�
+ style.value = 'vivid' // 榛樿璁剧疆vivid椋庢牸
+ } else if (model.key === 'dall-e-2') {
+ // DALL-E-2 妯″瀷鐗瑰畾鐨勫鐞�
+ style.value = 'natural' // 濡傛灉鏈夊叾浠朌ALL-E-2閫傚悎鐨勯粯璁ら鏍�
+ }
+
+ // 鏇存柊鍏朵粬鐩稿叧鍙傛暟
+ // 渚嬪鍙互榛樿閫夋嫨鏈�閫傚悎褰撳墠妯″瀷鐨勫昂瀵�
+ const recommendedSize = Dall3SizeList.find(
+ (size) =>
+ (model.key === 'dall-e-3' && size.key === '1024x1024') ||
+ (model.key === 'dall-e-2' && size.key === '512x512')
+ )
+
+ if (recommendedSize) {
+ selectSize.value = recommendedSize.key
+ }
+}
+
+/** 閫夋嫨 style 鏍峰紡 */
+const handleStyleClick = async (imageStyle: ImageModelVO) => {
+ style.value = imageStyle.key
+}
+
+/** 閫夋嫨 size 澶у皬 */
+const handleSizeClick = async (imageSize: ImageSizeVO) => {
+ selectSize.value = imageSize.key
+}
+
+/** 鍥剧墖鐢熶骇 */
+const handleGenerateImage = async () => {
+ // 浠� models 涓煡鎵惧尮閰嶇殑妯″瀷
+ const matchedModel = props.models.find(
+ (item) => item.model === selectModel.value && item.platform === AiPlatformEnum.OPENAI
+ )
+ if (!matchedModel) {
+ message.error('璇ユā鍨嬩笉鍙敤锛岃閫夋嫨鍏跺畠妯″瀷')
+ return
+ }
+
+ // 浜屾纭
+ await message.confirm(`纭鐢熸垚鍐呭?`)
+ try {
+ // 鍔犺浇涓�
+ drawIn.value = true
+ // 鍥炶皟
+ emits('onDrawStart', AiPlatformEnum.OPENAI)
+ const imageSize = Dall3SizeList.find((item) => item.key === selectSize.value) as ImageSizeVO
+ const form = {
+ platform: AiPlatformEnum.OPENAI,
+ prompt: prompt.value, // 鎻愮ず璇�
+ modelId: matchedModel.id, // 浣跨敤鍖归厤鍒扮殑妯″瀷
+ style: style.value, // 鍥惧儚鐢熸垚鐨勯鏍�
+ width: imageSize.width, // size 涓嶈兘涓虹┖
+ height: imageSize.height, // size 涓嶈兘涓虹┖
+ options: {
+ style: style.value // 鍥惧儚鐢熸垚鐨勯鏍�
+ }
+ } as ImageDrawReqVO
+ // 鍙戦�佽姹�
+ await ImageApi.drawImage(form)
+ } finally {
+ // 鍥炶皟
+ emits('onDrawComplete', AiPlatformEnum.OPENAI)
+ // 鍔犺浇缁撴潫
+ drawIn.value = false
+ }
+}
+
+/** 濉厖鍊� */
+const settingValues = async (detail: ImageVO) => {
+ prompt.value = detail.prompt
+ selectModel.value = detail.model
+ style.value = detail.options?.style
+ const imageSize = Dall3SizeList.find(
+ (item) => item.key === `${detail.width}x${detail.height}`
+ ) as ImageSizeVO
+ await handleSizeClick(imageSize)
+}
+
+/** 鏆撮湶缁勪欢鏂规硶 */
+defineExpose({ settingValues })
+</script>
+
diff --git a/src/views/ai/image/index/components/midjourney/index.vue b/src/views/ai/image/index/components/midjourney/index.vue
new file mode 100644
index 0000000..d59a83a
--- /dev/null
+++ b/src/views/ai/image/index/components/midjourney/index.vue
@@ -0,0 +1,236 @@
+<!-- dall3 -->
+<template>
+ <div class="prompt">
+ <el-text tag="b">鐢婚潰鎻忚堪</el-text>
+ <el-text tag="p">寤鸿浣跨敤鈥滃舰瀹硅瘝+鍔ㄨ瘝+椋庢牸鈥濈殑鏍煎紡锛屼娇鐢ㄢ�滐紝鈥濋殧寮�.</el-text>
+ <el-input
+ v-model="prompt"
+ maxlength="1024"
+ :rows="5"
+ class="w-100% mt-15px"
+ input-style="border-radius: 7px;"
+ placeholder="渚嬪锛氱璇濋噷鐨勫皬灞嬪簲璇ユ槸浠�涔堟牱瀛愶紵"
+ show-word-limit
+ type="textarea"
+ />
+ </div>
+ <div class="flex flex-col mt-30px">
+ <div>
+ <el-text tag="b">闅忔満鐑瘝</el-text>
+ </div>
+ <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
+ <el-button
+ round
+ class="m-0"
+ :type="selectHotWord === hotWord ? 'primary' : 'default'"
+ v-for="hotWord in ImageHotWords"
+ :key="hotWord"
+ @click="handleHotWordClick(hotWord)"
+ >
+ {{ hotWord }}
+ </el-button>
+ </el-space>
+ </div>
+ <div class="w-full mt-30px">
+ <div>
+ <el-text tag="b">灏哄</el-text>
+ </div>
+ <el-space wrap class="flex flex-row justify-between w-full mt-20px">
+ <div
+ class="flex flex-col items-center cursor-pointer"
+ v-for="imageSize in MidjourneySizeList"
+ :key="imageSize.key"
+ @click="handleSizeClick(imageSize)"
+ >
+ <div
+ :class="selectSize === imageSize.key ? 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-#1293ff' : 'flex flex-col items-center justify-center rounded-7px p-4px w-50px h-50px bg-white border-1 border-solid border-white'"
+ >
+ <div :style="imageSize.style"></div>
+ </div>
+ <div class="text-14px color-#3e3e3e font-bold">{{ imageSize.key }}</div>
+ </div>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">妯″瀷</el-text>
+ </div>
+ <el-space wrap class="mt-15px">
+ <div
+ :class="selectModel === model.key ? 'flex flex-col items-center w-150px overflow-hidden border-3 border-solid border-#1293ff rounded-5px cursor-pointer' : 'flex flex-col items-center w-150px overflow-hidden border-3 border-solid border-transparent cursor-pointer'"
+ v-for="model in MidjourneyModels"
+ :key="model.key"
+ >
+ <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" />
+ <div class="text-14px color-#3e3e3e font-bold">{{ model.name }}</div>
+ </div>
+ </el-space>
+ </div>
+ <div class="mt-20px">
+ <div>
+ <el-text tag="b">鐗堟湰</el-text>
+ </div>
+ <el-space wrap class="mt-20px w-full">
+ <el-select
+ v-model="selectVersion"
+ class="!w-350px"
+ clearable
+ placeholder="璇烽�夋嫨鐗堟湰"
+ >
+ <el-option
+ v-for="item in versionList"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">鍙傝�冨浘</el-text>
+ </div>
+ <el-space wrap class="mt-15px">
+ <UploadImg v-model="referImageUrl" height="120px" width="120px" />
+ </el-space>
+ </div>
+ <div class="flex justify-center mt-50px">
+ <el-button
+ type="primary"
+ size="large"
+ round
+ :disabled="prompt.length === 0"
+ @click="handleGenerateImage"
+ >
+ {{ drawIn ? '鐢熸垚涓�' : '鐢熸垚鍐呭' }}
+ </el-button>
+ </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageMidjourneyImagineReqVO, ImageVO } from '@/api/ai/image'
+import {
+ AiPlatformEnum,
+ ImageHotWords,
+ ImageSizeVO,
+ ImageModelVO,
+ MidjourneyModels,
+ MidjourneySizeList,
+ MidjourneyVersions,
+ NijiVersionList
+} from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 鎺ユ敹鐖剁粍浠朵紶鍏ョ殑妯″瀷鍒楄〃
+const props = defineProps({
+ models: {
+ type: Array<ModelVO>,
+ default: () => [] as ModelVO[]
+ }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 瀹氫箟 emits
+
+// 瀹氫箟灞炴��
+const drawIn = ref<boolean>(false) // 鐢熸垚涓�
+const selectHotWord = ref<string>('') // 閫変腑鐨勭儹璇�
+// 琛ㄥ崟
+const prompt = ref<string>('') // 鎻愮ず璇�
+const referImageUrl = ref<any>() // 鍙傝�冨浘
+const selectModel = ref<string>('midjourney') // 閫変腑鐨勬ā鍨�
+const selectSize = ref<string>('1:1') // 閫変腑 size
+const selectVersion = ref<any>('6.0') // 閫変腑鐨� version
+const versionList = ref<any>(MidjourneyVersions) // version 鍒楄〃
+
+/** 閫夋嫨鐑瘝 */
+const handleHotWordClick = async (hotWord: string) => {
+ // 鎯呭喌涓�锛氬彇娑堥�変腑
+ if (selectHotWord.value == hotWord) {
+ selectHotWord.value = ''
+ return
+ }
+
+ // 鎯呭喌浜岋細閫変腑
+ selectHotWord.value = hotWord // 閫変腑
+ prompt.value = hotWord // 璁剧疆鎻愮ず娆�
+}
+
+/** 鐐瑰嚮 size 灏哄 */
+const handleSizeClick = async (imageSize: ImageSizeVO) => {
+ selectSize.value = imageSize.key
+}
+
+/** 鐐瑰嚮 model 妯″瀷 */
+const handleModelClick = async (model: ImageModelVO) => {
+ selectModel.value = model.key
+ if (model.key === 'niji') {
+ versionList.value = NijiVersionList // 榛樿閫夋嫨 niji
+ } else {
+ versionList.value = MidjourneyVersions // 榛樿閫夋嫨 midjourney
+ }
+ selectVersion.value = versionList.value[0].value
+}
+
+/** 鍥剧墖鐢熸垚 */
+const handleGenerateImage = async () => {
+ // 浠� models 涓煡鎵惧尮閰嶇殑妯″瀷
+ const matchedModel = props.models.find(
+ (item) => item.model === selectModel.value && item.platform === AiPlatformEnum.MIDJOURNEY
+ )
+ if (!matchedModel) {
+ message.error('璇ユā鍨嬩笉鍙敤锛岃閫夋嫨鍏跺畠妯″瀷')
+ return
+ }
+
+ // 浜屾纭
+ await message.confirm(`纭鐢熸垚鍐呭?`)
+ try {
+ // 鍔犺浇涓�
+ drawIn.value = true
+ // 鍥炶皟
+ emits('onDrawStart', AiPlatformEnum.MIDJOURNEY)
+ // 鍙戦�佽姹�
+ const imageSize = MidjourneySizeList.find(
+ (item) => selectSize.value === item.key
+ ) as ImageSizeVO
+ const req = {
+ prompt: prompt.value,
+ modelId: matchedModel.id,
+ width: imageSize.width,
+ height: imageSize.height,
+ version: selectVersion.value,
+ referImageUrl: referImageUrl.value
+ } as ImageMidjourneyImagineReqVO
+ await ImageApi.midjourneyImagine(req)
+ } finally {
+ // 鍥炶皟
+ emits('onDrawComplete', AiPlatformEnum.MIDJOURNEY)
+ // 鍔犺浇缁撴潫
+ drawIn.value = false
+ }
+}
+
+/** 濉厖鍊� */
+const settingValues = async (detail: ImageVO) => {
+ // 鎻愮ず璇�
+ prompt.value = detail.prompt
+ // image size
+ const imageSize = MidjourneySizeList.find(
+ (item) => item.key === `${detail.width}:${detail.height}`
+ ) as ImageSizeVO
+ selectSize.value = imageSize.key
+ // 閫変腑妯″瀷
+ const model = MidjourneyModels.find((item) => item.key === detail.options?.model) as ImageModelVO
+ await handleModelClick(model)
+ // 鐗堟湰
+ selectVersion.value = versionList.value.find(
+ (item) => item.value === detail.options?.version
+ ).value
+ // image
+ referImageUrl.value = detail.options.referImageUrl
+}
+
+/** 鏆撮湶缁勪欢鏂规硶 */
+defineExpose({ settingValues })
+</script>
+
diff --git a/src/views/ai/image/index/components/stableDiffusion/index.vue b/src/views/ai/image/index/components/stableDiffusion/index.vue
new file mode 100644
index 0000000..1fa68d5
--- /dev/null
+++ b/src/views/ai/image/index/components/stableDiffusion/index.vue
@@ -0,0 +1,257 @@
+<!-- dall3 -->
+<template>
+ <div class="prompt">
+ <el-text tag="b">鐢婚潰鎻忚堪</el-text>
+ <el-text tag="p">寤鸿浣跨敤鈥滃舰瀹硅瘝 + 鍔ㄨ瘝 + 椋庢牸鈥濈殑鏍煎紡锛屼娇鐢ㄢ�滐紝鈥濋殧寮�</el-text>
+ <el-input
+ v-model="prompt"
+ maxlength="1024"
+ :rows="5"
+ class="w-100% mt-15px"
+ input-style="border-radius: 7px;"
+ placeholder="渚嬪锛氱璇濋噷鐨勫皬灞嬪簲璇ユ槸浠�涔堟牱瀛愶紵"
+ show-word-limit
+ type="textarea"
+ />
+ </div>
+ <div class="flex flex-col mt-30px">
+ <div>
+ <el-text tag="b">闅忔満鐑瘝</el-text>
+ </div>
+ <el-space wrap class="flex flex-row flex-wrap justify-start mt-15px">
+ <el-button
+ round
+ class="m-0"
+ :type="selectHotWord === hotWord ? 'primary' : 'default'"
+ v-for="hotWord in ImageHotEnglishWords"
+ :key="hotWord"
+ @click="handleHotWordClick(hotWord)"
+ >
+ {{ hotWord }}
+ </el-button>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">閲囨牱鏂规硶</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-select v-model="sampler" placeholder="Select" size="large" class="!w-350px">
+ <el-option
+ v-for="item in StableDiffusionSamplers"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">CLIP</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px">
+ <el-option
+ v-for="item in StableDiffusionClipGuidancePresets"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">椋庢牸</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-select v-model="stylePreset" placeholder="Select" size="large" class="!w-350px">
+ <el-option
+ v-for="item in StableDiffusionStylePresets"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">鍥剧墖灏哄</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-input v-model="width" class="w-170px" placeholder="鍥剧墖瀹藉害" />
+ <el-input v-model="height" class="w-170px" placeholder="鍥剧墖楂樺害" />
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">杩唬姝ユ暟</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-input
+ v-model="steps"
+ type="number"
+ size="large"
+ class="!w-350px"
+ placeholder="Please input"
+ />
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">寮曞绯绘暟</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-input
+ v-model="scale"
+ type="number"
+ size="large"
+ class="!w-350px"
+ placeholder="Please input"
+ />
+ </el-space>
+ </div>
+ <div class="mt-30px">
+ <div>
+ <el-text tag="b">闅忔満鍥犲瓙</el-text>
+ </div>
+ <el-space wrap class="mt-15px w-full">
+ <el-input
+ v-model="seed"
+ type="number"
+ size="large"
+ class="!w-350px"
+ placeholder="Please input"
+ />
+ </el-space>
+ </div>
+ <div class="flex justify-center mt-50px">
+ <el-button
+ type="primary"
+ size="large"
+ round
+ :loading="drawIn"
+ :disabled="prompt.length === 0"
+ @click="handleGenerateImage"
+ >
+ {{ drawIn ? '鐢熸垚涓�' : '鐢熸垚鍐呭' }}
+ </el-button>
+ </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image'
+import { hasChinese } from '@/views/ai/utils/utils'
+import {
+ AiPlatformEnum,
+ ImageHotEnglishWords,
+ StableDiffusionClipGuidancePresets,
+ StableDiffusionSamplers,
+ StableDiffusionStylePresets
+} from '@/views/ai/utils/constants'
+import { ModelVO } from '@/api/ai/model/model'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 鎺ユ敹鐖剁粍浠朵紶鍏ョ殑妯″瀷鍒楄〃
+const props = defineProps({
+ models: {
+ type: Array<ModelVO>,
+ default: () => [] as ModelVO[]
+ }
+})
+const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 瀹氫箟 emits
+
+// 瀹氫箟灞炴��
+const drawIn = ref<boolean>(false) // 鐢熸垚涓�
+const selectHotWord = ref<string>('') // 閫変腑鐨勭儹璇�
+// 琛ㄥ崟
+const prompt = ref<string>('') // 鎻愮ず璇�
+const width = ref<number>(512) // 鍥剧墖瀹藉害
+const height = ref<number>(512) // 鍥剧墖楂樺害
+const sampler = ref<string>('DDIM') // 閲囨牱鏂规硶
+const steps = ref<number>(20) // 杩唬姝ユ暟
+const seed = ref<number>(42) // 鎺у埗鐢熸垚鍥惧儚鐨勯殢鏈烘��
+const scale = ref<number>(7.5) // 寮曞绯绘暟
+const clipGuidancePreset = ref<string>('NONE') // 鏂囨湰鎻愮ず鐩稿尮閰嶇殑鍥惧儚(clip_guidance_preset) 绠�绉� CLIP
+const stylePreset = ref<string>('3d-model') // 椋庢牸
+
+/** 閫夋嫨鐑瘝 */
+const handleHotWordClick = async (hotWord: string) => {
+ // 鎯呭喌涓�锛氬彇娑堥�変腑
+ if (selectHotWord.value == hotWord) {
+ selectHotWord.value = ''
+ return
+ }
+
+ // 鎯呭喌浜岋細閫変腑
+ selectHotWord.value = hotWord // 閫変腑
+ prompt.value = hotWord // 鏇挎崲鎻愮ず璇�
+}
+
+/** 鍥剧墖鐢熸垚 */
+const handleGenerateImage = async () => {
+ // 浠� models 涓煡鎵惧尮閰嶇殑妯″瀷
+ const selectModel = 'stable-diffusion-v1-6'
+ const matchedModel = props.models.find(
+ (item) => item.model === selectModel && item.platform === AiPlatformEnum.STABLE_DIFFUSION
+ )
+ if (!matchedModel) {
+ message.error('璇ユā鍨嬩笉鍙敤锛岃閫夋嫨鍏跺畠妯″瀷')
+ return
+ }
+
+ // 浜屾纭
+ if (hasChinese(prompt.value)) {
+ message.alert('鏆備笉鏀寔涓枃锛�')
+ return
+ }
+ await message.confirm(`纭鐢熸垚鍐呭?`)
+
+ try {
+ // 鍔犺浇涓�
+ drawIn.value = true
+ // 鍥炶皟
+ emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION)
+ // 鍙戦�佽姹�
+ const form = {
+ modelId: matchedModel.id,
+ prompt: prompt.value, // 鎻愮ず璇�
+ width: width.value, // 鍥剧墖瀹藉害
+ height: height.value, // 鍥剧墖楂樺害
+ options: {
+ seed: seed.value, // 闅忔満绉嶅瓙
+ steps: steps.value, // 鍥剧墖鐢熸垚姝ユ暟
+ scale: scale.value, // 寮曞绯绘暟
+ sampler: sampler.value, // 閲囨牱绠楁硶
+ clipGuidancePreset: clipGuidancePreset.value, // 鏂囨湰鎻愮ず鐩稿尮閰嶇殑鍥惧儚 CLIP
+ stylePreset: stylePreset.value // 椋庢牸
+ }
+ } as ImageDrawReqVO
+ await ImageApi.drawImage(form)
+ } finally {
+ // 鍥炶皟
+ emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
+ // 鍔犺浇缁撴潫
+ drawIn.value = false
+ }
+}
+
+/** 濉厖鍊� */
+const settingValues = async (detail: ImageVO) => {
+ prompt.value = detail.prompt
+ width.value = detail.width
+ height.value = detail.height
+ seed.value = detail.options?.seed
+ steps.value = detail.options?.steps
+ scale.value = detail.options?.scale
+ sampler.value = detail.options?.sampler
+ clipGuidancePreset.value = detail.options?.clipGuidancePreset
+ stylePreset.value = detail.options?.stylePreset
+}
+
+/** 鏆撮湶缁勪欢鏂规硶 */
+defineExpose({ settingValues })
+</script>
+
diff --git a/src/views/ai/image/index/index.vue b/src/views/ai/image/index/index.vue
new file mode 100644
index 0000000..1550db8
--- /dev/null
+++ b/src/views/ai/image/index/index.vue
@@ -0,0 +1,114 @@
+<!-- image -->
+<template>
+ <div class="absolute inset-0 flex flex-row wh-full">
+ <div class="flex flex-col p-5 w-[390px]">
+ <div class="mb-[30px]">
+ <el-segmented
+ v-model="selectPlatform"
+ :options="platformOptions"
+ class="w-[350px] !bg-[#ececec] [--el-border-radius-base:16px] [--el-segmented-item-selected-color:#fff]"
+ />
+ </div>
+ <div class="h-full overflow-y-auto">
+ <Common
+ v-if="selectPlatform === 'common'"
+ ref="commonRef"
+ :models="models"
+ @on-draw-complete="handleDrawComplete"
+ />
+ <Dall3
+ v-if="selectPlatform === AiPlatformEnum.OPENAI"
+ ref="dall3Ref"
+ :models="models"
+ @on-draw-start="handleDrawStart"
+ @on-draw-complete="handleDrawComplete"
+ />
+ <Midjourney
+ v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
+ ref="midjourneyRef"
+ :models="models"
+ />
+ <StableDiffusion
+ v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
+ ref="stableDiffusionRef"
+ :models="models"
+ @on-draw-complete="handleDrawComplete"
+ />
+ </div>
+ </div>
+ <div class="flex-1 bg-white">
+ <ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import ImageList from './components/ImageList.vue'
+import { AiPlatformEnum } from '@/views/ai/utils/constants'
+import { ImageVO } from '@/api/ai/image'
+import Dall3 from './components/dall3/index.vue'
+import Midjourney from './components/midjourney/index.vue'
+import StableDiffusion from './components/stableDiffusion/index.vue'
+import Common from './components/common/index.vue'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+
+const imageListRef = ref<any>() // image 鍒楄〃 ref
+const dall3Ref = ref<any>() // dall3(openai) ref
+const midjourneyRef = ref<any>() // midjourney ref
+const stableDiffusionRef = ref<any>() // stable diffusion ref
+const commonRef = ref<any>() // stable diffusion ref
+
+// 瀹氫箟灞炴��
+const selectPlatform = ref('common') // 閫変腑鐨勫钩鍙�
+const platformOptions = [
+ {
+ label: '閫氱敤',
+ value: 'common'
+ },
+ {
+ label: 'DALL3 缁樼敾',
+ value: AiPlatformEnum.OPENAI
+ },
+ {
+ label: 'MJ 缁樼敾',
+ value: AiPlatformEnum.MIDJOURNEY
+ },
+ {
+ label: 'SD 缁樺浘',
+ value: AiPlatformEnum.STABLE_DIFFUSION
+ }
+]
+
+const models = ref<ModelVO[]>([]) // 妯″瀷鍒楄〃
+
+/** 缁樼敾 start */
+const handleDrawStart = async (_platform: string) => {}
+
+/** 缁樼敾 complete */
+const handleDrawComplete = async (_platform: string) => {
+ await imageListRef.value.getImageList()
+}
+
+/** 閲嶆柊鐢熸垚锛氬皢鐢诲浘璇︽儏濉厖鍒板搴斿钩鍙� */
+const handleRegeneration = async (image: ImageVO) => {
+ // 鍒囨崲骞冲彴
+ selectPlatform.value = image.platform
+ // 鏍规嵁涓嶅悓骞冲彴濉厖 image
+ await nextTick()
+ if (image.platform === AiPlatformEnum.MIDJOURNEY) {
+ midjourneyRef.value.settingValues(image)
+ } else if (image.platform === AiPlatformEnum.OPENAI) {
+ dall3Ref.value.settingValues(image)
+ } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
+ stableDiffusionRef.value.settingValues(image)
+ }
+ // TODO @fan锛氳矊浼� other 閲嶆柊璁剧疆涓嶈锛�
+}
+
+/** 缁勪欢鎸傝浇鐨勬椂鍊� */
+onMounted(async () => {
+ // 鑾峰彇妯″瀷鍒楄〃
+ models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
+})
+</script>
diff --git a/src/views/ai/image/manager/index.vue b/src/views/ai/image/manager/index.vue
new file mode 100644
index 0000000..6250e58
--- /dev/null
+++ b/src/views/ai/image/manager/index.vue
@@ -0,0 +1,253 @@
+<template>
+ <doc-alert title="AI 缁樺浘鍒涗綔" url="https://doc.iocoder.cn/ai/image/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="骞冲彴" prop="platform">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨骞冲彴" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缁樼敾鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨缁樼敾鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.AI_IMAGE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏄惁鍙戝竷" prop="publicStatus">
+ <el-select
+ v-model="queryParams.publicStatus"
+ placeholder="璇烽�夋嫨鏄惁鍙戝竷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="180" fixed="left" />
+ <el-table-column label="鍥剧墖" align="center" prop="picUrl" width="110px" fixed="left">
+ <template #default="{ row }">
+ <el-image
+ class="h-80px w-80px"
+ lazy
+ :src="row.picUrl"
+ :preview-src-list="[row.picUrl]"
+ preview-teleported
+ fit="cover"
+ v-if="row.picUrl?.length > 0"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛" align="center" prop="userId" width="180">
+ <template #default="scope">
+ <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="骞冲彴" align="center" prop="platform" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯″瀷" align="center" prop="model" width="180" />
+ <el-table-column label="缁樼敾鐘舵��" align="center" prop="status" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_IMAGE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁鍙戝竷" align="center" prop="publicStatus">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.publicStatus"
+ :active-value="true"
+ :inactive-value="false"
+ @change="handleUpdatePublicStatusChange(scope.row)"
+ :disabled="scope.row.status !== AiImageStatusEnum.SUCCESS"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮ず璇�" align="center" prop="prompt" width="180" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="瀹藉害" align="center" prop="width" />
+ <el-table-column label="楂樺害" align="center" prop="height" />
+ <el-table-column label="閿欒淇℃伅" align="center" prop="errorMessage" />
+ <el-table-column label="浠诲姟缂栧彿" align="center" prop="taskId" />
+ <el-table-column label="鎿嶄綔" align="center" width="100" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:image:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ImageApi, ImageVO } from '@/api/ai/image'
+import * as UserApi from '@/api/system/user'
+import { AiImageStatusEnum } from '@/views/ai/utils/constants'
+
+/** AI 缁樼敾 鍒楄〃 */
+defineOptions({ name: 'AiImageManager' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ImageVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: undefined,
+ platform: undefined,
+ status: undefined,
+ publicStatus: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ImageApi.getImagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ImageApi.deleteImage(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼鏄惁鍙戝竷 */
+const handleUpdatePublicStatusChange = async (row: ImageVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.publicStatus ? '鍏紑' : '绉佹湁'
+ await message.confirm('纭瑕�"' + text + '"璇ュ浘鐗囧悧?')
+ // 鍙戣捣淇敼鐘舵��
+ await ImageApi.updateImage({
+ id: row.id,
+ publicStatus: row.publicStatus
+ })
+ await getList()
+ } catch {
+ row.publicStatus = !row.publicStatus
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/ai/image/square/index.vue b/src/views/ai/image/square/index.vue
new file mode 100644
index 0000000..0f82c48
--- /dev/null
+++ b/src/views/ai/image/square/index.vue
@@ -0,0 +1,67 @@
+<template>
+ <div class="bg-white p-20px">
+ <!-- TODO @fan锛歴tyle 寤鸿鎹㈡垚 unocss -->
+ <!-- TODO @fan锛歋earch 鍙互鎹㈡垚 Icon 缁勪欢涔堬紵 -->
+ <el-input
+ v-model="queryParams.prompt"
+ class="!w-full !mb-20px"
+ size="large"
+ placeholder="璇疯緭鍏ヨ鎼滅储鐨勫唴瀹�"
+ :suffix-icon="Search"
+ @keyup.enter="handleQuery"
+ />
+ <div class="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-10px bg-white shadow-[0_0_10px_rgba(0,0,0,0.1)]">
+ <!-- TODO @fan锛氳繖涓浘鐗囩殑椋庢牸锛岃涓嶅拰 ImageCard.vue 鐣岄潰涓�鑷达紵锛堝彧鏈夊崱鐗囷紝娌℃湁鎿嶄綔锛夛紱鍥犱负鐪嬬潃鏇存湁鐩告鐨勬劅瑙墌~~ -->
+ <div v-for="item in list" :key="item.id" class="relative overflow-hidden bg-gray-100 cursor-pointer transition-transform duration-300 hover:scale-105">
+ <img :src="item.picUrl" class="w-full h-auto block transition-transform duration-300 hover:scale-110" />
+ </div>
+ </div>
+ <!-- TODO @fan锛氱己灏戠炕椤� -->
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </div>
+</template>
+<script setup lang="ts">
+import { ImageApi, ImageVO } from '@/api/ai/image'
+import { Search } from '@element-plus/icons-vue'
+
+// TODO @fan锛氬姞涓� loading 鍔犺浇涓殑鐘舵��
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ImageVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ publicStatus: true,
+ prompt: undefined
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ImageApi.getImagePageMy(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await getList()
+})
+</script>
+
diff --git a/src/views/ai/knowledge/document/form/ProcessStep.vue b/src/views/ai/knowledge/document/form/ProcessStep.vue
new file mode 100644
index 0000000..4b290ea
--- /dev/null
+++ b/src/views/ai/knowledge/document/form/ProcessStep.vue
@@ -0,0 +1,146 @@
+<template>
+ <div>
+ <!-- 鏂囦欢澶勭悊鍒楄〃 -->
+ <div class="mt-15px grid grid-cols-1 gap-2">
+ <div
+ v-for="(file, index) in modelValue.list"
+ :key="index"
+ class="flex items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
+ >
+ <!-- 鏂囦欢鍥炬爣鍜屽悕绉� -->
+ <div class="flex items-center min-w-[200px] mr-10px">
+ <Icon icon="ep:document" class="mr-8px text-[#409eff]" />
+ <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
+ </div>
+
+ <!-- 澶勭悊杩涘害 -->
+ <div class="flex-1">
+ <el-progress
+ :percentage="file.progress || 0"
+ :stroke-width="10"
+ :status="isProcessComplete(file) ? 'success' : ''"
+ />
+ </div>
+
+ <!-- 鍒嗘鏁伴噺 -->
+ <div class="ml-10px text-[13px] text-[#606266]">
+ 鍒嗘鏁伴噺锛歿{ file.count ? file.count : '-' }}
+ </div>
+ </div>
+ </div>
+
+ <!-- 搴曢儴瀹屾垚鎸夐挳 -->
+ <div class="flex justify-end mt-20px">
+ <el-button
+ :type="allProcessComplete ? 'success' : 'primary'"
+ :disabled="!allProcessComplete"
+ @click="handleComplete"
+ >
+ 瀹屾垚
+ </el-button>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+
+const props = defineProps({
+ modelValue: {
+ type: Object,
+ required: true
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+const parent = inject('parent') as any
+const pollingTimer = ref<number | null>(null) // 杞瀹氭椂鍣� ID锛岀敤浜庤窡韪拰娓呴櫎杞杩涚▼
+
+/** 鍒ゆ柇鏂囦欢澶勭悊鏄惁瀹屾垚 */
+const isProcessComplete = (file) => {
+ return file.progress === 100
+}
+
+/** 鍒ゆ柇鎵�鏈夋枃浠舵槸鍚﹂兘澶勭悊瀹屾垚 */
+const allProcessComplete = computed(() => {
+ return props.modelValue.list.every((file) => isProcessComplete(file))
+})
+
+/** 瀹屾垚鎸夐挳鐐瑰嚮浜嬩欢澶勭悊 */
+const handleComplete = () => {
+ if (parent?.exposed?.handleBack) {
+ parent.exposed.handleBack()
+ }
+}
+
+/** 鑾峰彇鏂囦欢澶勭悊杩涘害 */
+const getProcessList = async () => {
+ try {
+ // 1. 璋冪敤 API 鑾峰彇澶勭悊杩涘害
+ const documentIds = props.modelValue.list.filter((item) => item.id).map((item) => item.id)
+ if (documentIds.length === 0) {
+ return
+ }
+ const result = await KnowledgeSegmentApi.getKnowledgeSegmentProcessList(documentIds)
+
+ // 2.1鏇存柊杩涘害
+ const updatedList = props.modelValue.list.map((file) => {
+ const processInfo = result.find((item) => item.documentId === file.id)
+ if (processInfo) {
+ // 璁$畻杩涘害鐧惧垎姣旓細宸插祵鍏ユ暟閲� / 鎬绘暟閲� * 100
+ const progress =
+ processInfo.embeddingCount && processInfo.count
+ ? Math.floor((processInfo.embeddingCount / processInfo.count) * 100)
+ : 0
+ return {
+ ...file,
+ progress: progress,
+ count: processInfo.count || 0
+ }
+ }
+ return file
+ })
+
+ // 2.2 鏇存柊鏁版嵁
+ emit('update:modelValue', {
+ ...props.modelValue,
+ list: updatedList
+ })
+
+ // 3. 濡傛灉鏈畬鎴愶紝缁х画杞
+ if (!updatedList.every((file) => isProcessComplete(file))) {
+ pollingTimer.value = window.setTimeout(getProcessList, 3000)
+ }
+ } catch (error) {
+ // 鍑洪敊鍚庝篃缁х画杞
+ console.error('鑾峰彇澶勭悊杩涘害澶辫触:', error)
+ pollingTimer.value = window.setTimeout(getProcessList, 5000)
+ }
+}
+
+/** 缁勪欢鎸傝浇鏃跺紑濮嬭疆璇� */
+onMounted(() => {
+ // 1. 鍒濆鍖栬繘搴︿负 0
+ const initialList = props.modelValue.list.map((file) => ({
+ ...file,
+ progress: 0
+ }))
+
+ emit('update:modelValue', {
+ ...props.modelValue,
+ list: initialList
+ })
+
+ // 2. 寮�濮嬭疆璇㈣幏鍙栬繘搴�
+ getProcessList()
+})
+
+/** 缁勪欢鍗歌浇鍓嶆竻闄よ疆璇� */
+onBeforeUnmount(() => {
+ // 1. 娓呴櫎瀹氭椂鍣�
+ if (pollingTimer.value) {
+ clearTimeout(pollingTimer.value)
+ pollingTimer.value = null
+ }
+})
+</script>
diff --git a/src/views/ai/knowledge/document/form/SplitStep.vue b/src/views/ai/knowledge/document/form/SplitStep.vue
new file mode 100644
index 0000000..5b28ce3
--- /dev/null
+++ b/src/views/ai/knowledge/document/form/SplitStep.vue
@@ -0,0 +1,238 @@
+<template>
+ <div>
+ <!-- 涓婇儴鍒嗘璁剧疆閮ㄥ垎 -->
+ <div class="mb-20px">
+ <div class="mb-20px flex justify-between items-center">
+ <div class="text-16px font-bold flex items-center">
+ 鍒嗘璁剧疆
+ <el-tooltip
+ content="绯荤粺浼氳嚜鍔ㄥ皢鏂囨。鍐呭鍒嗗壊鎴愬涓钀斤紝鎮ㄥ彲浠ユ牴鎹渶瑕佽皟鏁村垎娈垫柟寮忓拰鍐呭銆�"
+ placement="top"
+ >
+ <Icon icon="ep:warning" class="ml-5px text-gray-400" />
+ </el-tooltip>
+ </div>
+ <div>
+ <el-button type="primary" plain size="small" @click="handleAutoSegment">
+ 棰勮鍒嗘
+ </el-button>
+ </div>
+ </div>
+
+ <div class="segment-settings mb-20px">
+ <el-form label-width="120px">
+ <el-form-item label="鏈�澶� Token 鏁�">
+ <el-input-number v-model="modelData.segmentMaxTokens" :min="1" :max="2048" />
+ </el-form-item>
+ </el-form>
+ </div>
+ </div>
+
+ <!-- 涓嬮儴鏂囦欢棰勮閮ㄥ垎 -->
+ <div class="mb-10px">
+ <div class="text-16px font-bold mb-10px">鍒嗘棰勮</div>
+
+ <!-- 鏂囦欢閫夋嫨鍣� -->
+ <div class="file-selector mb-10px">
+ <el-dropdown v-if="modelData.list && modelData.list.length > 0" trigger="click">
+ <div class="flex items-center cursor-pointer">
+ <Icon icon="ep:document" class="text-danger mr-5px" />
+ <span>{{ currentFile?.name || '璇烽�夋嫨鏂囦欢' }}</span>
+ <span v-if="currentFile?.segments" class="ml-5px text-gray-500 text-12px">
+ ({{ currentFile.segments.length }}涓垎鐗�)
+ </span>
+ <Icon icon="ep:arrow-down" class="ml-5px" />
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item
+ v-for="(file, index) in modelData.list"
+ :key="index"
+ @click="selectFile(index)"
+ >
+ {{ file.name }}
+ <span v-if="file.segments" class="ml-5px text-gray-500 text-12px">
+ ({{ file.segments.length }}涓垎鐗�)
+ </span>
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ <div v-else class="text-gray-400">鏆傛棤涓婁紶鏂囦欢</div>
+ </div>
+
+ <!-- 鏂囦欢鍐呭棰勮 -->
+ <div class="file-preview bg-gray-50 p-15px rounded-md max-h-600px overflow-y-auto">
+ <div v-if="splitLoading" class="flex justify-center items-center py-20px">
+ <Icon icon="ep:loading" class="is-loading" />
+ <span class="ml-10px">姝e湪鍔犺浇鍒嗘鍐呭...</span>
+ </div>
+ <template
+ v-else-if="currentFile && currentFile.segments && currentFile.segments.length > 0"
+ >
+ <div v-for="(segment, index) in currentFile.segments" :key="index" class="mb-10px">
+ <div class="text-gray-500 text-12px mb-5px">
+ 鍒嗙墖-{{ index + 1 }} 路 {{ segment.contentLength || 0 }} 瀛楃鏁� 路
+ {{ segment.tokens || 0 }} Token
+ </div>
+ <div class="bg-white p-10px rounded-md">{{ segment.content }}</div>
+ </div>
+ </template>
+ <el-empty v-else description="鏆傛棤棰勮鍐呭" />
+ </div>
+ </div>
+
+ <!-- 娣诲姞搴曢儴鎸夐挳 -->
+ <div class="mt-20px flex justify-between">
+ <div>
+ <el-button v-if="!modelData.id" @click="handlePrevStep">涓婁竴姝�</el-button>
+ </div>
+ <div>
+ <el-button type="primary" :loading="submitLoading" @click="handleSave">
+ 淇濆瓨骞跺鐞�
+ </el-button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, getCurrentInstance, inject, onMounted, PropType, ref } from 'vue'
+import { Icon } from '@/components/Icon'
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+import { useMessage } from '@/hooks/web/useMessage'
+import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
+
+const props = defineProps({
+ modelValue: {
+ type: Object as PropType<any>,
+ required: true
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+const message = useMessage() // 娑堟伅鎻愮ず
+const parent = inject('parent', null) // 鑾峰彇鐖剁粍浠跺疄渚�
+
+const modelData = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+}) // 琛ㄥ崟鏁版嵁
+
+const splitLoading = ref(false) // 鍒嗘鍔犺浇鐘舵��
+const currentFile = ref<any>(null) // 褰撳墠閫変腑鐨勬枃浠�
+const submitLoading = ref(false) // 鎻愪氦鎸夐挳鍔犺浇鐘舵��
+
+/** 閫夋嫨鏂囦欢 */
+const selectFile = async (index: number) => {
+ currentFile.value = modelData.value.list[index]
+ await splitContent(currentFile.value)
+}
+
+/** 鑾峰彇鏂囦欢鍒嗘鍐呭 */
+const splitContent = async (file: any) => {
+ if (!file || !file.url) {
+ message.warning('鏂囦欢 URL 涓嶅瓨鍦�')
+ return
+ }
+
+ splitLoading.value = true
+ try {
+ // 璋冪敤鍚庣鍒嗘鎺ュ彛锛岃幏鍙栨枃妗g殑鍒嗘鍐呭銆佸瓧绗︽暟鍜� Token 鏁�
+ file.segments = await KnowledgeSegmentApi.splitContent(
+ file.url,
+ modelData.value.segmentMaxTokens
+ )
+ } catch (error) {
+ console.error('鑾峰彇鍒嗘鍐呭澶辫触:', file, error)
+ } finally {
+ splitLoading.value = false
+ }
+}
+
+/** 澶勭悊棰勮鍒嗘 */
+const handleAutoSegment = async () => {
+ // 濡傛灉娌℃湁閫変腑鏂囦欢锛岄粯璁ら�変腑绗竴涓�
+ if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
+ currentFile.value = modelData.value.list[0]
+ }
+ // 濡傛灉娌℃湁閫変腑鏂囦欢锛屾彁绀鸿鍏堥�夋嫨鏂囦欢
+ if (!currentFile.value) {
+ message.warning('璇峰厛閫夋嫨鏂囦欢')
+ return
+ }
+
+ // 鑾峰彇鍒嗘鍐呭
+ await splitContent(currentFile.value)
+}
+
+/** 涓婁竴姝ユ寜閽鐞� */
+const handlePrevStep = () => {
+ const parentEl = parent || getCurrentInstance()?.parent
+ if (parentEl && typeof parentEl.exposed?.goToPrevStep === 'function') {
+ parentEl.exposed.goToPrevStep()
+ }
+}
+
+/** 淇濆瓨鎿嶄綔 */
+const handleSave = async () => {
+ // 淇濆瓨鍓嶉獙璇�
+ if (!currentFile?.value?.segments || currentFile.value.segments.length === 0) {
+ message.warning('璇峰厛棰勮鍒嗘鍐呭')
+ return
+ }
+
+ // 璁剧疆鎸夐挳鍔犺浇鐘舵��
+ submitLoading.value = true
+ try {
+ if (modelData.value.id) {
+ // 淇敼鍦烘櫙
+ await KnowledgeDocumentApi.updateKnowledgeDocument({
+ id: modelData.value.id,
+ segmentMaxTokens: modelData.value.segmentMaxTokens
+ })
+ } else {
+ // 鏂板鍦烘櫙
+ const data = await KnowledgeDocumentApi.createKnowledgeDocumentList({
+ knowledgeId: modelData.value.knowledgeId,
+ segmentMaxTokens: modelData.value.segmentMaxTokens,
+ list: modelData.value.list.map((item: any) => ({
+ name: item.name,
+ url: item.url
+ }))
+ })
+ modelData.value.list.forEach((document: any, index: number) => {
+ document.id = data[index]
+ })
+ }
+
+ // 杩涘叆涓嬩竴姝�
+ const parentEl = parent || getCurrentInstance()?.parent
+ if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
+ parentEl.exposed.goToNextStep()
+ }
+ } catch (error: any) {
+ console.error('淇濆瓨澶辫触:', modelData.value, error)
+ } finally {
+ // 鍏抽棴鎸夐挳鍔犺浇鐘舵��
+ submitLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ // 纭繚 segmentMaxTokens 瀛樺湪
+ if (!modelData.value.segmentMaxTokens) {
+ modelData.value.segmentMaxTokens = 500
+ }
+ // 濡傛灉娌℃湁閫変腑鏂囦欢锛岄粯璁ら�変腑绗竴涓�
+ if (!currentFile.value && modelData.value.list && modelData.value.list.length > 0) {
+ currentFile.value = modelData.value.list[0]
+ }
+
+ // 濡傛灉鏈夐�変腑鐨勬枃浠讹紝鑾峰彇鍒嗘鍐呭
+ if (currentFile.value) {
+ await splitContent(currentFile.value)
+ }
+})
+</script>
diff --git a/src/views/ai/knowledge/document/form/UploadStep.vue b/src/views/ai/knowledge/document/form/UploadStep.vue
new file mode 100644
index 0000000..5a4d700
--- /dev/null
+++ b/src/views/ai/knowledge/document/form/UploadStep.vue
@@ -0,0 +1,273 @@
+<template>
+ <el-form ref="formRef" :model="modelData" label-width="0" class="mt-20px">
+ <el-form-item class="mb-20px">
+ <div class="w-full">
+ <div
+ class="w-full border-2 border-dashed border-[#dcdfe6] rounded-md p-20px text-center hover:border-[#409eff]"
+ >
+ <el-upload
+ ref="uploadRef"
+ class="upload-demo"
+ drag
+ :action="uploadUrl"
+ :auto-upload="true"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :on-change="handleFileChange"
+ :on-remove="handleFileRemove"
+ :before-upload="beforeUpload"
+ :http-request="httpRequest"
+ :file-list="fileList"
+ :multiple="true"
+ :show-file-list="false"
+ :accept="acceptedFileTypes"
+ >
+ <div class="flex flex-col items-center justify-center py-20px">
+ <Icon icon="ep:upload-filled" class="text-[48px] text-[#c0c4cc] mb-10px" />
+ <div class="el-upload__text text-[16px] text-[#606266]">
+ 鎷栨嫿鏂囦欢鑷虫锛屾垨鑰�
+ <em class="text-[#409eff] not-italic cursor-pointer">閫夋嫨鏂囦欢</em>
+ </div>
+ <div class="el-upload__tip mt-10px text-[#909399] text-[12px]">
+ 宸叉敮鎸� {{ supportedFileTypes.join('銆�') }}锛屾瘡涓枃浠朵笉瓒呰繃 {{ maxFileSize }} MB銆�
+ </div>
+ </div>
+ </el-upload>
+ </div>
+
+ <div
+ v-if="modelData.list && modelData.list.length > 0"
+ class="mt-15px grid grid-cols-1 gap-2"
+ >
+ <div
+ v-for="(file, index) in modelData.list"
+ :key="index"
+ class="flex justify-between items-center py-4px px-12px border-l-4 border-l-[#409eff] rounded-sm shadow-sm hover:bg-[#ecf5ff] transition-all duration-300"
+ >
+ <div class="flex items-center">
+ <Icon icon="ep:document" class="mr-8px text-[#409eff]" />
+ <span class="text-[13px] text-[#303133] break-all">{{ file.name }}</span>
+ </div>
+ <el-button type="danger" link @click="removeFile(index)" class="ml-2">
+ <Icon icon="ep:delete" />
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+
+ <!-- 娣诲姞涓嬩竴姝ユ寜閽� -->
+ <el-form-item>
+ <div class="flex justify-end w-full">
+ <el-button type="primary" @click="handleNextStep" :disabled="!isAllUploaded">
+ 涓嬩竴姝�
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script lang="ts" setup>
+import { PropType, ref, computed, inject, getCurrentInstance, onMounted } from 'vue'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { generateAcceptedFileTypes } from '@/utils'
+import { Icon } from '@/components/Icon'
+
+const props = defineProps({
+ modelValue: {
+ type: Object as PropType<any>,
+ required: true
+ }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const formRef = ref() // 琛ㄥ崟寮曠敤
+const uploadRef = ref() // 涓婁紶缁勪欢寮曠敤
+const parent = inject('parent', null) // 鑾峰彇鐖剁粍浠跺疄渚�
+const { uploadUrl, httpRequest } = useUpload() // 浣跨敤涓婁紶缁勪欢鐨勯挬瀛�
+const message = useMessage() // 娑堟伅寮圭獥
+const fileList = ref([]) // 鏂囦欢鍒楄〃
+const uploadingCount = ref(0) // 涓婁紶涓殑鏂囦欢鏁伴噺
+
+// 鏀寔鐨勬枃浠剁被鍨嬪拰澶у皬闄愬埗
+const supportedFileTypes = [
+ 'TXT',
+ 'MARKDOWN',
+ 'MDX',
+ 'PDF',
+ 'HTML',
+ 'XLSX',
+ 'XLS',
+ 'DOC',
+ 'DOCX',
+ 'CSV',
+ 'EML',
+ 'MSG',
+ 'PPTX',
+ 'XML',
+ 'EPUB',
+ 'PPT',
+ 'MD',
+ 'HTM'
+]
+const allowedExtensions = supportedFileTypes.map((ext) => ext.toLowerCase()) // 灏忓啓鐨勬墿灞曞悕鍒楄〃
+const maxFileSize = 15 // 鏈�澶ф枃浠跺ぇ灏�(MB)
+
+// 鏋勫缓 accept 灞炴�у�硷紝鐢ㄤ簬闄愬埗鏂囦欢閫夋嫨瀵硅瘽妗嗕腑鍙鐨勬枃浠剁被鍨�
+const acceptedFileTypes = computed(() => generateAcceptedFileTypes(supportedFileTypes))
+
+/** 琛ㄥ崟鏁版嵁 */
+const modelData = computed({
+ get: () => {
+ return props.modelValue
+ },
+ set: (val) => emit('update:modelValue', val)
+})
+
+/** 纭繚 list 灞炴�у瓨鍦� */
+const ensureListExists = () => {
+ if (!props.modelValue.list) {
+ emit('update:modelValue', {
+ ...props.modelValue,
+ list: []
+ })
+ }
+}
+
+/** 鏄惁鎵�鏈夋枃浠堕兘宸蹭笂浼犲畬鎴� */
+const isAllUploaded = computed(() => {
+ return modelData.value.list && modelData.value.list.length > 0 && uploadingCount.value === 0
+})
+
+/**
+ * 涓婁紶鍓嶆鏌ユ枃浠剁被鍨嬪拰澶у皬
+ *
+ * @param file 寰呬笂浼犵殑鏂囦欢
+ * @returns 鏄惁鍏佽涓婁紶
+ */
+const beforeUpload = (file) => {
+ // 1.1 妫�鏌ユ枃浠舵墿灞曞悕
+ const fileName = file.name.toLowerCase()
+ const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1)
+ if (!allowedExtensions.includes(fileExtension)) {
+ message.error('涓嶆敮鎸佺殑鏂囦欢绫诲瀷锛�')
+ return false
+ }
+ // 1.2 妫�鏌ユ枃浠跺ぇ灏�
+ if (!(file.size / 1024 / 1024 < maxFileSize)) {
+ message.error(`鏂囦欢澶у皬涓嶈兘瓒呰繃 ${maxFileSize} MB锛乣)
+ return false
+ }
+
+ // 2. 澧炲姞涓婁紶涓殑鏂囦欢璁℃暟
+ uploadingCount.value++
+ return true
+}
+
+/**
+ * 鏂囦欢涓婁紶鎴愬姛澶勭悊
+ *
+ * @param response 涓婁紶鍝嶅簲
+ * @param file 涓婁紶鐨勬枃浠�
+ */
+const handleUploadSuccess = (response, file) => {
+ // 娣诲姞鍒版枃浠跺垪琛�
+ if (response && response.data) {
+ ensureListExists()
+ emit('update:modelValue', {
+ ...props.modelValue,
+ list: [
+ ...props.modelValue.list,
+ {
+ name: file.name,
+ url: response.data
+ }
+ ]
+ })
+ } else {
+ message.error(`鏂囦欢 ${file.name} 涓婁紶澶辫触`)
+ }
+
+ // 鍑忓皯涓婁紶涓殑鏂囦欢璁℃暟
+ uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+}
+
+/**
+ * 鏂囦欢涓婁紶澶辫触澶勭悊
+ *
+ * @param error 閿欒淇℃伅
+ * @param file 涓婁紶鐨勬枃浠�
+ */
+const handleUploadError = (error, file) => {
+ message.error(`鏂囦欢 ${file.name} 涓婁紶澶辫触: ${error}`)
+ // 鍑忓皯涓婁紶涓殑鏂囦欢璁℃暟
+ uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+}
+
+/**
+ * 鏂囦欢鍙樻洿澶勭悊
+ *
+ * @param file 鍙樻洿鐨勬枃浠�
+ */
+const handleFileChange = (file) => {
+ if (file.status === 'success' || file.status === 'fail') {
+ uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+ }
+}
+
+/**
+ * 鏂囦欢绉婚櫎澶勭悊
+ *
+ * @param file 琚Щ闄ょ殑鏂囦欢
+ */
+const handleFileRemove = (file) => {
+ if (file.status === 'uploading') {
+ uploadingCount.value = Math.max(0, uploadingCount.value - 1)
+ }
+}
+
+/**
+ * 浠庡垪琛ㄤ腑绉婚櫎鏂囦欢
+ *
+ * @param index 瑕佺Щ闄ょ殑鏂囦欢绱㈠紩
+ */
+const removeFile = (index: number) => {
+ // 浠庡垪琛ㄤ腑绉婚櫎鏂囦欢
+ const newList = [...props.modelValue.list]
+ newList.splice(index, 1)
+ // 鏇存柊琛ㄥ崟鏁版嵁
+ emit('update:modelValue', {
+ ...props.modelValue,
+ list: newList
+ })
+}
+
+/** 涓嬩竴姝ユ寜閽鐞� */
+const handleNextStep = () => {
+ // 1.1 妫�鏌ユ槸鍚︽湁鏂囦欢涓婁紶
+ if (!modelData.value.list || modelData.value.list.length === 0) {
+ message.warning('璇蜂笂浼犺嚦灏戜竴涓枃浠�')
+ return
+ }
+ // 1.2 妫�鏌ユ槸鍚︽湁鏂囦欢姝e湪涓婁紶
+ if (uploadingCount.value > 0) {
+ message.warning('璇风瓑寰呮墍鏈夋枃浠朵笂浼犲畬鎴�')
+ return
+ }
+
+ // 2. 鑾峰彇鐖剁粍浠剁殑goToNextStep鏂规硶
+ const parentEl = parent || getCurrentInstance()?.parent
+ if (parentEl && typeof parentEl.exposed?.goToNextStep === 'function') {
+ parentEl.exposed.goToNextStep()
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ ensureListExists()
+})
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/ai/knowledge/document/form/index.vue b/src/views/ai/knowledge/document/form/index.vue
new file mode 100644
index 0000000..722f57c
--- /dev/null
+++ b/src/views/ai/knowledge/document/form/index.vue
@@ -0,0 +1,193 @@
+<template>
+ <ContentWrap>
+ <div class="mx-auto">
+ <!-- 澶撮儴瀵艰埅鏍� -->
+ <div
+ class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+ >
+ <!-- 宸︿晶鏍囬 -->
+ <div class="w-200px flex items-center overflow-hidden">
+ <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+ <span class="ml-10px text-16px truncate">
+ {{ formData.id ? '缂栬緫鐭ヨ瘑搴撴枃妗�' : '鍒涘缓鐭ヨ瘑搴撴枃妗�' }}
+ </span>
+ </div>
+
+ <!-- 姝ラ鏉� -->
+ <div class="flex-1 flex items-center justify-center h-full">
+ <div class="w-400px flex items-center justify-between h-full">
+ <div
+ v-for="(step, index) in steps"
+ :key="index"
+ class="flex items-center mx-15px relative h-full"
+ :class="[
+ currentStep === index
+ ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+ : 'text-gray-500'
+ ]"
+ >
+ <div
+ class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+ :class="[
+ currentStep === index
+ ? 'bg-[#3473ff] text-white border-[#3473ff]'
+ : 'border-gray-300 bg-white text-gray-500'
+ ]"
+ >
+ {{ index + 1 }}
+ </div>
+ <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍙充晶鎸夐挳 - 宸茬Щ闄� -->
+ <div class="w-200px flex items-center justify-end gap-2"> </div>
+ </div>
+
+ <!-- 涓讳綋鍐呭 -->
+ <div class="mt-50px">
+ <!-- 绗竴姝ワ細涓婁紶鏂囨。 -->
+ <div v-if="currentStep === 0" class="mx-auto w-560px">
+ <UploadStep v-model="formData" ref="uploadDocumentRef" />
+ </div>
+
+ <!-- 绗簩姝ワ細鏂囨。鍒嗘 -->
+ <div v-if="currentStep === 1" class="mx-auto w-560px">
+ <SplitStep v-model="formData" ref="documentSegmentRef" />
+ </div>
+
+ <!-- 绗笁姝ワ細澶勭悊骞跺畬鎴� -->
+ <div v-if="currentStep === 2" class="mx-auto w-560px">
+ <ProcessStep v-model="formData" ref="processCompleteRef" />
+ </div>
+ </div>
+ </div>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { useRoute, useRouter } from 'vue-router'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UploadStep from './UploadStep.vue'
+import SplitStep from './SplitStep.vue'
+import ProcessStep from './ProcessStep.vue'
+import { KnowledgeDocumentApi } from '@/api/ai/knowledge/document'
+
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const route = useRoute() // 璺敱
+const router = useRouter() // 璺敱
+
+// 缁勪欢寮曠敤
+const uploadDocumentRef = ref()
+const documentSegmentRef = ref()
+const processCompleteRef = ref()
+const currentStep = ref(0) // 姝ラ鎺у埗
+const steps = [{ title: '涓婁紶鏂囨。' }, { title: '鏂囨。鍒嗘' }, { title: '澶勭悊骞跺畬鎴�' }]
+const formData = ref({
+ knowledgeId: undefined, // 鐭ヨ瘑搴撶紪鍙�
+ id: undefined, // 缂栬緫鐨勬枃妗g紪鍙�(documentId)
+ segmentMaxTokens: 500, // 鍒嗘鏈�澶� token 鏁�
+ list: [] as Array<{
+ id: number // 鏂囨。缂栧彿
+ name: string // 鏂囨。鍚嶇О
+ url: string // 鏂囨。 URL
+ segments: Array<{
+ content?: string
+ contentLength?: number
+ tokens?: number
+ }>
+ count?: number // 娈佃惤鏁伴噺
+ process?: number // 澶勭悊杩涘害
+ }> // 鐢ㄤ簬瀛樺偍涓婁紶鐨勬枃浠跺垪琛�
+}) // 琛ㄥ崟鏁版嵁
+
+provide('parent', getCurrentInstance()) // 鎻愪緵 parent 缁欏瓙缁勪欢浣跨敤
+
+/** 鍒濆鍖栨暟鎹� */
+const initData = async () => {
+ // 銆愭柊澧炲満鏅�戜粠璺敱鍙傛暟涓幏鍙栫煡璇嗗簱 ID
+ if (route.query.knowledgeId) {
+ formData.value.knowledgeId = route.query.knowledgeId as any
+ }
+
+ // 銆愪慨鏀瑰満鏅�戜粠璺敱鍙傛暟涓幏鍙栨枃妗� ID
+ const documentId = route.query.id
+ if (documentId) {
+ // 鑾峰彇鏂囨。淇℃伅
+ formData.value.id = documentId as any
+ const document = await KnowledgeDocumentApi.getKnowledgeDocument(documentId as any)
+ formData.value.segmentMaxTokens = document.segmentMaxTokens
+ formData.value.list = [
+ {
+ id: document.id,
+ name: document.name,
+ url: document.url,
+ segments: []
+ }
+ ]
+ // 杩涘叆涓嬩竴姝�
+ goToNextStep()
+ }
+}
+
+/** 鍒囨崲鍒颁笅涓�姝� */
+const goToNextStep = () => {
+ if (currentStep.value < steps.length - 1) {
+ currentStep.value++
+ }
+}
+
+/** 鍒囨崲鍒颁笂涓�姝� */
+const goToPrevStep = () => {
+ if (currentStep.value > 0) {
+ currentStep.value--
+ }
+}
+
+/** 杩斿洖鍒楄〃椤� */
+const handleBack = () => {
+ // 鍏堝垹闄ゅ綋鍓嶉〉绛�
+ delView(unref(router.currentRoute))
+ // 璺宠浆鍒板垪琛ㄩ〉
+ router.push({ name: 'AiKnowledgeDocument', query: { knowledgeId: formData.value.knowledgeId } })
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await initData()
+})
+
+/** 娣诲姞缁勪欢鍗歌浇鍓嶇殑娓呯悊浠g爜 */
+onBeforeUnmount(() => {
+ // 娓呯悊鎵�鏈夌殑寮曠敤
+ uploadDocumentRef.value = null
+ documentSegmentRef.value = null
+ processCompleteRef.value = null
+})
+
+/** 鏆撮湶鏂规硶缁欏瓙缁勪欢浣跨敤 */
+defineExpose({
+ goToNextStep,
+ goToPrevStep,
+ handleBack
+})
+</script>
+
+<style lang="scss" scoped>
+.border-bottom {
+ border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+ color: #3473ff;
+}
+
+.bg-primary {
+ background-color: #3473ff;
+}
+
+.border-primary {
+ border-color: #3473ff;
+}
+</style>
diff --git a/src/views/ai/knowledge/document/index.vue b/src/views/ai/knowledge/document/index.vue
new file mode 100644
index 0000000..e8ba711
--- /dev/null
+++ b/src/views/ai/knowledge/document/index.vue
@@ -0,0 +1,236 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏂囦欢鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ枃浠跺悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁鍚敤" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鏄惁鍚敤"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" plain @click="handleCreate" v-hasPermi="['ai:knowledge:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鏂囨。缂栧彿" align="center" prop="id" />
+ <el-table-column label="鏂囦欢鍚嶇О" align="center" prop="name" />
+ <el-table-column label="瀛楃鏁�" align="center" prop="contentLength" />
+ <el-table-column label="Token 鏁�" align="center" prop="tokens" />
+ <el-table-column label="鍒嗙墖鏈�澶� Token 鏁�" align="center" prop="segmentMaxTokens" />
+ <el-table-column label="鍙洖娆℃暟" align="center" prop="retrievalCount" />
+ <el-table-column label="鏄惁鍚敤" align="center" prop="status">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ :disabled="!checkPermi(['ai:knowledge:update'])"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓婁紶鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdate(scope.row.id)"
+ v-hasPermi="['ai:knowledge:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleSegment(scope.row.id)"
+ v-hasPermi="['ai:knowledge:query']"
+ >
+ 鍒嗘
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:knowledge:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <!-- <KnowledgeDocumentForm ref="formRef" @success="getList" /> -->
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { KnowledgeDocumentApi, KnowledgeDocumentVO } from '@/api/ai/knowledge/document'
+import { useRoute, useRouter } from 'vue-router'
+import { checkPermi } from '@/utils/permission'
+import { CommonStatusEnum } from '@/utils/constants'
+// import KnowledgeDocumentForm from './KnowledgeDocumentForm.vue'
+
+/** AI 鐭ヨ瘑搴撴枃妗� 鍒楄〃 */
+defineOptions({ name: 'KnowledgeDocument' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const route = useRoute() // 璺敱
+const router = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<KnowledgeDocumentVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ knowledgeId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await KnowledgeDocumentApi.getKnowledgeDocumentPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 璺宠浆鍒板垱寤烘枃妗i〉闈� */
+const handleCreate = () => {
+ router.push({
+ name: 'AiKnowledgeDocumentCreate',
+ query: { knowledgeId: queryParams.knowledgeId }
+ })
+}
+
+/** 璺宠浆鍒版洿鏂版枃妗i〉闈� */
+const handleUpdate = (id: number) => {
+ router.push({
+ name: 'AiKnowledgeDocumentUpdate',
+ query: { id, knowledgeId: queryParams.knowledgeId }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await KnowledgeDocumentApi.deleteKnowledgeDocument(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼鐘舵�佹搷浣� */
+const handleStatusChange = async (row: KnowledgeDocumentVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.status === CommonStatusEnum.ENABLE ? '鍚敤' : '绂佺敤'
+ await message.confirm('纭瑕�"' + text + '""' + row.name + '"鏂囨。鍚�?')
+ // 鍙戣捣淇敼鐘舵��
+ await KnowledgeDocumentApi.updateKnowledgeDocumentStatus({ id: row.id, status: row.status })
+ message.success(t('common.updateSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 璺宠浆鍒扮煡璇嗗簱鍒嗘椤甸潰 */
+const handleSegment = (id: number) => {
+ router.push({
+ name: 'AiKnowledgeSegment',
+ query: { documentId: id }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ // 濡傛灉鐭ヨ瘑搴� ID 涓嶅瓨鍦紝鏄剧ず閿欒鎻愮ず骞跺叧闂〉闈�
+ if (!route.query.knowledgeId) {
+ message.error('鐭ヨ瘑搴� ID 涓嶅瓨鍦紝鏃犳硶鏌ョ湅鏂囨。鍒楄〃')
+ // 鍏抽棴褰撳墠璺敱锛岃繑鍥炲埌鐭ヨ瘑搴撳垪琛ㄩ〉闈�
+ router.push({ name: 'AiKnowledge' })
+ return
+ }
+
+ // 浠庤矾鐢卞弬鏁颁腑鑾峰彇鐭ヨ瘑搴� ID
+ queryParams.knowledgeId = route.query.knowledgeId as any
+ getList()
+})
+</script>
diff --git a/src/views/ai/knowledge/knowledge/KnowledgeForm.vue b/src/views/ai/knowledge/knowledge/KnowledgeForm.vue
new file mode 100644
index 0000000..d72f37c
--- /dev/null
+++ b/src/views/ai/knowledge/knowledge/KnowledgeForm.vue
@@ -0,0 +1,162 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="130px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鐭ヨ瘑搴撳悕绉�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ煡璇嗗簱鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="鐭ヨ瘑搴撴弿杩�" prop="description">
+ <el-input
+ v-model="formData.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ョ煡璇嗗簱鎻忚堪"
+ />
+ </el-form-item>
+ <el-form-item label="鍚戦噺妯″瀷" prop="embeddingModelId">
+ <el-select
+ v-model="formData.embeddingModelId"
+ placeholder="璇烽�夋嫨鍚戦噺妯″瀷"
+ clearable
+ class="!w-full"
+ >
+ <el-option v-for="item in modelList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妫�绱� topK" prop="topK">
+ <el-input-number
+ v-model="formData.topK"
+ placeholder="璇疯緭鍏ユ绱� topK"
+ :min="0"
+ :max="10"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="妫�绱㈢浉浼煎害闃堝��" prop="similarityThreshold">
+ <el-input-number
+ v-model="formData.similarityThreshold"
+ placeholder="璇疯緭鍏ユ绱㈢浉浼煎害闃堝��"
+ :min="0"
+ :max="1"
+ :step="0.01"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁鍚敤" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
+import { CommonStatusEnum } from '@/utils/constants'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '../../utils/constants'
+
+/** AI 鐭ヨ瘑搴撹〃鍗� */
+defineOptions({ name: 'KnowledgeForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ embeddingModelId: undefined,
+ topK: undefined,
+ similarityThreshold: undefined,
+ status: CommonStatusEnum.ENABLE // 榛樿寮�鍚�
+})
+const formRules = reactive({
+ name: [{ required: true, message: '璇疯緭鍏ョ煡璇嗗簱鍚嶇О', trigger: 'blur' }],
+ embeddingModelId: [{ required: true, message: '璇疯緭鍏ュ悜閲忔ā鍨�', trigger: 'blur' }],
+ topK: [{ required: true, message: '璇疯緭鍏ユ绱� topK', trigger: 'blur' }],
+ similarityThreshold: [{ required: true, message: '璇疯緭鍏ユ绱㈢浉浼煎害闃堝��', trigger: 'blur' }],
+ status: [{ required: true, message: '璇烽�夋嫨鏄惁鍚敤', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const modelList = ref<ModelVO[]>([]) // 鍚戦噺妯″瀷閫夐」
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 鑾峰彇鍚戦噺妯″瀷鍒楄〃
+ modelList.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.EMBEDDING)
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await KnowledgeApi.getKnowledge(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as KnowledgeVO
+ if (formType.value === 'create') {
+ await KnowledgeApi.createKnowledge(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await KnowledgeApi.updateKnowledge(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ embeddingModelId: undefined,
+ topK: undefined,
+ similarityThreshold: undefined,
+ status: CommonStatusEnum.ENABLE // 榛樿寮�鍚�
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/knowledge/knowledge/index.vue b/src/views/ai/knowledge/knowledge/index.vue
new file mode 100644
index 0000000..a768c6b
--- /dev/null
+++ b/src/views/ai/knowledge/knowledge/index.vue
@@ -0,0 +1,221 @@
+<template>
+ <doc-alert title="AI 鐭ヨ瘑搴�" url="https://doc.iocoder.cn/ai/knowledge/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="95px"
+ >
+ <el-form-item label="鐭ヨ瘑搴撳悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ョ煡璇嗗簱鍚嶇О"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁鍚敤" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鏄惁鍚敤"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['ai:knowledge:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐭ヨ瘑搴撳悕绉�" align="center" prop="name" />
+ <el-table-column label="鐭ヨ瘑搴撴弿杩�" align="center" prop="description" />
+ <el-table-column label="鍚戦噺鍖栨ā鍨�" align="center" prop="embeddingModel" />
+ <el-table-column label="鏄惁鍚敤" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:knowledge:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleDocument(scope.row.id)"
+ v-hasPermi="['ai:knowledge:query']"
+ >
+ 鏂囨。
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleRetrieval(scope.row.id)"
+ v-hasPermi="['ai:knowledge:query']"
+ >
+ 鍙洖娴嬭瘯
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:knowledge:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <KnowledgeForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
+import KnowledgeForm from './KnowledgeForm.vue'
+import { useRouter } from 'vue-router'
+
+/** AI 鐭ヨ瘑搴撳垪琛� */
+defineOptions({ name: 'Knowledge' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<KnowledgeVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await KnowledgeApi.getKnowledgePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await KnowledgeApi.deleteKnowledge(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鏂囨。鎸夐挳鎿嶄綔 */
+const router = useRouter()
+const handleDocument = (id: number) => {
+ router.push({
+ name: 'AiKnowledgeDocument',
+ query: { knowledgeId: id }
+ })
+}
+
+/** 璺宠浆鍒版枃妗e彫鍥炴祴璇曢〉闈� */
+const handleRetrieval = (id: number) => {
+ router.push({
+ name: 'AiKnowledgeRetrieval',
+ query: { id }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/ai/knowledge/knowledge/retrieval/index.vue b/src/views/ai/knowledge/knowledge/retrieval/index.vue
new file mode 100644
index 0000000..aca521a
--- /dev/null
+++ b/src/views/ai/knowledge/knowledge/retrieval/index.vue
@@ -0,0 +1,163 @@
+<template>
+ <div class="flex gap-20px w-full">
+ <!-- 宸︿晶杈撳叆鍖哄煙 -->
+ <ContentWrap class="flex-1 min-w-300px">
+ <div class="mb-15px">
+ <h3 class="m-0 mb-5px">鍙洖娴嬭瘯</h3>
+ <div class="text-gray-500 text-14px">鏍规嵁缁欏畾鐨勬煡璇㈡枃鏈祴璇曞彫鍥炴晥鏋溿��</div>
+ </div>
+ <div>
+ <div class="relative mb-10px">
+ <el-input
+ v-model="queryParams.content"
+ type="textarea"
+ :rows="8"
+ placeholder="璇疯緭鍏ユ枃鏈�"
+ />
+ <div class="absolute bottom-10px right-10px text-gray-400 text-12px">
+ {{ queryParams.content?.length }} / 200
+ </div>
+ </div>
+ <div class="flex items-center mb-10px">
+ <span class="w-60px text-gray-500">topK:</span>
+ <el-input-number v-model="queryParams.topK" :min="1" :max="20" />
+ </div>
+ <div class="flex items-center mb-15px">
+ <span class="w-60px text-gray-500">鐩镐技搴�:</span>
+ <el-input-number
+ v-model="queryParams.similarityThreshold"
+ :min="0"
+ :max="1"
+ :precision="2"
+ :step="0.01"
+ />
+ </div>
+ <div class="flex justify-end">
+ <el-button type="primary" @click="getRetrievalResult" :loading="loading">娴嬭瘯</el-button>
+ </div>
+ </div>
+ </ContentWrap>
+
+ <!-- 鍙充晶鍙洖缁撴灉鍖哄煙 -->
+ <ContentWrap class="flex-1 min-w-300px">
+ <el-empty v-if="loading" description="姝e湪妫�绱腑..." />
+ <div v-else-if="segments.length > 0" class="font-bold mb-15px">
+ {{ segments.length }} 涓彫鍥炴钀�
+ </div>
+ <el-empty v-else description="鏆傛棤鍙洖缁撴灉" />
+ <div>
+ <div
+ v-for="(segment, index) in segments"
+ :key="index"
+ class="mb-20px border border-solid border-gray-200 rounded p-15px"
+ >
+ <div class="flex justify-between text-12px text-gray-500 mb-5px">
+ <span>
+ 鍒嗘({{ segment.id }}) 路 {{ segment.contentLength }} 瀛楃鏁� 路
+ {{ segment.tokens }} Token
+ </span>
+ <span class="px-8px py-4px bg-blue-50 text-blue-500 rounded-full text-12px font-bold">
+ score: {{ segment.score }}
+ </span>
+ </div>
+ <div
+ class="bg-gray-50 p-10px rounded mb-10px whitespace-pre-wrap overflow-hidden transition-all duration-100 text-13px"
+ :class="{
+ 'line-clamp-2 max-h-50px': !segment.expanded,
+ 'max-h-500px': segment.expanded
+ }"
+ >
+ {{ segment.content }}
+ </div>
+ <div class="flex justify-between items-center">
+ <div class="flex items-center text-gray-500 text-13px">
+ <Icon icon="ep:document" class="mr-5px" />
+ <span>{{ segment.documentName || '鏈煡鏂囨。' }}</span>
+ </div>
+ <el-button size="small" @click="toggleExpand(segment)">
+ {{ segment.expanded ? '鏀惰捣' : '灞曞紑' }}
+ <Icon :icon="segment.expanded ? 'ep:arrow-up' : 'ep:arrow-down'" />
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </ContentWrap>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useMessage } from '@/hooks/web/useMessage'
+import { KnowledgeSegmentApi } from '@/api/ai/knowledge/segment'
+import { KnowledgeApi } from '@/api/ai/knowledge/knowledge'
+/** 鏂囨。鍙洖娴嬭瘯 */
+defineOptions({ name: 'KnowledgeDocumentRetrieval' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const route = useRoute() // 璺敱
+const router = useRouter() // 璺敱
+
+const loading = ref(false) // 鍔犺浇鐘舵��
+const segments = ref<any[]>([]) // 鍙洖缁撴灉
+const queryParams = reactive({
+ id: undefined,
+ content: '',
+ topK: 10,
+ similarityThreshold: 0.5
+})
+
+/** 璋冪敤鏂囨。鍙洖娴嬭瘯鎺ュ彛 */
+const getRetrievalResult = async () => {
+ if (!queryParams.content) {
+ message.warning('璇疯緭鍏ユ煡璇㈡枃鏈�')
+ return
+ }
+
+ loading.value = true
+ segments.value = []
+
+ try {
+ const data = await KnowledgeSegmentApi.searchKnowledgeSegment({
+ knowledgeId: queryParams.id,
+ content: queryParams.content,
+ topK: queryParams.topK,
+ similarityThreshold: queryParams.similarityThreshold
+ })
+ segments.value = data || []
+ } catch (error) {
+ console.error(error)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 灞曞紑/鏀惰捣娈佃惤鍐呭 */
+const toggleExpand = (segment: any) => {
+ segment.expanded = !segment.expanded
+}
+
+/** 鑾峰彇鐭ヨ瘑搴撲俊鎭� */
+const getKnowledgeInfo = async (id: number) => {
+ try {
+ const knowledge = await KnowledgeApi.getKnowledge(id)
+ if (knowledge) {
+ queryParams.topK = knowledge.topK || queryParams.topK
+ queryParams.similarityThreshold =
+ knowledge.similarityThreshold || queryParams.similarityThreshold
+ }
+ } catch (error) {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ // 濡傛灉鐭ヨ瘑搴� ID 涓嶅瓨鍦紝鏄剧ず閿欒鎻愮ず骞跺叧闂〉闈�
+ if (!route.query.id) {
+ message.error('鐭ヨ瘑搴� ID 涓嶅瓨鍦紝鏃犳硶杩涜鍙洖娴嬭瘯')
+ router.back()
+ return
+ }
+ queryParams.id = route.query.id as any
+
+ // 鑾峰彇鐭ヨ瘑搴撲俊鎭苟璁剧疆榛樿鍊�
+ getKnowledgeInfo(queryParams.id as any)
+})
+</script>
diff --git a/src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue b/src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue
new file mode 100644
index 0000000..4818de0
--- /dev/null
+++ b/src/views/ai/knowledge/segment/KnowledgeSegmentForm.vue
@@ -0,0 +1,101 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍒囩墖鍐呭" prop="content">
+ <el-input
+ v-model="formData.content"
+ type="textarea"
+ :rows="6"
+ placeholder="璇疯緭鍏ュ垏鐗囧唴瀹�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segment'
+
+/** AI 鐭ヨ瘑搴撳垎娈佃〃鍗� */
+defineOptions({ name: 'KnowledgeSegmentForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ documentId: undefined,
+ content: undefined
+})
+const formRules = reactive({
+ content: [{ required: true, message: '鍒囩墖鍐呭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, documentId?: any) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ formData.value.documentId = documentId as any
+
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await KnowledgeSegmentApi.getKnowledgeSegment(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as KnowledgeSegmentVO
+ if (formType.value === 'create') {
+ await KnowledgeSegmentApi.createKnowledgeSegment(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await KnowledgeSegmentApi.updateKnowledgeSegment(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ documentId: undefined,
+ content: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/knowledge/segment/index.vue b/src/views/ai/knowledge/segment/index.vue
new file mode 100644
index 0000000..e2f8a67
--- /dev/null
+++ b/src/views/ai/knowledge/segment/index.vue
@@ -0,0 +1,242 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏂囨。缂栧彿" prop="documentId">
+ <el-input
+ v-model="queryParams.documentId"
+ placeholder="璇疯緭鍏ユ枃妗g紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁鍚敤" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鏄惁鍚敤"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['ai:knowledge:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍒嗘缂栧彿" align="center" prop="id" />
+ <el-table-column type="expand">
+ <template #default="props">
+ <div
+ class="content-expand"
+ style="
+ padding: 10px 20px;
+ white-space: pre-wrap;
+ line-height: 1.5;
+ background-color: #f9f9f9;
+ border-radius: 4px;
+ border-left: 3px solid #409eff;
+ "
+ >
+ <div
+ class="content-title"
+ style="margin-bottom: 8px; color: #606266; font-size: 14px; font-weight: bold"
+ >
+ 瀹屾暣鍐呭锛�
+ </div>
+ {{ props.row.content }}
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒囩墖鍐呭"
+ align="center"
+ prop="content"
+ min-width="250px"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="瀛楃鏁�" align="center" prop="contentLength" />
+ <el-table-column label="token 鏁伴噺" align="center" prop="tokens" />
+ <el-table-column label="鍙洖娆℃暟" align="center" prop="retrievalCount" />
+ <el-table-column label="鏄惁鍚敤" align="center" prop="status">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ :disabled="!checkPermi(['ai:knowledge:update'])"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:knowledge:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:knowledge:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <KnowledgeSegmentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { KnowledgeSegmentApi, KnowledgeSegmentVO } from '@/api/ai/knowledge/segment'
+import KnowledgeSegmentForm from './KnowledgeSegmentForm.vue'
+import { CommonStatusEnum } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+
+/** AI 鐭ヨ瘑搴撳垎娈� 鍒楄〃 */
+defineOptions({ name: 'KnowledgeSegment' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const router = useRouter() // 璺敱
+const route = useRoute() // 璺敱
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<KnowledgeSegmentVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ documentId: undefined,
+ content: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await KnowledgeSegmentApi.getKnowledgeSegmentPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id, queryParams.documentId)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await KnowledgeSegmentApi.deleteKnowledgeSegment(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼鐘舵�佹搷浣� */
+const handleStatusChange = async (row: KnowledgeSegmentVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.status === CommonStatusEnum.ENABLE ? '鍚敤' : '绂佺敤'
+ await message.confirm('纭瑕�"' + text + '"璇ュ垎娈靛悧?')
+ // 鍙戣捣淇敼鐘舵��
+ await KnowledgeSegmentApi.updateKnowledgeSegmentStatus({ id: row.id, status: row.status })
+ message.success(t('common.updateSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ // 濡傛灉鏂囨。 ID 涓嶅瓨鍦紝鏄剧ず閿欒鎻愮ず骞跺叧闂〉闈�
+ if (!route.query.documentId) {
+ message.error('鏂囨。 ID 涓嶅瓨鍦紝鏃犳硶鏌ョ湅鍒嗘鍒楄〃')
+ // 鍏抽棴褰撳墠璺敱锛岃繑鍥炲埌鏂囨。鍒楄〃椤甸潰
+ router.push({ name: 'AiKnowledgeDocument' })
+ return
+ }
+
+ // 浠庤矾鐢卞弬鏁颁腑鑾峰彇鏂囨。 ID
+ queryParams.documentId = route.query.documentId as any
+ getList()
+})
+</script>
diff --git a/src/views/ai/mindmap/index/components/Left.vue b/src/views/ai/mindmap/index/components/Left.vue
new file mode 100644
index 0000000..5c3dbf0
--- /dev/null
+++ b/src/views/ai/mindmap/index/components/Left.vue
@@ -0,0 +1,78 @@
+<template>
+ <div class="w-[350px] p-5 flex flex-col bg-[#f5f7f9]">
+ <h3 class="w-full h-full h-7 text-5 text-center leading-[28px] title">鎬濈淮瀵煎浘鍒涗綔涓績</h3>
+ <!--涓嬮潰琛ㄥ崟閮ㄥ垎-->
+ <div class="flex-grow overflow-y-auto">
+ <div class="mt-[30ppx]">
+ <el-text tag="b">鎮ㄧ殑闇�姹傦紵</el-text>
+ <el-input
+ v-model="formData.prompt"
+ maxlength="1024"
+ :rows="5"
+ class="w-100% mt-15px"
+ input-style="border-radius: 7px;"
+ placeholder="璇疯緭鍏ユ彁绀鸿瘝锛岃AI甯綘瀹屽杽"
+ show-word-limit
+ type="textarea"
+ />
+ <el-button
+ class="!w-full mt-[15px]"
+ type="primary"
+ :loading="isGenerating"
+ @click="emits('submit', formData)"
+ >
+ 鏅鸿兘鐢熸垚鎬濈淮瀵煎浘
+ </el-button>
+ </div>
+ <div class="mt-[30px]">
+ <el-text tag="b">浣跨敤宸叉湁鍐呭鐢熸垚锛�</el-text>
+ <el-input
+ v-model="generatedContent"
+ maxlength="1024"
+ :rows="5"
+ class="w-100% mt-15px"
+ input-style="border-radius: 7px;"
+ placeholder="渚嬪锛氱璇濋噷鐨勫皬灞嬪簲璇ユ槸浠�涔堟牱瀛愶紵"
+ show-word-limit
+ type="textarea"
+ />
+ <el-button
+ class="!w-full mt-[15px]"
+ type="primary"
+ @click="emits('directGenerate', generatedContent)"
+ :disabled="isGenerating"
+ >
+ 鐩存帴鐢熸垚
+ </el-button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { MindMapContentExample } from '@/views/ai/utils/constants'
+
+const emits = defineEmits(['submit', 'directGenerate'])
+defineProps<{
+ isGenerating: boolean
+}>()
+// 鎻愪氦鐨勬彁绀鸿瘝瀛楁
+const formData = reactive({
+ prompt: ''
+})
+
+const generatedContent = ref(MindMapContentExample) // 宸叉湁鐨勫唴瀹�
+
+defineExpose({
+ setGeneratedContent(newContent: string) {
+ // 璁剧疆宸叉湁鐨勫唴瀹癸紝鍦ㄧ敓鎴愮粨鏉熺殑鏃跺�欏皢缁撴灉璧嬪�肩粰璇ュ��
+ generatedContent.value = newContent
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+.title {
+ color: var(--el-color-primary);
+}
+</style>
diff --git a/src/views/ai/mindmap/index/components/Right.vue b/src/views/ai/mindmap/index/components/Right.vue
new file mode 100644
index 0000000..b1d04de
--- /dev/null
+++ b/src/views/ai/mindmap/index/components/Right.vue
@@ -0,0 +1,167 @@
+<template>
+ <el-card class="my-card h-full flex-grow">
+ <template #header>
+ <h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
+ <span>鎬濈淮瀵煎浘棰勮</span>
+ <!-- 灞曠ず鍦ㄥ彸涓婅 -->
+ <el-button v-show="isEnd" size="small" type="primary" @click="downloadImage">
+ <template #icon>
+ <Icon icon="ph:copy-bold" />
+ </template>
+ 涓嬭浇鍥剧墖
+ </el-button>
+ </h3>
+ </template>
+
+ <div ref="contentRef" class="hide-scroll-bar h-full box-border">
+ <!--灞曠ず markdown 鐨勫鍣紝鏈�缁堢敓鎴愮殑鏄� html 瀛楃涓诧紝鐩存帴鐢� v-html 宓屽叆-->
+ <div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
+ <div class="flex flex-col items-center justify-center" v-html="html"></div>
+ </div>
+
+ <div ref="mindMapRef" class="wh-full">
+ <svg ref="svgRef" :style="{ height: `${contentAreaHeight}px` }" class="w-full" />
+ <div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
+ </div>
+ </div>
+ </el-card>
+</template>
+
+<script lang="ts" setup>
+import { Markmap } from 'markmap-view'
+import { Transformer } from 'markmap-lib'
+import { Toolbar } from 'markmap-toolbar'
+import markdownit from 'markdown-it'
+import download from '@/utils/download'
+
+const md = markdownit()
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps<{
+ generatedContent: string // 鐢熸垚缁撴灉
+ isEnd: boolean // 鏄惁缁撴潫
+ isGenerating: boolean // 鏄惁姝e湪鐢熸垚
+ isStart: boolean // 寮�濮嬬姸鎬侊紝寮�濮嬫椂闇�瑕佹竻闄� html
+}>()
+const contentRef = ref<HTMLDivElement>() // 鍙充晶鍑烘潵 header 浠ヤ笅鐨勫尯鍩�
+const mdContainerRef = ref<HTMLDivElement>() // markdown 鐨勫鍣紝鐢ㄦ潵婊氬姩鍒板簳涓嬬殑
+const mindMapRef = ref<HTMLDivElement>() // 鎬濈淮瀵煎浘鐨勫鍣�
+const svgRef = ref<SVGElement>() // 鎬濈淮瀵煎浘鐨勬覆鏌� svg
+const toolBarRef = ref<HTMLDivElement>() // 鎬濈淮瀵煎浘鍙充笅瑙掔殑宸ュ叿鏍忥紝缂╂斁绛�
+const html = ref('') // 鐢熸垚杩囩▼涓殑鏂囨湰
+const contentAreaHeight = ref(0) // 鐢熸垚鍖哄煙鐨勯珮搴︼紝鍑哄幓 header 閮ㄥ垎
+let markMap: Markmap | null = null
+const transformer = new Transformer()
+
+onMounted(() => {
+ contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 鑾峰彇鍖哄煙楂樺害
+ /** 鍒濆鍖栨�濈淮瀵煎浘 **/
+ try {
+ markMap = Markmap.create(svgRef.value!)
+ const { el } = Toolbar.create(markMap)
+ toolBarRef.value?.append(el)
+ nextTick(update)
+ } catch (e) {
+ message.error('鎬濈淮瀵煎浘鍒濆鍖栧け璐�')
+ }
+})
+
+watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
+ // 寮�濮嬬敓鎴愮殑鏃跺�欐竻绌轰竴涓� markdown 鐨勫唴瀹�
+ if (isStart) {
+ html.value = ''
+ }
+ // 鐢熸垚鍐呭鐨勬椂鍊欎娇鐢� markdown 鏉ユ覆鏌�
+ if (isGenerating) {
+ html.value = md.render(generatedContent)
+ }
+ // 鐢熸垚缁撴潫鏃舵洿鏂版�濈淮瀵煎浘
+ if (isEnd) {
+ update()
+ }
+})
+
+/** 鏇存柊鎬濈淮瀵煎浘鐨勫睍绀� */
+const update = () => {
+ try {
+ const { root } = transformer.transform(processContent(props.generatedContent))
+ markMap?.setData(root)
+ markMap?.fit()
+ } catch (e) {
+ console.error(e)
+ }
+}
+
+/** 澶勭悊鍐呭 */
+const processContent = (text: string) => {
+ const arr: string[] = []
+ const lines = text.split('\n')
+ for (let line of lines) {
+ if (line.indexOf('```') !== -1) {
+ continue
+ }
+ line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
+ arr.push(line)
+ }
+ return arr.join('\n')
+}
+
+/** 涓嬭浇鍥剧墖锛歞ownload SVG to png file */
+const downloadImage = () => {
+ const svgElement = mindMapRef.value
+ // 灏� SVG 娓叉煋鍒板浘鐗囧璞�
+ const serializer = new XMLSerializer()
+ const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`
+ const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`
+ download.image({
+ url: base64Url,
+ canvasWidth: svgElement?.offsetWidth,
+ canvasHeight: svgElement?.offsetHeight,
+ drawWithImageSize: false
+ })
+}
+
+defineExpose({
+ scrollBottom() {
+ mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
+ }
+})
+</script>
+<style lang="scss" scoped>
+.hide-scroll-bar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ }
+}
+
+.my-card {
+ display: flex;
+ flex-direction: column;
+
+ :deep(.el-card__body) {
+ box-sizing: border-box;
+ flex-grow: 1;
+ overflow-y: auto;
+ padding: 0;
+ @extend .hide-scroll-bar;
+ }
+}
+
+// markmap鐨則ool鏍峰紡瑕嗙洊
+:deep(.markmap) {
+ width: 100%;
+}
+
+:deep(.mm-toolbar-brand) {
+ display: none;
+}
+
+:deep(.mm-toolbar) {
+ display: flex;
+ flex-direction: row;
+}
+</style>
diff --git a/src/views/ai/mindmap/index/index.vue b/src/views/ai/mindmap/index/index.vue
new file mode 100644
index 0000000..72f0553
--- /dev/null
+++ b/src/views/ai/mindmap/index/index.vue
@@ -0,0 +1,94 @@
+<template>
+ <div class="absolute top-0 left-0 right-0 bottom-0 flex">
+ <!--琛ㄥ崟鍖哄煙-->
+ <Left
+ ref="leftRef"
+ :is-generating="isGenerating"
+ @submit="submit"
+ @direct-generate="directGenerate"
+ />
+ <!--鍙宠竟鐢熸垚鎬濈淮瀵煎浘鍖哄煙-->
+ <Right
+ ref="rightRef"
+ :generatedContent="generatedContent"
+ :isEnd="isEnd"
+ :isGenerating="isGenerating"
+ :isStart="isStart"
+ />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import Left from './components/Left.vue'
+import Right from './components/Right.vue'
+import { AiMindMapApi, AiMindMapGenerateReqVO } from '@/api/ai/mindmap'
+import { MindMapContentExample } from '@/views/ai/utils/constants'
+
+defineOptions({
+ name: 'AiMindMap'
+})
+const ctrl = ref<AbortController>() // 璇锋眰鎺у埗
+const isGenerating = ref(false) // 鏄惁姝e湪鐢熸垚鎬濈淮瀵煎浘
+const isStart = ref(false) // 寮�濮嬬敓鎴愶紝鐢ㄦ潵娓呯┖鎬濈淮瀵煎浘
+const isEnd = ref(true) // 鐢ㄦ潵鍒ゆ柇缁撴潫鐨勬椂鍊欐覆鏌撴�濈淮瀵煎浘
+const message = useMessage() // 娑堟伅鎻愮ず
+
+const generatedContent = ref('') // 鐢熸垚鎬濈淮瀵煎浘缁撴灉
+
+const leftRef = ref<InstanceType<typeof Left>>() // 宸﹁竟缁勪欢
+const rightRef = ref<InstanceType<typeof Right>>() // 鍙宠竟缁勪欢
+
+/** 浣跨敤宸叉湁鍐呭鐩存帴鐢熸垚 **/
+const directGenerate = (existPrompt: string) => {
+ isEnd.value = false // 鍏堣缃负 false 鍐嶈缃负 true锛岃瀛愮粍寤虹殑 watch 鑳藉鐩戝惉鍒�
+ generatedContent.value = existPrompt
+ isEnd.value = true
+}
+
+/** 鍋滄 stream 鐢熸垚 */
+const stopStream = () => {
+ isGenerating.value = false
+ isStart.value = false
+ ctrl.value?.abort()
+}
+
+/** 鎻愪氦鐢熸垚 */
+const submit = (data: AiMindMapGenerateReqVO) => {
+ isGenerating.value = true
+ isStart.value = true
+ isEnd.value = false
+ ctrl.value = new AbortController() // 璇锋眰鎺у埗璧嬪��
+ generatedContent.value = '' // 娓呯┖鐢熸垚鏁版嵁
+ AiMindMapApi.generateMindMap({
+ data,
+ onMessage: async (res) => {
+ const { code, data, msg } = JSON.parse(res.data)
+ if (code !== 0) {
+ message.alert(`鐢熸垚鎬濈淮瀵煎浘寮傚父! ${msg}`)
+ stopStream()
+ return
+ }
+ generatedContent.value = generatedContent.value + data
+ await nextTick()
+ rightRef.value?.scrollBottom()
+ },
+ onClose() {
+ isEnd.value = true
+ leftRef.value?.setGeneratedContent(generatedContent.value)
+ stopStream()
+ },
+ onError(err) {
+ console.error('鐢熸垚鎬濈淮瀵煎浘澶辫触', err)
+ stopStream()
+ // 闇�瑕佹姏鍑哄紓甯革紝绂佹閲嶈瘯
+ throw error
+ },
+ ctrl: ctrl.value
+ })
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ generatedContent.value = MindMapContentExample
+})
+</script>
diff --git a/src/views/ai/mindmap/manager/index.vue b/src/views/ai/mindmap/manager/index.vue
new file mode 100644
index 0000000..09182f3
--- /dev/null
+++ b/src/views/ai/mindmap/manager/index.vue
@@ -0,0 +1,191 @@
+<template>
+ <doc-alert title="AI 鎬濈淮瀵煎浘" url="https://doc.iocoder.cn/ai/mindmap/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎻愮ず璇�" prop="prompt">
+ <el-input
+ v-model="queryParams.prompt"
+ placeholder="璇疯緭鍏ユ彁绀鸿瘝"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="180" fixed="left" />
+ <el-table-column label="鐢ㄦ埛" align="center" prop="userId" width="180">
+ <template #default="scope">
+ <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮ず璇�" align="center" prop="prompt" width="180" />
+ <el-table-column label="鎬濈淮瀵煎浘" align="center" prop="generatedContent" min-width="300" />
+ <el-table-column label="妯″瀷" align="center" prop="model" width="180" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="閿欒淇℃伅" align="center" prop="errorMessage" />
+ <el-table-column label="鎿嶄綔" align="center" width="120" fixed="right">
+ <template #default="scope">
+ <el-button link type="primary" @click="openPreview(scope.row)"> 棰勮 </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:mind-map:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 鎬濈淮瀵煎浘鐨勯瑙� -->
+ <el-drawer v-model="previewVisible" :with-header="false" size="800px">
+ <Right
+ v-if="previewVisible2"
+ :generatedContent="previewContent"
+ :isEnd="true"
+ :isGenerating="false"
+ :isStart="false"
+ />
+ </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { AiMindMapApi, MindMapVO } from '@/api/ai/mindmap'
+import * as UserApi from '@/api/system/user'
+import Right from '@/views/ai/mindmap/index/components/Right.vue'
+
+/** AI 鎬濈淮瀵煎浘 鍒楄〃 */
+defineOptions({ name: 'AiMindMapManager' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<MindMapVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: undefined,
+ prompt: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await AiMindMapApi.getMindMapPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await AiMindMapApi.deleteMindMap(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 棰勮鎿嶄綔鎸夐挳 */
+const previewVisible = ref(false) // drawer 鐨勬樉绀洪殣钘�
+const previewVisible2 = ref(false) // right 鐨勬樉绀洪殣钘�
+const previewContent = ref('')
+const openPreview = async (row: MindMapVO) => {
+ previewVisible2.value = false
+ previewVisible.value = true
+ // 鍦� drawer 娓叉煋瀹屽悗锛屽啀娓叉煋 right 棰勮锛屼笉鐒朵細鎶ラ敊锛岄渶瑕佷繚璇� width 瀹藉害鍏堝嚭鏉�
+ await nextTick()
+ previewVisible2.value = true
+ previewContent.value = row.generatedContent
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/ai/model/apiKey/ApiKeyForm.vue b/src/views/ai/model/apiKey/ApiKeyForm.vue
new file mode 100644
index 0000000..2d3d4bf
--- /dev/null
+++ b/src/views/ai/model/apiKey/ApiKeyForm.vue
@@ -0,0 +1,132 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鎵�灞炲钩鍙�" prop="platform">
+ <el-select v-model="formData.platform" placeholder="璇疯緭鍏ュ钩鍙�" clearable>
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="瀵嗛挜" prop="apiKey">
+ <el-input v-model="formData.apiKey" placeholder="璇疯緭鍏ュ瘑閽�" />
+ </el-form-item>
+ <el-form-item label="鑷畾涔� API URL" prop="url">
+ <el-input v-model="formData.url" placeholder="璇疯緭鍏ヨ嚜瀹氫箟 API URL" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** AI API 瀵嗛挜 琛ㄥ崟 */
+defineOptions({ name: 'ApiKeyForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ apiKey: undefined,
+ platform: undefined,
+ url: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ apiKey: [{ required: true, message: '瀵嗛挜涓嶈兘涓虹┖', trigger: 'blur' }],
+ platform: [{ required: true, message: '骞冲彴涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ApiKeyApi.getApiKey(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ApiKeyVO
+ if (formType.value === 'create') {
+ await ApiKeyApi.createApiKey(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ApiKeyApi.updateApiKey(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ apiKey: undefined,
+ platform: undefined,
+ url: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/model/apiKey/index.vue b/src/views/ai/model/apiKey/index.vue
new file mode 100644
index 0000000..c4c9f3c
--- /dev/null
+++ b/src/views/ai/model/apiKey/index.vue
@@ -0,0 +1,182 @@
+<template>
+ <doc-alert title="AI 鎵嬪唽" url="https://doc.iocoder.cn/ai/build/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="骞冲彴" prop="platform">
+ <el-select
+ v-model="queryParams.platform"
+ placeholder="璇疯緭鍏ュ钩鍙�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['ai:api-key:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鎵�灞炲钩鍙�" align="center" prop="platform">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="瀵嗛挜" align="center" prop="apiKey" />
+ <el-table-column label="鑷畾涔� API URL" align="center" prop="url" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:api-key:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:api-key:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ApiKeyForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
+import ApiKeyForm from './ApiKeyForm.vue'
+
+/** AI API 瀵嗛挜 鍒楄〃 */
+defineOptions({ name: 'AiApiKey' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ApiKeyVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ platform: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ApiKeyApi.getApiKeyPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ApiKeyApi.deleteApiKey(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/ai/model/chatRole/ChatRoleForm.vue b/src/views/ai/model/chatRole/ChatRoleForm.vue
new file mode 100644
index 0000000..8f5c0ef
--- /dev/null
+++ b/src/views/ai/model/chatRole/ChatRoleForm.vue
@@ -0,0 +1,223 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="瑙掕壊鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヨ鑹插悕绉�" />
+ </el-form-item>
+ <el-form-item label="瑙掕壊澶村儚" prop="avatar">
+ <UploadImg v-model="formData.avatar" height="60px" width="60px" />
+ </el-form-item>
+ <el-form-item label="缁戝畾妯″瀷" prop="modelId" v-if="!isUser">
+ <el-select v-model="formData.modelId" placeholder="璇烽�夋嫨妯″瀷" clearable>
+ <el-option
+ v-for="model in models"
+ :key="model.id"
+ :label="model.name"
+ :value="model.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瑙掕壊绫诲埆" prop="category" v-if="!isUser">
+ <el-input v-model="formData.category" placeholder="璇疯緭鍏ヨ鑹茬被鍒�" />
+ </el-form-item>
+ <el-form-item label="瑙掕壊鎻忚堪" prop="description">
+ <el-input type="textarea" v-model="formData.description" placeholder="璇疯緭鍏ヨ鑹叉弿杩�" />
+ </el-form-item>
+ <el-form-item label="瑙掕壊璁惧畾" prop="systemMessage">
+ <el-input type="textarea" v-model="formData.systemMessage" placeholder="璇疯緭鍏ヨ鑹茶瀹�" />
+ </el-form-item>
+ <el-form-item label="寮曠敤鐭ヨ瘑搴�" prop="knowledgeIds">
+ <el-select v-model="formData.knowledgeIds" placeholder="璇烽�夋嫨鐭ヨ瘑搴�" clearable multiple>
+ <el-option
+ v-for="item in knowledgeList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮曠敤宸ュ叿" prop="toolIds">
+ <el-select v-model="formData.toolIds" placeholder="璇烽�夋嫨宸ュ叿" clearable multiple>
+ <el-option v-for="item in toolList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮曠敤 MCP" prop="toolIds">
+ <el-select v-model="formData.mcpClientNames" placeholder="璇烽�夋嫨 MCP" clearable multiple>
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_MCP_CLIENT_NAME)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏紑" prop="publicStatus" v-if="!isUser">
+ <el-radio-group v-model="formData.publicStatus">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="瑙掕壊鎺掑簭" prop="sort" v-if="!isUser">
+ <el-input-number v-model="formData.sort" placeholder="璇疯緭鍏ヨ鑹叉帓搴�" class="!w-1/1" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status" v-if="!isUser">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
+import { CommonStatusEnum } from '@/utils/constants'
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { FormRules } from 'element-plus'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+import { KnowledgeApi, KnowledgeVO } from '@/api/ai/knowledge/knowledge'
+import { ToolApi, ToolVO } from '@/api/ai/model/tool'
+
+/** AI 鑱婂ぉ瑙掕壊 琛ㄥ崟 */
+defineOptions({ name: 'ChatRoleForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ modelId: undefined,
+ name: undefined,
+ avatar: undefined,
+ category: undefined,
+ sort: undefined,
+ description: undefined,
+ systemMessage: undefined,
+ publicStatus: true,
+ status: CommonStatusEnum.ENABLE,
+ knowledgeIds: [] as number[],
+ toolIds: [] as number[],
+ mcpClientNames: [] as string[]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const models = ref([] as ModelVO[]) // 鑱婂ぉ妯″瀷鍒楄〃
+const knowledgeList = ref([] as KnowledgeVO[]) // 鐭ヨ瘑搴撳垪琛�
+const toolList = ref([] as ToolVO[]) // 宸ュ叿鍒楄〃
+
+/** 鏄惁銆愭垜銆戣嚜宸卞垱寤猴紝绉佹湁瑙掕壊 */
+const isUser = computed(() => {
+ return formType.value === 'my-create' || formType.value === 'my-update'
+})
+
+const formRules = reactive<FormRules>({
+ name: [{ required: true, message: '瑙掕壊鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ avatar: [{ required: true, message: '瑙掕壊澶村儚涓嶈兘涓虹┖', trigger: 'blur' }],
+ category: [{ required: true, message: '瑙掕壊绫诲埆涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '瑙掕壊鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ description: [{ required: true, message: '瑙掕壊鎻忚堪涓嶈兘涓虹┖', trigger: 'blur' }],
+ systemMessage: [{ required: true, message: '瑙掕壊璁惧畾涓嶈兘涓虹┖', trigger: 'blur' }],
+ publicStatus: [{ required: true, message: '鏄惁鍏紑涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+/** 鎵撳紑寮圭獥 */
+// TODO @fan锛歵itle 鏄笉鏄敹鏁涘埌 type 鍒ゆ柇鐢熸垚 title锛屼細鏇村悎鐞�
+const open = async (type: string, id?: number, title?: string) => {
+ dialogVisible.value = true
+ dialogTitle.value = title || t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ChatRoleApi.getChatRole(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱涓嬫媺鏁版嵁
+ models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+ // 鑾峰彇鐭ヨ瘑搴撳垪琛�
+ knowledgeList.value = await KnowledgeApi.getSimpleKnowledgeList()
+ // 鑾峰彇宸ュ叿鍒楄〃
+ toolList.value = await ToolApi.getToolSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ChatRoleVO
+ // tip: my-create銆乵y-update 鏄� chat 瑙掕壊浠撳簱璋冪敤
+ // tip: create銆乪lse 鏄悗鍙扮鐞嗚皟鐢�
+ if (formType.value === 'my-create') {
+ await ChatRoleApi.createMy(data)
+ message.success(t('common.createSuccess'))
+ } else if (formType.value === 'my-update') {
+ await ChatRoleApi.updateMy(data)
+ message.success(t('common.updateSuccess'))
+ } else if (formType.value === 'create') {
+ await ChatRoleApi.createChatRole(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ChatRoleApi.updateChatRole(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ modelId: undefined,
+ name: undefined,
+ avatar: undefined,
+ category: undefined,
+ sort: undefined,
+ description: undefined,
+ systemMessage: undefined,
+ publicStatus: true,
+ status: CommonStatusEnum.ENABLE,
+ knowledgeIds: [],
+ toolIds: [],
+ mcpClientNames: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/model/chatRole/index.vue b/src/views/ai/model/chatRole/index.vue
new file mode 100644
index 0000000..0ba0d79
--- /dev/null
+++ b/src/views/ai/model/chatRole/index.vue
@@ -0,0 +1,201 @@
+<template>
+ <doc-alert title="AI 瀵硅瘽鑱婂ぉ" url="https://doc.iocoder.cn/ai/chat/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="瑙掕壊鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヨ鑹插悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瑙掕壊绫诲埆" prop="category">
+ <el-input
+ v-model="queryParams.category"
+ placeholder="璇疯緭鍏ヨ鑹茬被鍒�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁鍏紑" prop="publicStatus">
+ <el-select
+ v-model="queryParams.publicStatus"
+ placeholder="璇烽�夋嫨鏄惁鍏紑"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['ai:chat-role:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="瑙掕壊鍚嶇О" align="center" prop="name" />
+ <el-table-column label="缁戝畾妯″瀷" align="center" prop="modelName" />
+ <el-table-column label="瑙掕壊澶村儚" align="center" prop="avatar">
+ <template #default="scope">
+ <el-image :src="scope?.row.avatar" class="w-32px h-32px" />
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙掕壊绫诲埆" align="center" prop="category" />
+ <el-table-column label="瑙掕壊鎻忚堪" align="center" prop="description" />
+ <el-table-column label="瑙掕壊璁惧畾" align="center" prop="systemMessage" />
+ <el-table-column label="鐭ヨ瘑搴�" align="center" prop="knowledgeIds">
+ <template #default="scope">
+ <span v-if="!scope.row.knowledgeIds || scope.row.knowledgeIds.length === 0">-</span>
+ <span v-else>寮曠敤 {{ scope.row.knowledgeIds.length }} 涓�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ュ叿" align="center" prop="toolIds">
+ <template #default="scope">
+ <span v-if="!scope.row.toolIds || scope.row.toolIds.length === 0">-</span>
+ <span v-else>寮曠敤 {{ scope.row.toolIds.length }} 涓�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁鍏紑" align="center" prop="publicStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.publicStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙掕壊鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:chat-role:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:chat-role:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ChatRoleForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
+import ChatRoleForm from './ChatRoleForm.vue'
+
+/** AI 鑱婂ぉ瑙掕壊 鍒楄〃 */
+defineOptions({ name: 'AiChatRole' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ChatRoleVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ category: undefined,
+ publicStatus: true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ChatRoleApi.getChatRolePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ChatRoleApi.deleteChatRole(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/ai/model/model/ModelForm.vue b/src/views/ai/model/model/ModelForm.vue
new file mode 100644
index 0000000..32b2c94
--- /dev/null
+++ b/src/views/ai/model/model/ModelForm.vue
@@ -0,0 +1,223 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="130px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鎵�灞炲钩鍙�" prop="platform">
+ <el-select v-model="formData.platform" placeholder="璇疯緭鍏ュ钩鍙�" clearable>
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯″瀷绫诲瀷" prop="type">
+ <el-select
+ v-model="formData.type"
+ placeholder="璇疯緭鍏ユā鍨嬬被鍨�"
+ clearable
+ :disabled="formData.id"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.AI_MODEL_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="API 绉橀挜" prop="keyId">
+ <el-select v-model="formData.keyId" placeholder="璇烽�夋嫨 API 绉橀挜" clearable>
+ <el-option
+ v-for="apiKey in apiKeyList"
+ :key="apiKey.id"
+ :label="apiKey.name"
+ :value="apiKey.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯″瀷鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユā鍨嬪悕瀛�" />
+ </el-form-item>
+ <el-form-item label="妯″瀷鏍囪瘑" prop="model">
+ <el-input v-model="formData.model" placeholder="璇疯緭鍏ユā鍨嬫爣璇�" />
+ </el-form-item>
+ <el-form-item label="妯″瀷鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" placeholder="璇疯緭鍏ユā鍨嬫帓搴�" class="!w-1/1" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ label="娓╁害鍙傛暟"
+ prop="temperature"
+ v-if="formData.type === AiModelTypeEnum.CHAT"
+ >
+ <el-input-number
+ v-model="formData.temperature"
+ placeholder="璇疯緭鍏ユ俯搴﹀弬鏁�"
+ :min="0"
+ :max="2"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item
+ label="鍥炲鏁� Token 鏁�"
+ prop="maxTokens"
+ v-if="formData.type === AiModelTypeEnum.CHAT"
+ >
+ <el-input-number
+ v-model="formData.maxTokens"
+ placeholder="璇疯緭鍏ュ洖澶嶆暟 Token 鏁�"
+ :min="0"
+ :max="8192"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item
+ label="涓婁笅鏂囨暟閲�"
+ prop="maxContexts"
+ v-if="formData.type === AiModelTypeEnum.CHAT"
+ >
+ <el-input-number
+ v-model="formData.maxContexts"
+ placeholder="璇疯緭鍏ヤ笂涓嬫枃鏁伴噺"
+ :min="0"
+ :max="20"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+
+/** API 妯″瀷鐨勮〃鍗� */
+defineOptions({ name: 'ModelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ keyId: undefined,
+ name: undefined,
+ model: undefined,
+ platform: undefined,
+ type: undefined,
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE,
+ temperature: undefined,
+ maxTokens: undefined,
+ maxContexts: undefined
+})
+const formRules = reactive({
+ keyId: [{ required: true, message: 'API 绉橀挜涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '妯″瀷鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ model: [{ required: true, message: '妯″瀷鏍囪瘑涓嶈兘涓虹┖', trigger: 'blur' }],
+ platform: [{ required: true, message: '鎵�灞炲钩鍙颁笉鑳戒负绌�', trigger: 'blur' }],
+ type: [{ required: true, message: '妯″瀷绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ temperature: [{ required: true, message: '娓╁害鍙傛暟涓嶈兘涓虹┖', trigger: 'blur' }],
+ maxTokens: [{ required: true, message: '鍥炲鏁� Token 鏁颁笉鑳戒负绌�', trigger: 'blur' }],
+ maxContexts: [{ required: true, message: '涓婁笅鏂囨暟閲忎笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const apiKeyList = ref([] as ApiKeyVO[]) // API 瀵嗛挜鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ModelApi.getModel(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱涓嬫媺鏁版嵁
+ apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ModelVO
+ if (data.type !== AiModelTypeEnum.CHAT) {
+ delete data.temperature
+ delete data.maxTokens
+ delete data.maxContexts
+ }
+ if (formType.value === 'create') {
+ await ModelApi.createModel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ModelApi.updateModel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ keyId: undefined,
+ name: undefined,
+ model: undefined,
+ platform: undefined,
+ type: undefined,
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE,
+ temperature: undefined,
+ maxTokens: undefined,
+ maxContexts: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/model/model/index.vue b/src/views/ai/model/model/index.vue
new file mode 100644
index 0000000..b86bee2
--- /dev/null
+++ b/src/views/ai/model/model/index.vue
@@ -0,0 +1,192 @@
+<template>
+ <doc-alert title="AI 鎵嬪唽" url="https://doc.iocoder.cn/ai/build/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="妯″瀷鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユā鍨嬪悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="妯″瀷鏍囪瘑" prop="model">
+ <el-input
+ v-model="queryParams.model"
+ placeholder="璇疯緭鍏ユā鍨嬫爣璇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="妯″瀷骞冲彴" prop="platform">
+ <el-input
+ v-model="queryParams.platform"
+ placeholder="璇疯緭鍏ユā鍨嬪钩鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['ai:model:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鎵�灞炲钩鍙�" align="center" prop="platform" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯″瀷绫诲瀷" align="center" prop="platform" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_MODEL_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯″瀷鍚嶅瓧" align="center" prop="name" min-width="180" />
+ <el-table-column label="妯″瀷鏍囪瘑" align="center" prop="model" min-width="180" />
+ <el-table-column label="API 绉橀挜" align="center" prop="keyId" min-width="140">
+ <template #default="scope">
+ <span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掑簭" align="center" prop="sort" min-width="80" />
+ <el-table-column label="鐘舵��" align="center" prop="status" min-width="80">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="娓╁害鍙傛暟" align="center" prop="temperature" min-width="80" />
+ <el-table-column label="鍥炲鏁� Token 鏁�" align="center" prop="maxTokens" min-width="140" />
+ <el-table-column label="涓婁笅鏂囨暟閲�" align="center" prop="maxContexts" min-width="100" />
+ <el-table-column label="鎿嶄綔" align="center" width="180" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:model:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:model:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ModelForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { ModelApi, ModelVO } from '@/api/ai/model/model'
+import ModelForm from './ModelForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
+
+/** API 妯″瀷鍒楄〃 */
+defineOptions({ name: 'AiModel' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ModelVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ model: undefined,
+ platform: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const apiKeyList = ref([] as ApiKeyVO[]) // API 瀵嗛挜鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ModelApi.getModelPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ModelApi.deleteModel(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱涓嬫媺鏁版嵁
+ apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
+})
+</script>
diff --git a/src/views/ai/model/tool/ToolForm.vue b/src/views/ai/model/tool/ToolForm.vue
new file mode 100644
index 0000000..bec5961
--- /dev/null
+++ b/src/views/ai/model/tool/ToolForm.vue
@@ -0,0 +1,112 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="宸ュ叿鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ伐鍏峰悕绉�" />
+ </el-form-item>
+ <el-form-item label="宸ュ叿鎻忚堪" prop="description">
+ <el-input type="textarea" v-model="formData.description" placeholder="璇疯緭鍏ュ伐鍏锋弿杩�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ToolApi, ToolVO } from '@/api/ai/model/tool'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** AI 宸ュ叿琛ㄥ崟 */
+defineOptions({ name: 'ToolForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '宸ュ叿鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ToolApi.getTool(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ToolVO
+ if (formType.value === 'create') {
+ await ToolApi.createTool(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ToolApi.updateTool(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/ai/model/tool/index.vue b/src/views/ai/model/tool/index.vue
new file mode 100644
index 0000000..583b3e1
--- /dev/null
+++ b/src/views/ai/model/tool/index.vue
@@ -0,0 +1,178 @@
+<template>
+ <doc-alert title="AI 宸ュ叿璋冪敤锛坒unction calling锛�" url="https://doc.iocoder.cn/ai/tool/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="宸ュ叿鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ伐鍏峰悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" plain @click="openForm('create')" v-hasPermi="['ai:tool:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="宸ュ叿缂栧彿" align="center" prop="id" />
+ <el-table-column label="宸ュ叿鍚嶇О" align="center" prop="name" />
+ <el-table-column label="宸ュ叿鎻忚堪" align="center" prop="description" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:tool:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:tool:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ToolForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ToolApi, ToolVO } from '@/api/ai/model/tool'
+import ToolForm from './ToolForm.vue'
+
+/** AI 宸ュ叿 鍒楄〃 */
+defineOptions({ name: 'AiTool' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ToolVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ description: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ToolApi.getToolPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ToolApi.deleteTool(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/ai/music/index/index.vue b/src/views/ai/music/index/index.vue
new file mode 100644
index 0000000..413792a
--- /dev/null
+++ b/src/views/ai/music/index/index.vue
@@ -0,0 +1,26 @@
+<template>
+<div class="flex h-full items-stretch">
+ <!-- 妯″紡 -->
+ <Mode class="flex-none" @generate-music="generateMusic"/>
+ <!-- 闊抽鍒楄〃 -->
+ <List ref="listRef" class="flex-auto"/>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import Mode from './mode/index.vue'
+import List from './list/index.vue'
+
+defineOptions({ name: 'Index' })
+
+const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null)
+
+/*
+ *@Description: 鎷垮埌宸︿晶閰嶇疆淇℃伅璋冪敤鍙充晶闊充箰鐢熸垚鐨勬柟娉�
+ *@MethodAuthor: xiaohong
+ *@Date: 2024-07-19 11:13:38
+*/
+function generateMusic (args: {formData: Recordable}) {
+ unref(listRef)?.generateMusic(args.formData)
+}
+</script>
diff --git a/src/views/ai/music/index/list/audioBar/index.vue b/src/views/ai/music/index/list/audioBar/index.vue
new file mode 100644
index 0000000..db7f767
--- /dev/null
+++ b/src/views/ai/music/index/list/audioBar/index.vue
@@ -0,0 +1,70 @@
+<template>
+ <div class="flex items-center justify-between px-2 h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">
+ <!-- 姝屾洸淇℃伅 -->
+ <div class="flex gap-[10px]">
+ <el-image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" class="w-[45px]"/>
+ <div>
+ <div>{{currentSong.name}}</div>
+ <div class="text-[12px] text-gray-400">{{currentSong.singer}}</div>
+ </div>
+ </div>
+
+ <!-- 闊抽controls -->
+ <div class="flex gap-[12px] items-center">
+ <Icon icon="majesticons:back-circle" :size="20" class="text-gray-300 cursor-pointer"/>
+ <Icon :icon="audioProps.paused ? 'mdi:arrow-right-drop-circle' : 'solar:pause-circle-bold'" :size="30" class=" cursor-pointer" @click="toggleStatus('paused')"/>
+ <Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer"/>
+ <div class="flex gap-[16px] items-center">
+ <span>{{audioProps.currentTime}}</span>
+ <el-slider v-model="audioProps.duration" color="#409eff" class="w-[160px!important] "/>
+ <span>{{ audioProps.duration }}</span>
+ </div>
+ <!-- 闊抽 -->
+ <audio v-bind="audioProps" ref="audioRef" controls v-show="!audioProps" @timeupdate="audioTimeUpdate">
+ <source :src="audioUrl"/>
+ </audio>
+ </div>
+
+ <!-- 闊抽噺鎺у埗鍣� -->
+ <div class="flex gap-[16px] items-center">
+ <Icon :icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'" :size="20" class="cursor-pointer" @click="toggleStatus('muted')"/>
+ <el-slider v-model="audioProps.volume" color="#409eff" class="w-[160px!important] "/>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { formatPast } from '@/utils/formatTime'
+import audioUrl from '@/assets/audio/response.mp3'
+
+defineOptions({ name: 'Index' })
+
+const currentSong = inject('currentSong', {})
+
+const audioRef = ref<Nullable<HTMLElement>>(null)
+ // 闊抽鐩稿叧灞炴�ttps://www.runoob.com/tags/ref-av-dom.html
+const audioProps = reactive({
+ autoplay: true,
+ paused: false,
+ currentTime: '00:00',
+ duration: '00:00',
+ muted: false,
+ volume: 50,
+})
+
+function toggleStatus (type: string) {
+ audioProps[type] = !audioProps[type]
+ if (type === 'paused' && audioRef.value) {
+ if (audioProps[type]) {
+ audioRef.value.pause()
+ } else {
+ audioRef.value.play()
+ }
+ }
+}
+
+// 鏇存柊鎾斁浣嶇疆
+function audioTimeUpdate (args) {
+ audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss')
+}
+</script>
diff --git a/src/views/ai/music/index/list/index.vue b/src/views/ai/music/index/list/index.vue
new file mode 100644
index 0000000..6c33f56
--- /dev/null
+++ b/src/views/ai/music/index/list/index.vue
@@ -0,0 +1,108 @@
+<template>
+ <div class="flex flex-col">
+ <div class="flex-auto flex overflow-hidden">
+ <el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
+ <!-- 鎴戠殑鍒涗綔 -->
+ <el-tab-pane v-loading="loading" label="鎴戠殑鍒涗綔" name="mine">
+ <el-row v-if="mySongList.length" :gutter="12">
+ <el-col v-for="song in mySongList" :key="song.id" :span="24">
+ <songCard :songInfo="song" @play="setCurrentSong(song)"/>
+ </el-col>
+ </el-row>
+ <el-empty v-else description="鏆傛棤闊充箰"/>
+ </el-tab-pane>
+
+ <!-- 璇曞惉骞垮満 -->
+ <el-tab-pane v-loading="loading" label="璇曞惉骞垮満" name="square">
+ <el-row v-if="squareSongList.length" v-loading="loading" :gutter="12">
+ <el-col v-for="song in squareSongList" :key="song.id" :span="24">
+ <songCard :songInfo="song" @play="setCurrentSong(song)"/>
+ </el-col>
+ </el-row>
+ <el-empty v-else description="鏆傛棤闊充箰"/>
+ </el-tab-pane>
+ </el-tabs>
+ <!-- songInfo -->
+ <songInfo class="flex-none"/>
+ </div>
+ <audioBar class="flex-none"/>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import songCard from './songCard/index.vue'
+import songInfo from './songInfo/index.vue'
+import audioBar from './audioBar/index.vue'
+
+defineOptions({ name: 'Index' })
+
+
+const currentType = ref('mine')
+// loading 鐘舵��
+const loading = ref(false)
+// 褰撳墠闊充箰
+const currentSong = ref({})
+
+const mySongList = ref<Recordable[]>([])
+const squareSongList = ref<Recordable[]>([])
+
+provide('currentSong', currentSong)
+
+/*
+ *@Description: 璋冩帴鍙g敓鎴愰煶涔愬垪琛�
+ *@MethodAuthor: xiaohong
+ *@Date: 2024-06-27 17:06:44
+*/
+function generateMusic (formData: Recordable) {
+ console.log(formData);
+ loading.value = true
+ setTimeout(() => {
+ mySongList.value = Array.from({ length: 20 }, (_, index) => {
+ return {
+ id: index,
+ audioUrl: '',
+ videoUrl: '',
+ title: '鎴戣蛋鍚�' + index,
+ imageUrl: 'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
+ desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
+ date: '2024骞�04鏈�30鏃� 14:02:57',
+ lyric: `<div class="_words_17xen_66"><div>澶ф睙涓滃幓锛屾氮娣樺敖锛屽崈鍙ら娴佷汉鐗┿��
+ </div><div>鏁呭瀿瑗胯竟锛屼汉閬撴槸锛屼笁鍥藉懆閮庤丹澹併��
+ </div><div>涔辩煶绌跨┖锛屾儕娑涙媿宀革紝鍗疯捣鍗冨爢闆��
+ </div><div>姹熷北濡傜敾锛屼竴鏃跺灏戣豹鏉般��
+ </div><div>
+ </div><div>閬ユ兂鍏懢褰撳勾锛屽皬涔斿垵瀚佷簡锛岄泟濮胯嫳鍙戙��
+ </div><div>缇芥墖绾跺肪锛岃皥绗戦棿锛屾ǒ姗圭伆椋炵儫鐏��
+ </div><div>鏁呭浗绁炴父锛屽鎯呭簲绗戞垜锛屾棭鐢熷崕鍙戙��
+ </div><div>浜虹敓濡傛ⅵ锛屼竴灏婅繕閰规睙鏈堛��</div></div>`
+ }
+ })
+ loading.value = false
+ }, 3000)
+}
+
+/*
+ *@Description: 璁剧疆褰撳墠鎾斁鐨勯煶涔�
+ *@MethodAuthor: xiaohong
+ *@Date: 2024-07-19 11:22:33
+*/
+function setCurrentSong (music: Recordable) {
+ currentSong.value = music
+}
+
+defineExpose({
+ generateMusic
+})
+</script>
+
+
+<style lang="scss" scoped>
+:deep(.el-tabs) {
+ display: flex;
+ flex-direction: column;
+ .el-tabs__content {
+ padding: 0 7px;
+ overflow: auto;
+ }
+}
+</style>
diff --git a/src/views/ai/music/index/list/songCard/index.vue b/src/views/ai/music/index/list/songCard/index.vue
new file mode 100644
index 0000000..0534251
--- /dev/null
+++ b/src/views/ai/music/index/list/songCard/index.vue
@@ -0,0 +1,36 @@
+<template>
+ <div class="flex bg-[var(--el-bg-color-overlay)] p-12px mb-12px rounded-1">
+ <div class="relative" @click="playSong">
+ <el-image :src="songInfo.imageUrl" class="flex-none w-80px"/>
+ <div class="bg-black bg-op-40 absolute top-0 left-0 w-full h-full flex items-center justify-center cursor-pointer">
+ <Icon :icon="currentSong.id === songInfo.id ? 'solar:pause-circle-bold':'mdi:arrow-right-drop-circle'" :size="30" />
+ </div>
+ </div>
+ <div class="ml-8px">
+ <div>{{ songInfo.title }}</div>
+ <div class="mt-8px text-12px text-[var(--el-text-color-secondary)] line-clamp-2">
+ {{ songInfo.desc }}
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+
+defineOptions({ name: 'Index' })
+
+defineProps({
+ songInfo: {
+ type: Object,
+ default: () => ({})
+ }
+})
+
+const emits = defineEmits(['play'])
+
+const currentSong = inject('currentSong', {})
+
+function playSong () {
+ emits('play')
+}
+</script>
diff --git a/src/views/ai/music/index/list/songInfo/index.vue b/src/views/ai/music/index/list/songInfo/index.vue
new file mode 100644
index 0000000..8d67c4d
--- /dev/null
+++ b/src/views/ai/music/index/list/songInfo/index.vue
@@ -0,0 +1,22 @@
+<template>
+ <ContentWrap class="w-300px mb-[0!important] line-height-24px">
+ <el-image :src="currentSong.imageUrl"/>
+ <div class="">{{ currentSong.title }}</div>
+ <div class="text-[var(--el-text-color-secondary)] text-12px line-clamp-1">
+ {{ currentSong.desc }}
+ </div>
+ <div class="text-[var(--el-text-color-secondary)] text-12px">
+ {{ currentSong.date }}
+ </div>
+ <el-button size="small" round class="my-6px">淇℃伅澶嶇敤</el-button>
+ <div class="text-[var(--el-text-color-secondary)] text-12px" v-html="currentSong.lyric"></div>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+
+defineOptions({ name: 'Index' })
+
+const currentSong = inject('currentSong', {})
+
+</script>
diff --git a/src/views/ai/music/index/mode/desc.vue b/src/views/ai/music/index/mode/desc.vue
new file mode 100644
index 0000000..4488461
--- /dev/null
+++ b/src/views/ai/music/index/mode/desc.vue
@@ -0,0 +1,55 @@
+<template>
+ <div>
+ <Title title="闊充箰/姝岃瘝璇存槑" desc="鎻忚堪鎮ㄦ兂瑕佺殑闊充箰椋庢牸鍜屼富棰橈紝浣跨敤娴佹淳鍜屾皼鍥磋�屼笉鏄壒瀹氱殑鑹烘湳瀹跺拰姝屾洸">
+ <el-input
+ v-model="formData.desc"
+ :autosize="{ minRows: 6, maxRows: 6}"
+ resize="none"
+ type="textarea"
+ maxlength="1200"
+ show-word-limit
+ placeholder="涓�棣栧叧浜庣碂绯曞垎鎵嬬殑娆㈠揩姝屾洸"
+ />
+ </Title>
+
+ <Title title="绾煶涔�" desc="鍒涘缓涓�棣栨病鏈夋瓕璇嶇殑姝屾洸">
+ <template #extra>
+ <el-switch v-model="formData.pure" size="small"/>
+ </template>
+ </Title>
+
+ <Title title="鐗堟湰" desc="鎻忚堪鎮ㄦ兂瑕佺殑闊充箰椋庢牸鍜屼富棰橈紝浣跨敤娴佹淳鍜屾皼鍥磋�屼笉鏄壒瀹氱殑鑹烘湳瀹跺拰姝屾洸">
+ <el-select v-model="formData.version" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in [{
+ value: '3',
+ label: 'V3'
+ }, {
+ value: '2',
+ label: 'V2'
+ }]"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </Title>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import Title from '../title/index.vue'
+
+defineOptions({ name: 'Desc' })
+
+const formData = reactive({
+ desc: '',
+ pure: false,
+ version: '3'
+})
+
+defineExpose({
+ formData
+})
+
+</script>
diff --git a/src/views/ai/music/index/mode/index.vue b/src/views/ai/music/index/mode/index.vue
new file mode 100644
index 0000000..32cad7e
--- /dev/null
+++ b/src/views/ai/music/index/mode/index.vue
@@ -0,0 +1,35 @@
+<template>
+ <ContentWrap class="w-300px h-full mb-[0!important]">
+ <el-radio-group v-model="generateMode" class="mb-15px">
+ <el-radio-button value="desc"> 鎻忚堪妯″紡 </el-radio-button>
+ <el-radio-button value="lyric"> 姝岃瘝妯″紡 </el-radio-button>
+ </el-radio-group>
+
+ <!-- 鎻忚堪妯″紡/姝岃瘝妯″紡 鍒囨崲 -->
+ <component :is="generateMode === 'desc' ? desc : lyric" ref="modeRef" />
+
+ <el-button type="primary" round class="w-full" @click="generateMusic"> 鍒涗綔闊充箰 </el-button>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import desc from './desc.vue'
+import lyric from './lyric.vue'
+
+defineOptions({ name: 'Index' })
+
+const emits = defineEmits(['generate-music'])
+
+const generateMode = ref('lyric')
+
+const modeRef = ref<Nullable<{ formData: Recordable }>>(null)
+
+/*
+ *@Description: 鏍规嵁淇℃伅鐢熸垚闊充箰
+ *@MethodAuthor: xiaohong
+ *@Date: 2024-06-27 16:40:16
+ */
+function generateMusic() {
+ emits('generate-music', { formData: unref(modeRef)?.formData })
+}
+</script>
diff --git a/src/views/ai/music/index/mode/lyric.vue b/src/views/ai/music/index/mode/lyric.vue
new file mode 100644
index 0000000..f774003
--- /dev/null
+++ b/src/views/ai/music/index/mode/lyric.vue
@@ -0,0 +1,83 @@
+<template>
+ <div class="">
+ <Title title="姝岃瘝" desc="鑷繁缂栧啓姝岃瘝鎴栦娇鐢ˋi鐢熸垚姝岃瘝锛屼袱鑺�/8琛屾晥鏋滄渶浣�">
+ <el-input
+ v-model="formData.lyric"
+ :autosize="{ minRows: 6, maxRows: 6}"
+ resize="none"
+ type="textarea"
+ maxlength="1200"
+ show-word-limit
+ placeholder="璇疯緭鍏ユ偍鑷繁鐨勬瓕璇�"
+ />
+ </Title>
+
+ <Title title="闊充箰椋庢牸">
+ <el-space class="flex-wrap">
+ <el-tag v-for="tag in tags" :key="tag" round class="mb-8px">{{tag}}</el-tag>
+ </el-space>
+
+ <el-button
+ :type="showCustom ? 'primary': 'default'"
+ round
+ size="small"
+ class="mb-6px"
+ @click="showCustom = !showCustom"
+ >鑷畾涔夐鏍�
+ </el-button>
+ </Title>
+
+ <Title v-show="showCustom" desc="鎻忚堪鎮ㄦ兂瑕佺殑闊充箰椋庢牸锛孲uno鏃犳硶璇嗗埆鑹烘湳瀹剁殑鍚嶅瓧锛屼絾鍙互鐞嗚В娴佹淳鍜屾皼鍥�" class="-mt-12px">
+ <el-input
+ v-model="formData.style"
+ :autosize="{ minRows: 4, maxRows: 4}"
+ resize="none"
+ type="textarea"
+ maxlength="256"
+ show-word-limit
+ placeholder="杈撳叆闊充箰椋庢牸(鑻辨枃)"
+ />
+ </Title>
+
+ <Title title="闊充箰/姝屾洸鍚嶇О">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ煶涔�/姝屾洸鍚嶇О"/>
+ </Title>
+
+ <Title title="鐗堟湰">
+ <el-select v-model="formData.version" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in [{
+ value: '3',
+ label: 'V3'
+ }, {
+ value: '2',
+ label: 'V2'
+ }]"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </Title>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import Title from '../title/index.vue'
+defineOptions({ name: 'Lyric' })
+
+const tags = ['rock', 'punk', 'jazz', 'soul', 'country', 'kidsmusic', 'pop']
+
+const showCustom = ref(false)
+
+const formData = reactive({
+ lyric: '',
+ style: '',
+ name: '',
+ version: ''
+})
+
+defineExpose({
+ formData
+})
+</script>
diff --git a/src/views/ai/music/index/title/index.vue b/src/views/ai/music/index/title/index.vue
new file mode 100644
index 0000000..a065802
--- /dev/null
+++ b/src/views/ai/music/index/title/index.vue
@@ -0,0 +1,25 @@
+<template>
+ <div class="mb-12px">
+ <div class="flex text-[var(--el-text-color-primary)] justify-between items-center">
+ <span>{{title}}</span>
+ <slot name="extra"></slot>
+ </div>
+ <div class="text-[var(--el-text-color-secondary)] text-12px my-8px">
+ {{desc}}
+ </div>
+ <slot></slot>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'Index' })
+
+defineProps({
+ title: {
+ type: String
+ },
+ desc: {
+ type: String
+ }
+})
+</script>
diff --git a/src/views/ai/music/manager/index.vue b/src/views/ai/music/manager/index.vue
new file mode 100644
index 0000000..27daf13
--- /dev/null
+++ b/src/views/ai/music/manager/index.vue
@@ -0,0 +1,294 @@
+<template>
+ <doc-alert title="AI 闊充箰鍒涗綔" url="https://doc.iocoder.cn/ai/music/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="闊充箰鍚嶇О" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ラ煶涔愬悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="闊充箰鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨闊充箰鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢熸垚妯″紡" prop="generateMode">
+ <el-select
+ v-model="queryParams.generateMode"
+ placeholder="璇烽�夋嫨鐢熸垚妯″紡"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.AI_GENERATE_MODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁鍙戝竷" prop="publicStatus">
+ <el-select
+ v-model="queryParams.publicStatus"
+ placeholder="璇烽�夋嫨鏄惁鍙戝竷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="180" fixed="left" />
+ <el-table-column label="闊充箰鍚嶇О" align="center" prop="title" width="180px" fixed="left" />
+ <el-table-column label="鐢ㄦ埛" align="center" prop="userId" width="180">
+ <template #default="scope">
+ <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="闊充箰鐘舵��" align="center" prop="status" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_MUSIC_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯″瀷" align="center" prop="model" width="180" />
+ <el-table-column label="鍐呭" align="center" width="180">
+ <template #default="{ row }">
+ <el-link
+ v-if="row.audioUrl?.length > 0"
+ type="primary"
+ :href="row.audioUrl"
+ target="_blank"
+ >
+ 闊充箰
+ </el-link>
+ <el-link
+ v-if="row.videoUrl?.length > 0"
+ type="primary"
+ :href="row.videoUrl"
+ target="_blank"
+ class="!pl-5px"
+ >
+ 瑙嗛
+ </el-link>
+ <el-link
+ v-if="row.imageUrl?.length > 0"
+ type="primary"
+ :href="row.imageUrl"
+ target="_blank"
+ class="!pl-5px"
+ >
+ 灏侀潰
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏃堕暱锛堢锛�" align="center" prop="duration" width="100" />
+ <el-table-column label="鎻愮ず璇�" align="center" prop="prompt" width="180" />
+ <el-table-column label="姝岃瘝" align="center" prop="lyric" width="180" />
+ <el-table-column label="鎻忚堪" align="center" prop="gptDescriptionPrompt" width="180" />
+ <el-table-column label="鐢熸垚妯″紡" align="center" prop="generateMode" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_GENERATE_MODE" :value="scope.row.generateMode" />
+ </template>
+ </el-table-column>
+ <el-table-column label="椋庢牸鏍囩" align="center" prop="tags" width="180">
+ <template #default="scope">
+ <el-tag v-for="tag in scope.row.tags" :key="tag" round class="ml-2px">
+ {{ tag }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁鍙戝竷" align="center" prop="publicStatus">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.publicStatus"
+ :active-value="true"
+ :inactive-value="false"
+ @change="handleUpdatePublicStatusChange(scope.row)"
+ :disabled="scope.row.status !== AiMusicStatusEnum.SUCCESS"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="浠诲姟缂栧彿" align="center" prop="taskId" width="180" />
+ <el-table-column label="閿欒淇℃伅" align="center" prop="errorMessage" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="100" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:music:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { MusicApi, MusicVO } from '@/api/ai/music'
+import * as UserApi from '@/api/system/user'
+import { AiMusicStatusEnum } from '@/views/ai/utils/constants'
+
+/** AI 闊充箰 鍒楄〃 */
+defineOptions({ name: 'AiMusicManager' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<MusicVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: undefined,
+ title: undefined,
+ status: undefined,
+ generateMode: undefined,
+ createTime: [],
+ publicStatus: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MusicApi.getMusicPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await MusicApi.deleteMusic(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼鏄惁鍙戝竷 */
+const handleUpdatePublicStatusChange = async (row: MusicVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.publicStatus ? '鍏紑' : '绉佹湁'
+ await message.confirm('纭瑕�"' + text + '"璇ラ煶涔愬悧?')
+ // 鍙戣捣淇敼鐘舵��
+ await MusicApi.updateMusic({
+ id: row.id,
+ publicStatus: row.publicStatus
+ })
+ await getList()
+ } catch {
+ row.publicStatus = !row.publicStatus
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/ai/utils/constants.ts b/src/views/ai/utils/constants.ts
new file mode 100644
index 0000000..6f4e839
--- /dev/null
+++ b/src/views/ai/utils/constants.ts
@@ -0,0 +1,470 @@
+/**
+ * Created by 鑺嬮亾婧愮爜
+ *
+ * AI 鏋氫妇绫�
+ *
+ * 闂锛氫负浠�涔堜笉鏀惧湪 src/utils/constants.ts 鍛紵
+ * 鍥炵瓟锛氫富瑕� AI 鏄彲閫夋ā鍧楋紝鑰冭檻鍒扮嫭绔嬨�佽В鑰︼紝鎵�浠ユ斁鍦ㄤ簡 /views/ai/utils/constants.ts
+ */
+
+/**
+ * AI 骞冲彴鐨勬灇涓�
+ */
+export const AiPlatformEnum = {
+ TONG_YI: 'TongYi', // 闃块噷
+ YI_YAN: 'YiYan', // 鐧惧害
+ DEEP_SEEK: 'DeepSeek', // DeepSeek
+ ZHI_PU: 'ZhiPu', // 鏅鸿氨 AI
+ XING_HUO: 'XingHuo', // 璁
+ SiliconFlow: 'SiliconFlow', // 纭呭熀娴佸姩
+ OPENAI: 'OpenAI',
+ Ollama: 'Ollama',
+ STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
+ MIDJOURNEY: 'Midjourney', // Midjourney
+ SUNO: 'Suno' // Suno AI
+}
+
+export const AiModelTypeEnum = {
+ CHAT: 1, // 鑱婂ぉ
+ IMAGE: 2, // 鍥惧儚
+ VOICE: 3, // 闊抽
+ VIDEO: 4, // 瑙嗛
+ EMBEDDING: 5, // 鍚戦噺
+ RERANK: 6 // 閲嶆帓
+}
+
+export const OtherPlatformEnum: ImageModelVO[] = [
+ {
+ key: AiPlatformEnum.TONG_YI,
+ name: '閫氫箟涓囩浉'
+ },
+ {
+ key: AiPlatformEnum.YI_YAN,
+ name: '鐧惧害鍗冨竼'
+ },
+ {
+ key: AiPlatformEnum.ZHI_PU,
+ name: '鏅鸿氨 AI'
+ },
+ {
+ key: AiPlatformEnum.SiliconFlow,
+ name: '纭呭熀娴佸姩'
+ }
+]
+
+/**
+ * AI 鍥惧儚鐢熸垚鐘舵�佺殑鏋氫妇
+ */
+export const AiImageStatusEnum = {
+ IN_PROGRESS: 10, // 杩涜涓�
+ SUCCESS: 20, // 宸插畬鎴�
+ FAIL: 30 // 宸插け璐�
+}
+
+/**
+ * AI 闊充箰鐢熸垚鐘舵�佺殑鏋氫妇
+ */
+export const AiMusicStatusEnum = {
+ IN_PROGRESS: 10, // 杩涜涓�
+ SUCCESS: 20, // 宸插畬鎴�
+ FAIL: 30 // 宸插け璐�
+}
+
+/**
+ * AI 鍐欎綔绫诲瀷鐨勬灇涓�
+ */
+export enum AiWriteTypeEnum {
+ WRITING = 1, // 鎾板啓
+ REPLY // 鍥炲
+}
+
+// 琛ㄦ牸灞曠ず瀵圭収map
+export const AiWriteTypeTableRender = {
+ [AiWriteTypeEnum.WRITING]: '鎾板啓',
+ [AiWriteTypeEnum.REPLY]: '鍥炲'
+}
+
+// ========== 銆愬浘鐗� UI銆戠浉鍏崇殑鏋氫妇 ==========
+
+export const ImageHotWords = [
+ '涓浗鏃楄',
+ '鍙よ缇庡コ',
+ '鍗¢�氬ご鍍�',
+ '鏈虹敳鎴樺+',
+ '绔ヨ瘽灏忓眿',
+ '涓浗闀垮煄'
+] // 鍥剧墖鐑瘝
+
+export const ImageHotEnglishWords = [
+ 'Chinese Cheongsam',
+ 'Ancient Beauty',
+ 'Cartoon Avatar',
+ 'Mech Warrior',
+ 'Fairy Tale Cottage',
+ 'The Great Wall of China'
+] // 鍥剧墖鐑瘝锛堣嫳鏂囷級
+
+export interface ImageModelVO {
+ key: string
+ name: string
+ image?: string
+}
+
+export const StableDiffusionSamplers: ImageModelVO[] = [
+ {
+ key: 'DDIM',
+ name: 'DDIM'
+ },
+ {
+ key: 'DDPM',
+ name: 'DDPM'
+ },
+ {
+ key: 'K_DPMPP_2M',
+ name: 'K_DPMPP_2M'
+ },
+ {
+ key: 'K_DPMPP_2S_ANCESTRAL',
+ name: 'K_DPMPP_2S_ANCESTRAL'
+ },
+ {
+ key: 'K_DPM_2',
+ name: 'K_DPM_2'
+ },
+ {
+ key: 'K_DPM_2_ANCESTRAL',
+ name: 'K_DPM_2_ANCESTRAL'
+ },
+ {
+ key: 'K_EULER',
+ name: 'K_EULER'
+ },
+ {
+ key: 'K_EULER_ANCESTRAL',
+ name: 'K_EULER_ANCESTRAL'
+ },
+ {
+ key: 'K_HEUN',
+ name: 'K_HEUN'
+ },
+ {
+ key: 'K_LMS',
+ name: 'K_LMS'
+ }
+]
+
+export const StableDiffusionStylePresets: ImageModelVO[] = [
+ {
+ key: '3d-model',
+ name: '3d-model'
+ },
+ {
+ key: 'analog-film',
+ name: 'analog-film'
+ },
+ {
+ key: 'anime',
+ name: 'anime'
+ },
+ {
+ key: 'cinematic',
+ name: 'cinematic'
+ },
+ {
+ key: 'comic-book',
+ name: 'comic-book'
+ },
+ {
+ key: 'digital-art',
+ name: 'digital-art'
+ },
+ {
+ key: 'enhance',
+ name: 'enhance'
+ },
+ {
+ key: 'fantasy-art',
+ name: 'fantasy-art'
+ },
+ {
+ key: 'isometric',
+ name: 'isometric'
+ },
+ {
+ key: 'line-art',
+ name: 'line-art'
+ },
+ {
+ key: 'low-poly',
+ name: 'low-poly'
+ },
+ {
+ key: 'modeling-compound',
+ name: 'modeling-compound'
+ },
+ // neon-punk origami photographic pixel-art tile-texture
+ {
+ key: 'neon-punk',
+ name: 'neon-punk'
+ },
+ {
+ key: 'origami',
+ name: 'origami'
+ },
+ {
+ key: 'photographic',
+ name: 'photographic'
+ },
+ {
+ key: 'pixel-art',
+ name: 'pixel-art'
+ },
+ {
+ key: 'tile-texture',
+ name: 'tile-texture'
+ }
+]
+
+export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [
+ {
+ key: 'NONE',
+ name: 'NONE'
+ },
+ {
+ key: 'FAST_BLUE',
+ name: 'FAST_BLUE'
+ },
+ {
+ key: 'FAST_GREEN',
+ name: 'FAST_GREEN'
+ },
+ {
+ key: 'SIMPLE',
+ name: 'SIMPLE'
+ },
+ {
+ key: 'SLOW',
+ name: 'SLOW'
+ },
+ {
+ key: 'SLOWER',
+ name: 'SLOWER'
+ },
+ {
+ key: 'SLOWEST',
+ name: 'SLOWEST'
+ }
+]
+
+export const Dall3Models: ImageModelVO[] = [
+ {
+ key: 'dall-e-3',
+ name: 'DALL路E 3',
+ image: `/src/assets/ai/dall2.jpg`
+ },
+ {
+ key: 'dall-e-2',
+ name: 'DALL路E 2',
+ image: `/src/assets/ai/dall3.jpg`
+ }
+]
+
+export const Dall3StyleList: ImageModelVO[] = [
+ {
+ key: 'vivid',
+ name: '娓呮櫚',
+ image: `/src/assets/ai/qingxi.jpg`
+ },
+ {
+ key: 'natural',
+ name: '鑷劧',
+ image: `/src/assets/ai/ziran.jpg`
+ }
+]
+
+export interface ImageSizeVO {
+ key: string
+ name?: string
+ style: string
+ width: string
+ height: string
+}
+
+export const Dall3SizeList: ImageSizeVO[] = [
+ {
+ key: '1024x1024',
+ name: '1:1',
+ width: '1024',
+ height: '1024',
+ style: 'width: 30px; height: 30px;background-color: #dcdcdc;'
+ },
+ {
+ key: '1024x1792',
+ name: '3:5',
+ width: '1024',
+ height: '1792',
+ style: 'width: 30px; height: 50px;background-color: #dcdcdc;'
+ },
+ {
+ key: '1792x1024',
+ name: '5:3',
+ width: '1792',
+ height: '1024',
+ style: 'width: 50px; height: 30px;background-color: #dcdcdc;'
+ }
+]
+
+export const MidjourneyModels: ImageModelVO[] = [
+ {
+ key: 'midjourney',
+ name: 'MJ',
+ image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png'
+ },
+ {
+ key: 'niji',
+ name: 'NIJI',
+ image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png'
+ }
+]
+
+export const MidjourneySizeList: ImageSizeVO[] = [
+ {
+ key: '1:1',
+ width: '1',
+ height: '1',
+ style: 'width: 30px; height: 30px;background-color: #dcdcdc;'
+ },
+ {
+ key: '3:4',
+ width: '3',
+ height: '4',
+ style: 'width: 30px; height: 40px;background-color: #dcdcdc;'
+ },
+ {
+ key: '4:3',
+ width: '4',
+ height: '3',
+ style: 'width: 40px; height: 30px;background-color: #dcdcdc;'
+ },
+ {
+ key: '9:16',
+ width: '9',
+ height: '16',
+ style: 'width: 30px; height: 50px;background-color: #dcdcdc;'
+ },
+ {
+ key: '16:9',
+ width: '16',
+ height: '9',
+ style: 'width: 50px; height: 30px;background-color: #dcdcdc;'
+ }
+]
+
+export const MidjourneyVersions = [
+ {
+ value: '6.0',
+ label: 'v6.0'
+ },
+ {
+ value: '5.2',
+ label: 'v5.2'
+ },
+ {
+ value: '5.1',
+ label: 'v5.1'
+ },
+ {
+ value: '5.0',
+ label: 'v5.0'
+ },
+ {
+ value: '4.0',
+ label: 'v4.0'
+ }
+]
+
+export const NijiVersionList = [
+ {
+ value: '5',
+ label: 'v5'
+ }
+]
+
+// ========== 銆愬啓浣� UI銆戠浉鍏崇殑鏋氫妇 ==========
+
+/** 鍐欎綔鐐瑰嚮绀轰緥鏃剁殑鏁版嵁 **/
+export const WriteExample = {
+ write: {
+ prompt: 'vue',
+ data: 'Vue.js 鏄竴绉嶇敤浜庢瀯寤虹敤鎴风晫闈㈢殑娓愯繘寮� JavaScript 妗嗘灦銆傚畠鐨勬牳蹇冨簱鍙叧娉ㄨ鍥惧眰锛屾槗浜庝笂鎵嬶紝鍚屾椂涔熶究浜庝笌鍏朵粬搴撴垨宸叉湁椤圭洰鏁村悎銆俓n\nVue.js 鐨勭壒鐐瑰寘鎷細\n- 鍝嶅簲寮忕殑鏁版嵁缁戝畾锛歏ue.js 浼氳嚜鍔ㄥ皢鏁版嵁涓� DOM 鍚屾锛屼娇寰楃姸鎬佺鐞嗗彉寰楁洿鍔犵畝鍗曘�俓n- 缁勪欢鍖栵細Vue.js 鍏佽寮�鍙戣�呴�氳繃灏忓瀷銆佺嫭绔嬪拰閫氬父鍙鐢ㄧ殑缁勪欢鏋勫缓澶у瀷搴旂敤銆俓n- 铏氭嫙 DOM锛歏ue.js 浣跨敤铏氭嫙 DOM 瀹炵幇蹇�熸覆鏌擄紝鎻愰珮浜嗘�ц兘銆俓n\n鍦� Vue.js 涓紝涓�涓吀鍨嬬殑搴旂敤缁撴瀯鍙兘鍖呮嫭锛歕n1. 鏍瑰疄渚嬶細姣忎釜 Vue 搴旂敤閮介渶瑕佷竴涓牴瀹炰緥浣滀负鍏ュ彛鐐广�俓n2. 缁勪欢绯荤粺锛氬彲浠ュ垱寤鸿嚜瀹氫箟鐨勫彲澶嶇敤缁勪欢銆俓n3. 鎸囦护锛氱壒娈婄殑甯︽湁鍓嶇紑 v- 鐨勫睘鎬э紝涓� DOM 鍏冪礌鎻愪緵鐗规畩鐨勮涓恒�俓n4. 鎻掑�硷細鐢ㄤ簬鏂囨湰鍐呭锛屽皢鏁版嵁鍔ㄦ�佸湴鎻掑叆鍒� HTML銆俓n5. 璁$畻灞炴�у拰渚﹀惉鍣細鐢ㄤ簬澶勭悊鏁版嵁鐨勫鏉傞�昏緫鍜屽搷搴旀暟鎹彉鍖栥�俓n6. 鏉′欢娓叉煋锛氭牴鎹潯浠跺喅瀹氬厓绱犵殑娓叉煋銆俓n7. 鍒楄〃娓叉煋锛氱敤浜庢樉绀哄垪琛ㄦ暟鎹�俓n8. 浜嬩欢澶勭悊锛氬搷搴旂敤鎴蜂氦浜掋�俓n9. 琛ㄥ崟杈撳叆缁戝畾锛氬鐞嗚〃鍗曡緭鍏ュ拰楠岃瘉銆俓n10. 缁勪欢鐢熷懡鍛ㄦ湡閽╁瓙锛氬湪缁勪欢鐨勪笉鍚岄樁娈垫墽琛岀壒瀹氱殑鍑芥暟銆俓n\nVue.js 杩樻彁渚涗簡瀹樻柟鐨勮矾鐢卞櫒 Vue Router 鍜岀姸鎬佺鐞嗗簱 Vuex锛屼互鏀寔鏋勫缓澶嶆潅鐨勫崟椤靛簲鐢紙SPA锛夈�俓n\n鍦ㄥ紑鍙戣繃绋嬩腑锛屽紑鍙戣�呴�氬父浼氫娇鐢� Vue CLI锛岃繖鏄竴涓己澶х殑鍛戒护琛屽伐鍏凤紝鐢ㄤ簬蹇�熺敓鎴� Vue 椤圭洰鑴氭墜鏋讹紝闆嗘垚浜嗚濡� Babel銆乄ebpack 绛夌幇浠e墠绔伐鍏凤紝浠ュ強鐑噸杞姐�佷唬鐮佹娴嬬瓑寮�鍙戜綋楠屼紭鍖栧姛鑳姐�俓n\nVue.js 鐨勭敓鎬佺郴缁熻繕鍖呮嫭澶ч噺鐨勭涓夋柟搴撳拰鎻掍欢锛屽 Vuetify锛圲I 缁勪欢搴擄級銆乂ue Test Utils锛堟祴璇曞伐鍏凤級绛夛紝杩欎簺閮芥瀬澶у湴涓板瘜浜� Vue.js 鐨勫紑鍙戠敓鎬併�俓n\n鎬荤殑鏉ヨ锛孷ue.js 鏄竴涓伒娲汇�侀珮鏁堢殑鍓嶇妗嗘灦锛岄�傚悎浠庡皬鍨嬮」鐩埌澶у瀷浼佷笟绾у簲鐢ㄧ殑寮�鍙戙�傚畠鐨勬槗鐢ㄦ�с�佺伒娲绘�у拰寮哄ぇ鐨勭ぞ鍖烘敮鎸佷娇鍏舵垚涓鸿澶氬紑鍙戣�呯殑棣栭�夋鏋朵箣涓�銆�'
+ },
+ reply: {
+ originalContent: '棰嗗锛屾垜鎯宠鍋�',
+ prompt: '涓嶆壒',
+ data: '鎮ㄧ殑璇峰亣鐢宠宸叉敹鎮夛紝缁忔牳瀹炲拰鑰冭檻锛屾殏鏃舵棤娉曟壒鍑嗘偍鐨勮鍋囩敵璇枫�俓n\n濡傛湁鐗规畩鎯呭喌鎴栫揣鎬ヤ簨鍔★紝璇峰強鏃朵笌鎴戣仈绯汇�俓n\n绁濆伐浣滈『鍒┿�俓n\n璋㈣阿銆�'
+ }
+}
+
+// ========== 銆愭�濈淮瀵煎浘 UI銆戠浉鍏崇殑鏋氫妇 ==========
+
+/** 鎬濈淮瀵煎浘宸叉湁鍐呭鐢熸垚绀轰緥 **/
+export const MindMapContentExample = `# Java 鎶�鏈爤
+
+## 鏍稿績鎶�鏈�
+### Java SE
+### Java EE
+
+## 妗嗘灦
+### Spring
+#### Spring Boot
+#### Spring MVC
+#### Spring Data
+### Hibernate
+### MyBatis
+
+## 鏋勫缓宸ュ叿
+### Maven
+### Gradle
+
+## 鐗堟湰鎺у埗
+### Git
+### SVN
+
+## 娴嬭瘯宸ュ叿
+### JUnit
+### Mockito
+### Selenium
+
+## 搴旂敤鏈嶅姟鍣�
+### Tomcat
+### Jetty
+### WildFly
+
+## 鏁版嵁搴�
+### MySQL
+### PostgreSQL
+### Oracle
+### MongoDB
+
+## 娑堟伅闃熷垪
+### Kafka
+### RabbitMQ
+### ActiveMQ
+
+## 寰湇鍔�
+### Spring Cloud
+### Dubbo
+
+## 瀹瑰櫒鍖�
+### Docker
+### Kubernetes
+
+## 浜戞湇鍔�
+### AWS
+### Azure
+### Google Cloud
+
+## 寮�鍙戝伐鍏�
+### IntelliJ IDEA
+### Eclipse
+### Visual Studio Code`
diff --git a/src/views/ai/utils/utils.ts b/src/views/ai/utils/utils.ts
new file mode 100644
index 0000000..ab45ae1
--- /dev/null
+++ b/src/views/ai/utils/utils.ts
@@ -0,0 +1,13 @@
+/**
+ * Created by 鑺嬮亾婧愮爜
+ *
+ * AI 鏋氫妇绫�
+ *
+ * 闂锛氫负浠�涔堜笉鏀惧湪 src/utils/common-utils.ts 鍛紵
+ * 鍥炵瓟锛氫富瑕� AI 鏄彲閫夋ā鍧楋紝鑰冭檻鍒扮嫭绔嬨�佽В鑰︼紝鎵�浠ユ斁鍦ㄤ簡 /views/ai/utils/common-utils.ts
+ */
+
+/** 鍒ゆ柇瀛楃涓叉槸鍚﹀寘鍚腑鏂� */
+export const hasChinese = (str: string) => {
+ return /[\u4e00-\u9fa5]/.test(str)
+}
diff --git a/src/views/ai/workflow/form/BasicInfo.vue b/src/views/ai/workflow/form/BasicInfo.vue
new file mode 100644
index 0000000..6b5426c
--- /dev/null
+++ b/src/views/ai/workflow/form/BasicInfo.vue
@@ -0,0 +1,54 @@
+<template>
+ <el-form ref="formRef" :model="modelData" :rules="formRules" label-width="120px">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="娴佺▼鏍囪瘑" prop="code">
+ <el-input v-model="modelData.code" placeholder="璇疯緭鍏ユ祦绋嬫爣璇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="娴佺▼鍚嶇О" prop="name">
+ <el-input v-model="modelData.name" placeholder="璇疯緭鍏ユ祦绋嬪悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="modelData.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="modelData.remark" :rows="2" type="textarea" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import { FormRules } from 'element-plus'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+const modelData = defineModel<any>()
+
+const formRef = ref() // 琛ㄥ崟 Ref
+const formRules = reactive<FormRules>({
+ code: [{ required: true, message: '娴佺▼鏍囪瘑涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '娴佺▼鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }]
+})
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = async () => {
+ await formRef.value?.validate()
+}
+defineExpose({
+ validate
+})
+</script>
diff --git a/src/views/ai/workflow/form/WorkflowDesign.vue b/src/views/ai/workflow/form/WorkflowDesign.vue
new file mode 100644
index 0000000..1346f9c
--- /dev/null
+++ b/src/views/ai/workflow/form/WorkflowDesign.vue
@@ -0,0 +1,250 @@
+<template>
+ <div class="relative" style="width: 100%; height: 700px">
+ <Tinyflow
+ v-if="workflowData"
+ ref="tinyflowRef"
+ :className="'custom-class'"
+ :style="{ width: '100%', height: '100%' }"
+ :data="workflowData"
+ :provider="provider"
+ />
+ <div class="absolute top-30px right-30px">
+ <el-button @click="testWorkflowModel" type="primary" v-hasPermi="['ai:workflow:test']">
+ 娴嬭瘯
+ </el-button>
+ </div>
+
+ <!-- 娴嬭瘯绐楀彛 -->
+ <el-drawer v-model="showTestDrawer" title="宸ヤ綔娴佹祴璇�" :modal="false">
+ <fieldset>
+ <legend class="ml-15px"><h3>杩愯鍙傛暟閰嶇疆</h3></legend>
+ <div class="p-20px">
+ <div
+ class="flex justify-around mb-10px"
+ v-for="(param, index) in params4Test"
+ :key="index"
+ >
+ <el-select class="w-200px!" v-model="param.key" placeholder="鍙傛暟鍚�">
+ <el-option
+ v-for="(value, key) in paramsOfStartNode"
+ :key="key"
+ :label="value?.description || key"
+ :value="key"
+ :disabled="!!value?.disabled"
+ />
+ </el-select>
+ <el-input class="w-200px!" v-model="param.value" placeholder="鍙傛暟鍊�" />
+ <el-button type="danger" plain :icon="Delete" circle @click="removeParam(index)" />
+ </div>
+ <!-- TODO @lesan锛氭槸涓嶆槸涓嶇敤娣诲姞鍜屽垹闄ゅ弬鏁帮紝鐩存帴鎶婂繀濉拰閫夊~鍒楀嚭鏉ワ紝鐒跺悗鍔犱笂鍙傛暟鏍¢獙锛� -->
+ <el-button type="primary" plain @click="addParam">娣诲姞鍙傛暟</el-button>
+ </div>
+ </fieldset>
+ <fieldset class="mt-20px bg-#f8f9fa">
+ <legend class="ml-15px"><h3>杩愯缁撴灉</h3></legend>
+ <div class="p-20px">
+ <div v-if="loading"> <el-text type="primary">鎵ц涓�...</el-text></div>
+ <div v-else-if="error">
+ <el-text type="danger">{{ error }}</el-text>
+ </div>
+ <pre v-else-if="testResult" class="result-content"
+ >{{ JSON.stringify(testResult, null, 2) }}
+ </pre>
+ <div v-else> <el-text type="info">鐐瑰嚮杩愯鏌ョ湅缁撴灉</el-text> </div>
+ </div>
+ </fieldset>
+ <el-button class="mt-20px w-100%" size="large" type="success" @click="goRun">
+ 杩愯娴佺▼
+ </el-button>
+ </el-drawer>
+ </div>
+</template>
+
+<script setup lang="ts">
+import Tinyflow from '@/components/Tinyflow/Tinyflow.vue'
+import * as WorkflowApi from '@/api/ai/workflow'
+// TODO @lesan锛氳涓嶄娇鐢� ICon 鍝釜缁勪欢鍝�
+import { Delete } from '@element-plus/icons-vue'
+
+defineProps<{
+ provider: any
+}>()
+
+const tinyflowRef = ref()
+const workflowData = inject('workflowData') as Ref
+const showTestDrawer = ref(false)
+const params4Test = ref([])
+const paramsOfStartNode = ref({})
+const testResult = ref(null)
+const loading = ref(false)
+const error = ref(null)
+
+/** 灞曠ず宸ヤ綔娴佹祴璇曟娊灞� */
+const testWorkflowModel = () => {
+ showTestDrawer.value = !showTestDrawer.value
+}
+
+/** 杩愯娴佺▼ */
+const goRun = async () => {
+ try {
+ const val = tinyflowRef.value.getData()
+ loading.value = true
+ error.value = null
+ testResult.value = null
+ /// 鏌ユ壘start鑺傜偣
+ const startNode = getStartNode()
+
+ // 鑾峰彇鍙傛暟瀹氫箟
+ const parameters = startNode.data?.parameters || []
+ const paramDefinitions = {}
+ parameters.forEach((param) => {
+ paramDefinitions[param.name] = param.dataType
+ })
+
+ // 鍙傛暟绫诲瀷杞崲
+ const convertedParams = {}
+ for (const { key, value } of params4Test.value) {
+ const paramKey = key.trim()
+ if (!paramKey) continue
+
+ let dataType = paramDefinitions[paramKey]
+ if (!dataType) {
+ dataType = 'String'
+ }
+
+ try {
+ convertedParams[paramKey] = convertParamValue(value, dataType)
+ } catch (e) {
+ throw new Error(`鍙傛暟 ${paramKey} 杞崲澶辫触: ${e.message}`)
+ }
+ }
+
+ const data = {
+ graph: JSON.stringify(val),
+ params: convertedParams
+ }
+
+ const response = await WorkflowApi.testWorkflow(data)
+ testResult.value = response
+ } catch (err) {
+ error.value = err.response?.data?.message || '杩愯澶辫触锛岃妫�鏌ュ弬鏁板拰缃戠粶杩炴帴'
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鐩戝惉娴嬭瘯鎶藉眽鐨勫紑鍚紝鑾峰彇寮�濮嬭妭鐐瑰弬鏁板垪琛� */
+watch(showTestDrawer, (value) => {
+ if (!value) return
+
+ /// 鏌ユ壘start鑺傜偣
+ const startNode = getStartNode()
+
+ // 鑾峰彇鍙傛暟瀹氫箟
+ const parameters = startNode.data?.parameters || []
+ const paramDefinitions = {}
+
+ // 鍔犲叆鍙傛暟閫夐」鏂逛究鐢ㄦ埛娣诲姞闈炲繀椤诲弬鏁�
+ parameters.forEach((param) => {
+ paramDefinitions[param.name] = param
+ })
+
+ function mergeIfRequiredButNotSet(target) {
+ let needPushList = []
+ for (let key in paramDefinitions) {
+ let param = paramDefinitions[key]
+
+ if (param.required) {
+ let item = target.find((item) => item.key === key)
+
+ if (!item) {
+ needPushList.push({ key: param.name, value: param.defaultValue || '' })
+ }
+ }
+ }
+ target.push(...needPushList)
+ }
+ // 鑷姩瑁呰浇闇�蹇呭~鐨勫弬鏁�
+ mergeIfRequiredButNotSet(params4Test.value)
+
+ paramsOfStartNode.value = paramDefinitions
+})
+
+/** 鑾峰彇寮�濮嬭妭鐐� */
+const getStartNode = () => {
+ const val = tinyflowRef.value.getData()
+ const startNode = val.nodes.find((node) => node.type === 'startNode')
+ if (!startNode) {
+ throw new Error('娴佺▼缂哄皯寮�濮嬭妭鐐�')
+ }
+ return startNode
+}
+
+/** 娣诲姞鍙傛暟椤� */
+const addParam = () => {
+ params4Test.value.push({ key: '', value: '' })
+}
+
+/** 鍒犻櫎鍙傛暟椤� */
+const removeParam = (index) => {
+ params4Test.value.splice(index, 1)
+}
+
+/** 绫诲瀷杞崲鍑芥暟 */
+const convertParamValue = (value, dataType) => {
+ if (value === '') return null // 绌哄�煎鐞�
+
+ switch (dataType) {
+ case 'String':
+ return String(value)
+ case 'Number':
+ const num = Number(value)
+ if (isNaN(num)) throw new Error('闈炴暟瀛楁牸寮�')
+ return num
+ case 'Boolean':
+ if (value.toLowerCase() === 'true') return true
+ if (value.toLowerCase() === 'false') return false
+ throw new Error('蹇呴』涓� true/false')
+ case 'Object':
+ case 'Array':
+ try {
+ return JSON.parse(value)
+ } catch (e) {
+ throw new Error(`JSON鏍煎紡閿欒: ${e.message}`)
+ }
+ default:
+ throw new Error(`涓嶆敮鎸佺殑绫诲瀷: ${dataType}`)
+ }
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = async () => {
+ try {
+ // 鑾峰彇鏈�鏂扮殑娴佺▼鏁版嵁
+ if (!workflowData.value) {
+ throw new Error('璇疯璁℃祦绋�')
+ }
+ workflowData.value = tinyflowRef.value.getData()
+ return true
+ } catch (error) {
+ throw error
+ }
+}
+defineExpose({
+ validate
+})
+</script>
+
+<style lang="css" scoped>
+.result-content {
+ background: white;
+ padding: 12px;
+ border-radius: 4px;
+ max-height: 300px;
+ overflow: auto;
+ font-family: Monaco, Consolas, monospace;
+ font-size: 14px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+}
+</style>
diff --git a/src/views/ai/workflow/form/index.vue b/src/views/ai/workflow/form/index.vue
new file mode 100644
index 0000000..dddb7a5
--- /dev/null
+++ b/src/views/ai/workflow/form/index.vue
@@ -0,0 +1,240 @@
+<template>
+ <ContentWrap>
+ <div class="mx-auto">
+ <!-- 澶撮儴瀵艰埅鏍� -->
+ <div
+ class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+ >
+ <!-- 宸︿晶鏍囬 -->
+ <div class="w-200px flex items-center overflow-hidden">
+ <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+ <span class="ml-10px text-16px truncate" :title="formData.name || '鍒涘缓娴佺▼'">
+ {{ formData.name || '鍒涘缓娴佺▼' }}
+ </span>
+ </div>
+
+ <!-- 姝ラ鏉� -->
+ <div class="flex-1 flex items-center justify-center h-full">
+ <div class="w-400px flex items-center justify-between h-full">
+ <div
+ v-for="(step, index) in steps"
+ :key="index"
+ class="flex items-center cursor-pointer mx-15px relative h-full"
+ :class="[
+ currentStep === index
+ ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+ : 'text-gray-500'
+ ]"
+ @click="handleStepClick(index)"
+ >
+ <div
+ class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+ :class="[
+ currentStep === index
+ ? 'bg-[#3473ff] text-white border-[#3473ff]'
+ : 'border-gray-300 bg-white text-gray-500'
+ ]"
+ >
+ {{ index + 1 }}
+ </div>
+ <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍙充晶鎸夐挳 -->
+ <div class="w-200px flex items-center justify-end gap-2">
+ <el-button type="primary" @click="handleSave"> 淇� 瀛� </el-button>
+ </div>
+ </div>
+
+ <!-- 涓讳綋鍐呭 -->
+ <div class="mt-50px">
+ <!-- 绗竴姝ワ細鍩烘湰淇℃伅 -->
+ <div v-if="currentStep === 0" class="mx-auto w-560px">
+ <BasicInfo v-model="formData" ref="basicInfoRef" />
+ </div>
+
+ <!-- 绗簩姝ワ細宸ヤ綔娴佽璁� -->
+ <WorkflowDesign
+ v-if="currentStep === 1"
+ v-model="formData"
+ :provider="llmProvider"
+ ref="workflowDesignRef"
+ />
+ </div>
+ </div>
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as WorkflowApi from '@/api/ai/workflow'
+import BasicInfo from './BasicInfo.vue'
+import WorkflowDesign from './WorkflowDesign.vue'
+import { ModelApi } from '@/api/ai/model/model'
+import { AiModelTypeEnum } from '@/views/ai/utils/constants'
+
+const router = useRouter()
+const { delView } = useTagsViewStore()
+const route = useRoute()
+const message = useMessage()
+
+const basicInfoRef = ref()
+const workflowDesignRef = ref()
+
+const validateBasic = async () => {
+ await basicInfoRef.value?.validate()
+}
+const validateWorkflow = async () => {
+ await workflowDesignRef.value?.validate()
+}
+
+const currentStep = ref(-1)
+const steps = [
+ { title: '鍩烘湰淇℃伅', validator: validateBasic },
+ { title: '宸ヤ綔娴佽璁�', validator: validateWorkflow }
+]
+
+const formData: any = ref({
+ id: undefined,
+ name: '',
+ code: '',
+ remark: '',
+ graph: '',
+ status: CommonStatusEnum.ENABLE
+})
+const llmProvider = ref<any>([])
+const workflowData = ref<any>({})
+provide('workflowData', workflowData)
+
+/** 鍒濆鍖栨暟鎹� */
+const actionType = route.params.type as string
+const initData = async () => {
+ // 缂栬緫鎯呭喌涓嬶紝闇�瑕佸姞杞藉伐浣滄祦閰嶇疆
+ if (actionType === 'update') {
+ const workflowId = route.params.id as string
+ formData.value = await WorkflowApi.getWorkflow(workflowId)
+ workflowData.value = JSON.parse(formData.value.graph)
+ }
+
+ // 鍔犺浇妯″瀷鍒楄〃
+ const models = await ModelApi.getModelSimpleList(AiModelTypeEnum.CHAT)
+ llmProvider.value = {
+ llm: () =>
+ models.map(({ id, name }) => ({
+ value: id,
+ label: name
+ })),
+ knowledge: () => [],
+ internal: () => []
+ }
+ // TODO @lesan锛氱煡璇嗗簱锛堝彲浠ョ湅涓� knowledge锛�
+ // TODO @lesan锛氭悳绱㈠紩鎿庯紙杩欎釜涔嬪墠鏈変釜 pr 鎼炰簡锛岋紝锛屽彲鑳芥潵鎺ヤ笅锛�
+
+ // 璁剧疆褰撳墠姝ラ
+ currentStep.value = 0
+}
+
+/** 鏍¢獙鎵�鏈夋楠ゆ暟鎹槸鍚﹀畬鏁� */
+const validateAllSteps = async () => {
+ try {
+ // 鍩烘湰淇℃伅鏍¢獙
+ try {
+ await validateBasic()
+ } catch (error) {
+ currentStep.value = 0
+ throw new Error('璇峰畬鍠勫熀鏈俊鎭�')
+ }
+
+ // 宸ヤ綔娴佽璁℃牎楠�
+ try {
+ await validateWorkflow()
+ } catch (error) {
+ currentStep.value = 1
+ throw new Error('璇峰畬鍠勫伐浣滄祦淇℃伅')
+ }
+ return true
+ } catch (error) {
+ throw error
+ }
+}
+
+/** 淇濆瓨鎿嶄綔 */
+const handleSave = async () => {
+ try {
+ // 淇濆瓨鍓嶆牎楠屾墍鏈夋楠ょ殑鏁版嵁
+ await validateAllSteps()
+
+ // 鏇存柊琛ㄥ崟鏁版嵁
+ const data = {
+ ...formData.value,
+ graph: JSON.stringify(workflowData.value)
+ }
+ if (actionType === 'update') {
+ await WorkflowApi.updateWorkflow(data)
+ } else {
+ await WorkflowApi.createWorkflow(data)
+ }
+
+ // 淇濆瓨鎴愬姛锛屾彁绀哄苟璺宠浆鍒板垪琛ㄩ〉
+ message.success('淇濆瓨鎴愬姛')
+ delView(unref(router.currentRoute))
+ await router.push({ name: 'AiWorkflow' })
+ } catch (error: any) {
+ console.error('淇濆瓨澶辫触:', error)
+ message.warning(error.message || '璇峰畬鍠勬墍鏈夋楠ょ殑蹇呭~淇℃伅')
+ }
+}
+
+/** 姝ラ鍒囨崲澶勭悊 */
+const handleStepClick = async (index: number) => {
+ try {
+ if (index !== 0) {
+ await validateBasic()
+ }
+ if (index !== 1) {
+ await validateWorkflow()
+ }
+
+ // 鍒囨崲姝ラ
+ currentStep.value = index
+ } catch (error) {
+ console.error('姝ラ鍒囨崲澶辫触:', error)
+ message.warning('璇峰厛瀹屽杽褰撳墠姝ラ蹇呭~淇℃伅')
+ }
+}
+
+/** 杩斿洖鍒楄〃椤� */
+const handleBack = () => {
+ // 鍏堝垹闄ゅ綋鍓嶉〉绛�
+ delView(unref(router.currentRoute))
+ // 璺宠浆鍒板垪琛ㄩ〉
+ router.push({ name: 'AiWorkflow' })
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await initData()
+})
+</script>
+
+<!-- TODO @lesan锛氬彲浠ョ敤 cursor 鎼炴垚 unocss 鍝� -->
+<style lang="scss" scoped>
+.border-bottom {
+ border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+ color: #3473ff;
+}
+
+.bg-primary {
+ background-color: #3473ff;
+}
+
+.border-primary {
+ border-color: #3473ff;
+}
+</style>
diff --git a/src/views/ai/workflow/index.vue b/src/views/ai/workflow/index.vue
new file mode 100644
index 0000000..6a874c7
--- /dev/null
+++ b/src/views/ai/workflow/index.vue
@@ -0,0 +1,193 @@
+<template>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="娴佺▼鏍囪瘑" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ユ祦绋嬫爣璇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娴佺▼鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['ai:workflow:create']"
+ >
+ <Icon icon="ep:plus" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="娴佺▼鏍囪瘑" align="center" prop="code" />
+ <el-table-column label="娴佺▼鍚嶇О" align="center" prop="name" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鐘舵��" align="center" key="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['ai:workflow:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:workflow:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 娣诲姞鎴栦慨鏀瑰伐浣滄祦瀵硅瘽妗� -->
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as WorkflowApi from '@/api/ai/workflow'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'AiWorkflow' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ code: '',
+ name: '',
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await WorkflowApi.getWorkflowPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await WorkflowApi.deleteWorkflow(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const openForm = async (type: string, id?: number) => {
+ if (type === 'create') {
+ await push({ name: 'AiWorkflowCreate' })
+ } else {
+ await push({
+ name: 'AiWorkflowUpdate',
+ params: { id, type }
+ })
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/ai/write/index/components/Left.vue b/src/views/ai/write/index/components/Left.vue
new file mode 100644
index 0000000..74e5d58
--- /dev/null
+++ b/src/views/ai/write/index/components/Left.vue
@@ -0,0 +1,213 @@
+<template>
+ <!-- 瀹氫箟 tab 缁勪欢锛氭挵鍐�/鍥炲绛� -->
+ <DefineTab v-slot="{ active, text, itemClick }">
+ <span
+ :class="active ? 'text-black shadow-md' : 'hover:bg-[#DDDFE3]'"
+ class="inline-block w-1/2 rounded-full cursor-pointer text-center leading-[30px] relative z-1 text-[5C6370] hover:text-black"
+ @click="itemClick"
+ >
+ {{ text }}
+ </span>
+ </DefineTab>
+ <!-- 瀹氫箟 label 缁勪欢锛氶暱搴�/鏍煎紡/璇皵/璇█绛� -->
+ <DefineLabel v-slot="{ label, hint, hintClick }">
+ <h3 class="mt-5 mb-3 flex items-center justify-between text-[14px]">
+ <span>{{ label }}</span>
+ <span
+ v-if="hint"
+ class="flex items-center text-[12px] text-[#846af7] cursor-pointer select-none"
+ @click="hintClick"
+ >
+ <Icon icon="ep:question-filled" />
+ {{ hint }}
+ </span>
+ </h3>
+ </DefineLabel>
+
+ <div class="flex flex-col" v-bind="$attrs">
+ <!-- tab -->
+ <div class="w-full pt-2 bg-[#f5f7f9] flex justify-center">
+ <div class="w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10">
+ <div
+ :class="
+ selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]'
+ "
+ class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full"
+ >
+ <ReuseTab
+ v-for="tab in tabs"
+ :key="tab.value"
+ :active="tab.value === selectedTab"
+ :itemClick="() => switchTab(tab.value)"
+ :text="tab.text"
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ class="px-7 pb-2 flex-grow overflow-y-auto lg:block w-[380px] box-border bg-[#f5f7f9] h-full"
+ >
+ <div>
+ <template v-if="selectedTab === 1">
+ <ReuseLabel :hint-click="() => example('write')" hint="绀轰緥" label="鍐欎綔鍐呭" />
+ <el-input
+ v-model="formData.prompt"
+ :maxlength="500"
+ :rows="5"
+ placeholder="璇疯緭鍏ュ啓浣滃唴瀹�"
+ showWordLimit
+ type="textarea"
+ />
+ </template>
+
+ <template v-else>
+ <ReuseLabel :hint-click="() => example('reply')" hint="绀轰緥" label="鍘熸枃" />
+ <el-input
+ v-model="formData.originalContent"
+ :maxlength="500"
+ :rows="5"
+ placeholder="璇疯緭鍏ュ師鏂�"
+ showWordLimit
+ type="textarea"
+ />
+
+ <ReuseLabel label="鍥炲鍐呭" />
+ <el-input
+ v-model="formData.prompt"
+ :maxlength="500"
+ :rows="5"
+ placeholder="璇疯緭鍏ュ洖澶嶅唴瀹�"
+ showWordLimit
+ type="textarea"
+ />
+ </template>
+
+ <ReuseLabel label="闀垮害" />
+ <Tag v-model="formData.length" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)" />
+ <ReuseLabel label="鏍煎紡" />
+ <Tag v-model="formData.format" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)" />
+ <ReuseLabel label="璇皵" />
+ <Tag v-model="formData.tone" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)" />
+ <ReuseLabel label="璇█" />
+ <Tag v-model="formData.language" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)" />
+
+ <div class="flex items-center justify-center mt-3">
+ <el-button :disabled="isWriting" @click="reset">閲嶇疆</el-button>
+ <el-button :loading="isWriting" color="#846af7" @click="submit">鐢熸垚</el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { createReusableTemplate } from '@vueuse/core'
+import { ref } from 'vue'
+import Tag from './Tag.vue'
+import { WriteVO } from '@/api/ai/write'
+import { omit } from 'lodash-es'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants'
+
+type TabType = WriteVO['type']
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+defineProps<{
+ isWriting: boolean
+}>()
+
+const emits = defineEmits<{
+ (e: 'submit', params: Partial<WriteVO>)
+ (e: 'example', param: 'write' | 'reply')
+ (e: 'reset')
+}>()
+
+/** 鐐瑰嚮绀轰緥鐨勬椂鍊欙紝灏嗗畾涔夊ソ鐨勬枃绔犱綔涓虹ず渚嬪睍绀哄嚭鏉� **/
+const example = (type: 'write' | 'reply') => {
+ formData.value = {
+ ...initData,
+ ...omit(WriteExample[type], ['data'])
+ }
+ emits('example', type)
+}
+
+/** 閲嶇疆锛屽皢琛ㄥ崟鍊间綔涓哄垵閫夊�� **/
+const reset = () => {
+ formData.value = { ...initData }
+ emits('reset')
+}
+
+const selectedTab = ref<TabType>(AiWriteTypeEnum.WRITING)
+const tabs: {
+ text: string
+ value: TabType
+}[] = [
+ { text: '鎾板啓', value: AiWriteTypeEnum.WRITING },
+ { text: '鍥炲', value: AiWriteTypeEnum.REPLY }
+]
+const [DefineTab, ReuseTab] = createReusableTemplate<{
+ active?: boolean
+ text: string
+ itemClick: () => void
+}>()
+
+/**
+ * 鍙互鍦� template 閲岃竟瀹氫箟鍙鐢ㄧ殑缁勪欢锛孌efineLabel锛孯euseLabel 鏄噰鐢ㄧ殑瑙f瀯璧嬪�硷紝閮芥槸 Vue 缁勪欢
+ *
+ * 鐩存帴閫氳繃缁勪欢鐨勫舰寮忎娇鐢紝<DefineLabel v-slot="{ label, hint, hintClick }"> 涓棿鏄渶瑕佸鐢ㄧ殑缁勪欢浠g爜 <DefineLabel />锛岄�氳繃 <ReuseLabel /> 鏉ヤ娇鐢ㄥ畾涔夌殑缁勪欢
+ * DefineLabel 閲岃竟鐨� v-slot="{ label, hint, hintClick }"鐩稿綋浜庢槸瑙f瀯浜嗙粍浠剁殑 prop锛岄渶瑕佹敞鎰忕殑鏄� boolean 绫诲瀷锛岄渶瑕佹樉寮忕殑璧嬪�兼瘮濡� <ReuseLabel :flag="true" />
+ * 浜嬩欢涔熷緱浠� prop 褰㈠紡浼犲叆锛屼笉鑳芥槸 @event鐨勫舰寮忥紝姣斿涓嬮潰鐨� hintClick 闇�瑕�<ReuseLabel :hintClick="() => { doSomething }"/>
+ *
+ * @see https://vueuse.org/createReusableTemplate
+ */
+const [DefineLabel, ReuseLabel] = createReusableTemplate<{
+ label: string
+ class?: string
+ hint?: string
+ hintClick?: () => void
+}>()
+
+const initData: WriteVO = {
+ type: 1,
+ prompt: '',
+ originalContent: '',
+ tone: 1,
+ language: 1,
+ length: 1,
+ format: 1
+}
+const formData = ref<WriteVO>({ ...initData })
+
+/** 鐢ㄦ潵璁板綍鍒囨崲涔嬪墠鎵�濉啓鐨勬暟鎹紝鍒囨崲鐨勬椂鍊欑粰璧嬪�煎洖鏉� **/
+const recordFormData = {} as Record<AiWriteTypeEnum, WriteVO>
+
+/** 鍒囨崲tab **/
+const switchTab = (value: TabType) => {
+ if (value !== selectedTab.value) {
+ // 淇濆瓨涔嬪墠鐨勪箙鏁版嵁
+ recordFormData[selectedTab.value] = formData.value
+ selectedTab.value = value
+ // 灏嗕箣鍓嶇殑鏃ф暟鎹祴鍊煎洖鏉�
+ formData.value = { ...initData, ...recordFormData[value] }
+ }
+}
+
+/** 鎻愪氦鍐欎綔 */
+const submit = () => {
+ if (selectedTab.value === 2 && !formData.value.originalContent) {
+ message.warning('璇疯緭鍏ュ師鏂�')
+ return
+ }
+ if (!formData.value.prompt) {
+ message.warning(`璇疯緭鍏�${selectedTab.value === 1 ? '鍐欎綔' : '鍥炲'}鍐呭`)
+ return
+ }
+ emits('submit', {
+ /** 鎾板啓鐨勬椂鍊欐病鏈� originalContent 瀛楁**/
+ ...(selectedTab.value === 1 ? omit(formData.value, ['originalContent']) : formData.value),
+ /** 浣跨敤閫変腑 tab 鍊艰鐩栧綋鍓嶇殑 type 绫诲瀷 **/
+ type: selectedTab.value
+ })
+}
+</script>
diff --git a/src/views/ai/write/index/components/Right.vue b/src/views/ai/write/index/components/Right.vue
new file mode 100644
index 0000000..1eb66a0
--- /dev/null
+++ b/src/views/ai/write/index/components/Right.vue
@@ -0,0 +1,120 @@
+<template>
+ <el-card class="my-card h-full">
+ <template #header>
+ <h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
+ <span>棰勮</span>
+ <!-- 灞曠ず鍦ㄥ彸涓婅 -->
+ <el-button color="#846af7" v-show="showCopy" @click="copyContent" size="small">
+ <template #icon>
+ <Icon icon="ph:copy-bold" />
+ </template>
+ 澶嶅埗
+ </el-button>
+ </h3>
+ </template>
+
+ <div ref="contentRef" class="hide-scroll-bar h-full box-border overflow-y-auto">
+ <div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7">
+ <!-- 缁堟鐢熸垚鍐呭鐨勬寜閽� -->
+ <el-button
+ v-show="isWriting"
+ class="absolute bottom-2 sm:bottom-5 left-1/2 -translate-x-1/2 z-36"
+ @click="emits('stopStream')"
+ size="small"
+ >
+ <template #icon>
+ <Icon icon="material-symbols:stop" />
+ </template>
+ 缁堟鐢熸垚
+ </el-button>
+ <el-input
+ id="inputId"
+ type="textarea"
+ v-model="compContent"
+ autosize
+ :input-style="{ boxShadow: 'none' }"
+ resize="none"
+ placeholder="鐢熸垚鐨勫唴瀹光�︹��"
+ />
+ </div>
+ </div>
+ </el-card>
+</template>
+
+<script setup lang="ts">
+import { useClipboard } from '@vueuse/core'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { copied, copy } = useClipboard({ legacy: true }) // 绮樿创鏉�
+
+const props = defineProps({
+ content: {
+ // 鐢熸垚鐨勭粨鏋�
+ type: String,
+ default: ''
+ },
+ isWriting: {
+ // 鏄惁姝e湪鐢熸垚鏂囩珷
+ type: Boolean,
+ default: false
+ }
+})
+
+const emits = defineEmits(['update:content', 'stopStream'])
+
+/** 閫氳繃璁$畻灞炴�э紝鍙屽悜缁戝畾锛屾洿鏀圭敓鎴愮殑鍐呭锛岃�冭檻鍒扮敤鎴锋兂瑕佹洿鏀圭敓鎴愭枃绔犵殑鎯呭喌 */
+const compContent = computed({
+ get() {
+ return props.content
+ },
+ set(val) {
+ emits('update:content', val)
+ }
+})
+
+/** 婊氬姩 */
+const contentRef = ref<HTMLDivElement>()
+defineExpose({
+ scrollToBottom() {
+ contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight)
+ }
+})
+
+/** 鐐瑰嚮澶嶅埗鐨勬椂鍊欏鍒跺唴瀹� */
+const showCopy = computed(() => props.content && !props.isWriting) // 鏄惁灞曠ず澶嶅埗鎸夐挳锛屽湪鐢熸垚鍐呭瀹屾垚鐨勬椂鍊欏睍绀�
+const copyContent = () => {
+ copy(props.content)
+}
+
+/** 澶嶅埗鎴愬姛鐨勬椂鍊� copied.value 涓� true */
+watch(copied, (val) => {
+ if (val) {
+ message.success('澶嶅埗鎴愬姛')
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+.hide-scroll-bar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ }
+}
+
+.my-card {
+ display: flex;
+ flex-direction: column;
+
+ :deep(.el-card__body) {
+ box-sizing: border-box;
+ flex-grow: 1;
+ overflow-y: auto;
+ padding: 0;
+ @extend .hide-scroll-bar;
+ }
+}
+</style>
diff --git a/src/views/ai/write/index/components/Tag.vue b/src/views/ai/write/index/components/Tag.vue
new file mode 100644
index 0000000..8c0ad79
--- /dev/null
+++ b/src/views/ai/write/index/components/Tag.vue
@@ -0,0 +1,31 @@
+<!-- 鏍囩閫夐」 -->
+<template>
+ <div class="flex flex-wrap gap-[8px]">
+ <span
+ v-for="tag in props.tags"
+ :key="tag.value"
+ class="tag mb-2 border-[2px] border-solid border-[#DDDFE3] px-2 leading-6 text-[12px] bg-[#DDDFE3] rounded-[4px] cursor-pointer"
+ :class="modelValue === tag.value && '!border-[#846af7] text-[#846af7]'"
+ @click="emits('update:modelValue', tag.value)"
+ >
+ {{ tag.label }}
+ </span>
+ </div>
+</template>
+
+<script setup lang="ts">
+const props = withDefaults(
+ defineProps<{
+ tags: { label: string; value: string }[]
+ modelValue: string
+ [k: string]: any
+ }>(),
+ {
+ tags: () => []
+ }
+)
+
+const emits = defineEmits<{
+ (e: 'update:modelValue', value: string): void
+}>()
+</script>
diff --git a/src/views/ai/write/index/index.vue b/src/views/ai/write/index/index.vue
new file mode 100644
index 0000000..0079eed
--- /dev/null
+++ b/src/views/ai/write/index/index.vue
@@ -0,0 +1,78 @@
+<template>
+ <div class="absolute top-0 left-0 right-0 bottom-0 flex">
+ <Left
+ :is-writing="isWriting"
+ class="h-full"
+ @submit="submit"
+ @reset="reset"
+ @example="handleExampleClick"
+ />
+ <Right
+ :is-writing="isWriting"
+ @stop-stream="stopStream"
+ ref="rightRef"
+ class="flex-grow"
+ v-model:content="writeResult"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import Left from './components/Left.vue'
+import Right from './components/Right.vue'
+import { WriteApi, WriteVO } from '@/api/ai/write'
+import { WriteExample } from '@/views/ai/utils/constants'
+
+const message = useMessage()
+
+const writeResult = ref('') // 鍐欎綔缁撴灉
+const isWriting = ref(false) // 鏄惁姝e湪鍐欎綔涓�
+const abortController = ref<AbortController>() // // 鍐欎綔杩涜涓� abort 鎺у埗鍣�(鎺у埗 stream 鍐欎綔)
+
+/** 鍋滄 stream 鐢熸垚 */
+const stopStream = () => {
+ abortController.value?.abort()
+ isWriting.value = false
+}
+
+/** 鎵ц鍐欎綔 */
+const rightRef = ref<InstanceType<typeof Right>>()
+const submit = (data: WriteVO) => {
+ abortController.value = new AbortController()
+ writeResult.value = ''
+ isWriting.value = true
+ WriteApi.writeStream({
+ data,
+ onMessage: async (res) => {
+ const { code, data, msg } = JSON.parse(res.data)
+ if (code !== 0) {
+ message.alert(`鍐欎綔寮傚父! ${msg}`)
+ stopStream()
+ return
+ }
+ writeResult.value = writeResult.value + data
+ // 婊氬姩鍒板簳閮�
+ await nextTick()
+ rightRef.value?.scrollToBottom()
+ },
+ ctrl: abortController.value,
+ onClose: stopStream,
+ onError: (error) => {
+ console.error('鍐欎綔寮傚父', error)
+ stopStream()
+ // 闇�瑕佹姏鍑哄紓甯革紝绂佹閲嶈瘯
+ throw error
+ }
+ })
+}
+
+/** 鐐瑰嚮绀轰緥瑙﹀彂 */
+const handleExampleClick = (type: keyof typeof WriteExample) => {
+ writeResult.value = WriteExample[type].data
+}
+
+/** 鐐瑰嚮閲嶇疆鐨勬椂鍊欐竻绌哄啓浣滅殑缁撴灉**/
+const reset = () => {
+ writeResult.value = ''
+}
+</script>
diff --git a/src/views/ai/write/manager/index.vue b/src/views/ai/write/manager/index.vue
new file mode 100644
index 0000000..f220108
--- /dev/null
+++ b/src/views/ai/write/manager/index.vue
@@ -0,0 +1,227 @@
+<template>
+ <doc-alert title="AI 鍐欎綔鍔╂墜" url="https://doc.iocoder.cn/ai/write/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍐欎綔绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ placeholder="璇烽�夋嫨鍐欎綔绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.AI_WRITE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="骞冲彴" prop="platform">
+ <el-select
+ v-model="queryParams.platform"
+ placeholder="璇烽�夋嫨骞冲彴"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="120" fixed="left" />
+ <el-table-column label="鐢ㄦ埛" align="center" prop="userId" width="180">
+ <template #default="scope">
+ <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍐欎綔绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_WRITE_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="骞冲彴" align="center" prop="platform" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯″瀷" align="center" prop="model" width="180" />
+ <el-table-column
+ label="鐢熸垚鍐呭鎻愮ず"
+ align="center"
+ prop="prompt"
+ width="180"
+ show-overflow-tooltip
+ />
+ <el-table-column label="鐢熸垚鐨勫唴瀹�" align="center" prop="generatedContent" width="180" />
+ <el-table-column label="鍘熸枃" align="center" prop="originalContent" width="180" />
+ <el-table-column label="闀垮害" align="center" prop="length">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_WRITE_LENGTH" :value="scope.row.length" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏍煎紡" align="center" prop="format">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_WRITE_FORMAT" :value="scope.row.format" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璇皵" align="center" prop="tone">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_WRITE_TONE" :value="scope.row.tone" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璇█" align="center" prop="language">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.AI_WRITE_LANGUAGE" :value="scope.row.language" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="閿欒淇℃伅" align="center" prop="errorMessage" />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['ai:write:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { useRouter } from 'vue-router'
+import { WriteApi, AiWritePageReqVO, AiWriteRespVo } from '@/api/ai/write'
+import * as UserApi from '@/api/system/user'
+
+/** AI 鍐欎綔鍒楄〃 */
+defineOptions({ name: 'AiWriteManager' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const router = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<AiWriteRespVo[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive<AiWritePageReqVO>({
+ pageNo: 1,
+ pageSize: 10,
+ userId: undefined,
+ type: undefined,
+ platform: undefined,
+ createTime: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await WriteApi.getWritePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await WriteApi.deleteWrite(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/bpm/category/CategoryForm.vue b/src/views/bpm/category/CategoryForm.vue
new file mode 100644
index 0000000..defd760
--- /dev/null
+++ b/src/views/bpm/category/CategoryForm.vue
@@ -0,0 +1,130 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍒嗙被鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ垎绫诲悕" />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鏍囧織" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ュ垎绫绘爣蹇�" />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鎻忚堪" prop="description">
+ <el-input v-model="formData.description" type="textarea" placeholder="璇疯緭鍏ュ垎绫绘弿杩�" />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍒嗙被鎺掑簭" prop="sort">
+ <el-input-number
+ v-model="formData.sort"
+ placeholder="璇疯緭鍏ュ垎绫绘帓搴�"
+ class="!w-1/1"
+ :precision="0"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 娴佺▼鍒嗙被 琛ㄥ崟 */
+defineOptions({ name: 'CategoryForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ code: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sort: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍒嗙被鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ code: [{ required: true, message: '鍒嗙被鏍囧織涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鍒嗙被鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鍒嗙被鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await CategoryApi.getCategory(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as CategoryVO
+ if (formType.value === 'create') {
+ await CategoryApi.createCategory(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await CategoryApi.updateCategory(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ code: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sort: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/category/index.vue b/src/views/bpm/category/index.vue
new file mode 100644
index 0000000..085b371
--- /dev/null
+++ b/src/views/bpm/category/index.vue
@@ -0,0 +1,199 @@
+<template>
+ <doc-alert title="宸ヤ綔娴佹墜鍐�" url="https://doc.iocoder.cn/bpm/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙被鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ垎绫诲悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鏍囧織" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ュ垎绫绘爣蹇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鍒嗙被鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['bpm:category:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍒嗙被缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍒嗙被鍚�" align="center" prop="name" />
+ <el-table-column label="鍒嗙被鏍囧織" align="center" prop="code" />
+ <el-table-column label="鍒嗙被鎻忚堪" align="center" prop="description" />
+ <el-table-column label="鍒嗙被鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒嗙被鎺掑簭" align="center" prop="sort" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['bpm:category:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['bpm:category:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import CategoryForm from './CategoryForm.vue'
+
+/** BPM 娴佺▼鍒嗙被 鍒楄〃 */
+defineOptions({ name: 'BpmCategory' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<CategoryVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ code: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CategoryApi.getCategoryPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await CategoryApi.deleteCategory(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/form/editor/index.vue b/src/views/bpm/form/editor/index.vue
new file mode 100644
index 0000000..4165fcc
--- /dev/null
+++ b/src/views/bpm/form/editor/index.vue
@@ -0,0 +1,174 @@
+<template>
+ <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
+ <!-- 琛ㄥ崟璁捐鍣� -->
+ <div
+ class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
+ >
+ <fc-designer class="my-designer" ref="designer" :config="designerConfig">
+ <template #handle>
+ <el-button size="small" type="success" plain @click="handleSave">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 淇濆瓨
+ </el-button>
+ </template>
+ </fc-designer>
+ </div>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟淇濆瓨鐨勫脊绐� -->
+ <Dialog v-model="dialogVisible" title="淇濆瓨琛ㄥ崟" width="600">
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
+ <el-form-item label="琛ㄥ崟鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヨ〃鍗曞悕" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+import FcDesigner from '@form-create/designer'
+import { encodeConf, encodeFields, setConfAndFields } from '@/utils/formCreate'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useFormCreateDesigner } from '@/components/FormCreate'
+import { useRoute } from 'vue-router'
+
+defineOptions({ name: 'BpmFormEditor' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅
+const route = useRoute() // 璺敱
+const { push, currentRoute } = useRouter() // 璺敱
+const { query } = useRoute() // 璺敱淇℃伅
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+
+// 琛ㄥ崟璁捐鍣ㄩ厤缃�
+const designerConfig = ref({
+ switchType: [], // 鏄惁鍙互鍒囨崲缁勪欢绫诲瀷,鎴栬�呭彲浠ョ浉浜掑垏鎹㈢殑瀛楁
+ autoActive: true, // 鏄惁鑷姩閫変腑鎷栧叆鐨勭粍浠�
+ useTemplate: false, // 鏄惁鐢熸垚vue2璇硶鐨勬ā鏉跨粍浠�
+ formOptions: {
+ form: {
+ labelWidth: '100px' // 璁剧疆榛樿鐨� label 瀹藉害涓� 100px
+ }
+ }, // 瀹氫箟琛ㄥ崟閰嶇疆榛樿鍊�
+ fieldReadonly: false, // 閰嶇疆field鏄惁鍙互缂栬緫
+ hiddenDragMenu: false, // 闅愯棌鎷栨嫿鎿嶄綔鎸夐挳
+ hiddenDragBtn: false, // 闅愯棌鎷栨嫿鎸夐挳
+ hiddenMenu: [], // 闅愯棌閮ㄥ垎鑿滃崟
+ hiddenItem: [], // 闅愯棌閮ㄥ垎缁勪欢
+ hiddenItemConfig: {}, // 闅愯棌缁勪欢鐨勯儴鍒嗛厤缃」
+ disabledItemConfig: {}, // 绂佺敤缁勪欢鐨勯儴鍒嗛厤缃」
+ showSaveBtn: false, // 鏄惁鏄剧ず淇濆瓨鎸夐挳
+ showConfig: true, // 鏄惁鏄剧ず鍙充晶鐨勯厤缃晫闈�
+ showBaseForm: true, // 鏄惁鏄剧ず缁勪欢鐨勫熀纭�閰嶇疆琛ㄥ崟
+ showControl: true, // 鏄惁鏄剧ず缁勪欢鑱斿姩
+ showPropsForm: true, // 鏄惁鏄剧ず缁勪欢鐨勫睘鎬ч厤缃〃鍗�
+ showEventForm: true, // 鏄惁鏄剧ず缁勪欢鐨勪簨浠堕厤缃〃鍗�
+ showValidateForm: true, // 鏄惁鏄剧ず缁勪欢鐨勯獙璇侀厤缃〃鍗�
+ showFormConfig: true, // 鏄惁鏄剧ず琛ㄥ崟閰嶇疆
+ showInputData: true, // 鏄惁鏄剧ず褰曞叆鎸夐挳
+ showDevice: true, // 鏄惁鏄剧ず澶氱閫傞厤閫夐」
+ appendConfigData: [] // 瀹氫箟娓叉煋瑙勫垯鎵�闇�鐨刦ormData
+})
+const designer = ref() // 琛ㄥ崟璁捐鍣�
+useFormCreateDesigner(designer) // 琛ㄥ崟璁捐鍣ㄥ寮�
+const dialogVisible = ref(false) // 寮圭獥鏄惁灞曠ず
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛氭彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ name: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '琛ㄥ崟鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 澶勭悊淇濆瓨鎸夐挳 */
+const handleSave = () => {
+ dialogVisible.value = true
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as FormApi.FormVO
+ data.conf = encodeConf(designer) // 琛ㄥ崟閰嶇疆
+ data.fields = encodeFields(designer) // 琛ㄥ崟瀛楁
+ if (!data.id) {
+ await FormApi.createForm(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await FormApi.updateForm(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ close()
+ } finally {
+ formLoading.value = false
+ }
+}
+/** 鍏抽棴鎸夐挳 */
+const close = () => {
+ delView(unref(currentRoute))
+ push('/bpm/manager/form')
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 鍦烘櫙涓�锛氭柊澧炶〃鍗�
+ const id = query.id as unknown as number
+ if (!id) {
+ return
+ }
+ // 鍦烘櫙浜岋細淇敼琛ㄥ崟
+ const data = await FormApi.getForm(id)
+ formData.value = data
+ setConfAndFields(designer, data.conf, data.fields)
+
+ if (route.query.type !== 'copy') {
+ return
+ }
+ // 鍦烘櫙涓夛細 澶嶅埗琛ㄥ崟
+ const { id: foo, ...copied } = data
+ formData.value = copied
+ formData.value.name += '_copy'
+})
+</script>
+
+<style>
+.my-designer {
+ ._fc-l,
+ ._fc-m,
+ ._fc-r {
+ border-top: none;
+ }
+}
+</style>
diff --git a/src/views/bpm/form/index.vue b/src/views/bpm/form/index.vue
new file mode 100644
index 0000000..57b44a3
--- /dev/null
+++ b/src/views/bpm/form/index.vue
@@ -0,0 +1,205 @@
+<template>
+ <doc-alert title="瀹℃壒鎺ュ叆锛堟祦绋嬭〃鍗曪級" url="https://doc.iocoder.cn/bpm/use-bpm-form/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="琛ㄥ崟鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ〃鍗曞悕"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['bpm:form:create']" plain type="primary" @click="openForm">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="缂栧彿" prop="id" />
+ <el-table-column align="center" label="琛ㄥ崟鍚�" prop="name" />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['bpm:form:update']"
+ link
+ type="primary"
+ @click="openForm('copy', scope.row.id)"
+ >
+ 澶嶅埗
+ </el-button>
+ <el-button
+ v-hasPermi="['bpm:form:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button v-hasPermi="['bpm:form:query']" link @click="openDetail(scope.row.id)">
+ 璇︽儏
+ </el-button>
+ <el-button
+ v-hasPermi="['bpm:form:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟璇︽儏鐨勫脊绐� -->
+ <Dialog v-model="detailVisible" title="琛ㄥ崟璇︽儏" width="800">
+ <form-create :option="detailData.option" :rule="detailData.rule" />
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+
+defineOptions({ name: 'BpmForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { currentRoute, push } = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FormApi.getFormPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const openForm = (type: string, id?: number) => {
+ const toRouter: { name: string; query: { type: string; id?: number } } = {
+ name: 'BpmFormEditor',
+ query: {
+ type
+ }
+ }
+ console.log(typeof id)
+ // 琛ㄥ崟鏂板缓鐨勬椂鍊檌d浼犵殑鏄痚vent闇�瑕佹帓闄�
+ if (typeof id === 'number' || typeof id === 'string') {
+ toRouter.query.id = id
+ }
+ push(toRouter)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await FormApi.deleteForm(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailVisible = ref(false)
+const detailData = ref({
+ rule: [],
+ option: {}
+})
+const openDetail = async (rowId: number) => {
+ // 璁剧疆琛ㄥ崟
+ const data = await FormApi.getForm(rowId)
+ setConfAndFields2(detailData, data.conf, data.fields)
+ // 寮圭獥鎵撳紑
+ detailVisible.value = true
+}
+/**琛ㄥ崟淇濆瓨杩斿洖鍚庨噸鏂板姞杞藉垪琛� */
+watch(
+ () => currentRoute.value,
+ () => {
+ getList()
+ },
+ {
+ immediate: true
+ }
+)
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/group/UserGroupForm.vue b/src/views/bpm/group/UserGroupForm.vue
new file mode 100644
index 0000000..3c825eb
--- /dev/null
+++ b/src/views/bpm/group/UserGroupForm.vue
@@ -0,0 +1,132 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="缁勫悕" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ粍鍚�" />
+ </el-form-item>
+ <el-form-item label="鎻忚堪">
+ <el-input v-model="formData.description" placeholder="璇疯緭鍏ユ弿杩�" type="textarea" />
+ </el-form-item>
+ <el-form-item label="鎴愬憳" prop="userIds">
+ <el-select v-model="formData.userIds" multiple placeholder="璇烽�夋嫨鎴愬憳">
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'UserGroupForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ userIds: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '缁勫悕涓嶈兘涓虹┖', trigger: 'blur' }],
+ description: [{ required: true, message: '鎻忚堪涓嶈兘涓虹┖', trigger: 'blur' }],
+ userIds: [{ required: true, message: '鎴愬憳涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const userList = ref<any[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await UserGroupApi.getUserGroup(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as UserGroupApi.UserGroupVO
+ if (formType.value === 'create') {
+ await UserGroupApi.createUserGroup(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await UserGroupApi.updateUserGroup(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ userIds: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/group/index.vue b/src/views/bpm/group/index.vue
new file mode 100644
index 0000000..62785a9
--- /dev/null
+++ b/src/views/bpm/group/index.vue
@@ -0,0 +1,191 @@
+<template>
+ <doc-alert title="宸ヤ綔娴佹墜鍐�" url="https://doc.iocoder.cn/bpm/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="缁勫悕" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ョ粍鍚�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['bpm:user-group:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="缁勫悕" align="center" prop="name" />
+ <el-table-column label="鎻忚堪" align="center" prop="description" />
+ <el-table-column label="鎴愬憳" align="center">
+ <template #default="scope">
+ <span v-for="userId in scope.row.userIds" :key="userId" class="pr-5px">
+ {{ userList.find((user) => user.id === userId)?.nickname }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['bpm:user-group:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['bpm:user-group:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <UserGroupForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserGroupApi from '@/api/bpm/userGroup'
+import * as UserApi from '@/api/system/user'
+import UserGroupForm from './UserGroupForm.vue'
+import { UserVO } from '@/api/system/user'
+
+defineOptions({ name: 'BpmUserGroup' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await UserGroupApi.getUserGroupPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await UserGroupApi.deleteUserGroup(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/bpm/model/CategoryDraggableModel.vue b/src/views/bpm/model/CategoryDraggableModel.vue
new file mode 100644
index 0000000..003e46f
--- /dev/null
+++ b/src/views/bpm/model/CategoryDraggableModel.vue
@@ -0,0 +1,665 @@
+<template>
+ <div class="flex items-center h-50px" v-memo="[categoryInfo.name, isCategorySorting]">
+ <!-- 澶撮儴锛氬垎绫诲悕 -->
+ <div class="flex items-center">
+ <el-tooltip content="鎷栧姩鎺掑簭" v-if="isCategorySorting">
+ <Icon
+ :size="22"
+ icon="ic:round-drag-indicator"
+ class="ml-10px category-drag-icon cursor-move text-#8a909c"
+ />
+ </el-tooltip>
+ <h3 class="ml-20px mr-8px text-18px">{{ categoryInfo.name }}</h3>
+ <div class="color-gray-600 text-16px"> ({{ categoryInfo.modelList?.length || 0 }}) </div>
+ </div>
+ <!-- 澶撮儴锛氭搷浣� -->
+ <div class="flex-1 flex" v-show="!isCategorySorting">
+ <div
+ v-if="categoryInfo.modelList.length > 0"
+ class="ml-20px flex items-center"
+ :class="[
+ 'transition-transform duration-300 cursor-pointer',
+ isExpand ? 'rotate-180' : 'rotate-0'
+ ]"
+ @click="isExpand = !isExpand"
+ >
+ <Icon icon="ep:arrow-down-bold" color="#999" />
+ </div>
+ <div class="ml-auto flex items-center" :class="isModelSorting ? 'mr-15px' : 'mr-45px'">
+ <template v-if="!isModelSorting">
+ <el-button
+ v-if="categoryInfo.modelList.length > 0"
+ link
+ type="info"
+ class="mr-20px"
+ @click.stop="handleModelSort"
+ >
+ <Icon icon="fa:sort-amount-desc" class="mr-5px" />
+ 鎺掑簭
+ </el-button>
+ <el-button v-else link type="info" class="mr-20px" @click.stop="openModelForm('create')">
+ <Icon icon="fa:plus" class="mr-5px" />
+ 鏂板缓
+ </el-button>
+ <el-dropdown
+ @command="(command) => handleCategoryCommand(command, categoryInfo)"
+ placement="bottom"
+ >
+ <el-button link type="info">
+ <Icon icon="ep:setting" class="mr-5px" />
+ 鍒嗙被
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleRename"> 閲嶅懡鍚� </el-dropdown-item>
+ <el-dropdown-item command="handleDeleteCategory"> 鍒犻櫎璇ョ被 </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ <template v-else>
+ <el-button @click.stop="handleModelSortCancel"> 鍙� 娑� </el-button>
+ <el-button type="primary" @click.stop="handleModelSortSubmit"> 淇濆瓨鎺掑簭 </el-button>
+ </template>
+ </div>
+ </div>
+ </div>
+
+ <!-- 妯″瀷鍒楄〃 -->
+ <el-collapse-transition>
+ <div v-show="isExpand">
+ <el-table
+ v-if="modelList && modelList.length > 0"
+ :class="categoryInfo.name"
+ ref="tableRef"
+ :data="modelList"
+ row-key="id"
+ :header-cell-style="tableHeaderStyle"
+ :cell-style="tableCellStyle"
+ :row-style="{ height: '68px' }"
+ >
+ <el-table-column label="娴佺▼鍚�" prop="name" min-width="150">
+ <template #default="{ row }">
+ <div class="flex items-center">
+ <el-tooltip content="鎷栧姩鎺掑簭" v-if="isModelSorting">
+ <Icon
+ icon="ic:round-drag-indicator"
+ class="drag-icon cursor-move text-#8a909c mr-10px"
+ />
+ </el-tooltip>
+ <el-image v-if="row.icon" :src="row.icon" class="h-38px w-38px mr-10px rounded" />
+ <div v-else class="flow-icon">
+ <span style="font-size: 12px; color: #fff">{{ subString(row.name, 0, 2) }}</span>
+ </div>
+ {{ row.name }}
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙鑼冨洿" prop="startUserIds" min-width="150">
+ <template #default="{ row }">
+ <el-text v-if="!row.startUsers?.length && !row.startDepts?.length"> 鍏ㄩ儴鍙 </el-text>
+ <el-text v-else-if="row.startUsers.length === 1">
+ {{ row.startUsers[0].nickname }}
+ </el-text>
+ <el-text v-else-if="row.startDepts?.length === 1">
+ {{ row.startDepts[0].name }}
+ </el-text>
+ <el-text v-else-if="row.startDepts?.length > 1">
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="row.startDepts.map((dept: any) => dept.name).join('銆�')"
+ >
+ {{ row.startDepts[0].name }}绛� {{ row.startDepts.length }} 涓儴闂ㄥ彲瑙�
+ </el-tooltip>
+ </el-text>
+ <el-text v-else>
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="row.startUsers.map((user: any) => user.nickname).join('銆�')"
+ >
+ {{ row.startUsers[0].nickname }}绛� {{ row.startUsers.length }} 浜哄彲瑙�
+ </el-tooltip>
+ </el-text>
+ </template>
+ </el-table-column>
+ <el-table-column label="娴佺▼绫诲瀷" prop="type" min-width="120">
+ <template #default="{ row }">
+ <dict-tag :value="row.type" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ㄥ崟淇℃伅" prop="formType" min-width="150">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.formType === BpmModelFormType.NORMAL"
+ type="primary"
+ link
+ @click="handleFormDetail(scope.row)"
+ >
+ <span>{{ scope.row.formName }}</span>
+ </el-button>
+ <el-button
+ v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+ type="primary"
+ link
+ @click="handleFormDetail(scope.row)"
+ >
+ <span>{{ scope.row.formCustomCreatePath }}</span>
+ </el-button>
+ <label v-else>鏆傛棤琛ㄥ崟</label>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈�鍚庡彂甯�" prop="deploymentTime" min-width="250">
+ <template #default="scope">
+ <div class="flex items-center">
+ <span v-if="scope.row.processDefinition" class="w-150px">
+ {{ formatDate(scope.row.processDefinition.deploymentTime) }}
+ </span>
+ <el-tag v-if="scope.row.processDefinition">
+ v{{ scope.row.processDefinition.version }}
+ </el-tag>
+ <el-tag v-else type="warning">鏈儴缃�</el-tag>
+ <el-tag
+ v-if="scope.row.processDefinition?.suspensionState === 2"
+ type="warning"
+ class="ml-10px"
+ >
+ 宸插仠鐢�
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openModelForm('update', scope.row.id)"
+ :disabled="!isManagerUser(scope.row) && !hasPermiUpdate"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openModelForm('copy', scope.row.id)"
+ :disabled="!isManagerUser(scope.row) && !hasPermiUpdate"
+ >
+ 澶嶅埗
+ </el-button>
+ <el-button
+ link
+ class="!ml-5px"
+ type="primary"
+ @click="handleDeploy(scope.row)"
+ :disabled="!isManagerUser(scope.row) && !hasPermiDeploy"
+ >
+ 鍙戝竷
+ </el-button>
+ <el-dropdown
+ class="!align-middle ml-5px"
+ @command="(command) => handleModelCommand(command, scope.row)"
+ v-if="hasPermiMore"
+ >
+ <el-button type="primary" link>鏇村</el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleDefinitionList" v-if="hasPermiPdQuery">
+ 鍘嗗彶
+ </el-dropdown-item>
+ <el-dropdown-item
+ command="handleReport"
+ v-if="
+ checkPermi(['bpm:process-instance:manager-query']) &&
+ scope.row.processDefinition
+ "
+ :disabled="!isManagerUser(scope.row)"
+ >
+ 鎶ヨ〃
+ </el-dropdown-item>
+ <el-dropdown-item
+ command="handleChangeState"
+ v-if="hasPermiUpdate && scope.row.processDefinition"
+ :disabled="!isManagerUser(scope.row)"
+ >
+ {{ scope.row.processDefinition.suspensionState === 1 ? '鍋滅敤' : '鍚敤' }}
+ </el-dropdown-item>
+ <el-dropdown-item
+ type="danger"
+ command="handleClean"
+ v-if="checkPermi(['bpm:model:clean'])"
+ :disabled="!isManagerUser(scope.row)"
+ >
+ 娓呯悊
+ </el-dropdown-item>
+ <el-dropdown-item
+ type="danger"
+ command="handleDelete"
+ v-if="hasPermiDelete"
+ :disabled="!isManagerUser(scope.row)"
+ >
+ 鍒犻櫎
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-collapse-transition>
+
+ <!-- 寮圭獥锛氶噸鍛藉悕鍒嗙被 -->
+ <Dialog :fullscreen="false" class="rename-dialog" v-model="renameCategoryVisible" width="400">
+ <template #title>
+ <div class="pl-10px font-bold text-18px"> 閲嶅懡鍚嶅垎绫� </div>
+ </template>
+ <div class="px-30px">
+ <el-input v-model="renameCategoryForm.name" />
+ </div>
+ <template #footer>
+ <div class="pr-25px pb-25px">
+ <el-button @click="renameCategoryVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleRenameConfirm">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </Dialog>
+
+ <!-- 寮圭獥锛氳〃鍗曡鎯� -->
+ <Dialog title="琛ㄥ崟璇︽儏" :fullscreen="true" v-model="formDetailVisible">
+ <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import Sortable from 'sortablejs'
+import { formatDate } from '@/utils/formatTime'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType } from '@/utils/constants'
+import { checkPermi } from '@/utils/permission'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { useAppStore } from '@/store/modules/app'
+import { cloneDeep, isEqual } from 'lodash-es'
+import { useDebounceFn } from '@vueuse/core'
+import { subString } from '@/utils/index'
+
+defineOptions({ name: 'BpmModel' })
+
+// 浼樺寲 Props 绫诲瀷瀹氫箟
+interface UserInfo {
+ nickname: string
+ [key: string]: any
+}
+
+interface ProcessDefinition {
+ deploymentTime: string
+ version: number
+ suspensionState: number
+}
+
+interface ModelInfo {
+ id: number
+ name: string
+ icon?: string
+ startUsers?: UserInfo[]
+ processDefinition?: ProcessDefinition
+ formType?: number
+ formId?: number
+ formName?: string
+ formCustomCreatePath?: string
+ managerUserIds?: number[]
+ [key: string]: any
+}
+
+interface CategoryInfoProps {
+ id: number
+ name: string
+ modelList: ModelInfo[]
+}
+
+const props = defineProps<{
+ categoryInfo: CategoryInfoProps
+ isCategorySorting: boolean
+}>()
+
+const emit = defineEmits(['success'])
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter() // 璺敱
+const userStore = useUserStoreWithOut() // 鐢ㄦ埛淇℃伅缂撳瓨
+const isDark = computed(() => useAppStore().getIsDark) // 鏄惁榛戞殫妯″紡
+const router = useRouter() // 璺敱
+
+const isModelSorting = ref(false) // 鏄惁姝e浜庢帓搴忕姸鎬�
+const originalData = ref<ModelInfo[]>([]) // 鍘熷鏁版嵁
+const modelList = ref<ModelInfo[]>([]) // 妯″瀷鍒楄〃
+const isExpand = ref(false) // 鏄惁澶勪簬灞曞紑鐘舵��
+
+// 浣跨敤 computed 浼樺寲琛ㄦ牸鏍峰紡璁$畻
+const tableHeaderStyle = computed(() => ({
+ backgroundColor: isDark.value ? '' : '#edeff0',
+ paddingLeft: '10px'
+}))
+
+const tableCellStyle = computed(() => ({
+ paddingLeft: '10px'
+}))
+
+/** 鏉冮檺鏍¢獙锛氶�氳繃 computed 瑙e喅鍒楄〃鐨勫崱椤块棶棰� */
+const hasPermiUpdate = computed(() => {
+ return checkPermi(['bpm:model:update'])
+})
+const hasPermiDelete = computed(() => {
+ return checkPermi(['bpm:model:delete'])
+})
+const hasPermiDeploy = computed(() => {
+ return checkPermi(['bpm:model:deploy'])
+})
+const hasPermiMore = computed(() => {
+ return checkPermi(['bpm:process-definition:query', 'bpm:model:update', 'bpm:model:delete'])
+})
+const hasPermiPdQuery = computed(() => {
+ return checkPermi(['bpm:process-definition:query'])
+})
+
+/** '鏇村'鎿嶄綔鎸夐挳 */
+const handleModelCommand = (command: string, row: any) => {
+ switch (command) {
+ case 'handleDefinitionList':
+ handleDefinitionList(row)
+ break
+ case 'handleDelete':
+ handleDelete(row)
+ break
+ case 'handleChangeState':
+ handleChangeState(row)
+ break
+ case 'handleClean':
+ handleClean(row)
+ break
+ case 'handleReport':
+ router.push({
+ name: 'BpmProcessInstanceReport',
+ query: {
+ processDefinitionId: row.processDefinition.id,
+ processDefinitionKey: row.key
+ }
+ })
+ break
+ default:
+ break
+ }
+}
+
+/** '鍒嗙被'鎿嶄綔鎸夐挳 */
+const handleCategoryCommand = async (command: string, row: any) => {
+ switch (command) {
+ case 'handleRename':
+ renameCategoryForm.value = await CategoryApi.getCategory(row.id)
+ renameCategoryVisible.value = true
+ break
+ case 'handleDeleteCategory':
+ await handleDeleteCategory()
+ break
+ default:
+ break
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (row: any) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ModelApi.deleteModel(row.id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 娓呯悊鎸夐挳鎿嶄綔 */
+const handleClean = async (row: any) => {
+ try {
+ // 娓呯悊鐨勪簩娆$‘璁�
+ await message.confirm('鏄惁纭娓呯悊娴佺▼鍚嶅瓧涓�"' + row.name + '"鐨勬暟鎹」?')
+ // 鍙戣捣娓呯悊
+ await ModelApi.cleanModel(row.id)
+ message.success('娓呯悊鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 鏇存柊鐘舵�佹搷浣� */
+const handleChangeState = async (row: any) => {
+ const state = row.processDefinition.suspensionState
+ const newState = state === 1 ? 2 : 1
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const id = row.id
+ const statusState = state === 1 ? '鍋滅敤' : '鍚敤'
+ const content = '鏄惁纭' + statusState + '娴佺▼鍚嶅瓧涓�"' + row.name + '"鐨勬暟鎹」?'
+ await message.confirm(content)
+ // 鍙戣捣淇敼鐘舵��
+ await ModelApi.updateModelState(id, newState)
+ message.success(statusState + '鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 鍙戝竷娴佺▼ */
+const handleDeploy = async (row: any) => {
+ try {
+ await message.confirm('鏄惁纭鍙戝竷璇ユ祦绋嬶紵')
+ // 鍙戣捣閮ㄧ讲
+ await ModelApi.deployModel(row.id)
+ message.success(t('鍙戝竷鎴愬姛'))
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 璺宠浆鍒版寚瀹氭祦绋嬪畾涔夊垪琛� */
+const handleDefinitionList = (row: any) => {
+ push({
+ name: 'BpmProcessDefinition',
+ query: {
+ key: row.key
+ }
+ })
+}
+
+/** 娴佺▼琛ㄥ崟鐨勮鎯呮寜閽搷浣� */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+ rule: [],
+ option: {}
+})
+const handleFormDetail = async (row: any) => {
+ if (row.formType == BpmModelFormType.NORMAL) {
+ // 璁剧疆琛ㄥ崟
+ const data = await FormApi.getForm(row.formId)
+ setConfAndFields2(formDetailPreview, data.conf, data.fields)
+ // 寮圭獥鎵撳紑
+ formDetailVisible.value = true
+ } else {
+ await push({
+ path: row.formCustomCreatePath
+ })
+ }
+}
+
+/** 鍒ゆ柇鏄惁鍙互鎿嶄綔 */
+const isManagerUser = (row: any) => {
+ const userId = userStore.getUser.id
+ return row.managerUserIds && row.managerUserIds.includes(userId)
+}
+
+/** 澶勭悊妯″瀷鐨勬帓搴� **/
+const handleModelSort = () => {
+ if (isModelSorting.value) {
+ // 濡傛灉宸茬粡鍦ㄦ帓搴忕姸鎬侊紝鍒欏彇娑堟帓搴�
+ handleModelSortCancel()
+ } else {
+ // 淇濆瓨鍒濆鏁版嵁
+ originalData.value = cloneDeep(props.categoryInfo.modelList)
+ isModelSorting.value = true
+ initSort()
+ }
+}
+
+/** 澶勭悊妯″瀷鐨勬帓搴忔彁浜� */
+const handleModelSortSubmit = async () => {
+ // 淇濆瓨鎺掑簭
+ const ids = modelList.value.map((item: any) => item.id)
+ await ModelApi.updateModelSortBatch(ids)
+ // 鍒锋柊鍒楄〃
+ isModelSorting.value = false
+ message.success('鎺掑簭妯″瀷鎴愬姛')
+ emit('success')
+}
+
+/** 澶勭悊妯″瀷鐨勬帓搴忓彇娑� */
+const handleModelSortCancel = () => {
+ // 鎭㈠鍒濆鏁版嵁
+ modelList.value = cloneDeep(originalData.value)
+ isModelSorting.value = false
+}
+
+/** 鍒涘缓鎷栨嫿瀹炰緥 */
+const tableRef = ref()
+const initSort = useDebounceFn(() => {
+ const table = document.querySelector(`.${props.categoryInfo.name} .el-table__body-wrapper tbody`)
+ if (!table) return
+
+ Sortable.create(table, {
+ group: 'shared',
+ animation: 150,
+ draggable: '.el-table__row',
+ handle: '.drag-icon',
+ onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+ if (oldDraggableIndex !== newDraggableIndex) {
+ modelList.value.splice(
+ newDraggableIndex,
+ 0,
+ modelList.value.splice(oldDraggableIndex, 1)[0]
+ )
+ }
+ }
+ })
+}, 200)
+
+/** 鏇存柊 modelList 妯″瀷鍒楄〃 */
+const updateModeList = useDebounceFn(() => {
+ const newModelList = props.categoryInfo.modelList
+ if (!isEqual(modelList.value, newModelList)) {
+ modelList.value = cloneDeep(newModelList)
+ if (newModelList?.length > 0) {
+ isExpand.value = true
+ }
+ }
+}, 100)
+
+/** 閲嶅懡鍚嶅脊绐楃‘瀹� */
+const renameCategoryVisible = ref(false)
+const renameCategoryForm = ref({
+ name: ''
+})
+const handleRenameConfirm = async () => {
+ if (renameCategoryForm.value?.name.length === 0) {
+ return message.warning('璇疯緭鍏ュ悕绉�')
+ }
+ // 鍙戣捣淇敼
+ await CategoryApi.updateCategory(renameCategoryForm.value as CategoryVO)
+ message.success('閲嶅懡鍚嶆垚鍔�')
+ // 鍒锋柊鍒楄〃
+ renameCategoryVisible.value = false
+ emit('success')
+}
+
+/** 鍒犻櫎鍒嗙被 */
+const handleDeleteCategory = async () => {
+ try {
+ if (props.categoryInfo.modelList.length > 0) {
+ return message.warning('璇ュ垎绫讳笅浠嶆湁娴佺▼瀹氫箟,涓嶅厑璁稿垹闄�')
+ }
+ await message.confirm('纭鍒犻櫎鍒嗙被鍚�?')
+ // 鍙戣捣鍒犻櫎
+ await CategoryApi.deleteCategory(props.categoryInfo.id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ emit('success')
+ } catch {}
+}
+
+/** 娣诲姞/淇敼/澶嶅埗娴佺▼妯″瀷寮圭獥 */
+const openModelForm = async (type: string, id?: number) => {
+ if (type === 'create') {
+ await push({ name: 'BpmModelCreate' })
+ } else {
+ await push({
+ name: 'BpmModelUpdate',
+ params: { id, type }
+ })
+ }
+}
+
+watchEffect(() => {
+ if (props.categoryInfo?.modelList) {
+ updateModeList()
+ }
+
+ if (props.isCategorySorting) {
+ isExpand.value = false
+ }
+})
+</script>
+
+<style lang="scss">
+.rename-dialog.el-dialog {
+ padding: 0 !important;
+
+ .el-dialog__header {
+ border-bottom: none;
+ }
+
+ .el-dialog__footer {
+ border-top: none !important;
+ }
+}
+</style>
+<style lang="scss" scoped>
+.flow-icon {
+ display: flex;
+ width: 38px;
+ height: 38px;
+ margin-right: 10px;
+ background-color: var(--el-color-primary);
+ border-radius: 0.25rem;
+ align-items: center;
+ justify-content: center;
+}
+
+.category-draggable-model {
+ :deep(.el-table__cell) {
+ overflow: hidden;
+ border-bottom: none !important;
+ }
+
+ // 浼樺寲琛ㄦ牸娓叉煋鎬ц兘
+ :deep(.el-table__body) {
+ will-change: transform;
+ transform: translateZ(0);
+ }
+}
+</style>
diff --git a/src/views/bpm/model/definition/index.vue b/src/views/bpm/model/definition/index.vue
new file mode 100644
index 0000000..2b061f4
--- /dev/null
+++ b/src/views/bpm/model/definition/index.vue
@@ -0,0 +1,174 @@
+<template>
+ <doc-alert title="宸ヤ綔娴佹墜鍐�" url="https://doc.iocoder.cn/bpm/" />
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="瀹氫箟缂栧彿" align="center" prop="id" min-width="250" />
+ <el-table-column label="娴佺▼鍚嶇О" align="center" prop="name" min-width="150" />
+ <el-table-column label="娴佺▼鍥炬爣" align="center" min-width="50">
+ <template #default="{ row }">
+ <el-image v-if="row.icon" :src="row.icon" class="h-24px w-24pxrounded" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙鑼冨洿" prop="startUserIds" min-width="100">
+ <template #default="{ row }">
+ <el-text v-if="!row.startUsers?.length"> 鍏ㄩ儴鍙 </el-text>
+ <el-text v-else-if="row.startUsers.length === 1">
+ {{ row.startUsers[0].nickname }}
+ </el-text>
+ <el-text v-else>
+ <el-tooltip
+ class="box-item"
+ effect="dark"
+ placement="top"
+ :content="row.startUsers.map((user: any) => user.nickname).join('銆�')"
+ >
+ {{ row.startUsers[0].nickname }}绛� {{ row.startUsers.length }} 浜哄彲瑙�
+ </el-tooltip>
+ </el-text>
+ </template>
+ </el-table-column>
+ <el-table-column label="娴佺▼绫诲瀷" prop="modelType" min-width="120">
+ <template #default="{ row }">
+ <dict-tag :value="row.modelType" :type="DICT_TYPE.BPM_MODEL_TYPE" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ㄥ崟淇℃伅" prop="formType" min-width="150">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.formType === BpmModelFormType.NORMAL"
+ type="primary"
+ link
+ @click="handleFormDetail(scope.row)"
+ >
+ <span>{{ scope.row.formName }}</span>
+ </el-button>
+ <el-button
+ v-else-if="scope.row.formType === BpmModelFormType.CUSTOM"
+ type="primary"
+ link
+ @click="handleFormDetail(scope.row)"
+ >
+ <span>{{ scope.row.formCustomCreatePath }}</span>
+ </el-button>
+ <label v-else>鏆傛棤琛ㄥ崟</label>
+ </template>
+ </el-table-column>
+ <el-table-column label="娴佺▼鐗堟湰" align="center" min-width="80">
+ <template #default="scope">
+ <el-tag>v{{ scope.row.version }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="閮ㄧ讲鏃堕棿"
+ align="center"
+ prop="deploymentTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openModelForm(scope.row.id)"
+ v-hasPermi="['bpm:model:update']"
+ >
+ 鎭㈠
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 寮圭獥锛氳〃鍗曡鎯� -->
+ <Dialog title="琛ㄥ崟璇︽儏" v-model="formDetailVisible" width="800">
+ <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { DICT_TYPE } from '@/utils/dict'
+import { BpmModelFormType } from '@/utils/constants'
+
+defineOptions({ name: 'BpmProcessDefinition' })
+
+const { push } = useRouter() // 璺敱
+const { query } = useRoute() // 鏌ヨ鍙傛暟
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ key: query.key
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DefinitionApi.getProcessDefinitionPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娴佺▼琛ㄥ崟鐨勮鎯呮寜閽搷浣� */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+ rule: [],
+ option: {}
+})
+const handleFormDetail = async (row: any) => {
+ if (row.formType == BpmModelFormType.NORMAL) {
+ // 璁剧疆琛ㄥ崟
+ setConfAndFields2(formDetailPreview, row.formConf, row.formFields)
+ // 寮圭獥鎵撳紑
+ formDetailVisible.value = true
+ } else {
+ await push({
+ path: row.formCustomCreatePath
+ })
+ }
+}
+
+/** 鎭㈠娴佺▼妯″瀷寮圭獥 */
+const openModelForm = async (id?: number) => {
+ await push({
+ name: 'BpmModelUpdate',
+ params: { id, type: 'definition' }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.flow-icon {
+ display: flex;
+ width: 38px;
+ height: 38px;
+ margin-right: 10px;
+ background-color: var(--el-color-primary);
+ border-radius: 0.25rem;
+ align-items: center;
+ justify-content: center;
+}
+</style>
diff --git a/src/views/bpm/model/form/BasicInfo.vue b/src/views/bpm/model/form/BasicInfo.vue
new file mode 100644
index 0000000..b937b61
--- /dev/null
+++ b/src/views/bpm/model/form/BasicInfo.vue
@@ -0,0 +1,360 @@
+<template>
+ <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+ <el-form-item label="娴佺▼鏍囪瘑" prop="key" class="mb-20px">
+ <div class="flex items-center">
+ <el-input
+ class="!w-440px"
+ v-model="modelData.key"
+ :disabled="!!modelData.id"
+ placeholder="璇疯緭鍏ユ祦绋嬫爣璇嗭紝浠ュ瓧姣嶆垨涓嬪垝绾垮紑澶�"
+ />
+ <el-tooltip
+ class="item"
+ :content="modelData.id ? '娴佺▼鏍囪瘑涓嶅彲淇敼锛�' : '鏂板缓鍚庯紝娴佺▼鏍囪瘑涓嶅彲淇敼锛�'"
+ effect="light"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" class="ml-5px" />
+ </el-tooltip>
+ </div>
+ </el-form-item>
+ <el-form-item label="娴佺▼鍚嶇О" prop="name" class="mb-20px">
+ <el-input
+ v-model="modelData.name"
+ :disabled="!!modelData.id"
+ clearable
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ />
+ </el-form-item>
+ <el-form-item label="娴佺▼鍒嗙被" prop="category" class="mb-20px">
+ <el-select
+ class="!w-full"
+ v-model="modelData.category"
+ clearable
+ placeholder="璇烽�夋嫨娴佺▼鍒嗙被"
+ >
+ <el-option
+ v-for="category in categoryList"
+ :key="category.code"
+ :label="category.name"
+ :value="category.code"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娴佺▼鍥炬爣" class="mb-20px">
+ <UploadImg v-model="modelData.icon" :limit="1" height="64px" width="64px" />
+ </el-form-item>
+ <el-form-item label="娴佺▼鎻忚堪" prop="description" class="mb-20px">
+ <el-input v-model="modelData.description" clearable type="textarea" />
+ </el-form-item>
+ <el-form-item label="娴佺▼绫诲瀷" prop="type" class="mb-20px">
+ <el-radio-group v-model="modelData.type">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏄惁鍙" prop="visible" class="mb-20px">
+ <el-radio-group v-model="modelData.visible">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value as string"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="璋佸彲浠ュ彂璧�" prop="startUserType" class="mb-20px">
+ <el-select
+ v-model="modelData.startUserType"
+ placeholder="璇烽�夋嫨璋佸彲浠ュ彂璧�"
+ @change="handleStartUserTypeChange"
+ >
+ <el-option label="鍏ㄥ憳" :value="0" />
+ <el-option label="鎸囧畾浜哄憳" :value="1" />
+ <el-option label="鎸囧畾閮ㄩ棬" :value="2" />
+ </el-select>
+ <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
+ <div
+ v-for="user in selectedStartUsers"
+ :key="user.id"
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+ >
+ <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ user.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ user.nickname }}
+ <Icon
+ icon="ep:close"
+ class="ml-2 cursor-pointer hover:text-red-500"
+ @click="handleRemoveStartUser(user)"
+ />
+ </div>
+ <el-button type="primary" link @click="openStartUserSelect">
+ <Icon icon="ep:plus" /> 閫夋嫨浜哄憳
+ </el-button>
+ </div>
+ <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
+ <div
+ v-for="dept in selectedStartDepts"
+ :key="dept.id"
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+ >
+ <Icon icon="ep:office-building" class="!m-5px text-20px" />
+ {{ dept.name }}
+ <Icon
+ icon="ep:close"
+ class="ml-2 cursor-pointer hover:text-red-500"
+ @click="handleRemoveStartDept(dept)"
+ />
+ </div>
+ <el-button type="primary" link @click="openStartDeptSelect">
+ <Icon icon="ep:plus" /> 閫夋嫨閮ㄩ棬
+ </el-button>
+ </div>
+ </el-form-item>
+ <el-form-item label="娴佺▼绠$悊鍛�" prop="managerUserIds" class="mb-20px">
+ <div class="flex flex-wrap gap-2">
+ <div
+ v-for="user in selectedManagerUsers"
+ :key="user.id"
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+ >
+ <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ user.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ user.nickname }}
+ <Icon
+ icon="ep:close"
+ class="ml-2 cursor-pointer hover:text-red-500"
+ @click="handleRemoveManagerUser(user)"
+ />
+ </div>
+ <el-button type="primary" link @click="openManagerUserSelect">
+ <Icon icon="ep:plus" />閫夋嫨浜哄憳
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鐢ㄦ埛閫夋嫨寮圭獥 -->
+ <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+ <!-- 閮ㄩ棬閫夋嫨寮圭獥 -->
+ <DeptSelectForm
+ ref="deptSelectFormRef"
+ :multiple="true"
+ :check-strictly="true"
+ @confirm="handleDeptSelectConfirm"
+ />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import { UserVO } from '@/api/system/user'
+import { DeptVO } from '@/api/system/dept'
+import { CategoryVO } from '@/api/bpm/category'
+
+const props = defineProps({
+ categoryList: {
+ type: Array as PropType<CategoryVO[]>,
+ required: true
+ },
+ userList: {
+ type: Array,
+ required: true
+ },
+ deptList: {
+ type: Array,
+ required: true
+ }
+})
+
+const formRef = ref()
+const selectedStartUsers = ref<UserVO[]>([])
+const selectedStartDepts = ref<DeptVO[]>([])
+const selectedManagerUsers = ref<UserVO[]>([])
+const userSelectFormRef = ref()
+const deptSelectFormRef = ref()
+const currentSelectType = ref<'start' | 'manager'>('start')
+
+const rules = {
+ name: [{ required: true, message: '娴佺▼鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ key: [
+ { required: true, message: '娴佺▼鏍囪瘑涓嶈兘涓虹┖', trigger: 'blur' },
+ {
+ validator: (_rule: any, value: string, callback: any) => {
+ if (!value) {
+ callback()
+ return
+ }
+ if (!/^[a-zA-Z_][\-_.0-9_a-zA-Z$]*$/.test(value)) {
+ callback(new Error('鍙兘鍖呭惈瀛楁瘝銆佹暟瀛椼�佷笅鍒掔嚎銆佽繛瀛楃鍜岀偣鍙凤紝涓斿繀椤讳互瀛楁瘝鎴栦笅鍒掔嚎寮�澶�'))
+ return
+ }
+ callback()
+ },
+ trigger: 'blur'
+ }
+ ],
+ category: [{ required: true, message: '娴佺▼鍒嗙被涓嶈兘涓虹┖', trigger: 'blur' }],
+ type: [{ required: true, message: '鏄惁鍙涓嶈兘涓虹┖', trigger: 'blur' }],
+ visible: [{ required: true, message: '鏄惁鍙涓嶈兘涓虹┖', trigger: 'blur' }],
+ managerUserIds: [{ required: true, message: '娴佺▼绠$悊鍛樹笉鑳戒负绌�', trigger: 'blur' }]
+}
+
+// 鍒涘缓鏈湴鏁版嵁鍓湰
+const modelData = defineModel<any>()
+
+// 鍒濆鍖栭�変腑鐨勭敤鎴�
+watch(
+ () => modelData.value,
+ (newVal) => {
+ if (newVal.startUserIds?.length) {
+ selectedStartUsers.value = props.userList.filter((user: UserVO) =>
+ newVal.startUserIds.includes(user.id)
+ ) as UserVO[]
+ } else {
+ selectedStartUsers.value = []
+ }
+ if (newVal.startDeptIds?.length) {
+ selectedStartDepts.value = props.deptList.filter((dept: DeptVO) =>
+ newVal.startDeptIds.includes(dept.id)
+ ) as DeptVO[]
+ } else {
+ selectedStartDepts.value = []
+ }
+ if (newVal.managerUserIds?.length) {
+ selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
+ newVal.managerUserIds.includes(user.id)
+ ) as UserVO[]
+ } else {
+ selectedManagerUsers.value = []
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 鎵撳紑鍙戣捣浜洪�夋嫨 */
+const openStartUserSelect = () => {
+ currentSelectType.value = 'start'
+ userSelectFormRef.value.open(0, selectedStartUsers.value)
+}
+
+/** 鎵撳紑閮ㄩ棬閫夋嫨 */
+const openStartDeptSelect = () => {
+ deptSelectFormRef.value.open(selectedStartDepts.value)
+}
+
+/** 鎵撳紑绠$悊鍛橀�夋嫨 */
+const openManagerUserSelect = () => {
+ currentSelectType.value = 'manager'
+ userSelectFormRef.value.open(0, selectedManagerUsers.value)
+}
+
+/** 澶勭悊鐢ㄦ埛閫夋嫨纭 */
+const handleUserSelectConfirm = (_, users: UserVO[]) => {
+ if (currentSelectType.value === 'start') {
+ modelData.value = {
+ ...modelData.value,
+ startUserIds: users.map((u) => u.id)
+ }
+ } else {
+ modelData.value = {
+ ...modelData.value,
+ managerUserIds: users.map((u) => u.id)
+ }
+ }
+}
+
+/** 澶勭悊閮ㄩ棬閫夋嫨纭 */
+const handleDeptSelectConfirm = (depts: DeptVO[]) => {
+ modelData.value = {
+ ...modelData.value,
+ startDeptIds: depts.map((d) => d.id)
+ }
+}
+
+/** 澶勭悊鍙戣捣浜虹被鍨嬪彉鍖� */
+const handleStartUserTypeChange = (value: number) => {
+ if (value === 0) {
+ modelData.value = {
+ ...modelData.value,
+ startUserIds: [],
+ startDeptIds: []
+ }
+ } else if (value === 1) {
+ modelData.value = {
+ ...modelData.value,
+ startDeptIds: []
+ }
+ } else if (value === 2) {
+ modelData.value = {
+ ...modelData.value,
+ startUserIds: []
+ }
+ }
+}
+
+/** 绉婚櫎鍙戣捣浜� */
+const handleRemoveStartUser = (user: UserVO) => {
+ modelData.value = {
+ ...modelData.value,
+ startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
+ }
+}
+
+/** 绉婚櫎閮ㄩ棬 */
+const handleRemoveStartDept = (dept: DeptVO) => {
+ modelData.value = {
+ ...modelData.value,
+ startDeptIds: modelData.value.startDeptIds.filter((id: number) => id !== dept.id)
+ }
+}
+
+/** 绉婚櫎绠$悊鍛� */
+const handleRemoveManagerUser = (user: UserVO) => {
+ modelData.value = {
+ ...modelData.value,
+ managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
+ }
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = async () => {
+ await formRef.value?.validate()
+}
+
+defineExpose({
+ validate
+})
+</script>
+
+<style lang="scss" scoped>
+.bg-gray-100 {
+ background-color: #f5f7fa;
+ transition: all 0.3s;
+
+ &:hover {
+ background-color: #e6e8eb;
+ }
+
+ .ep-close {
+ font-size: 14px;
+ color: #909399;
+ transition: color 0.3s;
+
+ &:hover {
+ color: #f56c6c;
+ }
+ }
+}
+</style>
diff --git a/src/views/bpm/model/form/ExtraSettings.vue b/src/views/bpm/model/form/ExtraSettings.vue
new file mode 100644
index 0000000..e250eec
--- /dev/null
+++ b/src/views/bpm/model/form/ExtraSettings.vue
@@ -0,0 +1,507 @@
+<template>
+ <el-form ref="formRef" :model="modelData" label-width="130px" class="mt-20px">
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">鎻愪氦浜烘潈闄�</el-text>
+ </template>
+ <div class="flex flex-col">
+ <el-checkbox v-model="modelData.allowCancelRunningProcess" label="鍏佽鎾ら攢瀹℃壒涓殑鐢宠" />
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">瀹℃壒浜烘潈闄�</el-text>
+ </template>
+ <div class="flex flex-col">
+ <el-checkbox v-model="modelData.allowWithdrawTask" label="鍏佽瀹℃壒浜烘挙鍥炰换鍔�" />
+ <div class="ml-22px">
+ <el-text type="info"> 瀹℃壒浜哄彲鎾ゅ洖姝e湪瀹℃壒鑺傜偣鐨勫墠涓�鑺傜偣 </el-text>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item v-if="modelData.processIdRule" class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">娴佺▼缂栫爜</el-text>
+ </template>
+ <div class="flex flex-col">
+ <div>
+ <el-input
+ v-model="modelData.processIdRule.prefix"
+ class="w-130px!"
+ placeholder="鍓嶇紑"
+ :disabled="!modelData.processIdRule.enable"
+ >
+ <template #prepend>
+ <el-checkbox v-model="modelData.processIdRule.enable" />
+ </template>
+ </el-input>
+ <el-select
+ v-model="modelData.processIdRule.infix"
+ class="w-130px! ml-5px"
+ placeholder="涓紑"
+ :disabled="!modelData.processIdRule.enable"
+ >
+ <el-option
+ v-for="item in timeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-input
+ v-model="modelData.processIdRule.postfix"
+ class="w-80px! ml-5px"
+ placeholder="鍚庣紑"
+ :disabled="!modelData.processIdRule.enable"
+ />
+ <el-input-number
+ v-model="modelData.processIdRule.length"
+ class="w-120px! ml-5px"
+ :min="5"
+ :disabled="!modelData.processIdRule.enable"
+ />
+ </div>
+ <div class="ml-22px" v-if="modelData.processIdRule.enable">
+ <el-text type="info"> 缂栫爜绀轰緥锛歿{ numberExample }} </el-text>
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">鑷姩鍘婚噸</el-text>
+ </template>
+ <div class="flex flex-col">
+ <div>
+ <el-text> 鍚屼竴瀹℃壒浜哄湪娴佺▼涓噸澶嶅嚭鐜版椂锛� </el-text>
+ </div>
+ <el-radio-group v-model="modelData.autoApprovalType">
+ <div class="flex flex-col">
+ <el-radio :value="0">涓嶈嚜鍔ㄩ�氳繃</el-radio>
+ <el-radio :value="1">浠呭鎵逛竴娆★紝鍚庣画閲嶅鐨勫鎵硅妭鐐瑰潎鑷姩閫氳繃</el-radio>
+ <el-radio :value="2">浠呴拡瀵硅繛缁鎵圭殑鑺傜偣鑷姩閫氳繃</el-radio>
+ </div>
+ </el-radio-group>
+ </div>
+ </el-form-item>
+ <el-form-item v-if="modelData.titleSetting" class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">鏍囬璁剧疆</el-text>
+ </template>
+ <div class="flex flex-col">
+ <el-radio-group v-model="modelData.titleSetting.enable">
+ <div class="flex flex-col">
+ <el-radio :value="false"
+ >绯荤粺榛樿 <el-text type="info"> 灞曠ず娴佺▼鍚嶇О </el-text></el-radio
+ >
+ <el-radio :value="true">
+ 鑷畾涔夋爣棰�
+ <el-text>
+ <el-tooltip content="杈撳叆瀛楃 '{' 鍗冲彲鎻掑叆琛ㄥ崟瀛楁" effect="light" placement="top">
+ <Icon icon="ep:question-filled" class="ml-5px" />
+ </el-tooltip>
+ </el-text>
+ </el-radio>
+ </div>
+ </el-radio-group>
+ <el-mention
+ v-if="modelData.titleSetting.enable"
+ v-model="modelData.titleSetting.title"
+ type="textarea"
+ prefix="{"
+ split="}"
+ whole
+ :options="formFieldOptions4Title"
+ placeholder="璇锋彃鍏ヨ〃鍗曞瓧娈碉紙杈撳叆 '{' 鍙互閫夋嫨琛ㄥ崟瀛楁锛夋垨杈撳叆鏂囨湰"
+ class="w-600px!"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item
+ v-if="modelData.summarySetting && modelData.formType === BpmModelFormType.NORMAL"
+ class="mb-20px"
+ >
+ <template #label>
+ <el-text size="large" tag="b">鎽樿璁剧疆</el-text>
+ </template>
+ <div class="flex flex-col">
+ <el-radio-group v-model="modelData.summarySetting.enable">
+ <div class="flex flex-col">
+ <el-radio :value="false">
+ 绯荤粺榛樿 <el-text type="info"> 灞曠ず琛ㄥ崟鍓� 3 涓瓧娈� </el-text>
+ </el-radio>
+ <el-radio :value="true"> 鑷畾涔夋憳瑕� </el-radio>
+ </div>
+ </el-radio-group>
+ <el-select
+ class="w-500px!"
+ v-if="modelData.summarySetting.enable"
+ v-model="modelData.summarySetting.summary"
+ multiple
+ placeholder="璇烽�夋嫨瑕佸睍绀虹殑琛ㄥ崟瀛楁"
+ >
+ <el-option
+ v-for="item in formFieldOptions4Summary"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">娴佺▼鍓嶇疆閫氱煡</el-text>
+ </template>
+ <div class="flex flex-col w-100%">
+ <div class="flex">
+ <el-switch
+ v-model="processBeforeTriggerEnable"
+ @change="handleProcessBeforeTriggerEnableChange"
+ />
+ <div class="ml-80px">娴佺▼鍚姩鍚庨�氱煡</div>
+ </div>
+ <HttpRequestSetting
+ v-if="processBeforeTriggerEnable"
+ v-model:setting="modelData.processBeforeTriggerSetting"
+ :responseEnable="true"
+ :formItemPrefix="'processBeforeTriggerSetting'"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">娴佺▼鍚庣疆閫氱煡</el-text>
+ </template>
+ <div class="flex flex-col w-100%">
+ <div class="flex">
+ <el-switch
+ v-model="processAfterTriggerEnable"
+ @change="handleProcessAfterTriggerEnableChange"
+ />
+ <div class="ml-80px">娴佺▼缁撴潫鍚庨�氱煡</div>
+ </div>
+ <HttpRequestSetting
+ v-if="processAfterTriggerEnable"
+ v-model:setting="modelData.processAfterTriggerSetting"
+ :responseEnable="true"
+ :formItemPrefix="'processAfterTriggerSetting'"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">浠诲姟鍓嶇疆閫氱煡</el-text>
+ </template>
+ <div class="flex flex-col w-100%">
+ <div class="flex">
+ <el-switch
+ v-model="taskBeforeTriggerEnable"
+ @change="handleTaskBeforeTriggerEnableChange"
+ />
+ <div class="ml-80px">浠诲姟鎵ц鏃堕�氱煡</div>
+ </div>
+ <HttpRequestSetting
+ v-if="taskBeforeTriggerEnable"
+ v-model:setting="modelData.taskBeforeTriggerSetting"
+ :responseEnable="true"
+ :formItemPrefix="'taskBeforeTriggerSetting'"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">浠诲姟鍚庣疆閫氱煡</el-text>
+ </template>
+ <div class="flex flex-col w-100%">
+ <div class="flex">
+ <el-switch
+ v-model="taskAfterTriggerEnable"
+ @change="handleTaskAfterTriggerEnableChange"
+ />
+ <div class="ml-80px">浠诲姟缁撴潫鍚庨�氱煡</div>
+ </div>
+ <HttpRequestSetting
+ v-if="taskAfterTriggerEnable"
+ v-model:setting="modelData.taskAfterTriggerSetting"
+ :responseEnable="true"
+ :formItemPrefix="'taskAfterTriggerSetting'"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item class="mb-20px">
+ <template #label>
+ <el-text size="large" tag="b">鑷畾涔夋墦鍗版ā鏉�</el-text>
+ </template>
+ <div class="flex flex-col w-100%">
+ <div class="flex">
+ <el-switch
+ v-model="modelData.printTemplateSetting.enable"
+ @change="handlePrintTemplateEnableChange"
+ />
+ <el-button
+ v-if="modelData.printTemplateSetting.enable"
+ class="ml-80px"
+ type="primary"
+ link
+ @click="handleEditPrintTemplate"
+ >
+ 缂栬緫妯℃澘
+ </el-button>
+ </div>
+ </div>
+ </el-form-item>
+ </el-form>
+ <print-template ref="printTemplateRef" @confirm="confirmPrintTemplate" />
+</template>
+
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { BpmAutoApproveType, BpmModelFormType } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { ProcessVariableEnum } from '@/components/SimpleProcessDesignerV2/src/consts'
+import HttpRequestSetting from '@/components/SimpleProcessDesignerV2/src/nodes-config/components/HttpRequestSetting.vue'
+import PrintTemplate from './PrintTemplate/Index.vue'
+
+const modelData = defineModel<any>()
+
+/** 鑷畾涔� ID 娴佺▼缂栫爜 */
+const timeOptions = ref([
+ {
+ value: '',
+ label: '鏃�'
+ },
+ {
+ value: 'DAY',
+ label: '绮剧‘鍒版棩'
+ },
+ {
+ value: 'HOUR',
+ label: '绮剧‘鍒版椂'
+ },
+ {
+ value: 'MINUTE',
+ label: '绮剧‘鍒板垎'
+ },
+ {
+ value: 'SECOND',
+ label: '绮剧‘鍒扮'
+ }
+])
+const numberExample = computed(() => {
+ if (modelData.value.processIdRule.enable) {
+ let infix = ''
+ switch (modelData.value.processIdRule.infix) {
+ case 'DAY':
+ infix = dayjs().format('YYYYMMDD')
+ break
+ case 'HOUR':
+ infix = dayjs().format('YYYYMMDDHH')
+ break
+ case 'MINUTE':
+ infix = dayjs().format('YYYYMMDDHHmm')
+ break
+ case 'SECOND':
+ infix = dayjs().format('YYYYMMDDHHmmss')
+ break
+ default:
+ break
+ }
+ return (
+ modelData.value.processIdRule.prefix +
+ infix +
+ modelData.value.processIdRule.postfix +
+ '1'.padStart(modelData.value.processIdRule.length - 1, '0')
+ )
+ } else {
+ return ''
+ }
+})
+
+/** 鏄惁寮�鍚祦绋嬪墠缃�氱煡 */
+const processBeforeTriggerEnable = ref(false)
+const handleProcessBeforeTriggerEnableChange = (val: boolean | string | number) => {
+ if (val) {
+ modelData.value.processBeforeTriggerSetting = {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ }
+ } else {
+ modelData.value.processBeforeTriggerSetting = null
+ }
+}
+
+/** 鏄惁寮�鍚祦绋嬪悗缃�氱煡 */
+const processAfterTriggerEnable = ref(false)
+const handleProcessAfterTriggerEnableChange = (val: boolean | string | number) => {
+ if (val) {
+ modelData.value.processAfterTriggerSetting = {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ }
+ } else {
+ modelData.value.processAfterTriggerSetting = null
+ }
+}
+
+/** 鏄惁寮�鍚换鍔″墠缃�氱煡 */
+const taskBeforeTriggerEnable = ref(false)
+const handleTaskBeforeTriggerEnableChange = (val: boolean | string | number) => {
+ if (val) {
+ modelData.value.taskBeforeTriggerSetting = {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ }
+ } else {
+ modelData.value.taskBeforeTriggerSetting = null
+ }
+}
+
+/** 鏄惁寮�鍚换鍔″悗缃�氱煡 */
+const taskAfterTriggerEnable = ref(false)
+const handleTaskAfterTriggerEnableChange = (val: boolean | string | number) => {
+ if (val) {
+ modelData.value.taskAfterTriggerSetting = {
+ url: '',
+ header: [],
+ body: [],
+ response: []
+ }
+ } else {
+ modelData.value.taskAfterTriggerSetting = null
+ }
+}
+
+/** 宸茶В鏋愯〃鍗曞瓧娈� */
+const formFields = ref<Array<{ field: string; title: string }>>([])
+const formFieldOptions4Title = computed(() => {
+ let cloneFormField = formFields.value.map((item) => {
+ return {
+ label: item.title,
+ value: item.field
+ }
+ })
+ // 鍥哄畾娣诲姞鍙戣捣浜� ID 瀛楁
+ cloneFormField.unshift({
+ label: '娴佺▼鍚嶇О',
+ value: ProcessVariableEnum.PROCESS_DEFINITION_NAME
+ })
+ cloneFormField.unshift({
+ label: '鍙戣捣鏃堕棿',
+ value: ProcessVariableEnum.START_TIME
+ })
+ cloneFormField.unshift({
+ label: '鍙戣捣浜�',
+ value: ProcessVariableEnum.START_USER_ID
+ })
+ return cloneFormField
+})
+const formFieldOptions4Summary = computed(() => {
+ return formFields.value.map((item) => {
+ return {
+ label: item.title,
+ value: item.field
+ }
+ })
+})
+
+/** 鏈В鏋愮殑琛ㄥ崟瀛楁 */
+const unParsedFormFields = ref<string[]>([])
+/** 鏆撮湶缁欏瓙缁勪欢 HttpRequestSetting 浣跨敤 */
+provide('formFields', unParsedFormFields)
+provide('formFieldsObj', formFields)
+
+/** 鍏煎浠ュ墠鏈厤缃洿澶氳缃殑娴佺▼ */
+const initData = () => {
+ if (!modelData.value.processIdRule) {
+ modelData.value.processIdRule = {
+ enable: false,
+ prefix: '',
+ infix: '',
+ postfix: '',
+ length: 5
+ }
+ }
+ if (!modelData.value.autoApprovalType) {
+ modelData.value.autoApprovalType = BpmAutoApproveType.NONE
+ }
+ if (!modelData.value.titleSetting) {
+ modelData.value.titleSetting = {
+ enable: false,
+ title: ''
+ }
+ }
+ if (!modelData.value.summarySetting) {
+ modelData.value.summarySetting = {
+ enable: false,
+ summary: []
+ }
+ }
+ if (modelData.value.processBeforeTriggerSetting) {
+ processBeforeTriggerEnable.value = true
+ }
+ if (modelData.value.processAfterTriggerSetting) {
+ processAfterTriggerEnable.value = true
+ }
+ if (modelData.value.taskBeforeTriggerSetting) {
+ taskBeforeTriggerEnable.value = true
+ }
+ if (modelData.value.taskAfterTriggerSetting) {
+ taskAfterTriggerEnable.value = true
+ }
+ if (modelData.value.allowWithdrawTask) {
+ modelData.value.allowWithdrawTask = false
+ }
+ if (!modelData.value.printTemplateSetting) {
+ modelData.value.printTemplateSetting = {
+ enable: false
+ }
+ }
+}
+defineExpose({ initData })
+
+/** 鐩戝惉琛ㄥ崟 ID 鍙樺寲锛屽姞杞借〃鍗曟暟鎹� */
+watch(
+ () => modelData.value.formId,
+ async (newFormId) => {
+ if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+ const data = await FormApi.getForm(newFormId)
+ const result: Array<{ field: string; title: string }> = []
+ if (data.fields) {
+ unParsedFormFields.value = data.fields
+ data.fields.forEach((fieldStr: string) => {
+ parseFormFields(JSON.parse(fieldStr), result)
+ })
+ }
+ formFields.value = result
+ } else {
+ formFields.value = []
+ unParsedFormFields.value = []
+ }
+ },
+ { immediate: true }
+)
+
+const defaultTemplate =
+ '<p style="text-align: center;"><span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="娴佺▼鍚嶇О" data-info="%7B%22id%22%3A%22processName%22%7D">@娴佺▼鍚嶇О</span></p><p style="text-align: right;">鎵撳嵃浜猴細<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="鎵撳嵃浜�" data-info="%7B%22id%22%3A%22printUser%22%7D">@鎵撳嵃浜�</span></p><p style="text-align: right;">娴佺▼缂栧彿锛�<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="娴佺▼缂栧彿" data-info="%7B%22id%22%3A%22processNum%22%7D">@娴佺▼缂栧彿</span> 鎵撳嵃鏃堕棿锛�<span data-w-e-type="mention" data-w-e-is-void="" data-w-e-is-inline="" data-value="鎵撳嵃鏃堕棿" data-info="%7B%22id%22%3A%22printTime%22%7D">@鎵撳嵃鏃堕棿</span></p><table style="width: 100%;"><tbody><tr><td colSpan="1" rowSpan="1" width="auto">鍙戣捣浜�</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="鍙戣捣浜�" data-info="%7B%22id%22%3A%22startUser%22%7D">@鍙戣捣浜�</span></td><td colSpan="1" rowSpan="1" width="auto">鍙戣捣鏃堕棿</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="鍙戣捣鏃堕棿" data-info="%7B%22id%22%3A%22startTime%22%7D">@鍙戣捣鏃堕棿</span></td></tr><tr><td colSpan="1" rowSpan="1" width="auto">鎵�灞為儴闂�</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="鍙戣捣浜洪儴闂�" data-info="%7B%22id%22%3A%22startUserDept%22%7D">@鍙戣捣浜洪儴闂�</span></td><td colSpan="1" rowSpan="1" width="auto">娴佺▼鐘舵��</td><td colSpan="1" rowSpan="1" width="auto"><span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="娴佺▼鐘舵��" data-info="%7B%22id%22%3A%22processStatus%22%7D">@娴佺▼鐘舵��</span></td></tr></tbody></table><p><span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>娴佺▼璁板綍</span></p>'
+const handlePrintTemplateEnableChange = (val: boolean) => {
+ if (val) {
+ if (!modelData.value.printTemplateSetting.template) {
+ modelData.value.printTemplateSetting.template = defaultTemplate
+ }
+ }
+}
+const printTemplateRef = ref()
+const handleEditPrintTemplate = () => {
+ printTemplateRef.value.open(modelData.value.printTemplateSetting.template)
+}
+const confirmPrintTemplate = (template: any) => {
+ modelData.value.printTemplateSetting.template = template
+}
+</script>
diff --git a/src/views/bpm/model/form/FormDesign.vue b/src/views/bpm/model/form/FormDesign.vue
new file mode 100644
index 0000000..e1ca27f
--- /dev/null
+++ b/src/views/bpm/model/form/FormDesign.vue
@@ -0,0 +1,129 @@
+<template>
+ <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+ <el-form-item label="琛ㄥ崟绫诲瀷" prop="formType" class="mb-20px">
+ <el-radio-group v-model="modelData.formType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="modelData.formType === BpmModelFormType.NORMAL" label="娴佺▼琛ㄥ崟" prop="formId">
+ <el-select v-model="modelData.formId" clearable style="width: 100%">
+ <el-option v-for="form in formList" :key="form.id" :label="form.name" :value="form.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="琛ㄥ崟鎻愪氦璺敱" prop="formCustomCreatePath">
+ <el-input
+ v-model="modelData.formCustomCreatePath"
+ placeholder="璇疯緭鍏ヨ〃鍗曟彁浜よ矾鐢�"
+ style="width: 330px"
+ />
+ <el-tooltip
+ class="item"
+ content="鑷畾涔夎〃鍗曠殑鎻愪氦璺緞锛屼娇鐢� Vue 鐨勮矾鐢卞湴鍧�锛屼緥濡傝锛歜pm/oa/leave/create.vue"
+ effect="light"
+ placement="top"
+ >
+ <Icon icon="ep:question" class="ml-5px" />
+ </el-tooltip>
+ </el-form-item>
+ <el-form-item v-if="modelData.formType === BpmModelFormType.CUSTOM" label="琛ㄥ崟鏌ョ湅鍦板潃" prop="formCustomViewPath">
+ <el-input
+ v-model="modelData.formCustomViewPath"
+ placeholder="璇疯緭鍏ヨ〃鍗曟煡鐪嬬殑缁勪欢鍦板潃"
+ style="width: 330px"
+ />
+ <el-tooltip
+ class="item"
+ content="鑷畾涔夎〃鍗曠殑鏌ョ湅缁勪欢鍦板潃锛屼娇鐢� Vue 鐨勭粍浠跺湴鍧�锛屼緥濡傝锛歜pm/oa/leave/detail.vue"
+ effect="light"
+ placement="top"
+ >
+ <Icon icon="ep:question" class="ml-5px" />
+ </el-tooltip>
+ </el-form-item>
+ <!-- 琛ㄥ崟棰勮 -->
+ <div
+ v-if="modelData.formType === BpmModelFormType.NORMAL && modelData.formId && formPreview.rule.length > 0"
+ class="mt-20px"
+ >
+ <div class="flex items-center mb-15px">
+ <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div>
+ <span class="text-15px font-bold">琛ㄥ崟棰勮</span>
+ </div>
+ <form-create
+ v-model="formPreview.formData"
+ :rule="formPreview.rule"
+ :option="formPreview.option"
+ />
+ </div>
+ </el-form>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelFormType } from '@/utils/constants'
+
+const props = defineProps({
+ formList: {
+ type: Array,
+ required: true
+ }
+})
+
+const formRef = ref()
+
+// 鍒涘缓鏈湴鏁版嵁鍓湰
+const modelData = defineModel<any>()
+
+// 琛ㄥ崟棰勮鏁版嵁
+const formPreview = ref({
+ formData: {},
+ rule: [],
+ option: {
+ submitBtn: false,
+ resetBtn: false,
+ formData: {}
+ }
+})
+
+// 鐩戝惉琛ㄥ崟ID鍙樺寲锛屽姞杞借〃鍗曟暟鎹�
+watch(
+ () => modelData.value.formId,
+ async (newFormId) => {
+ if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+ const data = await FormApi.getForm(newFormId)
+ setConfAndFields2(formPreview.value, data.conf, data.fields)
+ // 璁剧疆鍙
+ formPreview.value.rule.forEach((item: any) => {
+ item.props = { ...item.props, disabled: true }
+ })
+ } else {
+ formPreview.value.rule = []
+ }
+ },
+ { immediate: true }
+)
+
+const rules = {
+ formType: [{ required: true, message: '琛ㄥ崟绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ formId: [{ required: true, message: '娴佺▼琛ㄥ崟涓嶈兘涓虹┖', trigger: 'blur' }],
+ formCustomCreatePath: [{ required: true, message: '琛ㄥ崟鎻愪氦璺敱涓嶈兘涓虹┖', trigger: 'blur' }],
+ formCustomViewPath: [{ required: true, message: '琛ㄥ崟鏌ョ湅鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }]
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = async () => {
+ await formRef.value?.validate()
+}
+
+defineExpose({
+ validate
+})
+</script>
diff --git a/src/views/bpm/model/form/PrintTemplate/Index.vue b/src/views/bpm/model/form/PrintTemplate/Index.vue
new file mode 100644
index 0000000..8459466
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/Index.vue
@@ -0,0 +1,116 @@
+<script setup lang="ts">
+import { Editor, Toolbar } from '@wangeditor-next/editor-for-vue'
+import { IDomEditor } from '@wangeditor-next/editor'
+import MentionModal from './MentionModal.vue'
+
+const emit = defineEmits(['confirm'])
+
+// @mention 鐩稿叧
+const isShowModal = ref(false)
+const showModal = () => {
+ isShowModal.value = true
+}
+const hideModal = () => {
+ isShowModal.value = false
+}
+const insertMention = (id: any, name: any) => {
+ const mentionNode = {
+ type: 'mention',
+ value: name,
+ info: { id },
+ children: [{ text: '' }]
+ }
+ const editor = editorRef.value
+ if (editor) {
+ editor.restoreSelection()
+ editor.deleteBackward('character')
+ editor.insertNode(mentionNode)
+ editor.move(1)
+ }
+}
+
+// Dialog 鐩稿叧
+const dialogVisible = ref(false)
+const open = async (template: string) => {
+ dialogVisible.value = true
+ valueHtml.value = template
+}
+defineExpose({ open })
+const handleConfirm = () => {
+ emit('confirm', valueHtml.value)
+ dialogVisible.value = false
+}
+
+// Editor 鐩稿叧
+const editorRef = shallowRef<IDomEditor>()
+const editorId = ref('wangEditor-1')
+const toolbarConfig = {
+ excludeKeys: ['group-video'],
+ insertKeys: {
+ index: 31,
+ keys: ['ProcessRecordMenu']
+ }
+}
+const editorConfig = {
+ placeholder: '璇疯緭鍏ュ唴瀹�...',
+ EXTEND_CONF: {
+ mentionConfig: {
+ showModal,
+ hideModal
+ }
+ }
+}
+const valueHtml = ref()
+const handleCreated = (editor: IDomEditor) => {
+ editorRef.value = editor
+}
+
+/** 鍒濆鍖� */
+onBeforeUnmount(() => {
+ const editor = editorRef.value
+ if (editor == null) {
+ return
+ }
+ editor.destroy()
+})
+</script>
+
+<template>
+ <el-dialog v-model="dialogVisible" title="鑷畾涔夋ā鏉�" fullscreen>
+ <div style="margin: 0 10px">
+ <el-alert
+ title="杈撳叆 @ 鍙�夋嫨鎻掑叆娴佺▼琛ㄥ崟閫夐」鍜岄粯璁ら�夐」"
+ type="info"
+ show-icon
+ :closable="false"
+ />
+ </div>
+ <!-- TODO @unocss 绠�鍖� style -->
+ <div style=" margin: 10px;border: 1px solid #ccc">
+ <Toolbar
+ style="border-bottom: 1px solid #ccc"
+ :editor="editorRef"
+ :editorId="editorId"
+ :defaultConfig="toolbarConfig"
+ />
+ <Editor
+ style="height: 500px; overflow-y: hidden"
+ v-model="valueHtml"
+ :defaultConfig="editorConfig"
+ :editorId="editorId"
+ @on-created="handleCreated"
+ />
+ <MentionModal
+ v-if="isShowModal"
+ @hide-mention-modal="hideModal"
+ @insert-mention="insertMention"
+ />
+ </div>
+ <div style=" float: right;margin-right: 10px">
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleConfirm">纭� 瀹�</el-button>
+ </div>
+ </el-dialog>
+</template>
+
+<style src="@wangeditor-next/editor/dist/css/style.css"></style>
diff --git a/src/views/bpm/model/form/PrintTemplate/MentionModal.vue b/src/views/bpm/model/form/PrintTemplate/MentionModal.vue
new file mode 100644
index 0000000..badeb42
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/MentionModal.vue
@@ -0,0 +1,110 @@
+<script setup lang="ts">
+const emit = defineEmits(['hideMentionModal', 'insertMention'])
+
+const inputRef = ref()
+const top = ref('')
+const left = ref('')
+const searchVal = ref('')
+const list = ref([
+ { id: 'startUser', name: '鍙戣捣浜�' },
+ { id: 'startUserDept', name: '鍙戣捣浜洪儴闂�' },
+ { id: 'processName', name: '娴佺▼鍚嶇О' },
+ { id: 'processNum', name: '娴佺▼缂栧彿' },
+ { id: 'startTime', name: '鍙戣捣鏃堕棿' },
+ { id: 'endTime', name: '缁撴潫鏃堕棿' },
+ { id: 'processStatus', name: '娴佺▼鐘舵��' },
+ { id: 'printUser', name: '鎵撳嵃浜�' },
+ { id: 'printTime', name: '鎵撳嵃鏃堕棿' }
+])
+const searchedList = computed(() => {
+ const searchValStr = searchVal.value.trim().toLowerCase()
+ return list.value.filter((item) => {
+ const name = item.name.toLowerCase()
+ return name.indexOf(searchValStr) >= 0
+ })
+})
+const inputKeyupHandler = (event: any) => {
+ if (event.key === 'Escape') {
+ emit('hideMentionModal')
+ }
+ if (event.key === 'Enter') {
+ const firstOne = searchedList.value[0]
+ if (firstOne) {
+ const { id, name } = firstOne
+ insertMentionHandler(id, name)
+ }
+ }
+}
+const insertMentionHandler = (id: any, name: any) => {
+ emit('insertMention', id, name)
+ emit('hideMentionModal')
+}
+
+const formFields = inject<any>('formFieldsObj')
+onMounted(() => {
+ if (formFields.value && formFields.value.length > 0) {
+ const cloneFormField = formFields.value.map((item) => {
+ return {
+ name: '[琛ㄥ崟]' + item.title,
+ id: item.field
+ }
+ })
+ list.value.push(...cloneFormField)
+ }
+ const domSelection = document.getSelection()
+ const domRange = domSelection?.getRangeAt(0)
+ if (domRange == null) return
+ const rect = domRange.getBoundingClientRect()
+
+ top.value = `${rect.top + 20}px`
+ left.value = `${rect.left + 5}px`
+
+ inputRef.value.focus()
+})
+</script>
+
+<template>
+ <div id="mention-modal" :style="{ top: top, left: left }">
+ <!-- TODO @lesan锛歝ss 鍙互鐢� unocss 鍝囷紵 -->
+ <input id="mention-input" v-model="searchVal" ref="inputRef" @keyup="inputKeyupHandler" />
+ <ul id="mention-list">
+ <li
+ v-for="item in searchedList"
+ :key="item.id"
+ @click="insertMentionHandler(item.id, item.name)"
+ >
+ {{ item.name }}
+ </li>
+ </ul>
+ </div>
+</template>
+
+<style>
+#mention-modal {
+ position: absolute;
+ border: 1px solid #ccc;
+ background-color: #fff;
+ padding: 5px;
+}
+
+#mention-modal input {
+ width: 100px;
+ outline: none;
+}
+
+#mention-modal ul {
+ padding: 0;
+ margin: 0;
+}
+
+#mention-modal ul li {
+ list-style: none;
+ cursor: pointer;
+ padding: 3px 0;
+ text-align: left;
+}
+
+#mention-modal ul li:hover {
+ text-decoration: underline;
+}
+</style>
diff --git a/src/views/bpm/model/form/PrintTemplate/index.ts b/src/views/bpm/model/form/PrintTemplate/index.ts
new file mode 100644
index 0000000..be8273d
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/index.ts
@@ -0,0 +1,9 @@
+import { Boot } from '@wangeditor-next/editor'
+import processRecordModule from './module'
+import mentionModule from '@wangeditor-next/plugin-mention'
+
+// 娉ㄥ唽锛氳鍦ㄥ垱寤虹紪杈戝櫒涔嬪墠娉ㄥ唽锛屼笖鍙兘娉ㄥ唽涓�娆★紝涓嶅彲閲嶅娉ㄥ唽
+export const setupWangEditorPlugin = () => {
+ Boot.registerModule(processRecordModule)
+ Boot.registerModule(mentionModule)
+}
diff --git a/src/views/bpm/model/form/PrintTemplate/module/elem-to-html.ts b/src/views/bpm/model/form/PrintTemplate/module/elem-to-html.ts
new file mode 100644
index 0000000..3f3b79a
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/elem-to-html.ts
@@ -0,0 +1,12 @@
+import { SlateElement } from '@wangeditor-next/editor'
+
+function processRecordToHtml(_elem: SlateElement, _childrenHtml: string): string {
+ return `<span data-w-e-type="process-record" data-w-e-is-void data-w-e-is-inline>娴佺▼璁板綍</span>`
+}
+
+const conf = {
+ type: 'process-record',
+ elemToHtml: processRecordToHtml
+}
+
+export default conf
diff --git a/src/views/bpm/model/form/PrintTemplate/module/index.ts b/src/views/bpm/model/form/PrintTemplate/module/index.ts
new file mode 100644
index 0000000..1f57592
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/index.ts
@@ -0,0 +1,17 @@
+import { IModuleConf } from '@wangeditor-next/editor'
+import withProcessRecord from './plugin'
+import renderElemConf from './render-elem'
+import elemToHtmlConf from './elem-to-html'
+import parseHtmlConf from './parse-elem-html'
+import processRecordMenu from './menu/ProcessRecordMenu'
+
+// 鍙弬鑰� wangEditor 瀹樻柟鏂囨。杩涜鑷畾涔夋墿灞曟彃浠讹細https://www.wangeditor.com/v5/development.html#%E5%AE%9A%E4%B9%89%E6%96%B0%E5%85%83%E7%B4%A0
+const module: Partial<IModuleConf> = {
+ editorPlugin: withProcessRecord,
+ renderElems: [renderElemConf],
+ elemsToHtml: [elemToHtmlConf],
+ parseElemsHtml: [parseHtmlConf],
+ menus: [processRecordMenu]
+}
+
+export default module
diff --git a/src/views/bpm/model/form/PrintTemplate/module/menu/ProcessRecordMenu.ts b/src/views/bpm/model/form/PrintTemplate/module/menu/ProcessRecordMenu.ts
new file mode 100644
index 0000000..88d0671
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/menu/ProcessRecordMenu.ts
@@ -0,0 +1,42 @@
+import { IButtonMenu, IDomEditor } from '@wangeditor-next/editor'
+
+class ProcessRecordMenu implements IButtonMenu {
+ readonly tag: string
+ readonly title: string
+
+ constructor() {
+ this.title = '娴佺▼璁板綍'
+ this.tag = 'button'
+ }
+
+ getValue(_editor: IDomEditor): string {
+ return ''
+ }
+
+ isActive(_editor: IDomEditor): boolean {
+ return false
+ }
+
+ isDisabled(_editor: IDomEditor): boolean {
+ return false
+ }
+
+ exec(editor: IDomEditor, _value: string) {
+ if (this.isDisabled(editor)) return
+ const processRecordElem = {
+ type: 'process-record',
+ children: [{ text: '' }]
+ }
+ editor.insertNode(processRecordElem)
+ editor.move(1)
+ }
+}
+
+const ProcessRecordMenuConf = {
+ key: 'ProcessRecordMenu',
+ factory() {
+ return new ProcessRecordMenu()
+ }
+}
+
+export default ProcessRecordMenuConf
diff --git a/src/views/bpm/model/form/PrintTemplate/module/parse-elem-html.ts b/src/views/bpm/model/form/PrintTemplate/module/parse-elem-html.ts
new file mode 100644
index 0000000..e57336f
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/parse-elem-html.ts
@@ -0,0 +1,33 @@
+import { DOMElement } from './utils/dom'
+import { IDomEditor, SlateDescendant, SlateElement } from '@wangeditor-next/editor'
+
+/**
+ * 瑙f瀽 HTML 瀛楃涓诧紝鐢熸垚鈥滈檮浠垛�濆厓绱�
+ * @param domElem HTML 瀵瑰簲鐨� DOM Element
+ * @param children 瀛愯妭鐐�
+ * @param editor editor 瀹炰緥
+ * @returns 鈥滈檮浠垛�濆厓绱狅紝濡備笂鏂囩殑 myResume
+ */
+function parseHtml(
+ _domElem: DOMElement,
+ _children: SlateDescendant[],
+ _editor: IDomEditor
+): SlateElement {
+ // TS 璇硶
+
+
+ // 鐢熸垚鈥滄祦绋嬭褰曗�濆厓绱狅紙鎸夌収姝ゅ墠绾﹀畾鐨勬暟鎹粨鏋勶級
+ const processRecord = {
+ type: 'process-record',
+ children: [{ text: '' }], // void node 蹇呴』鏈� children 锛屽叾涓湁涓�涓┖瀛楃涓诧紝閲嶈锛侊紒锛�
+ }
+
+ return processRecord
+}
+
+const parseHtmlConf = {
+ selector: 'span[data-w-e-type="process-record"]',
+ parseElemHtml: parseHtml
+}
+
+export default parseHtmlConf
diff --git a/src/views/bpm/model/form/PrintTemplate/module/plugin.ts b/src/views/bpm/model/form/PrintTemplate/module/plugin.ts
new file mode 100644
index 0000000..6379ea1
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/plugin.ts
@@ -0,0 +1,28 @@
+import { DomEditor, IDomEditor } from '@wangeditor-next/editor'
+
+function withProcessRecord<T extends IDomEditor>(editor: T) {
+ const { isInline, isVoid } = editor
+ const newEditor = editor
+
+ newEditor.isInline = (elem) => {
+ const type = DomEditor.getNodeType(elem)
+ if (type === 'process-record') {
+ return true
+ }
+
+ return isInline(elem)
+ }
+
+ newEditor.isVoid = (elem) => {
+ const type = DomEditor.getNodeType(elem)
+ if (type === 'process-record') {
+ return true
+ }
+
+ return isVoid(elem)
+ }
+
+ return newEditor
+}
+
+export default withProcessRecord
diff --git a/src/views/bpm/model/form/PrintTemplate/module/render-elem.ts b/src/views/bpm/model/form/PrintTemplate/module/render-elem.ts
new file mode 100644
index 0000000..1f3db96
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/render-elem.ts
@@ -0,0 +1,73 @@
+import { h, VNode } from 'snabbdom'
+import { DomEditor, IDomEditor, SlateElement } from '@wangeditor-next/editor'
+
+function renderProcessRecord(
+ elem: SlateElement,
+ _children: VNode[] | null,
+ editor: IDomEditor
+): VNode {
+ const selected = DomEditor.isNodeSelected(editor, elem)
+
+ return h(
+ 'table',
+ {
+ props: {
+ contentEditable: false
+ },
+ style: {
+ width: '100%',
+ border: selected ? '2px solid var(--w-e-textarea-selected-border-color)' : ''
+ }
+ },
+ [
+ h('thead', [h('tr', [h('th', { attrs: { colSpan: 3 } }, '娴佺▼璁板綍')])]),
+ h('tbody', [
+ h('tr', [
+ h('td', [
+ h(
+ 'span',
+ {
+ props: {
+ contentEditable: false
+ },
+ style: {
+ marginLeft: '3px',
+ marginRight: '3px',
+ backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
+ borderRadius: '3px',
+ padding: '0 3px'
+ }
+ },
+ `鑺傜偣`
+ )
+ ]),
+ h('td', [
+ h(
+ 'span',
+ {
+ props: {
+ contentEditable: false
+ },
+ style: {
+ marginLeft: '3px',
+ marginRight: '3px',
+ backgroundColor: 'var(--w-e-textarea-slight-bg-color)',
+ borderRadius: '3px',
+ padding: '0 3px'
+ }
+ },
+ `鎿嶄綔`
+ )
+ ])
+ ])
+ ])
+ ]
+ )
+}
+
+const conf = {
+ type: 'process-record',
+ renderElem: renderProcessRecord
+}
+
+export default conf
diff --git a/src/views/bpm/model/form/PrintTemplate/module/utils/dom.ts b/src/views/bpm/model/form/PrintTemplate/module/utils/dom.ts
new file mode 100644
index 0000000..89ab553
--- /dev/null
+++ b/src/views/bpm/model/form/PrintTemplate/module/utils/dom.ts
@@ -0,0 +1,21 @@
+import $, { append, on, hide, click } from 'dom7'
+
+if (hide) $.fn.hide = hide
+if (append) $.fn.append = append
+if (click) $.fn.click = click
+if (on) $.fn.on = on
+
+export { Dom7Array } from 'dom7'
+export default $
+
+// COMPAT: This is required to prevent TypeScript aliases from doing some very
+// weird things for Slate's types with the same name as globals. (2019/11/27)
+// https://github.com/microsoft/TypeScript/issues/35002
+import DOMNode = globalThis.Node
+import DOMComment = globalThis.Comment
+import DOMElement = globalThis.Element
+import DOMText = globalThis.Text
+import DOMRange = globalThis.Range
+import DOMSelection = globalThis.Selection
+import DOMStaticRange = globalThis.StaticRange
+export { DOMNode, DOMComment, DOMElement, DOMText, DOMRange, DOMSelection, DOMStaticRange }
diff --git a/src/views/bpm/model/form/ProcessDesign.vue b/src/views/bpm/model/form/ProcessDesign.vue
new file mode 100644
index 0000000..172e042
--- /dev/null
+++ b/src/views/bpm/model/form/ProcessDesign.vue
@@ -0,0 +1,72 @@
+<template>
+ <!-- BPMN璁捐鍣� -->
+ <template v-if="modelData.type === BpmModelType.BPMN">
+ <BpmModelEditor
+ v-if="showDesigner"
+ :model-id="modelData.id"
+ :model-key="modelData.key"
+ :model-name="modelData.name"
+ @success="handleDesignSuccess"
+ />
+ </template>
+
+ <!-- Simple璁捐鍣� -->
+ <template v-else>
+ <SimpleModelDesign
+ v-if="showDesigner"
+ :model-name="modelData.name"
+ :model-form-id="modelData.formId"
+ :model-form-type="modelData.formType"
+ :start-user-ids="modelData.startUserIds"
+ :start-dept-ids="modelData.startDeptIds"
+ @success="handleDesignSuccess"
+ />
+ </template>
+</template>
+
+<script lang="ts" setup>
+import { BpmModelType } from '@/utils/constants'
+import BpmModelEditor from './editor/index.vue'
+import SimpleModelDesign from '../../simple/SimpleModelDesign.vue'
+
+// 鍒涘缓鏈湴鏁版嵁鍓湰
+const modelData = defineModel<any>()
+
+const processData = inject('processData') as Ref
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = async () => {
+ try {
+ // 鑾峰彇鏈�鏂扮殑娴佺▼鏁版嵁
+ if (!processData.value) {
+ throw new Error('璇疯璁℃祦绋�')
+ }
+ return true
+ } catch (error) {
+ throw error
+ }
+}
+/** 澶勭悊璁捐鍣ㄤ繚瀛樻垚鍔� */
+const handleDesignSuccess = async (data?: any) => {
+ if (data) {
+ // 鍒涘缓鏂扮殑瀵硅薄浠ヨЕ鍙戝搷搴斿紡鏇存柊
+ const newModelData = {
+ ...modelData.value,
+ bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null,
+ simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data
+ }
+ // 浣跨敤emit鏇存柊鐖剁粍浠剁殑鏁版嵁
+ await nextTick()
+ //鏇存柊琛ㄥ崟鐨勬ā鍨嬫暟鎹儴鍒�
+ modelData.value = newModelData
+ }
+}
+
+/** 鏄惁鏄剧ず璁捐鍣� */
+const showDesigner = computed(() => {
+ return Boolean(modelData.value?.key && modelData.value?.name)
+})
+defineExpose({
+ validate
+})
+</script>
diff --git a/src/views/bpm/model/form/editor/index.vue b/src/views/bpm/model/form/editor/index.vue
new file mode 100644
index 0000000..93c7261
--- /dev/null
+++ b/src/views/bpm/model/form/editor/index.vue
@@ -0,0 +1,124 @@
+<template>
+ <ContentWrap>
+ <!-- 娴佺▼璁捐鍣紝璐熻矗缁樺埗娴佺▼绛� -->
+ <MyProcessDesigner
+ key="designer"
+ v-model="xmlString"
+ :value="xmlString"
+ v-bind="controlForm"
+ keyboard
+ ref="processDesigner"
+ @init-finished="initModeler"
+ :additionalModel="controlForm.additionalModel"
+ :model="model"
+ @save="save"
+ :process-id="modelKey"
+ :process-name="modelName"
+ />
+ <!-- 娴佺▼灞炴�у櫒锛岃礋璐g紪杈戞瘡涓祦绋嬭妭鐐圭殑灞炴�� -->
+ <MyProcessPenal
+ v-if="modeler"
+ key="penal"
+ :bpmnModeler="modeler"
+ :prefix="controlForm.prefix"
+ class="process-panel"
+ :model="model"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { MyProcessDesigner, MyProcessPenal } from '@/components/bpmnProcessDesigner/package'
+// 鑷畾涔夊厓绱犻�変腑鏃剁殑寮瑰嚭鑿滃崟锛堜慨鏀� 榛樿浠诲姟 涓� 鐢ㄦ埛浠诲姟锛�
+import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/content-pad'
+// 鑷畾涔夊乏渚ц彍鍗曪紙淇敼 榛樿浠诲姟 涓� 鐢ㄦ埛浠诲姟锛�
+import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette'
+import * as ModelApi from '@/api/bpm/model'
+import { BpmModelFormType } from '@/utils/constants'
+import * as FormApi from '@/api/bpm/form'
+
+defineOptions({ name: 'BpmModelEditor' })
+
+defineProps<{
+ modelId?: string
+ modelKey: string
+ modelName: string
+ value?: string
+}>()
+
+const emit = defineEmits(['success', 'init-finished'])
+const message = useMessage() // 鍥介檯鍖�
+
+// 琛ㄥ崟淇℃伅
+const formFields = ref<string[]>([])
+// 琛ㄥ崟绫诲瀷锛屾殏浠呴檺娴佺▼琛ㄥ崟
+const formType = ref(BpmModelFormType.NORMAL)
+provide('formFields', formFields)
+provide('formType', formType)
+
+// 娉ㄥ叆娴佺▼鏁版嵁
+const xmlString = inject('processData') as Ref
+// 娉ㄥ叆妯″瀷鏁版嵁
+const modelData = inject('modelData') as Ref
+
+const modeler = shallowRef() // BPMN Modeler
+const processDesigner = ref()
+const controlForm = ref({
+ simulation: true,
+ labelEditing: false,
+ labelVisible: false,
+ prefix: 'flowable',
+ headerButtonSize: 'mini',
+ additionalModel: [CustomContentPadProvider, CustomPaletteProvider]
+})
+const model = ref<ModelApi.ModelVO>() // 娴佺▼妯″瀷鐨勪俊鎭�
+
+/** 鍒濆鍖� modeler */
+const initModeler = async (item: any) => {
+ // 鍏堝垵濮嬪寲妯″瀷鏁版嵁
+ model.value = modelData.value
+ modeler.value = item
+}
+
+/** 娣诲姞/淇敼妯″瀷 */
+const save = async (bpmnXml: string) => {
+ try {
+ xmlString.value = bpmnXml
+ emit('success', bpmnXml)
+ } catch (error) {
+ console.error('淇濆瓨澶辫触:', error)
+ message.error('淇濆瓨澶辫触')
+ }
+}
+
+/** 鐩戝惉琛ㄥ崟 ID 鍙樺寲锛屽姞杞借〃鍗曟暟鎹� */
+watch(
+ () => modelData.value.formId,
+ async (newFormId) => {
+ if (newFormId && modelData.value.formType === BpmModelFormType.NORMAL) {
+ const data = await FormApi.getForm(newFormId)
+ formFields.value = data.fields
+ } else {
+ formFields.value = []
+ }
+ },
+ { immediate: true }
+)
+
+// 鍦ㄧ粍浠跺嵏杞芥椂娓呯悊
+onBeforeUnmount(() => {
+ modeler.value = null
+ // 娓呯悊鍏ㄥ眬瀹炰緥
+ const w = window as any
+ if (w.bpmnInstances) {
+ w.bpmnInstances = null
+ }
+})
+</script>
+<style lang="scss">
+.process-panel__container {
+ position: absolute;
+ top: 172px;
+ right: 70px;
+}
+</style>
diff --git a/src/views/bpm/model/form/index.vue b/src/views/bpm/model/form/index.vue
new file mode 100644
index 0000000..9974f08
--- /dev/null
+++ b/src/views/bpm/model/form/index.vue
@@ -0,0 +1,456 @@
+<template>
+ <ContentWrap>
+ <div class="mx-auto">
+ <!-- 澶撮儴瀵艰埅鏍� -->
+ <div
+ class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+ >
+ <!-- 宸︿晶鏍囬 -->
+ <div class="w-200px flex items-center overflow-hidden">
+ <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="handleBack" />
+ <span class="ml-10px text-16px truncate" :title="formData.name || '鍒涘缓娴佺▼'">
+ {{ formData.name || '鍒涘缓娴佺▼' }}
+ </span>
+ </div>
+
+ <!-- 姝ラ鏉� -->
+ <div class="flex-1 flex items-center justify-center h-full">
+ <div class="w-400px flex items-center justify-between h-full">
+ <div
+ v-for="(step, index) in steps"
+ :key="index"
+ class="flex items-center cursor-pointer mx-15px relative h-full"
+ :class="[
+ currentStep === index
+ ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+ : 'text-gray-500'
+ ]"
+ @click="handleStepClick(index)"
+ >
+ <div
+ class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+ :class="[
+ currentStep === index
+ ? 'bg-[#3473ff] text-white border-[#3473ff]'
+ : 'border-gray-300 bg-white text-gray-500'
+ ]"
+ >
+ {{ index + 1 }}
+ </div>
+ <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍙充晶鎸夐挳 -->
+ <div class="w-200px flex items-center justify-end gap-2">
+ <el-button v-if="actionType === 'update'" type="success" @click="handleDeploy">
+ 鍙� 甯�
+ </el-button>
+ <el-button type="primary" @click="handleSave">
+ <span v-if="actionType === 'definition'">鎭� 澶�</span>
+ <span v-else>淇� 瀛�</span>
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 涓讳綋鍐呭 -->
+ <div class="mt-50px">
+ <!-- 绗竴姝ワ細鍩烘湰淇℃伅 -->
+ <div v-if="currentStep === 0" class="mx-auto w-560px">
+ <BasicInfo
+ v-model="formData"
+ :categoryList="categoryList"
+ :userList="userList"
+ :deptList="deptList"
+ ref="basicInfoRef"
+ />
+ </div>
+
+ <!-- 绗簩姝ワ細琛ㄥ崟璁捐 -->
+ <div v-if="currentStep === 1" class="mx-auto w-560px">
+ <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" />
+ </div>
+
+ <!-- 绗笁姝ワ細娴佺▼璁捐 -->
+ <ProcessDesign v-if="currentStep === 2" v-model="formData" ref="processDesignRef" />
+
+ <!-- 绗洓姝ワ細鏇村璁剧疆 -->
+ <div v-show="currentStep === 3" class="mx-auto w-700px">
+ <ExtraSettings ref="extraSettingsRef" v-model="formData" />
+ </div>
+ </div>
+ </div>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { useRoute, useRouter } from 'vue-router'
+import { useMessage } from '@/hooks/web/useMessage'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { BpmModelFormType, BpmModelType, BpmAutoApproveType } from '@/utils/constants'
+import BasicInfo from './BasicInfo.vue'
+import FormDesign from './FormDesign.vue'
+import ProcessDesign from './ProcessDesign.vue'
+import ExtraSettings from './ExtraSettings.vue'
+import { useTagsView } from '@/hooks/web/useTagsView'
+
+const router = useRouter()
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const tagsView = useTagsView()
+const route = useRoute()
+const message = useMessage()
+const userStore = useUserStoreWithOut()
+
+// 缁勪欢寮曠敤
+const basicInfoRef = ref()
+const formDesignRef = ref()
+const processDesignRef = ref()
+const extraSettingsRef = ref()
+
+/** 姝ラ鏍¢獙鍑芥暟 */
+const validateBasic = async () => {
+ await basicInfoRef.value?.validate()
+}
+
+/** 琛ㄥ崟璁捐鏍¢獙 */
+const validateForm = async () => {
+ await formDesignRef.value?.validate()
+}
+
+/** 娴佺▼璁捐鏍¢獙 */
+const validateProcess = async () => {
+ await processDesignRef.value?.validate()
+}
+
+const currentStep = ref(-1) // 姝ラ鎺у埗銆�-1 鐢ㄤ簬锛屼竴寮�濮嬪叏閮ㄤ笉灞曠ず绛夊綋鍓嶉〉闈㈡暟鎹垵濮嬪寲瀹屾垚
+
+const steps = [
+ { title: '鍩烘湰淇℃伅', validator: validateBasic },
+ { title: '琛ㄥ崟璁捐', validator: validateForm },
+ { title: '娴佺▼璁捐', validator: validateProcess },
+ { title: '鏇村璁剧疆', validator: null }
+]
+
+// 琛ㄥ崟鏁版嵁
+const formData: any = ref({
+ id: undefined,
+ name: '',
+ key: '',
+ category: undefined,
+ icon: undefined,
+ description: '',
+ type: BpmModelType.BPMN,
+ formType: BpmModelFormType.NORMAL,
+ formId: '',
+ formCustomCreatePath: '',
+ formCustomViewPath: '',
+ visible: true,
+ startUserType: undefined,
+ startUserIds: [],
+ startDeptIds: [],
+ managerUserIds: [],
+ allowCancelRunningProcess: true,
+ processIdRule: {
+ enable: false,
+ prefix: '',
+ infix: '',
+ postfix: '',
+ length: 5
+ },
+ autoApprovalType: BpmAutoApproveType.NONE,
+ titleSetting: {
+ enable: false,
+ title: ''
+ },
+ summarySetting: {
+ enable: false,
+ summary: []
+ },
+ allowWithdrawTask: false,
+ printTemplateSetting: {
+ enable: false
+ }
+})
+
+// 娴佺▼鏁版嵁
+const processData = ref<any>()
+
+provide('processData', processData)
+provide('modelData', formData)
+
+// 鏁版嵁鍒楄〃
+const formList = ref([])
+const categoryList = ref<CategoryVO[]>([])
+const userList = ref<UserApi.UserVO[]>([])
+const deptList = ref<DeptApi.DeptVO[]>([])
+
+/** 鍒濆鍖栨暟鎹� */
+const actionType = route.params.type as string
+const initData = async () => {
+ if (actionType === 'definition') {
+ // 鎯呭喌涓�锛氭祦绋嬪畾涔夊満鏅紙鎭㈠锛�
+ const definitionId = route.params.id as string
+ const data = await DefinitionApi.getProcessDefinition(definitionId)
+ // 灏� definition => model锛屾渶缁堣祴鍊�
+ data.type = data.modelType
+ delete data.modelType
+ data.id = data.modelId
+ delete data.modelId
+ if (data.simpleModel) {
+ data.simpleModel = JSON.parse(data.simpleModel)
+ }
+ formData.value = data
+ formData.value.startUserType =
+ formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+ } else if (['update', 'copy'].includes(actionType)) {
+ // 鎯呭喌浜岋細淇敼鍦烘櫙/澶嶅埗鍦烘櫙
+ const modelId = route.params.id as string
+ formData.value = await ModelApi.getModel(modelId)
+ formData.value.startUserType =
+ formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+
+ // 鐗规畩锛氬鍒跺満鏅�
+ if (route.params.type === 'copy') {
+ delete formData.value.id
+ if (formData.value.bpmnXml) {
+ formData.value.bpmnXml = formData.value.bpmnXml.replaceAll(
+ formData.value.name,
+ formData.value.name + '鍓湰'
+ )
+ formData.value.bpmnXml = formData.value.bpmnXml.replaceAll(
+ formData.value.key,
+ formData.value.key + '_copy'
+ )
+ }
+ formData.value.name += '鍓湰'
+ formData.value.key += '_copy'
+ tagsView.setTitle('澶嶅埗娴佺▼')
+ }
+ } else {
+ // 鎯呭喌涓夛細鏂板鍦烘櫙
+ formData.value.startUserType = 0 // 鍏ㄤ綋
+ formData.value.managerUserIds.push(userStore.getUser.id)
+ }
+
+ // 鑾峰彇琛ㄥ崟鍒楄〃
+ formList.value = await FormApi.getFormSimpleList()
+ // 鑾峰彇鍒嗙被鍒楄〃
+ categoryList.value = await CategoryApi.getCategorySimpleList()
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ deptList.value = await DeptApi.getSimpleDeptList()
+
+ // 鏈�缁堬紝璁剧疆 currentStep 鍒囨崲鍒扮涓�姝�
+ currentStep.value = 0
+
+ // 鍏煎锛屼互鍓嶆湭閰嶇疆鏇村璁剧疆鐨勬祦绋�
+ extraSettingsRef.value.initData()
+}
+
+/** 鏍规嵁绫诲瀷鍒囨崲娴佺▼鏁版嵁 */
+watch(
+ async () => formData.value.type,
+ () => {
+ if (formData.value.type === BpmModelType.BPMN) {
+ processData.value = formData.value.bpmnXml
+ } else if (formData.value.type === BpmModelType.SIMPLE) {
+ processData.value = formData.value.simpleModel
+ }
+ console.log('鍔犺浇娴佺▼鏁版嵁', processData.value)
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 鏍¢獙鎵�鏈夋楠ゆ暟鎹槸鍚﹀畬鏁� */
+const validateAllSteps = async () => {
+ try {
+ // 鍩烘湰淇℃伅鏍¢獙
+ try {
+ await validateBasic()
+ } catch (error) {
+ currentStep.value = 0
+ throw new Error('璇峰畬鍠勫熀鏈俊鎭�')
+ }
+
+ // 琛ㄥ崟璁捐鏍¢獙
+ try {
+ await validateForm()
+ } catch (error) {
+ currentStep.value = 1
+ throw new Error('璇峰畬鍠勮嚜瀹氫箟琛ㄥ崟淇℃伅')
+ }
+
+ // 娴佺▼璁捐鏍¢獙
+
+ // 琛ㄥ崟璁捐鏍¢獙
+ try {
+ await validateProcess()
+ } catch (error) {
+ currentStep.value = 2
+ throw new Error('璇疯璁℃祦绋�')
+ }
+
+ return true
+ } catch (error) {
+ throw error
+ }
+}
+
+/** 淇濆瓨鎿嶄綔 */
+const handleSave = async () => {
+ try {
+ // 淇濆瓨鍓嶆牎楠屾墍鏈夋楠ょ殑鏁版嵁
+ await validateAllSteps()
+
+ // 鏇存柊琛ㄥ崟鏁版嵁
+ const modelData = {
+ ...formData.value
+ }
+
+ if (actionType === 'definition') {
+ // 鎯呭喌涓�锛氭祦绋嬪畾涔夊満鏅紙鎭㈠锛�
+ await ModelApi.updateModel(modelData)
+ // 鎻愮ず鎴愬姛
+ message.success('鎭㈠鎴愬姛锛屽彲鐐瑰嚮銆愬彂甯冦�戞寜閽紝杩涜鍙戝竷妯″瀷')
+ } else if (actionType === 'update') {
+ // 淇敼鍦烘櫙
+ await ModelApi.updateModel(modelData)
+ // 鎻愮ず鎴愬姛
+ message.success('淇敼鎴愬姛锛屽彲鐐瑰嚮銆愬彂甯冦�戞寜閽紝杩涜鍙戝竷妯″瀷')
+ } else if (actionType === 'copy') {
+ // 鎯呭喌涓夛細澶嶅埗鍦烘櫙
+ formData.value.id = await ModelApi.createModel(modelData)
+ // 鎻愮ず鎴愬姛
+ message.success('澶嶅埗鎴愬姛锛屽彲鐐瑰嚮銆愬彂甯冦�戞寜閽紝杩涜鍙戝竷妯″瀷')
+ } else {
+ // 鎯呭喌鍥涳細鏂板鍦烘櫙
+ formData.value.id = await ModelApi.createModel(modelData)
+ // 鎻愮ず鎴愬姛
+ message.success('鏂板缓鎴愬姛锛屽彲鐐瑰嚮銆愬彂甯冦�戞寜閽紝杩涜鍙戝竷妯″瀷')
+ }
+
+ // 杩斿洖鍒楄〃椤碉紙鎺掗櫎鏇存柊鐨勬儏鍐碉級
+ if (actionType !== 'update') {
+ await router.push({ name: 'BpmModel' })
+ }
+ } catch (error: any) {
+ console.error('淇濆瓨澶辫触:', error)
+ message.warning(error.message || '璇峰畬鍠勬墍鏈夋楠ょ殑蹇呭~淇℃伅')
+ }
+}
+
+/** 鍙戝竷鎿嶄綔 */
+const handleDeploy = async () => {
+ try {
+ // 淇敼鍦烘櫙涓嬬洿鎺ュ彂甯冿紝鏂板鍦烘櫙涓嬮渶瑕佸厛纭
+ if (!formData.value.id) {
+ await message.confirm('鏄惁纭鍙戝竷璇ユ祦绋嬶紵')
+ }
+ // 鏍¢獙鎵�鏈夋楠�
+ await validateAllSteps()
+
+ // 鏇存柊琛ㄥ崟鏁版嵁
+ const modelData = {
+ ...formData.value
+ }
+
+ // 鍏堜繚瀛樻墍鏈夋暟鎹�
+ if (formData.value.id) {
+ await ModelApi.updateModel(modelData)
+ } else {
+ const result = await ModelApi.createModel(modelData)
+ formData.value.id = result.id
+ }
+
+ // 鍙戝竷
+ await ModelApi.deployModel(formData.value.id)
+ message.success('鍙戝竷鎴愬姛')
+ // 杩斿洖鍒楄〃椤�
+ await router.push({ name: 'BpmModel' })
+ } catch (error: any) {
+ console.error('鍙戝竷澶辫触:', error)
+ message.warning(error.message || '鍙戝竷澶辫触')
+ }
+}
+
+/** 姝ラ鍒囨崲澶勭悊 */
+const handleStepClick = async (index: number) => {
+ try {
+ if (index !== 0) {
+ await validateBasic()
+ }
+ if (index !== 1) {
+ await validateForm()
+ }
+ if (index !== 2) {
+ await validateProcess()
+ }
+
+ // 鍒囨崲姝ラ
+ currentStep.value = index
+
+ // 濡傛灉鍒囨崲鍒版祦绋嬭璁℃楠わ紝绛夊緟缁勪欢娓叉煋瀹屾垚鍚庡埛鏂拌璁″櫒
+ if (index === 2) {
+ await nextTick()
+ // 绛夊緟鏇撮暱鏃堕棿纭繚缁勪欢瀹屽叏鍒濆鍖�
+ await new Promise((resolve) => setTimeout(resolve, 200))
+ if (processDesignRef.value?.refresh) {
+ await processDesignRef.value.refresh()
+ }
+ }
+ } catch (error) {
+ console.error('姝ラ鍒囨崲澶辫触:', error)
+ message.warning('璇峰厛瀹屽杽褰撳墠姝ラ蹇呭~淇℃伅')
+ }
+}
+
+/** 杩斿洖鍒楄〃椤� */
+const handleBack = () => {
+ // 鍏堝垹闄ゅ綋鍓嶉〉绛�
+ delView(unref(router.currentRoute))
+ // 璺宠浆鍒板垪琛ㄩ〉
+ router.push({ name: 'BpmModel' })
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await initData()
+})
+
+// 娣诲姞缁勪欢鍗歌浇鍓嶇殑娓呯悊浠g爜
+onBeforeUnmount(() => {
+ // 娓呯悊鎵�鏈夌殑寮曠敤
+ basicInfoRef.value = null
+ formDesignRef.value = null
+ processDesignRef.value = null
+})
+</script>
+
+<style lang="scss" scoped>
+.border-bottom {
+ border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+ color: #3473ff;
+}
+
+.bg-primary {
+ background-color: #3473ff;
+}
+
+.border-primary {
+ border-color: #3473ff;
+}
+</style>
diff --git a/src/views/bpm/model/index.vue b/src/views/bpm/model/index.vue
new file mode 100644
index 0000000..d272dcd
--- /dev/null
+++ b/src/views/bpm/model/index.vue
@@ -0,0 +1,228 @@
+<template>
+ <ContentWrap>
+ <div class="flex justify-between pl-20px items-center">
+ <h3 class="font-extrabold">娴佺▼妯″瀷</h3>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ v-if="!isCategorySorting"
+ class="-mb-15px flex mr-10px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ @submit.prevent
+ >
+ <el-form-item prop="name" class="ml-auto">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="鎼滅储娴佺▼"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ >
+ <template #prefix>
+ <Icon icon="ep:search" class="mx-10px" />
+ </template>
+ </el-input>
+ </el-form-item>
+ <!-- 鍙充笂瑙掞細鏂板缓妯″瀷銆佹洿澶氭搷浣� -->
+ <el-form-item>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['bpm:model:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板缓妯″瀷
+ </el-button>
+ </el-form-item>
+ <el-form-item>
+ <el-dropdown @command="(command) => handleCommand(command)" placement="bottom-end">
+ <el-button class="w-30px" plain>
+ <Icon icon="ep:setting" />
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleCategoryAdd">
+ <Icon icon="ep:circle-plus" :size="13" class="mr-5px" />
+ 鏂板缓鍒嗙被
+ </el-dropdown-item>
+ <el-dropdown-item command="handleCategorySort">
+ <Icon icon="fa:sort-amount-desc" :size="13" class="mr-5px" />
+ 鍒嗙被鎺掑簭
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </el-form-item>
+ </el-form>
+ <div class="mr-20px" v-else>
+ <el-button @click="handleCategorySortCancel"> 鍙� 娑� </el-button>
+ <el-button type="primary" @click="handleCategorySortSubmit"> 淇濆瓨鎺掑簭 </el-button>
+ </div>
+ </div>
+
+ <el-divider />
+
+ <!-- 鎸夌収鍒嗙被锛屽睍绀哄叾鎵�灞炵殑妯″瀷鍒楄〃 -->
+ <div class="px-15px">
+ <draggable
+ :disabled="!isCategorySorting"
+ v-model="categoryGroup"
+ item-key="id"
+ :animation="400"
+ >
+ <template #item="{ element }">
+ <ContentWrap
+ class="rounded-lg transition-all duration-300 ease-in-out hover:shadow-xl"
+ v-loading="loading"
+ :body-style="{ padding: 0 }"
+ :key="element.id"
+ >
+ <CategoryDraggableModel
+ :isCategorySorting="isCategorySorting"
+ :categoryInfo="element"
+ @success="getList"
+ />
+ </ContentWrap>
+ </template>
+ </draggable>
+ </div>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔犲垎绫� -->
+ <CategoryForm ref="categoryFormRef" @success="getList" />
+ <!-- 寮圭獥锛氳〃鍗曡鎯� -->
+ <Dialog title="琛ㄥ崟璇︽儏" v-model="formDetailVisible" width="800">
+ <form-create :rule="formDetailPreview.rule" :option="formDetailPreview.option" />
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import draggable from 'vuedraggable'
+import { CategoryApi } from '@/api/bpm/category'
+import * as ModelApi from '@/api/bpm/model'
+import CategoryForm from '../category/CategoryForm.vue'
+import { cloneDeep } from 'lodash-es'
+import CategoryDraggableModel from './CategoryDraggableModel.vue'
+
+defineOptions({ name: 'BpmModel' })
+
+const { push } = useRouter()
+const message = useMessage() // 娑堟伅寮圭獥
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const isCategorySorting = ref(false) // 鏄惁 category 姝e浜庢帓搴忕姸鎬�
+const queryParams = reactive({
+ name: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const categoryGroup: any = ref([]) // 鎸夌収 category 鍒嗙粍鐨勬暟鎹�
+const originalData: any = ref([]) // 鍘熷鏁版嵁
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const openForm = (type: string, id?: number) => {
+ if (type === 'create') {
+ push({ name: 'BpmModelCreate' })
+ } else {
+ push({
+ name: 'BpmModelUpdate',
+ params: { id }
+ })
+ }
+}
+
+/** 娴佺▼琛ㄥ崟鐨勮鎯呮寜閽搷浣� */
+const formDetailVisible = ref(false)
+const formDetailPreview = ref({
+ rule: [],
+ option: {}
+})
+
+/** 鍙充笂瑙掕缃寜閽� */
+const handleCommand = (command: string) => {
+ switch (command) {
+ case 'handleCategoryAdd':
+ handleCategoryAdd()
+ break
+ case 'handleCategorySort':
+ handleCategorySort()
+ break
+ default:
+ break
+ }
+}
+
+/** 鏂板缓鍒嗙被 */
+const categoryFormRef = ref()
+const handleCategoryAdd = () => {
+ categoryFormRef.value.open('create')
+}
+
+/** 鍒嗙被鎺掑簭鐨勬彁浜� */
+const handleCategorySort = () => {
+ // 淇濆瓨鍒濆鏁版嵁
+ originalData.value = cloneDeep(categoryGroup.value)
+ isCategorySorting.value = true
+}
+
+/** 鍒嗙被鎺掑簭鐨勫彇娑� */
+const handleCategorySortCancel = () => {
+ // 鎭㈠鍒濆鏁版嵁
+ categoryGroup.value = cloneDeep(originalData.value)
+ isCategorySorting.value = false
+}
+
+/** 鍒嗙被鎺掑簭鐨勪繚瀛� */
+const handleCategorySortSubmit = async () => {
+ // 淇濆瓨鎺掑簭
+ const ids = categoryGroup.value.map((item: any) => item.id)
+ await CategoryApi.updateCategorySortBatch(ids)
+ // 鍒锋柊鍒楄〃
+ isCategorySorting.value = false
+ message.success('鎺掑簭鍒嗙被鎴愬姛')
+ await getList()
+}
+
+/** 鍔犺浇鏁版嵁 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 鏌ヨ妯″瀷 + 鍒嗚鐨勫垪琛�
+ const modelList = await ModelApi.getModelList(queryParams.name)
+ const categoryList = await CategoryApi.getCategorySimpleList()
+ // 鎸夌収 category 鑱氬悎
+ // 娉ㄦ剰锛氬繀椤讳竴娆℃�ц祴鍊肩粰 categoryGroup锛屽惁鍒欐瘡娆℃搷浣滃悗锛屽垪琛ㄤ細閲嶆柊娓叉煋锛屾粴鍔ㄦ潯鐨勪綅缃細鍋忕锛侊紒锛�
+ categoryGroup.value = categoryList.map((category: any) => ({
+ ...category,
+ modelList: modelList.filter((model: any) => model.categoryName == category.name)
+ }))
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onActivated(() => {
+ getList()
+})
+</script>
+
+<style lang="scss" scoped>
+:deep() {
+ .el-table--fit .el-table__inner-wrapper::before {
+ height: 0;
+ }
+
+ .el-card {
+ border-radius: 8px;
+ }
+
+ .el-form--inline .el-form-item {
+ margin-right: 10px;
+ }
+
+ .el-divider--horizontal {
+ margin-top: 6px;
+ }
+}
+</style>
diff --git a/src/views/bpm/oa/leave/create.vue b/src/views/bpm/oa/leave/create.vue
new file mode 100644
index 0000000..b64f4ca
--- /dev/null
+++ b/src/views/bpm/oa/leave/create.vue
@@ -0,0 +1,257 @@
+<template>
+ <el-row :gutter="20">
+ <el-col :span="16">
+ <ContentWrap title="鐢宠淇℃伅">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="璇峰亣绫诲瀷" prop="type">
+ <el-select v-model="formData.type" clearable placeholder="璇烽�夋嫨璇峰亣绫诲瀷">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-date-picker
+ v-model="formData.startTime"
+ clearable
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ type="datetime"
+ value-format="x"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-date-picker
+ v-model="formData.endTime"
+ clearable
+ placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+ type="datetime"
+ value-format="x"
+ />
+ </el-form-item>
+ <el-form-item label="鍘熷洜" prop="reason">
+ <el-input v-model="formData.reason" placeholder="璇疯緭鍏ヨ鍋囧師鍥�" type="textarea" />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">
+ 纭� 瀹�
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ </el-col>
+
+ <!-- 瀹℃壒鐩稿叧锛氭祦绋嬩俊鎭� -->
+ <el-col :span="8">
+ <ContentWrap title="瀹℃壒娴佺▼" :bodyStyle="{ padding: '0 20px 0' }">
+ <ProcessInstanceTimeline
+ ref="timelineRef"
+ :activity-nodes="activityNodes"
+ :show-status-icon="false"
+ @select-user-confirm="selectUserConfirm"
+ />
+ </ContentWrap>
+ </el-col>
+ </el-row>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as LeaveApi from '@/api/bpm/leave'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 瀹℃壒鐩稿叧锛歩mport
+import * as DefinitionApi from '@/api/bpm/definition'
+import ProcessInstanceTimeline from '@/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CandidateStrategy, NodeId } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmOALeaveCreate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { push, currentRoute } = useRouter() // 璺敱
+const { query } = useRoute() // 鏌ヨ鍙傛暟
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ type: undefined,
+ reason: undefined,
+ startTime: undefined,
+ endTime: undefined
+})
+const formRules = reactive({
+ type: [{ required: true, message: '璇峰亣绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ reason: [{ required: true, message: '璇峰亣鍘熷洜涓嶈兘涓虹┖', trigger: 'change' }],
+ startTime: [{ required: true, message: '璇峰亣寮�濮嬫椂闂翠笉鑳戒负绌�', trigger: 'change' }],
+ endTime: [{ required: true, message: '璇峰亣缁撴潫鏃堕棿涓嶈兘涓虹┖', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// 瀹℃壒鐩稿叧锛氬彉閲�
+const processDefineKey = 'oa_leave' // 娴佺▼瀹氫箟 Key
+const startUserSelectTasks = ref([]) // 鍙戣捣浜洪渶瑕侀�夋嫨瀹℃壒浜虹殑鐢ㄦ埛浠诲姟鍒楄〃
+const startUserSelectAssignees = ref({}) // 鍙戣捣浜洪�夋嫨瀹℃壒浜虹殑鏁版嵁
+const tempStartUserSelectAssignees = ref({}) // 鍘嗗彶鍙戣捣浜洪�夋嫨瀹℃壒浜虹殑鏁版嵁锛岀敤浜庢瘡娆¤〃鍗曞彉鏇存椂锛屼复鏃朵繚瀛�
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 瀹℃壒鑺傜偣淇℃伅
+const processDefinitionId = ref('')
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 1.2 瀹℃壒鐩稿叧锛氭牎楠屾寚瀹氬鎵逛汉
+ if (startUserSelectTasks.value?.length > 0) {
+ for (const userTask of startUserSelectTasks.value) {
+ if (
+ Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+ startUserSelectAssignees.value[userTask.id].length === 0
+ ) {
+ return message.warning(`璇烽�夋嫨${userTask.name}鐨勫鎵逛汉`)
+ }
+ }
+ }
+
+ // 2. 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
+ // 瀹℃壒鐩稿叧锛氳缃寚瀹氬鎵逛汉
+ if (startUserSelectTasks.value?.length > 0) {
+ data.startUserSelectAssignees = startUserSelectAssignees.value
+ }
+ await LeaveApi.createLeave(data)
+ message.success('鍙戣捣鎴愬姛')
+ // 鍏抽棴褰撳墠 Tab
+ delView(unref(currentRoute))
+ await push({ name: 'BpmOALeave' })
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 瀹℃壒鐩稿叧锛氳幏鍙栧鎵硅鎯� */
+const getApprovalDetail = async () => {
+ try {
+ const data = await ProcessInstanceApi.getApprovalDetail({
+ processDefinitionId: processDefinitionId.value,
+ // TODO 灏忓寳锛氬彲浠ユ敮鎸� processDefinitionKey 鏌ヨ
+ activityId: NodeId.START_USER_NODE_ID,
+ processVariablesStr: JSON.stringify({ day: daysDifference() }) // 瑙e喅 GET 鏃犳硶浼犻�掑璞$殑闂锛屽悗绔� String 鍐嶈浆 JSON
+ })
+
+ if (!data) {
+ message.error('鏌ヨ涓嶅埌瀹℃壒璇︽儏淇℃伅锛�')
+ return
+ }
+ // 鑾峰彇瀹℃壒鑺傜偣锛屾樉绀� Timeline 鐨勬暟鎹�
+ activityNodes.value = data.activityNodes
+
+ // 鑾峰彇鍙戣捣浜鸿嚜閫夌殑浠诲姟
+ startUserSelectTasks.value = data.activityNodes?.filter(
+ (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+ )
+ // 鎭㈠涔嬪墠鐨勯�夋嫨瀹℃壒浜�
+ if (startUserSelectTasks.value?.length > 0) {
+ for (const node of startUserSelectTasks.value) {
+ if (
+ tempStartUserSelectAssignees.value[node.id] &&
+ tempStartUserSelectAssignees.value[node.id].length > 0
+ ) {
+ startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+ } else {
+ startUserSelectAssignees.value[node.id] = []
+ }
+ }
+ }
+ } finally {
+ }
+}
+
+/** 瀹℃壒鐩稿叧锛氶�夋嫨鍙戣捣浜� */
+const selectUserConfirm = (id: string, userList: any[]) => {
+ startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+// 璁$畻澶╂暟宸�
+// TODO @灏忓寳锛氬彲浠ユ悶鍒� formatTime 閲岄潰鍘伙紝鐒跺悗鐪嬬湅 dayjs 閲岄潰鏈夋病鏈夌幇鎴愮殑鏂规硶锛屾垨鑰呰緟鍔╄绠楃殑鏂规硶銆�
+const daysDifference = () => {
+ const oneDay = 24 * 60 * 60 * 1000 // 涓�澶╃殑姣鏁�
+ const diffTime = Math.abs(Number(formData.value.endTime) - Number(formData.value.startTime))
+ return Math.floor(diffTime / oneDay)
+}
+
+/** 鑾峰彇璇峰亣鏁版嵁锛岀敤浜庨噸鏂板彂璧锋椂鑷姩濉厖 */
+const getDetail = async (id: number) => {
+ try {
+ formLoading.value = true
+ const data = await LeaveApi.getLeave(id)
+ if (!data) {
+ message.error('閲嶆柊鍙戣捣璇峰亣澶辫触锛屽師鍥狅細璇峰亣鏁版嵁涓嶅瓨鍦�')
+ return
+ }
+ formData.value = {
+ type: data.type,
+ reason: data.reason,
+ startTime: data.startTime,
+ endTime: data.endTime
+ }
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ // TODO @灏忓寳锛氳繖閲屽彲浠ョ畝鍖栵紝缁熶竴閫氳繃 getApprovalDetail 澶勭悊涔堬紵
+ const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+ undefined,
+ processDefineKey
+ )
+
+ if (!processDefinitionDetail) {
+ message.error('OA 璇峰亣鐨勬祦绋嬫ā鍨嬫湭閰嶇疆锛岃妫�鏌ワ紒')
+ return
+ }
+ processDefinitionId.value = processDefinitionDetail.id
+ startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+
+ // 濡傛灉鏈変笟鍔$紪鍙凤紝璇存槑鏄噸鏂板彂璧凤紝闇�瑕佸姞杞藉師鏈夋暟鎹�
+ if (query.id) {
+ await getDetail(Number(query.id))
+ }
+
+ // 瀹℃壒鐩稿叧锛氬姞杞芥渶鏂扮殑瀹℃壒璇︽儏锛屼富瑕佺敤浜庤妭鐐归娴�
+ await getApprovalDetail()
+})
+
+/** 瀹℃壒鐩稿叧锛氶娴嬫祦绋嬭妭鐐逛細鍥犱负杈撳叆鐨勫弬鏁板�艰�屼骇鐢熸柊鐨勯娴嬬粨鏋滃�硷紝鎵�浠ラ渶閲嶆柊棰勬祴涓�娆�, formData.value鍙敼鎴愬疄闄呬笟鍔′腑鐨勭壒瀹氬瓧娈� */
+watch(
+ formData.value,
+ (newValue, oldValue) => {
+ if (!oldValue) {
+ return
+ }
+ if (newValue && Object.keys(newValue).length > 0) {
+ // 璁板綍涔嬪墠鐨勮妭鐐瑰鎵逛汉
+ tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+ startUserSelectAssignees.value = {}
+ // 鍔犺浇鏈�鏂扮殑瀹℃壒璇︽儏,涓昏鐢ㄤ簬鑺傜偣棰勬祴
+ getApprovalDetail()
+ }
+ },
+ {
+ immediate: true
+ }
+)
+</script>
diff --git a/src/views/bpm/oa/leave/detail.vue b/src/views/bpm/oa/leave/detail.vue
new file mode 100644
index 0000000..87036d8
--- /dev/null
+++ b/src/views/bpm/oa/leave/detail.vue
@@ -0,0 +1,51 @@
+<template>
+ <ContentWrap>
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="璇峰亣绫诲瀷">
+ <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="detailData.type" />
+ </el-descriptions-item>
+ <el-descriptions-item label="寮�濮嬫椂闂�">
+ {{ formatDate(detailData.startTime, 'YYYY-MM-DD') }}
+ </el-descriptions-item>
+ <el-descriptions-item label="缁撴潫鏃堕棿">
+ {{ formatDate(detailData.endTime, 'YYYY-MM-DD') }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍘熷洜">
+ {{ detailData.reason }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { propTypes } from '@/utils/propTypes'
+import * as LeaveApi from '@/api/bpm/leave'
+
+defineOptions({ name: 'BpmOALeaveDetail' })
+
+const { query } = useRoute() // 鏌ヨ鍙傛暟
+
+const props = defineProps({
+ id: propTypes.number.def(undefined)
+})
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref<any>({}) // 璇︽儏鏁版嵁
+const queryId = query.id as unknown as number // 浠� URL 浼犻�掕繃鏉ョ殑 id 缂栧彿
+
+/** 鑾峰緱鏁版嵁 */
+const getInfo = async () => {
+ detailLoading.value = true
+ try {
+ detailData.value = await LeaveApi.getLeave(props.id || queryId)
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open: getInfo }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getInfo()
+})
+</script>
diff --git a/src/views/bpm/oa/leave/index.vue b/src/views/bpm/oa/leave/index.vue
new file mode 100644
index 0000000..9fab923
--- /dev/null
+++ b/src/views/bpm/oa/leave/index.vue
@@ -0,0 +1,275 @@
+<template>
+ <doc-alert title="瀹℃壒鎺ュ叆锛堜笟鍔¤〃鍗曪級" url="https://doc.iocoder.cn/bpm/use-business-form/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="璇峰亣绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨璇峰亣绫诲瀷"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="瀹℃壒缁撴灉" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨瀹℃壒缁撴灉"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍘熷洜" prop="reason">
+ <el-input
+ v-model="queryParams.reason"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ師鍥�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button plain type="primary" @click="handleCreate()">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鍙戣捣璇峰亣
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="鐢宠缂栧彿" prop="id" />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�濮嬫椂闂�"
+ prop="startTime"
+ width="180"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ width="180"
+ />
+ <el-table-column align="center" label="璇峰亣绫诲瀷" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_OA_LEAVE_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍘熷洜" prop="reason" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鐢宠鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎿嶄綔" width="200">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['bpm:oa-leave:query']"
+ link
+ type="primary"
+ @click="handleDetail(scope.row)"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ v-hasPermi="['bpm:oa-leave:query']"
+ link
+ type="primary"
+ @click="handleProcessDetail(scope.row)"
+ >
+ 杩涘害
+ </el-button>
+ <el-button
+ v-if="scope.row.result === 1"
+ v-hasPermi="['bpm:oa-leave:create']"
+ link
+ type="danger"
+ @click="cancelLeave(scope.row)"
+ >
+ 鍙栨秷
+ </el-button>
+ <el-button
+ v-if="scope.row.status !== 1"
+ v-hasPermi="['bpm:oa-leave:create']"
+ link
+ type="primary"
+ @click="handleReCreate(scope.row)"
+ >
+ 閲嶆柊鍙戣捣
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as LeaveApi from '@/api/bpm/leave'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmOALeave' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const router = useRouter() // 璺敱
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: undefined,
+ status: undefined,
+ reason: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await LeaveApi.getLeavePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞鎿嶄綔 */
+const handleCreate = () => {
+ router.push({ name: 'OALeaveCreate' })
+}
+
+/** 閲嶆柊鍙戣捣鎿嶄綔 */
+const handleReCreate = (row: LeaveApi.LeaveVO) => {
+ router.push({
+ name: 'OALeaveCreate',
+ query: {
+ id: row.id
+ }
+ })
+}
+
+/** 璇︽儏鎿嶄綔 */
+const handleDetail = (row: LeaveApi.LeaveVO) => {
+ router.push({
+ name: 'OALeaveDetail',
+ query: {
+ id: row.id
+ }
+ })
+}
+
+/** 鍙栨秷璇峰亣鎿嶄綔 */
+const cancelLeave = async (row) => {
+ // 浜屾纭
+ const { value } = await ElMessageBox.prompt('璇疯緭鍏ュ彇娑堝師鍥�', '鍙栨秷娴佺▼', {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 鍒ゆ柇闈炵┖锛屼笖闈炵┖鏍�
+ inputErrorMessage: '鍙栨秷鍘熷洜涓嶈兘涓虹┖'
+ })
+ // 鍙戣捣鍙栨秷
+ await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
+ message.success('鍙栨秷鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+}
+
+/** 瀹℃壒杩涘害 */
+const handleProcessDetail = (row) => {
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.processInstanceId
+ }
+ })
+}
+
+watch(
+ () => router.currentRoute.value,
+ () => {
+ getList()
+ }
+)
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/processExpression/ProcessExpressionForm.vue b/src/views/bpm/processExpression/ProcessExpressionForm.vue
new file mode 100644
index 0000000..2e5ed2e
--- /dev/null
+++ b/src/views/bpm/processExpression/ProcessExpressionForm.vue
@@ -0,0 +1,114 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="琛ㄨ揪寮�" prop="expression">
+ <el-input type="textarea" v-model="formData.expression" placeholder="璇疯緭鍏ヨ〃杈惧紡" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 娴佺▼ 琛ㄥ崟 */
+defineOptions({ name: 'ProcessExpressionForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ status: undefined,
+ expression: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ expression: [{ required: true, message: '琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProcessExpressionApi.getProcessExpression(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProcessExpressionVO
+ if (formType.value === 'create') {
+ await ProcessExpressionApi.createProcessExpression(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProcessExpressionApi.updateProcessExpression(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ status: CommonStatusEnum.ENABLE,
+ expression: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/processExpression/index.vue b/src/views/bpm/processExpression/index.vue
new file mode 100644
index 0000000..ec2de5a
--- /dev/null
+++ b/src/views/bpm/processExpression/index.vue
@@ -0,0 +1,182 @@
+<template>
+ <doc-alert title="娴佺▼琛ㄨ揪寮�" url="https://doc.iocoder.cn/bpm/expression/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['bpm:process-expression:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ㄨ揪寮�" align="center" prop="expression" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['bpm:process-expression:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['bpm:process-expression:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProcessExpressionForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpression'
+import ProcessExpressionForm from './ProcessExpressionForm.vue'
+
+/** BPM 娴佺▼琛ㄨ揪寮忓垪琛� */
+defineOptions({ name: 'BpmProcessExpression' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProcessExpressionVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProcessExpressionApi.deleteProcessExpression(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
new file mode 100644
index 0000000..c1ad017
--- /dev/null
+++ b/src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
@@ -0,0 +1,357 @@
+<template>
+ <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
+ <div class="processInstance-wrap-main">
+ <el-scrollbar>
+ <div class="text-#878c93 h-15px">娴佺▼锛歿{ selectProcessDefinition.name }}</div>
+ <el-divider class="!my-8px" />
+
+ <!-- 涓棿涓昏鍐呭 tab 鏍� -->
+ <el-tabs v-model="activeTab">
+ <!-- 琛ㄥ崟淇℃伅 -->
+ <el-tab-pane label="琛ㄥ崟濉啓" name="form">
+ <div class="form-scroll-area" v-loading="processInstanceStartLoading">
+ <el-scrollbar>
+ <el-row>
+ <el-col :span="17">
+ <form-create
+ :rule="detailForm.rule"
+ v-model:api="fApi"
+ v-model="detailForm.value"
+ :option="detailForm.option"
+ @submit="submitForm"
+ />
+ </el-col>
+
+ <el-col :span="6" :offset="1">
+ <!-- 娴佺▼鏃堕棿绾� -->
+ <ProcessInstanceTimeline
+ ref="timelineRef"
+ :activity-nodes="activityNodes"
+ :show-status-icon="false"
+ @select-user-confirm="selectUserConfirm"
+ />
+ </el-col>
+ </el-row>
+ </el-scrollbar>
+ </div>
+ </el-tab-pane>
+ <!-- 娴佺▼鍥� -->
+ <el-tab-pane label="娴佺▼鍥�" name="diagram">
+ <div class="form-scroll-area">
+ <!-- BPMN 娴佺▼鍥鹃瑙� -->
+ <ProcessInstanceBpmnViewer
+ :bpmn-xml="bpmnXML"
+ v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
+ />
+
+ <!-- Simple 娴佺▼鍥鹃瑙� -->
+ <ProcessInstanceSimpleViewer
+ :simple-json="simpleJson"
+ v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
+ />
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+
+ <!-- 搴曢儴鎿嶄綔鏍� -->
+ <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+ <!-- 鎿嶄綔鏍忔寜閽� -->
+ <div
+ v-if="activeTab === 'form'"
+ class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+ >
+ <el-button plain type="success" @click="submitForm">
+ <Icon icon="ep:select" /> 鍙戣捣
+ </el-button>
+ <el-button plain type="danger" @click="handleCancel">
+ <Icon icon="ep:close" /> 鍙栨秷
+ </el-button>
+ </div>
+ </div>
+ </el-scrollbar>
+ </div>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
+import { BpmModelType, BpmModelFormType } from '@/utils/constants'
+import {
+ CandidateStrategy,
+ NodeId,
+ FieldPermissionType
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
+import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+import formCreate from '@form-create/element-ui'
+
+defineOptions({ name: 'ProcessDefinitionDetail' })
+const props = defineProps<{
+ selectProcessDefinition: any
+}>()
+const emit = defineEmits(['cancel'])
+const processInstanceStartLoading = ref(false) // 娴佺▼瀹炰緥鍙戣捣涓�
+const { push, currentRoute } = useRouter() // 璺敱
+const message = useMessage() // 娑堟伅寮圭獥
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+
+const detailForm: any = ref({
+ rule: [],
+ option: {},
+ value: {}
+}) // 娴佺▼琛ㄥ崟璇︽儏
+const fApi = ref<ApiAttrs>()
+// 鎸囧畾瀹℃壒浜�
+const startUserSelectTasks: any = ref([]) // 鍙戣捣浜洪渶瑕侀�夋嫨瀹℃壒浜烘垨鎶勯�佷汉鐨勪换鍔″垪琛�
+const startUserSelectAssignees = ref({}) // 鍙戣捣浜洪�夋嫨瀹℃壒浜虹殑鏁版嵁
+const tempStartUserSelectAssignees = ref({}) // 鍘嗗彶鍙戣捣浜洪�夋嫨瀹℃壒浜虹殑鏁版嵁锛岀敤浜庢瘡娆¤〃鍗曞彉鏇存椂锛屼复鏃朵繚瀛�
+const bpmnXML: any = ref(null) // BPMN 鏁版嵁
+const simpleJson = ref<string | undefined>() // Simple 璁捐鍣ㄦ暟鎹� json 鏍煎紡
+
+const activeTab = ref('form') // 褰撳墠鐨� Tab
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 瀹℃壒鑺傜偣淇℃伅
+
+/** 璁剧疆琛ㄥ崟淇℃伅銆佽幏鍙栨祦绋嬪浘鏁版嵁 **/
+const initProcessInfo = async (row: any, formVariables?: any) => {
+ // 閲嶇疆鎸囧畾瀹℃壒浜�
+ startUserSelectTasks.value = []
+ startUserSelectAssignees.value = {}
+
+ // 鎯呭喌涓�锛氭祦绋嬭〃鍗�
+ if (row.formType == BpmModelFormType.NORMAL) {
+ // 璁剧疆琛ㄥ崟
+ // 娉ㄦ剰锛氶渶瑕佷粠 formVariables 涓紝绉婚櫎涓嶅湪 row.formFields 鐨勫�笺��
+ // 鍘熷洜鏄細鍚庣杩斿洖鐨� formVariables 閲岄潰锛屼細鏈変竴浜涢潪琛ㄥ崟鐨勪俊鎭�備緥濡傝锛屾煇涓祦绋嬭妭鐐圭殑瀹℃壒浜恒��
+ // 杩欐牱锛屽氨鍙兘瀵艰嚧涓�涓祦绋嬭瀹℃壒涓嶉�氳繃鍚庯紝閲嶆柊鍙戣捣鏃讹紝浼氱洿鎺ュ悗绔姤閿欙紒锛侊紒
+ const formApi = formCreate.create(decodeFields(row.formFields))
+ const allowedFields = formApi.fields()
+ for (const key in formVariables) {
+ if (!allowedFields.includes(key)) {
+ delete formVariables[key]
+ }
+ }
+ setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+
+ await nextTick()
+ fApi.value?.btn.show(false) // 闅愯棌鎻愪氦鎸夐挳
+
+ // 鑾峰彇娴佺▼瀹℃壒淇℃伅,褰撳啀娆″彂璧锋椂锛屾祦绋嬪鎵硅妭鐐硅鏍规嵁鍘熷琛ㄥ崟鍙傛暟棰勬祴鍑烘潵
+ await getApprovalDetail({
+ id: row.id,
+ processVariablesStr: JSON.stringify(formVariables)
+ })
+
+ // 鍔犺浇娴佺▼鍥�
+ const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+ if (processDefinitionDetail) {
+ bpmnXML.value = processDefinitionDetail.bpmnXml
+ simpleJson.value = processDefinitionDetail.simpleModel
+ }
+ // 鎯呭喌浜岋細涓氬姟琛ㄥ崟
+ } else if (row.formCustomCreatePath) {
+ await push({
+ path: row.formCustomCreatePath
+ })
+ // 杩欓噷鏆傛椂鏃犻渶鍔犺浇娴佺▼鍥撅紝鍥犱负璺冲嚭鍒板彟澶栦釜 Tab锛�
+ }
+}
+
+/** 棰勬祴娴佺▼鑺傜偣浼氬洜涓鸿緭鍏ョ殑鍙傛暟鍊艰�屼骇鐢熸柊鐨勯娴嬬粨鏋滃�硷紝鎵�浠ラ渶閲嶆柊棰勬祴涓�娆� */
+watch(
+ detailForm.value,
+ (newValue) => {
+ if (newValue && Object.keys(newValue.value).length > 0) {
+ // 璁板綍涔嬪墠鐨勮妭鐐瑰鎵逛汉
+ tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+ startUserSelectAssignees.value = {}
+ // 鍔犺浇鏈�鏂扮殑瀹℃壒璇︽儏
+ getApprovalDetail({
+ id: props.selectProcessDefinition.id,
+ processVariablesStr: JSON.stringify(newValue.value) // 瑙e喅 GET 鏃犳硶浼犻�掑璞$殑闂锛屽悗绔� String 鍐嶈浆 JSON
+ })
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 鑾峰彇瀹℃壒璇︽儏 */
+const getApprovalDetail = async (row: any) => {
+ try {
+ // TODO 鑾峰彇瀹℃壒璇︽儏锛岃缃� activityId 涓哄彂璧蜂汉鑺傜偣锛堜负浜嗚幏鍙栧瓧娈垫潈闄愩�傛殏鏃跺彧瀵� Simple 璁捐鍣ㄦ湁鏁堬級锛汙jason锛氳繖閲屽彲浠ュ幓鎺� activityId 涔堬紵
+ const data = await ProcessInstanceApi.getApprovalDetail({
+ processDefinitionId: row.id,
+ activityId: NodeId.START_USER_NODE_ID,
+ processVariablesStr: row.processVariablesStr // 瑙e喅 GET 鏃犳硶浼犻�掑璞$殑闂锛屽悗绔� String 鍐嶈浆 JSON
+ })
+
+ if (!data) {
+ message.error('鏌ヨ涓嶅埌瀹℃壒璇︽儏淇℃伅锛�')
+ return
+ }
+ // 鑾峰彇瀹℃壒鑺傜偣锛屾樉绀� Timeline 鐨勬暟鎹�
+ activityNodes.value = data.activityNodes
+
+ // 鑾峰彇鍙戣捣浜鸿嚜閫夌殑浠诲姟
+ startUserSelectTasks.value = data.activityNodes?.filter(
+ (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+ )
+ // 鎭㈠涔嬪墠鐨勯�夋嫨瀹℃壒浜�
+ if (startUserSelectTasks.value?.length > 0) {
+ for (const node of startUserSelectTasks.value) {
+ if (
+ tempStartUserSelectAssignees.value[node.id] &&
+ tempStartUserSelectAssignees.value[node.id].length > 0
+ ) {
+ startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+ } else {
+ startUserSelectAssignees.value[node.id] = []
+ }
+ }
+ }
+
+ // 鑾峰彇琛ㄥ崟瀛楁鏉冮檺
+ const formFieldsPermission = data.formFieldsPermission
+ // 璁剧疆琛ㄥ崟瀛楁鏉冮檺
+ if (formFieldsPermission) {
+ Object.keys(formFieldsPermission).forEach((item) => {
+ setFieldPermission(item, formFieldsPermission[item])
+ })
+ }
+ } finally {
+ }
+}
+
+/**
+ * 璁剧疆琛ㄥ崟鏉冮檺
+ */
+const setFieldPermission = (field: string, permission: string) => {
+ if (permission === FieldPermissionType.READ) {
+ // 1. 璁剧疆瀛楁涓哄彧璇�
+ //@ts-ignore
+ fApi.value?.disabled(true, field)
+ // 2. 鍙瀛楁锛� 鍘绘帀楠岃瘉瑙勫垯
+ // fApi.value?.updateValidate(field, []); 杩欎釜鏂规硶璨屼技涓嶈捣浣滅敤锛�
+ try {
+ //@ts-ignore
+ const rule = fApi.value?.getRule(field)
+ if (rule) {
+ // 蹇呭~楠岃瘉璁剧疆涓篺alse
+ rule.$required = false
+ // 娓呯┖鎵�鏈夐獙璇佽鍒�
+ if (rule.validate) {
+ rule.validate = []
+ }
+ }
+ } catch (error) {
+ console.warn('淇敼瀛楁楠岃瘉瑙勫垯澶辫触:', error)
+ }
+ }
+ if (permission === FieldPermissionType.WRITE) {
+ //@ts-ignore
+ fApi.value?.disabled(false, field)
+ }
+ if (permission === FieldPermissionType.NONE) {
+ //@ts-ignore
+ fApi.value?.hidden(true, field)
+ }
+}
+
+/** 鎻愪氦鎸夐挳 */
+const submitForm = async () => {
+ if (!fApi.value || !props.selectProcessDefinition) {
+ return
+ }
+
+ try {
+ // 娴佺▼琛ㄥ崟鏍¢獙
+ await fApi.value.validate()
+ } catch (error) {
+ // 濡傛灉楠岃瘉澶辫触锛屾鏌ユ槸鍚︽槸鍙瀛楁鐨勯獙璇侀敊璇�
+ console.warn('琛ㄥ崟楠岃瘉澶辫触:', error)
+ return
+ }
+ // 濡傛灉鏈夋寚瀹氬鎵逛汉锛岄渶瑕佹牎楠�
+ if (startUserSelectTasks.value?.length > 0) {
+ for (const userTask of startUserSelectTasks.value) {
+ if (
+ Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+ startUserSelectAssignees.value[userTask.id].length === 0
+ )
+ return message.warning(`璇烽�夋嫨${userTask.name}鐨勫�欓�変汉`)
+ }
+ }
+
+ // 鎻愪氦璇锋眰
+ processInstanceStartLoading.value = true
+ try {
+ await ProcessInstanceApi.createProcessInstance({
+ processDefinitionId: props.selectProcessDefinition.id,
+ variables: detailForm.value.value,
+ startUserSelectAssignees: startUserSelectAssignees.value
+ })
+ // 鎻愮ず
+ message.success('鍙戣捣娴佺▼鎴愬姛')
+ // 璺宠浆鍥炲幓
+ delView(unref(currentRoute))
+ await push({
+ name: 'BpmProcessInstanceMy'
+ })
+ } finally {
+ processInstanceStartLoading.value = false
+ }
+}
+
+/** 鍙栨秷鍙戣捣瀹℃壒 */
+const handleCancel = () => {
+ emit('cancel')
+}
+
+/** 閫夋嫨鍙戣捣浜� */
+const selectUserConfirm = (id: string, userList: any[]) => {
+ startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+defineExpose({ initProcessInfo })
+</script>
+
+<style lang="scss" scoped>
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 105px;
+
+.processInstance-wrap-main {
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+ );
+ max-height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+ );
+ overflow: auto;
+
+ .form-scroll-area {
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+ $process-header-height - 40px
+ );
+ max-height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+ $process-header-height - 40px
+ );
+ overflow: auto;
+ }
+}
+
+.form-box {
+ :deep(.el-card) {
+ border: none;
+ }
+}
+</style>
diff --git a/src/views/bpm/processInstance/create/index.vue b/src/views/bpm/processInstance/create/index.vue
new file mode 100644
index 0000000..2c714d1
--- /dev/null
+++ b/src/views/bpm/processInstance/create/index.vue
@@ -0,0 +1,321 @@
+<template>
+ <!-- 绗竴姝ワ紝閫氳繃娴佺▼瀹氫箟鐨勫垪琛紝閫夋嫨瀵瑰簲鐨勬祦绋� -->
+ <template v-if="!selectProcessDefinition">
+ <el-input
+ v-model="searchName"
+ class="!w-50% mb-15px"
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ clearable
+ @input="handleQuery"
+ @clear="handleQuery"
+ >
+ <template #prefix>
+ <Icon icon="ep:search" />
+ </template>
+ </el-input>
+ <ContentWrap
+ :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
+ class="position-relative pb-20px h-700px"
+ v-loading="loading"
+ >
+ <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
+ <el-col :span="5">
+ <div class="flex flex-col">
+ <div
+ v-for="category in availableCategories"
+ :key="category.code"
+ class="flex items-center p-10px cursor-pointer text-14px rounded-md"
+ :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
+ @click="handleCategoryClick(category)"
+ >
+ {{ category.name }}
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="19">
+ <el-scrollbar ref="scrollWrapper" height="700" @scroll="handleScroll">
+ <div
+ class="mb-20px pl-10px"
+ v-for="(definitions, categoryCode) in processDefinitionGroup"
+ :key="categoryCode"
+ :ref="`category-${categoryCode}`"
+ >
+ <h3 class="text-18px font-bold mb-10px mt-5px">
+ {{ getCategoryName(categoryCode as any) }}
+ </h3>
+ <div class="grid grid-cols-3 gap3">
+ <el-tooltip
+ v-for="definition in definitions"
+ :key="definition.id"
+ :content="definition.description"
+ :disabled="!definition.description || definition.description.trim().length === 0"
+ placement="top"
+ >
+ <el-card
+ shadow="hover"
+ class="cursor-pointer definition-item-card"
+ @click="handleSelect(definition)"
+ >
+ <template #default>
+ <div class="flex">
+ <el-image
+ v-if="definition.icon"
+ :src="definition.icon"
+ class="w-32px h-32px"
+ />
+ <div v-else class="flow-icon">
+ <span style="font-size: 12px; color: #fff">
+ {{ subString(definition.name, 0, 2) }}
+ </span>
+ </div>
+ <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+ </div>
+ </template>
+ </el-card>
+ </el-tooltip>
+ </div>
+ </div>
+ </el-scrollbar>
+ </el-col>
+ </el-row>
+ <el-empty class="!py-200px" :image-size="200" description="娌℃湁鎵惧埌鎼滅储缁撴灉" v-else />
+ </ContentWrap>
+ </template>
+
+ <!-- 绗簩姝ワ紝濉啓琛ㄥ崟锛岃繘琛屾祦绋嬬殑鎻愪氦 -->
+ <ProcessDefinitionDetail
+ v-else
+ ref="processDefinitionDetailRef"
+ :selectProcessDefinition="selectProcessDefinition"
+ @cancel="selectProcessDefinition = undefined"
+ />
+</template>
+
+<script lang="ts" setup>
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
+import { groupBy } from 'lodash-es'
+import { subString } from '@/utils/index'
+
+defineOptions({ name: 'BpmProcessInstanceCreate' })
+
+const { proxy } = getCurrentInstance() as any
+const route = useRoute() // 璺敱
+const message = useMessage() // 娑堟伅
+
+const searchName = ref('') // 褰撳墠鎼滅储鍏抽敭瀛�
+const processInstanceId: any = route.query.processInstanceId // 娴佺▼瀹炰緥缂栧彿銆傚満鏅細閲嶆柊鍙戣捣鏃�
+const loading = ref(true) // 鍔犺浇涓�
+const categoryList: any = ref([]) // 鍒嗙被鐨勫垪琛�
+const categoryActive: any = ref({}) // 閫変腑鐨勫垎绫�
+const processDefinitionList = ref([]) // 娴佺▼瀹氫箟鐨勫垪琛�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 鎵�鏈夋祦绋嬪垎绫绘暟鎹�
+ await getCategoryList()
+ // 鎵�鏈夋祦绋嬪畾涔夋暟鎹�
+ await getProcessDefinitionList()
+
+ // 濡傛灉 processInstanceId 闈炵┖锛岃鏄庢槸閲嶆柊鍙戣捣
+ if (processInstanceId?.length > 0) {
+ const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+ if (!processInstance) {
+ message.error('閲嶆柊鍙戣捣娴佺▼澶辫触锛屽師鍥狅細娴佺▼瀹炰緥涓嶅瓨鍦�')
+ return
+ }
+ const processDefinition = processDefinitionList.value.find(
+ (item: any) => item.key == processInstance.processDefinition?.key
+ )
+ if (!processDefinition) {
+ message.error('閲嶆柊鍙戣捣娴佺▼澶辫触锛屽師鍥狅細娴佺▼瀹氫箟涓嶅瓨鍦�')
+ return
+ }
+ await handleSelect(processDefinition, processInstance.formVariables)
+ }
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鑾峰彇鎵�鏈夋祦绋嬪垎绫绘暟鎹� */
+const getCategoryList = async () => {
+ try {
+ // 娴佺▼鍒嗙被
+ categoryList.value = await CategoryApi.getCategorySimpleList()
+ } finally {
+ }
+}
+
+/** 鑾峰彇鎵�鏈夋祦绋嬪畾涔夋暟鎹� */
+const getProcessDefinitionList = async () => {
+ try {
+ // 娴佺▼瀹氫箟
+ processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+ suspensionState: 1
+ })
+ // 鍒濆鍖栬繃婊ゅ垪琛ㄤ负鍏ㄩ儴娴佺▼瀹氫箟
+ filteredProcessDefinitionList.value = processDefinitionList.value
+
+ // 鍦ㄨ幏鍙栧畬鎵�鏈夋暟鎹悗锛岃缃涓�涓湁鏁堝垎绫讳负婵�娲荤姸鎬�
+ if (availableCategories.value.length > 0 && !categoryActive.value?.code) {
+ categoryActive.value = availableCategories.value[0]
+ }
+ } finally {
+ }
+}
+
+/** 鎼滅储娴佺▼ */
+const filteredProcessDefinitionList = ref([]) // 鐢ㄤ簬瀛樺偍鎼滅储杩囨护鍚庣殑娴佺▼瀹氫箟
+const handleQuery = () => {
+ if (searchName.value.trim()) {
+ // 濡傛灉鏈夋悳绱㈠叧閿瓧锛岃繘琛岃繃婊�
+ filteredProcessDefinitionList.value = processDefinitionList.value.filter(
+ (definition: any) => definition.name.toLowerCase().includes(searchName.value.toLowerCase()) // 鍋囪鎼滅储渚濇嵁鏄祦绋嬪畾涔夌殑鍚嶇О
+ )
+ } else {
+ // 濡傛灉娌℃湁鎼滅储鍏抽敭瀛楋紝鎭㈠鎵�鏈夋暟鎹�
+ filteredProcessDefinitionList.value = processDefinitionList.value
+ }
+}
+
+/** 娴佺▼瀹氫箟鐨勫垎缁� */
+const processDefinitionGroup: any = computed(() => {
+ if (!processDefinitionList.value?.length) {
+ return {}
+ }
+
+ const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
+ // 鎸夌収 categoryList 鐨勯『搴忛噸鏂扮粍缁囨暟鎹�
+ const orderedGroup = {}
+ categoryList.value.forEach((category: any) => {
+ if (grouped[category.code]) {
+ orderedGroup[category.code] = grouped[category.code]
+ }
+ })
+ return orderedGroup
+})
+
+/** 宸︿晶鍒嗙被鍒囨崲 */
+const handleCategoryClick = (category: any) => {
+ categoryActive.value = category
+ const categoryRef = proxy.$refs[`category-${category.code}`] // 鑾峰彇鐐瑰嚮鍒嗙被瀵瑰簲鐨� DOM 鍏冪礌
+ if (categoryRef?.length) {
+ const scrollWrapper = proxy.$refs.scrollWrapper // 鑾峰彇鍙充晶婊氬姩瀹瑰櫒
+ const categoryOffsetTop = categoryRef[0].offsetTop
+
+ // 婊氬姩鍒板搴斾綅缃�
+ scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
+ }
+}
+
+/** 閫氳繃鍒嗙被 code 鑾峰彇瀵瑰簲鐨勫悕绉� */
+const getCategoryName = (categoryCode: string) => {
+ return categoryList.value?.find((ctg: any) => ctg.code === categoryCode)?.name
+}
+
+// ========== 琛ㄥ崟鐩稿叧 ==========
+const selectProcessDefinition = ref() // 閫夋嫨鐨勬祦绋嬪畾涔�
+const processDefinitionDetailRef = ref()
+
+/** 澶勭悊閫夋嫨娴佺▼鐨勬寜閽搷浣� **/
+const handleSelect = async (row, formVariables?) => {
+ // 璁剧疆閫夋嫨鐨勬祦绋�
+ selectProcessDefinition.value = row
+ // 鍒濆鍖栨祦绋嬪畾涔夎鎯�
+ await nextTick()
+ processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
+}
+
+/** 澶勭悊婊氬姩浜嬩欢锛屽拰宸︿晶鍒嗙被鑱斿姩 */
+const handleScroll = (e: any) => {
+ // 鐩存帴浣跨敤浜嬩欢瀵硅薄鑾峰彇婊氬姩浣嶇疆
+ const scrollTop = e.scrollTop
+
+ // 鑾峰彇鎵�鏈夊垎绫诲尯鍩熺殑浣嶇疆淇℃伅
+ const categoryPositions = categoryList.value
+ .map((category: CategoryVO) => {
+ const categoryRef = proxy.$refs[`category-${category.code}`]
+ if (categoryRef?.[0]) {
+ return {
+ code: category.code,
+ offsetTop: categoryRef[0].offsetTop,
+ height: categoryRef[0].offsetHeight
+ }
+ }
+ return null
+ })
+ .filter(Boolean)
+
+ // 鏌ユ壘褰撳墠婊氬姩浣嶇疆瀵瑰簲鐨勫垎绫�
+ let currentCategory = categoryPositions[0]
+ for (const position of categoryPositions) {
+ // 涓轰簡鏇村ソ鐨勭敤鎴蜂綋楠岋紝鍙互娣诲姞涓�涓紦鍐插尯鍩燂紙姣斿 50px锛�
+ if (scrollTop >= position.offsetTop - 50) {
+ currentCategory = position
+ } else {
+ break
+ }
+ }
+
+ // 鏇存柊褰撳墠 active 鐨勫垎绫�
+ if (currentCategory && categoryActive.value.code !== currentCategory.code) {
+ categoryActive.value = categoryList.value.find(
+ (c: CategoryVO) => c.code === currentCategory.code
+ )
+ }
+}
+
+/** 杩囨护鍑烘湁娴佺▼鐨勫垎绫诲垪琛ㄣ�傜洰鐨勶細鍙睍绀烘湁娴佺▼鐨勫垎绫� */
+const availableCategories = computed(() => {
+ if (!categoryList.value?.length || !processDefinitionGroup.value) {
+ return []
+ }
+
+ // 鑾峰彇鎵�鏈夋湁娴佺▼鐨勫垎绫讳唬鐮�
+ const availableCategoryCodes = Object.keys(processDefinitionGroup.value)
+
+ // 杩囨护鍑烘湁娴佺▼鐨勫垎绫�
+ return categoryList.value.filter((category: CategoryVO) =>
+ availableCategoryCodes.includes(category.code)
+ )
+})
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style lang="scss" scoped>
+.flow-icon {
+ display: flex;
+ width: 32px;
+ height: 32px;
+ margin-right: 10px;
+ background-color: var(--el-color-primary);
+ border-radius: 0.25rem;
+ align-items: center;
+ justify-content: center;
+}
+
+.process-definition-container::before {
+ position: absolute;
+ left: 20.8%;
+ height: 100%;
+ border-left: 1px solid #e6e6e6;
+ content: '';
+}
+
+:deep() {
+ .definition-item-card {
+ .el-card__body {
+ padding: 14px;
+ }
+ }
+}
+</style>
diff --git a/src/views/bpm/processInstance/detail/PrintDialog.vue b/src/views/bpm/processInstance/detail/PrintDialog.vue
new file mode 100644
index 0000000..fb8cd8b
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/PrintDialog.vue
@@ -0,0 +1,234 @@
+<script setup lang="ts">
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { useUserStore } from '@/store/modules/user'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { decodeFields } from '@/utils/formCreate'
+
+const userStore = useUserStore()
+
+const visible = ref(false)
+const loading = ref(false)
+
+const printData = ref()
+const userName = computed(() => userStore.user.nickname ?? '')
+const printTime = ref(formatDate(new Date(), 'YYYY-MM-DD HH:mm'))
+const formFields = ref()
+const printDataMap = ref({})
+
+const open = async (id: string) => {
+ loading.value = true
+ try {
+ printData.value = await ProcessInstanceApi.getProcessInstancePrintData(id)
+ initPrintDataMap()
+ parseFormFields()
+ } finally {
+ loading.value = false
+ }
+ visible.value = true
+}
+defineExpose({ open })
+
+const parseFormFields = () => {
+ if (!printData.value) return
+
+ const formFieldsObj = decodeFields(
+ printData.value.processInstance.processDefinition?.formFields || []
+ )
+ const processVariables = printData.value.processInstance.formVariables
+ let res: any = []
+ for (const item of formFieldsObj) {
+ const id = item['field']
+ const name = item['title']
+ const variable = processVariables[item['field']]
+ let html = variable
+ switch (item['type']) {
+ case 'UploadImg': {
+ let imgEl = document.createElement('img')
+ imgEl.setAttribute('src', variable)
+ imgEl.setAttribute('style', 'max-width: 600px;')
+ html = imgEl.outerHTML
+ break
+ }
+ case 'radio':
+ case 'checkbox':
+ case 'select': {
+ const options = item['options'] || []
+ const temp: any = []
+ if (Array.isArray(variable)) {
+ const labels = options.filter((o) => variable.includes(o.value)).map((o) => o.label)
+ temp.push(...labels)
+ } else {
+ const opt = options.find((o) => o.value === variable)
+ temp.push(opt.label)
+ }
+ html = temp.join(',')
+ }
+ // TODO 鏇村琛ㄥ崟鎵撳嵃灞曠ず
+ }
+ printDataMap.value[item['field']] = html
+ res.push({ id, name, html })
+ }
+ formFields.value = res
+}
+
+const initPrintDataMap = () => {
+ printDataMap.value['startUser'] = printData.value.processInstance.startUser.nickname
+ printDataMap.value['startUserDept'] = printData.value.processInstance.startUser.deptName
+ printDataMap.value['processName'] = printData.value.processInstance.name
+ printDataMap.value['processNum'] = printData.value.processInstance.id
+ printDataMap.value['startTime'] = formatDate(printData.value.processInstance.startTime)
+ printDataMap.value['endTime'] = formatDate(printData.value.processInstance.endTime)
+ printDataMap.value['processStatus'] = getDictLabel(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ printData.value.processInstance.status
+ )
+ printDataMap.value['printUser'] = userName.value
+ printDataMap.value['printTime'] = printTime.value
+}
+
+const getPrintTemplateHTML = () => {
+ const parser = new DOMParser()
+ let doc = parser.parseFromString(printData.value.printTemplateHtml, 'text/html')
+ // table 娣诲姞border
+ let tables = doc.querySelectorAll('table')
+ tables.forEach((item) => {
+ item.setAttribute('border', '1')
+ item.setAttribute('style', (item.getAttribute('style') || '') + 'border-collapse:collapse;')
+ })
+ // 鏇挎崲 mentions
+ let mentions = doc.querySelectorAll('[data-w-e-type="mention"]')
+ mentions.forEach((item) => {
+ const mentionId = JSON.parse(decodeURIComponent(item.getAttribute('data-info') ?? ''))['id']
+ item.innerHTML = printDataMap.value[mentionId] ?? ''
+ })
+ // 鏇挎崲娴佺▼璁板綍
+ let processRecords = doc.querySelectorAll('[data-w-e-type="process-record"]')
+ let processRecordTable: Element = document.createElement('table')
+ if (processRecords.length > 0) {
+ // 鏋勫缓娴佺▼璁板綍html
+ processRecordTable.setAttribute('border', '1')
+ processRecordTable.setAttribute('style', 'width:100%;border-collapse:collapse;')
+ const headTr = document.createElement('tr')
+ const headTd = document.createElement('td')
+ headTd.setAttribute('colspan', '2')
+ headTd.setAttribute('width', 'auto')
+ headTd.setAttribute('style', 'text-align: center;')
+ headTd.innerHTML = '娴佺▼鑺傜偣'
+ headTr.appendChild(headTd)
+ processRecordTable.appendChild(headTr)
+ printData.value.tasks.forEach((item) => {
+ const tr = document.createElement('tr')
+ const td1 = document.createElement('td')
+ td1.innerHTML = item.name
+ const td2 = document.createElement('td')
+ td2.innerHTML = item.description
+ tr.appendChild(td1)
+ tr.appendChild(td2)
+ processRecordTable.appendChild(tr)
+ })
+ }
+ processRecords.forEach((item) => {
+ item.innerHTML = processRecordTable.outerHTML
+ })
+ // 杩斿洖 html
+ return doc.body.innerHTML
+}
+
+const printObj = ref({
+ id: 'printDivTag',
+ popTitle: ' ',
+ extraCss: '/print.css',
+ extraHead: '',
+ zIndex: 20003
+})
+</script>
+
+<template>
+ <el-dialog v-loading="loading" v-model="visible" :show-close="false">
+ <div id="printDivTag" style="word-break: break-all">
+ <div v-if="printData.printTemplateEnable" v-html="getPrintTemplateHTML()"></div>
+ <div v-else>
+ <h2 class="text-center">{{ printData.processInstance.name }}</h2>
+ <div class="text-right text-15px">{{ '鎵撳嵃浜哄憳: ' + userName }}</div>
+ <div class="flex justify-between">
+ <div class="text-15px">{{ '娴佺▼缂栧彿: ' + printData.processInstance.id }}</div>
+ <div class="text-15px">{{ '鎵撳嵃鏃堕棿: ' + printTime }}</div>
+ </div>
+ <table class="mt-20px w-100%" border="1" style="border-collapse: collapse">
+ <tbody>
+ <tr>
+ <td class="p-5px w-25%">鍙戣捣浜�</td>
+ <td class="p-5px w-25%">{{ printData.processInstance.startUser.nickname }}</td>
+ <td class="p-5px w-25%">鍙戣捣鏃堕棿</td>
+ <td class="p-5px w-25%">{{ formatDate(printData.processInstance.startTime) }}</td>
+ </tr>
+ <tr>
+ <td class="p-5px w-25%">鎵�灞為儴闂�</td>
+ <td class="p-5px w-25%">{{ printData.processInstance.startUser.deptName }}</td>
+ <td class="p-5px w-25%">娴佺▼鐘舵��</td>
+ <td class="p-5px w-25%">
+ {{
+ getDictLabel(
+ DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS,
+ printData.processInstance.status
+ )
+ }}
+ </td>
+ </tr>
+ <tr>
+ <td class="p-5px w-100% text-center" colspan="4">
+ <h4>琛ㄥ崟鍐呭</h4>
+ </td>
+ </tr>
+ <tr v-for="item in formFields" :key="item.id">
+ <td class="p-5px w-20%">
+ {{ item.name }}
+ </td>
+ <td class="p-5px w-80%" colspan="3">
+ <div v-html="item.html"></div>
+ </td>
+ </tr>
+ <tr>
+ <td class="p-5px w-100% text-center" colspan="4">
+ <h4>娴佺▼鑺傜偣</h4>
+ </td>
+ </tr>
+ <tr v-for="item in printData.tasks" :key="item.id">
+ <td class="p-5px w-20%">
+ {{ item.name }}
+ </td>
+ <td class="p-5px w-80%" colspan="3">
+ {{ item.description }}
+ <div v-if="item.signPicUrl && item.signPicUrl.length > 0">
+ <img class="w-90px h-40px" :src="item.signPicUrl" alt="" />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="visible = false">鍙� 娑�</el-button>
+ <el-button type="primary" v-print="printObj"> 鎵� 鍗�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<style>
+/* 淇鎵撳嵃鍙樉绀轰竴椤� */
+@media print {
+ @page {
+ size: auto;
+ }
+
+ body,
+ html,
+ div {
+ height: auto !important;
+ }
+}
+</style>
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
new file mode 100644
index 0000000..781263d
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue
@@ -0,0 +1,61 @@
+<template>
+ <el-card v-loading="loading" class="box-card">
+ <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { MyProcessViewer } from '@/components/bpmnProcessDesigner/package'
+
+defineOptions({ name: 'BpmProcessInstanceBpmnViewer' })
+
+const props = defineProps({
+ loading: propTypes.bool.def(false), // 鏄惁鍔犺浇涓�
+ bpmnXml: propTypes.string, // BPMN XML
+ modelView: propTypes.object
+})
+
+const view = ref({
+ bpmnXml: ''
+}) // BPMN 娴佺▼鍥炬暟鎹�
+
+
+/** 鍙湁 loading 瀹屾垚鏃讹紝鎵嶅幓鍔犺浇娴佺▼鍒楄〃 */
+watch(
+ () => props.modelView,
+ async (newModelView) => {
+ // 鍔犺浇鏈�鏂�
+ if (newModelView) {
+ //@ts-ignore
+ view.value = newModelView
+ }
+ }
+)
+
+/** 鐩戝惉 bpmnXml */
+watch(
+ () => props.bpmnXml,
+ (value) => {
+ view.value.bpmnXml = value
+ }
+)
+</script>
+<style lang="scss" scoped>
+.box-card {
+ height: 100%;
+ width: 100%;
+ margin-bottom: 0;
+
+ :deep(.el-card__body) {
+ height: 100%;
+ padding: 0;
+ }
+
+ :deep(.process-viewer) {
+ height: 100% !important;
+ min-height: 100%;
+ width: 100%;
+ overflow: auto;
+ }
+}
+</style>
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
new file mode 100644
index 0000000..53b10bd
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceOperationButton.vue
@@ -0,0 +1,1140 @@
+<template>
+ <div
+ class="h-50px bottom-10 text-14px flex items-center color-#32373c dark:color-#fff font-bold btn-container"
+ >
+ <!-- 銆愰�氳繃銆戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.approve"
+ placement="top-end"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.APPROVE)"
+ >
+ <template #reference>
+ <el-button plain type="success" @click="openPopover('approve')">
+ <Icon icon="ep:select" /> {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
+ </el-button>
+ </template>
+ <!-- 瀹℃壒琛ㄥ崟 -->
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="approveFormRef"
+ :model="approveReasonForm"
+ :rules="approveReasonRule"
+ label-width="100px"
+ >
+ <el-card v-if="runningTask?.formId > 0" class="mb-15px !-mt-10px">
+ <template #header>
+ <span class="el-icon-picture-outline"> 濉啓琛ㄥ崟銆恵{ runningTask?.formName }}銆� </span>
+ </template>
+ <form-create
+ v-model="approveForm.value"
+ v-model:api="approveFormFApi"
+ :option="approveForm.option"
+ :rule="approveForm.rule"
+ />
+ </el-card>
+ <el-form-item :label="`${nodeTypeName}鎰忚`" prop="reason">
+ <el-input
+ v-model="approveReasonForm.reason"
+ :placeholder="`璇疯緭鍏�${nodeTypeName}鎰忚`"
+ type="textarea"
+ :rows="4"
+ />
+ </el-form-item>
+ <el-form-item
+ label="涓嬩竴涓妭鐐圭殑瀹℃壒浜�"
+ prop="nextAssignees"
+ v-if="nextAssigneesActivityNode.length > 0"
+ >
+ <div class="ml-10px -mt-15px -mb-35px">
+ <ProcessInstanceTimeline
+ ref="nextAssigneesTimelineRef"
+ :activity-nodes="nextAssigneesActivityNode"
+ :show-status-icon="false"
+ :enable-approve-user-select="true"
+ @select-user-confirm="selectNextAssigneesConfirm"
+ />
+ </div>
+ </el-form-item>
+ <el-form-item
+ v-if="runningTask.signEnable"
+ label="绛惧悕"
+ prop="signPicUrl"
+ ref="approveSignFormRef"
+ >
+ <el-button @click="signRef.open()">鐐瑰嚮绛惧悕</el-button>
+ <el-image
+ class="w-90px h-40px ml-5px"
+ v-if="approveReasonForm.signPicUrl"
+ :src="approveReasonForm.signPicUrl"
+ :preview-src-list="[approveReasonForm.signPicUrl]"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ :disabled="formLoading"
+ type="success"
+ @click="handleAudit(true, approveFormRef)"
+ >
+ {{ getButtonDisplayName(OperationButtonType.APPROVE) }}
+ </el-button>
+ <el-button @click="closePopover('approve', approveFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愭嫆缁濄�戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.reject"
+ placement="top-end"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.REJECT)"
+ >
+ <template #reference>
+ <el-button class="mr-20px" plain type="danger" @click="openPopover('reject')">
+ <Icon icon="ep:close" /> {{ getButtonDisplayName(OperationButtonType.REJECT) }}
+ </el-button>
+ </template>
+ <!-- 瀹℃壒琛ㄥ崟 -->
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="rejectFormRef"
+ :model="rejectReasonForm"
+ :rules="rejectReasonRule"
+ label-width="100px"
+ >
+ <el-form-item label="瀹℃壒鎰忚" prop="reason">
+ <el-input
+ v-model="rejectReasonForm.reason"
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ type="textarea"
+ :rows="4"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ :disabled="formLoading"
+ type="danger"
+ @click="handleAudit(false, rejectFormRef)"
+ >
+ {{ getButtonDisplayName(OperationButtonType.REJECT) }}
+ </el-button>
+ <el-button @click="closePopover('reject', rejectFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愭妱閫併�戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.copy"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.COPY)"
+ >
+ <template #reference>
+ <div @click="openPopover('copy')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="svg-icon:send" />
+ {{ getButtonDisplayName(OperationButtonType.COPY) }}
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="copyFormRef"
+ :model="copyForm"
+ :rules="copyFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="鎶勯�佷汉" prop="copyUserIds">
+ <el-select
+ v-model="copyForm.copyUserIds"
+ clearable
+ style="width: 100%"
+ multiple
+ placeholder="璇烽�夋嫨鎶勯�佷汉"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎶勯�佹剰瑙�" prop="copyReason">
+ <el-input
+ v-model="copyForm.copyReason"
+ clearable
+ placeholder="璇疯緭鍏ユ妱閫佹剰瑙�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handleCopy">
+ {{ getButtonDisplayName(OperationButtonType.COPY) }}
+ </el-button>
+ <el-button @click="closePopover('copy', copyFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愯浆鍔炪�戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.transfer"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.TRANSFER)"
+ >
+ <template #reference>
+ <div @click="openPopover('transfer')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="fa:share-square-o" />
+ {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="transferFormRef"
+ :model="transferForm"
+ :rules="transferFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="鏂板鎵逛汉" prop="assigneeUserId">
+ <el-select v-model="transferForm.assigneeUserId" clearable style="width: 100%">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃壒鎰忚" prop="reason">
+ <el-input
+ v-model="transferForm.reason"
+ clearable
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handleTransfer()">
+ {{ getButtonDisplayName(OperationButtonType.TRANSFER) }}
+ </el-button>
+ <el-button @click="closePopover('transfer', transferFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愬娲俱�戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.delegate"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.DELEGATE)"
+ >
+ <template #reference>
+ <div @click="openPopover('delegate')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="ep:position" />
+ {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="delegateFormRef"
+ :model="delegateForm"
+ :rules="delegateFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="鎺ユ敹浜�" prop="delegateUserId">
+ <el-select v-model="delegateForm.delegateUserId" clearable style="width: 100%">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃壒鎰忚" prop="reason">
+ <el-input
+ v-model="delegateForm.reason"
+ clearable
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handleDelegate()">
+ {{ getButtonDisplayName(OperationButtonType.DELEGATE) }}
+ </el-button>
+ <el-button @click="closePopover('delegate', delegateFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愬姞绛俱�戞寜閽� 褰撳墠浠诲姟瀹℃壒浜轰负A锛屽悜鍓嶅姞绛鹃�変簡涓�涓狢锛屽垯闇�瑕丆鍏堝鎵癸紝鐒跺悗鍐嶆槸A瀹℃壒锛屽悜鍚庡姞绛綛锛孉瀹℃壒瀹岋紝闇�瑕丅鍐嶅鎵瑰畬锛屾墠绠楀畬鎴愯繖涓换鍔¤妭鐐� -->
+ <el-popover
+ :visible="popOverVisible.addSign"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.ADD_SIGN)"
+ >
+ <template #reference>
+ <div @click="openPopover('addSign')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="ep:plus" />
+ {{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="addSignFormRef"
+ :model="addSignForm"
+ :rules="addSignFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="鍔犵澶勭悊浜�" prop="addSignUserIds">
+ <el-select v-model="addSignForm.addSignUserIds" multiple clearable style="width: 100%">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃壒鎰忚" prop="reason">
+ <el-input
+ v-model="addSignForm.reason"
+ clearable
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('before')">
+ 鍚戝墠{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+ </el-button>
+ <el-button :disabled="formLoading" type="primary" @click="handlerAddSign('after')">
+ 鍚戝悗{{ getButtonDisplayName(OperationButtonType.ADD_SIGN) }}
+ </el-button>
+ <el-button @click="closePopover('addSign', addSignFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愬噺绛俱�戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.deleteSign"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="runningTask?.children.length > 0"
+ >
+ <template #reference>
+ <div @click="openPopover('deleteSign')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="ep:semi-select" /> 鍑忕
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="deleteSignFormRef"
+ :model="deleteSignForm"
+ :rules="deleteSignFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="鍑忕浜哄憳" prop="deleteSignTaskId">
+ <el-select v-model="deleteSignForm.deleteSignTaskId" clearable style="width: 100%">
+ <el-option
+ v-for="item in runningTask.children"
+ :key="item.id"
+ :label="getDeleteSignUserLabel(item)"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃壒鎰忚" prop="reason">
+ <el-input
+ v-model="deleteSignForm.reason"
+ clearable
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handlerDeleteSign()">
+ 鍑忕
+ </el-button>
+ <el-button @click="closePopover('deleteSign', deleteSignFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!-- 銆愰��鍥炪�戞寜閽� -->
+ <el-popover
+ :visible="popOverVisible.return"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="runningTask && isHandleTaskStatus() && isShowButton(OperationButtonType.RETURN)"
+ >
+ <template #reference>
+ <div @click="openPopover('return')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="ep:back" />
+ {{ getButtonDisplayName(OperationButtonType.RETURN) }}
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="returnFormRef"
+ :model="returnForm"
+ :rules="returnFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="閫�鍥炶妭鐐�" prop="targetTaskDefinitionKey">
+ <el-select v-model="returnForm.targetTaskDefinitionKey" clearable style="width: 100%">
+ <el-option
+ v-for="item in returnList"
+ :key="item.taskDefinitionKey"
+ :label="item.name"
+ :value="item.taskDefinitionKey"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�鍥炵悊鐢�" prop="returnReason">
+ <el-input
+ v-model="returnForm.returnReason"
+ clearable
+ placeholder="璇疯緭鍏ラ��鍥炵悊鐢�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handleReturn()">
+ {{ getButtonDisplayName(OperationButtonType.RETURN) }}
+ </el-button>
+ <el-button @click="closePopover('return', returnFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+
+ <!--銆愬彇娑堛�戞寜閽� 杩欎釜瀵瑰簲鍙戣捣浜虹殑鍙栨秷, 鍙湁鍙戣捣浜哄彲浠ュ彇娑� -->
+ <el-popover
+ :visible="popOverVisible.cancel"
+ placement="top-start"
+ :width="420"
+ trigger="click"
+ v-if="
+ userId === processInstance?.startUser?.id && !isEndProcessStatus(processInstance?.status)
+ "
+ >
+ <template #reference>
+ <div @click="openPopover('cancel')" class="hover-bg-gray-100 rounded-xl p-6px">
+ <Icon :size="14" icon="fa:mail-reply" /> 鍙栨秷
+ </div>
+ </template>
+ <div class="flex flex-col flex-1 pt-20px px-20px" v-loading="formLoading">
+ <el-form
+ label-position="top"
+ class="mb-auto"
+ ref="cancelFormRef"
+ :model="cancelForm"
+ :rules="cancelFormRule"
+ label-width="100px"
+ >
+ <el-form-item label="鍙栨秷鐞嗙敱" prop="cancelReason">
+ <span class="text-#878c93 text-12px"> 鍙栨秷鍚庯紝璇ュ鎵规祦绋嬪皢鑷姩缁撴潫</span>
+ <el-input
+ v-model="cancelForm.cancelReason"
+ clearable
+ placeholder="璇疯緭鍏ュ彇娑堢悊鐢�"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button :disabled="formLoading" type="primary" @click="handleCancel()">
+ 纭
+ </el-button>
+ <el-button @click="closePopover('cancel', cancelFormRef)"> 鍙栨秷 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-popover>
+ <!-- 銆愬啀娆℃彁浜ゃ�� 鎸夐挳-->
+ <div
+ @click="handleReCreate()"
+ class="hover-bg-gray-100 rounded-xl p-6px"
+ v-if="
+ userId === processInstance?.startUser?.id &&
+ isEndProcessStatus(processInstance?.status) &&
+ processDefinition?.formType === 10
+ "
+ >
+ <Icon :size="14" icon="ep:refresh" /> 鍐嶆鎻愪氦
+ </div>
+ </div>
+
+ <!-- 绛惧悕寮圭獥 -->
+ <SignDialog ref="signRef" @success="handleSignFinish" />
+</template>
+<script lang="ts" setup>
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import * as TaskApi from '@/api/bpm/task'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as UserApi from '@/api/system/user'
+import {
+ NodeType,
+ OPERATION_BUTTON_NAME,
+ OperationButtonType,
+ CandidateStrategy
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import { BpmModelFormType, BpmProcessInstanceStatus } from '@/utils/constants'
+import type { FormInstance, FormRules } from 'element-plus'
+import SignDialog from './SignDialog.vue'
+import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'ProcessInstanceBtnContainer' })
+
+const router = useRouter() // 璺敱
+const message = useMessage() // 娑堟伅寮圭獥
+
+const userId = useUserStoreWithOut().getUser.id // 褰撳墠鐧诲綍鐨勭紪鍙�
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+
+const props = defineProps<{
+ processInstance: any // 娴佺▼瀹炰緥淇℃伅
+ processDefinition: any // 娴佺▼瀹氫箟淇℃伅
+ userOptions: UserApi.UserVO[]
+ normalForm: any // 娴佺▼琛ㄥ崟 formCreate
+ normalFormApi: any // 娴佺▼琛ㄥ崟 formCreate Api
+ writableFields: string[] // 娴佺▼琛ㄥ崟鍙互缂栬緫鐨勫瓧娈�
+}>()
+
+const formLoading = ref(false) // 琛ㄥ崟鍔犺浇涓�
+const popOverVisible = ref({
+ approve: false,
+ reject: false,
+ transfer: false,
+ delegate: false,
+ addSign: false,
+ return: false,
+ copy: false,
+ cancel: false,
+ deleteSign: false
+}) // 姘旀场鍗℃槸鍚﹀睍绀�
+const returnList = ref([] as any) // 閫�鍥炶妭鐐�
+
+// ========== 瀹℃壒淇℃伅 ==========
+const runningTask = ref<any>() // 杩愯涓殑浠诲姟
+const approveForm = ref<any>({}) // 瀹℃壒閫氳繃鏃讹紝棰濆鐨勮ˉ鍏呬俊鎭�
+const approveFormFApi = ref<any>({}) // approveForms 鐨� fAPi
+const nodeTypeName = ref('瀹℃壒') // 鑺傜偣绫诲瀷鍚嶇О
+
+// 瀹℃壒閫氳繃鎰忚琛ㄥ崟
+const reasonRequire = ref()
+const approveFormRef = ref<FormInstance>()
+const signRef = ref()
+const approveSignFormRef = ref()
+const nextAssigneesActivityNode = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 涓嬩竴涓鎵硅妭鐐逛俊鎭�
+const nextAssigneesTimelineRef = ref() // 涓嬩竴涓妭鐐瑰鎵逛汉鏃堕棿绾跨粍浠剁殑寮曠敤
+const approveReasonForm = reactive({
+ reason: '',
+ signPicUrl: '',
+ nextAssignees: {}
+})
+const approveReasonRule = computed(() => {
+ return {
+ reason: [
+ { required: reasonRequire.value, message: nodeTypeName.value + '鎰忚涓嶈兘涓虹┖', trigger: 'blur' }
+ ],
+ signPicUrl: [{ required: true, message: '绛惧悕涓嶈兘涓虹┖', trigger: 'change' }],
+ nextAssignees: [{ required: true, message: '瀹℃壒浜轰笉鑳戒负绌�', trigger: 'blur' }]
+ }
+})
+
+// 鎷掔粷琛ㄥ崟
+const rejectFormRef = ref<FormInstance>()
+const rejectReasonForm = reactive({
+ reason: ''
+})
+const rejectReasonRule = computed(() => {
+ return {
+ reason: [{ required: reasonRequire.value, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]
+ }
+})
+
+// 鎶勯�佽〃鍗�
+const copyFormRef = ref<FormInstance>()
+const copyForm = reactive({
+ copyUserIds: [],
+ copyReason: ''
+})
+const copyFormRule = reactive<FormRules<typeof copyForm>>({
+ copyUserIds: [{ required: true, message: '鎶勯�佷汉涓嶈兘涓虹┖', trigger: 'change' }]
+})
+
+// 杞姙琛ㄥ崟
+const transferFormRef = ref<FormInstance>()
+const transferForm = reactive({
+ assigneeUserId: undefined,
+ reason: ''
+})
+const transferFormRule = reactive<FormRules<typeof transferForm>>({
+ assigneeUserId: [{ required: true, message: '鏂板鎵逛汉涓嶈兘涓虹┖', trigger: 'change' }],
+ reason: [{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+// 濮旀淳琛ㄥ崟
+const delegateFormRef = ref<FormInstance>()
+const delegateForm = reactive({
+ delegateUserId: undefined,
+ reason: ''
+})
+const delegateFormRule = reactive<FormRules<typeof delegateForm>>({
+ delegateUserId: [{ required: true, message: '鎺ユ敹浜轰笉鑳戒负绌�', trigger: 'change' }],
+ reason: [{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+// 鍔犵琛ㄥ崟
+const addSignFormRef = ref<FormInstance>()
+const addSignForm = reactive({
+ addSignUserIds: undefined,
+ reason: ''
+})
+const addSignFormRule = reactive<FormRules<typeof addSignForm>>({
+ addSignUserIds: [{ required: true, message: '鍔犵澶勭悊浜轰笉鑳戒负绌�', trigger: 'change' }],
+ reason: [{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+// 鍑忕琛ㄥ崟
+const deleteSignFormRef = ref<FormInstance>()
+const deleteSignForm = reactive({
+ deleteSignTaskId: undefined,
+ reason: ''
+})
+const deleteSignFormRule = reactive<FormRules<typeof deleteSignForm>>({
+ deleteSignTaskId: [{ required: true, message: '鍑忕浜哄憳涓嶈兘涓虹┖', trigger: 'change' }],
+ reason: [{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+// 閫�鍥炶〃鍗�
+const returnFormRef = ref<FormInstance>()
+const returnForm = reactive({
+ targetTaskDefinitionKey: undefined,
+ returnReason: ''
+})
+const returnFormRule = reactive<FormRules<typeof returnForm>>({
+ targetTaskDefinitionKey: [{ required: true, message: '閫�鍥炶妭鐐逛笉鑳戒负绌�', trigger: 'change' }],
+ returnReason: [{ required: true, message: '閫�鍥炵悊鐢变笉鑳戒负绌�', trigger: 'blur' }]
+})
+
+// 鍙栨秷琛ㄥ崟
+const cancelFormRef = ref<FormInstance>()
+const cancelForm = reactive({
+ cancelReason: ''
+})
+const cancelFormRule = reactive<FormRules<typeof cancelForm>>({
+ cancelReason: [{ required: true, message: '鍙栨秷鐞嗙敱涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+/** 鐩戝惉 approveFormFApis锛屽疄鐜板畠瀵瑰簲鐨� form-create 鍒濆鍖栧悗锛岄殣钘忔帀瀵瑰簲鐨勮〃鍗曟彁浜ゆ寜閽� */
+watch(
+ () => approveFormFApi.value,
+ (val) => {
+ val?.btn?.show(false)
+ val?.resetBtn?.show(false)
+ },
+ {
+ deep: true
+ }
+)
+
+/** 寮瑰嚭姘旀场鍗� */
+const openPopover = async (type: string) => {
+ if (popOverVisible.value[type] === true) return
+ if (type === 'approve') {
+ // 鏍¢獙娴佺▼琛ㄥ崟
+ const valid = await validateNormalForm()
+ if (!valid) {
+ message.warning('琛ㄥ崟鏍¢獙涓嶉�氳繃锛岃鍏堝畬鍠勮〃鍗�!!')
+ return
+ }
+ initNextAssigneesFormField()
+ }
+ if (type === 'return') {
+ // 鑾峰彇閫�鍥炶妭鐐�
+ returnList.value = await TaskApi.getTaskListByReturn(runningTask.value.id)
+ if (returnList.value.length === 0) {
+ message.warning('褰撳墠娌℃湁鍙��鍥炵殑鑺傜偣')
+ return
+ }
+ }
+ Object.keys(popOverVisible.value).forEach((item) => {
+ popOverVisible.value[item] = item === type
+ })
+ // await nextTick()
+ // formRef.value.resetFields()
+}
+
+/** 鍏抽棴姘旀场鍗� */
+const closePopover = (type: string, formRef: FormInstance | undefined) => {
+ if (formRef) {
+ formRef.resetFields()
+ }
+ popOverVisible.value[type] = false
+ nextAssigneesActivityNode.value = []
+ // 娓呯悊 Timeline 缁勪欢涓殑鑷畾涔夊鎵逛汉鏁版嵁
+ if (nextAssigneesTimelineRef.value) {
+ nextAssigneesTimelineRef.value.batchSetCustomApproveUsers({})
+ }
+}
+
+/** 娴佺▼閫氳繃鏃讹紝鏍规嵁琛ㄥ崟鍙橀噺鏌ヨ鏂扮殑娴佺▼鑺傜偣锛屽垽鏂笅涓�涓妭鐐圭被鍨嬫槸鍚︿负鑷�夊鎵逛汉 */
+const initNextAssigneesFormField = async () => {
+ // 鑾峰彇淇敼鐨勬祦绋嬪彉閲�, 鏆傛椂鍙敮鎸佹祦绋嬭〃鍗�
+ const variables = getUpdatedProcessInstanceVariables()
+ const data = await ProcessInstanceApi.getNextApprovalNodes({
+ processInstanceId: props.processInstance.id,
+ taskId: runningTask.value.id,
+ processVariablesStr: JSON.stringify(variables)
+ })
+ if (data && data.length > 0) {
+ const customApproveUsersData: Record<string, any[]> = {} // 鐢ㄤ簬鏀堕泦闇�瑕佽缃埌 Timeline 缁勪欢鐨勮嚜瀹氫箟瀹℃壒浜烘暟鎹�
+ data.forEach((node: any) => {
+ if (
+ // 鎯呭喌涓�锛氬綋鍓嶈妭鐐规病鏈夊鎵逛汉锛屽苟涓旀槸鍙戣捣浜鸿嚜閫�
+ (isEmpty(node.tasks) &&
+ isEmpty(node.candidateUsers) &&
+ CandidateStrategy.START_USER_SELECT === node.candidateStrategy) ||
+ // 鎯呭喌浜岋細褰撳墠鑺傜偣鏄鎵逛汉鑷��
+ CandidateStrategy.APPROVE_USER_SELECT === node.candidateStrategy
+ ) {
+ nextAssigneesActivityNode.value.push(node)
+ }
+
+ // 濡傛灉鑺傜偣鏈� candidateUsers锛岃缃埌 customApproveUsers 涓�
+ if (node.candidateUsers && node.candidateUsers.length > 0) {
+ customApproveUsersData[node.id] = node.candidateUsers
+ }
+ })
+
+ // 灏� candidateUsers 璁剧疆鍒� Timeline 缁勪欢涓�
+ await nextTick() // 绛夊緟涓嬩竴涓� tick锛岀‘淇� Timeline 缁勪欢宸茬粡娓叉煋
+ if (nextAssigneesTimelineRef.value && Object.keys(customApproveUsersData).length > 0) {
+ nextAssigneesTimelineRef.value.batchSetCustomApproveUsers(customApproveUsersData)
+ }
+ }
+}
+
+/** 閫夋嫨涓嬩竴涓妭鐐圭殑瀹℃壒浜� */
+const selectNextAssigneesConfirm = (id: string, userList: any[]) => {
+ approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id)
+}
+/** 瀹℃壒閫氳繃鏃讹紝鏍¢獙姣忎釜鑷�夊鎵逛汉鐨勮妭鐐规槸鍚﹂兘宸查厤缃簡瀹℃壒浜� */
+const validateNextAssignees = () => {
+ if (Object.keys(nextAssigneesActivityNode.value).length === 0) {
+ return true
+ }
+ // 濡傛灉闇�瑕佽嚜閫夊鎵逛汉锛屽垯鏍¢獙姣忎釜鑺傜偣鏄惁閮藉凡閰嶇疆瀹℃壒浜�
+ for (const item of nextAssigneesActivityNode.value) {
+ if (isEmpty(approveReasonForm.nextAssignees[item.id])) {
+ message.warning('涓嬩竴涓妭鐐圭殑瀹℃壒浜轰笉鑳戒负绌�!')
+ return false
+ }
+ }
+ return true
+}
+
+/** 澶勭悊瀹℃壒閫氳繃鍜屼笉閫氳繃鐨勬搷浣� */
+const handleAudit = async (pass: boolean, formRef: FormInstance | undefined) => {
+ formLoading.value = true
+ try {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ await formRef.validate()
+ // 鏍¢獙娴佺▼琛ㄥ崟蹇呭~瀛楁
+ const valid = await validateNormalForm()
+ if (!valid) {
+ message.warning('琛ㄥ崟鏍¢獙涓嶉�氳繃锛岃鍏堝畬鍠勮〃鍗�!!')
+ return
+ }
+
+ if (pass) {
+ const nextAssigneesValid = validateNextAssignees()
+ if (!nextAssigneesValid) return
+ const variables = getUpdatedProcessInstanceVariables()
+ // 瀹℃壒閫氳繃鏁版嵁
+ const data = {
+ id: runningTask.value.id,
+ reason: approveReasonForm.reason,
+ variables, // 瀹℃壒閫氳繃, 鎶婁慨鏀圭殑瀛楁鍊艰祴浜庢祦绋嬪疄渚嬪彉閲�
+ nextAssignees: approveReasonForm.nextAssignees // 涓嬩釜鑷�夎妭鐐归�夋嫨鐨勫鎵逛汉淇℃伅
+ } as any
+ // 绛惧悕
+ if (runningTask.value.signEnable) {
+ data.signPicUrl = approveReasonForm.signPicUrl
+ }
+ // 澶氳〃鍗曞鐞嗭紝骞朵笖鏈夐澶栫殑 approveForm 琛ㄥ崟锛岄渶瑕佹牎楠� + 鎷兼帴鍒� data 琛ㄥ崟閲屾彁浜�
+ // TODO 鑺嬭壙 浠诲姟鏈夊琛ㄥ崟杩欓噷瑕佸浣曞鐞嗭紝浼氬拰鍙紪杈戠殑瀛楁鍐茬獊
+ const formCreateApi = approveFormFApi.value
+ if (Object.keys(formCreateApi)?.length > 0) {
+ await formCreateApi.validate()
+ // @ts-ignore
+ data.variables = approveForm.value.value
+ }
+ await TaskApi.approveTask(data)
+ popOverVisible.value.approve = false
+ nextAssigneesActivityNode.value = []
+ // 娓呯悊 Timeline 缁勪欢涓殑鑷畾涔夊鎵逛汉鏁版嵁
+ if (nextAssigneesTimelineRef.value) {
+ nextAssigneesTimelineRef.value.batchSetCustomApproveUsers({})
+ }
+ message.success('瀹℃壒閫氳繃鎴愬姛')
+ } else {
+ // 瀹℃壒涓嶉�氳繃鏁版嵁
+ const data = {
+ id: runningTask.value.id,
+ reason: rejectReasonForm.reason
+ }
+ await TaskApi.rejectTask(data)
+ popOverVisible.value.reject = false
+ message.success('瀹℃壒涓嶉�氳繃鎴愬姛')
+ }
+ // 閲嶇疆琛ㄥ崟
+ formRef.resetFields()
+ // 鍔犺浇鏈�鏂版暟鎹�
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊鎶勯�� */
+const handleCopy = async () => {
+ formLoading.value = true
+ try {
+ // 1. 鏍¢獙琛ㄥ崟
+ if (!copyFormRef.value) return
+ await copyFormRef.value.validate()
+ // 2. 鎻愪氦鎶勯��
+ const data = {
+ id: runningTask.value.id,
+ reason: copyForm.copyReason,
+ copyUserIds: copyForm.copyUserIds
+ }
+ await TaskApi.copyTask(data)
+ copyFormRef.value.resetFields()
+ popOverVisible.value.copy = false
+ message.success('鎿嶄綔鎴愬姛')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊杞氦 */
+const handleTransfer = async () => {
+ formLoading.value = true
+ try {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!transferFormRef.value) return
+ await transferFormRef.value.validate()
+ // 1.2 鎻愪氦杞氦
+ const data = {
+ id: runningTask.value.id,
+ reason: transferForm.reason,
+ assigneeUserId: transferForm.assigneeUserId
+ }
+ await TaskApi.transferTask(data)
+ transferFormRef.value.resetFields()
+ popOverVisible.value.transfer = false
+ message.success('鎿嶄綔鎴愬姛')
+ // 2. 鍔犺浇鏈�鏂版暟鎹�
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊濮旀淳 */
+const handleDelegate = async () => {
+ formLoading.value = true
+ try {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!delegateFormRef.value) return
+ await delegateFormRef.value.validate()
+ // 1.2 澶勭悊濮旀淳
+ const data = {
+ id: runningTask.value.id,
+ reason: delegateForm.reason,
+ delegateUserId: delegateForm.delegateUserId
+ }
+
+ await TaskApi.delegateTask(data)
+ popOverVisible.value.delegate = false
+ delegateFormRef.value.resetFields()
+ message.success('鎿嶄綔鎴愬姛')
+ // 2. 鍔犺浇鏈�鏂版暟鎹�
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊鍔犵 */
+const handlerAddSign = async (type: string) => {
+ formLoading.value = true
+ try {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!addSignFormRef.value) return
+ await addSignFormRef.value.validate()
+ // 1.2 鎻愪氦鍔犵
+ const data = {
+ id: runningTask.value.id,
+ type,
+ reason: addSignForm.reason,
+ userIds: addSignForm.addSignUserIds
+ }
+ await TaskApi.signCreateTask(data)
+ message.success('鎿嶄綔鎴愬姛')
+ addSignFormRef.value.resetFields()
+ popOverVisible.value.addSign = false
+ // 2 鍔犺浇鏈�鏂版暟鎹�
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊閫�鍥� */
+const handleReturn = async () => {
+ formLoading.value = true
+ try {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!returnFormRef.value) return
+ await returnFormRef.value.validate()
+ // 1.2 鎻愪氦閫�鍥�
+ const data = {
+ id: runningTask.value.id,
+ reason: returnForm.returnReason,
+ targetTaskDefinitionKey: returnForm.targetTaskDefinitionKey
+ }
+
+ await TaskApi.returnTask(data)
+ popOverVisible.value.return = false
+ returnFormRef.value.resetFields()
+ message.success('鎿嶄綔鎴愬姛')
+ // 2 閲嶆柊鍔犺浇鏁版嵁
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊鍙栨秷 */
+const handleCancel = async () => {
+ formLoading.value = true
+ try {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!cancelFormRef.value) return
+ await cancelFormRef.value.validate()
+ // 1.2 鎻愪氦鍙栨秷
+ await ProcessInstanceApi.cancelProcessInstanceByStartUser(
+ props.processInstance.id,
+ cancelForm.cancelReason
+ )
+ popOverVisible.value.return = false
+ message.success('鎿嶄綔鎴愬姛')
+ cancelFormRef.value.resetFields()
+ // 2 閲嶆柊鍔犺浇鏁版嵁
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊鍐嶆鎻愪氦 */
+const handleReCreate = async () => {
+ // 璺宠浆鍙戣捣娴佺▼鐣岄潰
+ await router.push({
+ name: 'BpmProcessInstanceCreate',
+ query: { processInstanceId: props.processInstance?.id }
+ })
+}
+
+/** 鑾峰彇鍑忕浜哄憳鏍囩 */
+const getDeleteSignUserLabel = (task: any): string => {
+ const deptName = task?.assigneeUser?.deptName || task?.ownerUser?.deptName
+ const nickname = task?.assigneeUser?.nickname || task?.ownerUser?.nickname
+ return `${nickname} ( 鎵�灞為儴闂細${deptName} )`
+}
+/** 澶勭悊鍑忕 */
+const handlerDeleteSign = async () => {
+ formLoading.value = true
+ try {
+ // 1.1 鏍¢獙琛ㄥ崟
+ if (!deleteSignFormRef.value) return
+ await deleteSignFormRef.value.validate()
+ // 1.2 鎻愪氦鍑忕
+ const data = {
+ id: deleteSignForm.deleteSignTaskId,
+ reason: deleteSignForm.reason
+ }
+ await TaskApi.signDeleteTask(data)
+ message.success('鍑忕鎴愬姛')
+ deleteSignFormRef.value.resetFields()
+ popOverVisible.value.deleteSign = false
+ // 2 鍔犺浇鏈�鏂版暟鎹�
+ reload()
+ } finally {
+ formLoading.value = false
+ }
+}
+/** 閲嶆柊鍔犺浇鏁版嵁 */
+const reload = () => {
+ emit('success')
+}
+
+/** 浠诲姟鏄惁涓哄鐞嗕腑鐘舵�� */
+const isHandleTaskStatus = () => {
+ let canHandle = false
+ if (TaskApi.TaskStatusEnum.RUNNING === runningTask.value?.status) {
+ canHandle = true
+ }
+ return canHandle
+}
+
+/** 娴佺▼鐘舵�佹槸鍚︿负缁撴潫鐘舵�� */
+const isEndProcessStatus = (status: number) => {
+ let isEndStatus = false
+ if (
+ BpmProcessInstanceStatus.APPROVE === status ||
+ BpmProcessInstanceStatus.REJECT === status ||
+ BpmProcessInstanceStatus.CANCEL === status
+ ) {
+ isEndStatus = true
+ }
+ return isEndStatus
+}
+
+/** 鏄惁鏄剧ず鎸夐挳 */
+const isShowButton = (btnType: OperationButtonType): boolean => {
+ let isShow = true
+ if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
+ isShow = runningTask.value.buttonsSetting[btnType].enable
+ }
+ return isShow
+}
+
+/** 鑾峰彇鎸夐挳鐨勬樉绀哄悕绉� */
+const getButtonDisplayName = (btnType: OperationButtonType) => {
+ let displayName = OPERATION_BUTTON_NAME.get(btnType)
+ if (runningTask.value?.buttonsSetting && runningTask.value?.buttonsSetting[btnType]) {
+ displayName = runningTask.value.buttonsSetting[btnType].displayName
+ }
+ return displayName
+}
+
+const loadTodoTask = (task: any) => {
+ approveForm.value = {}
+ runningTask.value = task
+ approveFormFApi.value = {}
+ reasonRequire.value = task?.reasonRequire ?? false
+ nodeTypeName.value = task?.nodeType === NodeType.TRANSACTOR_NODE ? '鍔炵悊' : '瀹℃壒'
+ // 澶勭悊 approve 琛ㄥ崟.
+ if (task && task.formId && task.formConf) {
+ const tempApproveForm = {}
+ setConfAndFields2(tempApproveForm, task.formConf, task.formFields, task.formVariables)
+ approveForm.value = tempApproveForm
+ } else {
+ approveForm.value = {} // 鍗犱綅锛岄伩鍏嶄负绌�
+ }
+}
+
+/** 鏍¢獙娴佺▼琛ㄥ崟 */
+const validateNormalForm = async () => {
+ if (props.processDefinition?.formType === BpmModelFormType.NORMAL) {
+ let valid = true
+ try {
+ await props.normalFormApi?.validate()
+ } catch {
+ valid = false
+ }
+ return valid
+ } else {
+ return true
+ }
+}
+
+/** 浠庡彲浠ョ紪杈戠殑娴佺▼琛ㄥ崟瀛楁锛岃幏鍙栭渶瑕佷慨鏀圭殑娴佺▼瀹炰緥鐨勫彉閲� */
+const getUpdatedProcessInstanceVariables = () => {
+ const variables = {}
+ props.writableFields.forEach((field) => {
+ variables[field] = props.normalFormApi.getValue(field)
+ })
+ return variables
+}
+
+/** 澶勭悊绛惧悕瀹屾垚 */
+const handleSignFinish = (url: string) => {
+ approveReasonForm.signPicUrl = url
+ approveSignFormRef.value.validate('change')
+}
+
+defineExpose({ loadTodoTask })
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-affix--fixed) {
+ background-color: var(--el-bg-color);
+}
+
+.btn-container {
+ > div {
+ display: flex;
+ margin: 0 8px;
+ cursor: pointer;
+ align-items: center;
+
+ &:hover {
+ color: #6db5ff;
+ }
+ }
+}
+</style>
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue b/src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
new file mode 100644
index 0000000..87f8119
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue
@@ -0,0 +1,174 @@
+<template>
+ <div v-loading="loading" class="process-viewer-container">
+ <SimpleProcessViewer
+ :flow-node="simpleModel"
+ :tasks="tasks"
+ :process-instance="processInstance"
+ />
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { SimpleFlowNode, NodeType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { SimpleProcessViewer } from '@/components/SimpleProcessDesignerV2/src/'
+defineOptions({ name: 'BpmProcessInstanceSimpleViewer' })
+
+const props = defineProps({
+ loading: propTypes.bool.def(false), // 鏄惁鍔犺浇涓�
+ modelView: propTypes.object,
+ simpleJson: propTypes.string // Simple 妯″瀷缁撴瀯鏁版嵁 (json 鏍煎紡)
+})
+const simpleModel = ref<any>({})
+// 鐢ㄦ埛浠诲姟
+const tasks = ref([])
+// 娴佺▼瀹炰緥
+const processInstance = ref()
+
+/** 鐩戞帶妯″瀷瑙嗗浘 鍖呮嫭浠诲姟鍒楄〃銆佽繘琛屼腑鐨勬椿鍔ㄨ妭鐐圭紪鍙风瓑 */
+watch(
+ () => props.modelView,
+ async (newModelView) => {
+ if (newModelView) {
+ tasks.value = newModelView.tasks
+ processInstance.value = newModelView.processInstance
+ // 宸茬粡鎷掔粷鐨勬椿鍔ㄨ妭鐐圭紪鍙烽泦鍚堬紝鍙寘鎷� UserTask
+ const rejectedTaskActivityIds: string[] = newModelView.rejectedTaskActivityIds
+ // 杩涜涓殑娲诲姩鑺傜偣缂栧彿闆嗗悎锛� 鍙寘鎷� UserTask
+ const unfinishedTaskActivityIds: string[] = newModelView.unfinishedTaskActivityIds
+ // 宸茬粡瀹屾垚鐨勬椿鍔ㄨ妭鐐圭紪鍙烽泦鍚堬紝 鍖呮嫭 UserTask銆丟ateway 绛�
+ const finishedActivityIds: string[] = newModelView.finishedTaskActivityIds
+ // 宸茬粡瀹屾垚鐨勮繛绾胯妭鐐圭紪鍙烽泦鍚堬紝鍙寘鎷� SequenceFlow
+ const finishedSequenceFlowActivityIds: string[] = newModelView.finishedSequenceFlowActivityIds
+ setSimpleModelNodeTaskStatus(
+ newModelView.simpleModel,
+ newModelView.processInstance?.status,
+ rejectedTaskActivityIds,
+ unfinishedTaskActivityIds,
+ finishedActivityIds,
+ finishedSequenceFlowActivityIds
+ )
+ simpleModel.value = newModelView.simpleModel ? newModelView.simpleModel : {}
+ }
+ }
+)
+/** 鐩戞帶妯″瀷缁撴瀯鏁版嵁 */
+watch(
+ () => props.simpleJson,
+ async (value) => {
+ if (value) {
+ simpleModel.value = JSON.parse(value)
+ }
+ }
+)
+const setSimpleModelNodeTaskStatus = (
+ simpleModel: SimpleFlowNode | undefined,
+ processStatus: number,
+ rejectedTaskActivityIds: string[],
+ unfinishedTaskActivityIds: string[],
+ finishedActivityIds: string[],
+ finishedSequenceFlowActivityIds: string[]
+) => {
+ if (!simpleModel) {
+ return
+ }
+ // 缁撴潫鑺傜偣
+ if (simpleModel.type === NodeType.END_EVENT_NODE) {
+ if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = processStatus
+ } else {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ }
+ return
+ }
+ // 瀹℃壒鑺傜偣
+ if (
+ simpleModel.type === NodeType.START_USER_NODE ||
+ simpleModel.type === NodeType.USER_TASK_NODE ||
+ simpleModel.type === NodeType.TRANSACTOR_NODE ||
+ simpleModel.type === NodeType.CHILD_PROCESS_NODE
+ ) {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ if (rejectedTaskActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.REJECT
+ } else if (unfinishedTaskActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.RUNNING
+ } else if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ }
+ // TODO 鏄笉鏄繕缂轰竴涓� cancel 鐨勭姸鎬�
+ }
+ // 鎶勯�佽妭鐐�
+ if (simpleModel.type === NodeType.COPY_TASK_NODE) {
+ // 鎶勯�佽妭鐐�,鍙湁閫氳繃鍜屾湭鎵ц鐘舵��
+ if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ } else {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ }
+ }
+ // 寤惰繜鍣ㄨ妭鐐�
+ if (simpleModel.type === NodeType.DELAY_TIMER_NODE) {
+ // 寤惰繜鍣ㄨ妭鐐�,鍙湁閫氳繃鍜屾湭鎵ц鐘舵��
+ if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ } else {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ }
+ }
+ // 瑙﹀彂鍣ㄨ妭鐐�
+ if (simpleModel.type === NodeType.TRIGGER_NODE) {
+ // 瑙﹀彂鍣ㄨ妭鐐�,鍙湁閫氳繃鍜屾湭鎵ц鐘舵��
+ if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ } else {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ }
+ }
+
+ // 鏉′欢鑺傜偣瀵瑰簲 SequenceFlow
+ if (simpleModel.type === NodeType.CONDITION_NODE) {
+ // 鏉′欢鑺傜偣,鍙湁閫氳繃鍜屾湭鎵ц鐘舵��
+ if (finishedSequenceFlowActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ } else {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ }
+ }
+ // 缃戝叧鑺傜偣
+ if (
+ simpleModel.type === NodeType.CONDITION_BRANCH_NODE ||
+ simpleModel.type === NodeType.PARALLEL_BRANCH_NODE ||
+ simpleModel.type === NodeType.INCLUSIVE_BRANCH_NODE ||
+ simpleModel.type === NodeType.ROUTER_BRANCH_NODE
+ ) {
+ // 缃戝叧鑺傜偣銆傚彧鏈夐�氳繃鍜屾湭鎵ц鐘舵��
+ if (finishedActivityIds.includes(simpleModel.id)) {
+ simpleModel.activityStatus = TaskStatusEnum.APPROVE
+ } else {
+ simpleModel.activityStatus = TaskStatusEnum.NOT_START
+ }
+ simpleModel.conditionNodes?.forEach((node) => {
+ setSimpleModelNodeTaskStatus(
+ node,
+ processStatus,
+ rejectedTaskActivityIds,
+ unfinishedTaskActivityIds,
+ finishedActivityIds,
+ finishedSequenceFlowActivityIds
+ )
+ })
+ }
+
+ setSimpleModelNodeTaskStatus(
+ simpleModel.childNode,
+ processStatus,
+ rejectedTaskActivityIds,
+ unfinishedTaskActivityIds,
+ finishedActivityIds,
+ finishedSequenceFlowActivityIds
+ )
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
new file mode 100644
index 0000000..8690e58
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTaskList.vue
@@ -0,0 +1,103 @@
+<template>
+ <el-table :data="tasks" border header-cell-class-name="table-header-gray">
+ <el-table-column label="瀹℃壒鑺傜偣" prop="name" min-width="120" align="center" />
+ <el-table-column label="瀹℃壒浜�" min-width="100" align="center">
+ <template #default="scope">
+ {{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�濮嬫椂闂�"
+ prop="createTime"
+ min-width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ min-width="140"
+ />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" min-width="90">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹℃壒寤鸿" prop="reason" min-width="200">
+ <template #default="scope">
+ {{ scope.row.reason }}
+ <el-button
+ class="ml-10px"
+ size="small"
+ v-if="scope.row.formId > 0"
+ @click="handleFormDetail(scope.row)"
+ >
+ <Icon icon="ep:document" /> 鏌ョ湅琛ㄥ崟
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" min-width="100">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 寮圭獥锛氳〃鍗� -->
+ <Dialog title="琛ㄥ崟璇︽儏" v-model="taskFormVisible" width="600">
+ <form-create
+ ref="fApi"
+ v-model="taskForm.value"
+ :option="taskForm.option"
+ :rule="taskForm.rule"
+ />
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { propTypes } from '@/utils/propTypes'
+import { DICT_TYPE } from '@/utils/dict'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import * as TaskApi from '@/api/bpm/task'
+
+defineOptions({ name: 'BpmProcessInstanceTaskList' })
+
+const props = defineProps({
+ loading: propTypes.bool.def(false), // 鏄惁鍔犺浇涓�
+ id: propTypes.string // 娴佺▼瀹炰緥鐨勭紪鍙�
+})
+const tasks = ref([]) // 娴佺▼浠诲姟鐨勬暟缁�
+
+/** 鏌ョ湅琛ㄥ崟 */
+const fApi = ref<ApiAttrs>() // form-create 鐨� API 鎿嶄綔绫�
+const taskForm = ref({
+ rule: [],
+ option: {},
+ value: {}
+}) // 娴佺▼浠诲姟鐨勮〃鍗曡鎯�
+const taskFormVisible = ref(false)
+const handleFormDetail = async (row: any) => {
+ // 璁剧疆琛ㄥ崟
+ setConfAndFields2(taskForm, row.formConf, row.formFields, row.formVariables)
+ // 寮圭獥鎵撳紑
+ taskFormVisible.value = true
+ // 闅愯棌鎻愪氦銆侀噸缃寜閽紝璁剧疆绂佺敤鍙
+ await nextTick()
+ fApi.value.fapi.btn.show(false)
+ fApi.value?.fapi?.resetBtn.show(false)
+ fApi.value?.fapi?.disabled(true)
+}
+
+/** 鍙湁 loading 瀹屾垚鏃讹紝鎵嶅幓鍔犺浇娴佺▼鍒楄〃 */
+watch(
+ () => props.loading,
+ async (value) => {
+ if (value) {
+ tasks.value = await TaskApi.getTaskListByProcessInstanceId(props.id)
+ }
+ }
+)
+</script>
diff --git a/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
new file mode 100644
index 0000000..91f333e
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
@@ -0,0 +1,361 @@
+<!-- 瀹℃壒璇︽儏鐨勫彸渚э細瀹℃壒娴� -->
+<template>
+ <el-timeline class="pt-20px">
+ <!-- 閬嶅巻姣忎釜瀹℃壒鑺傜偣 -->
+ <el-timeline-item
+ v-for="(activity, index) in activityNodes"
+ :key="index"
+ size="large"
+ :icon="getApprovalNodeIcon(activity.status, activity.nodeType)"
+ :color="getApprovalNodeColor(activity.status)"
+ >
+ <template #dot>
+ <div
+ class="position-absolute left--10px top--6px rounded-full border border-solid border-#dedede w-30px h-30px flex justify-center items-center bg-#3f73f7 p-5px"
+ >
+ <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
+ <div
+ v-if="props.showStatusIcon"
+ class="position-absolute top-17px left-17px rounded-full flex items-center p-1px border-2 border-white border-solid"
+ :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
+ >
+ <el-icon :size="11" color="#fff">
+ <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
+ </el-icon>
+ </div>
+ </div>
+ </template>
+ <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}-${index}`">
+ <!-- 绗竴琛岋細鑺傜偣鍚嶇О銆佹椂闂� -->
+ <div class="flex w-full">
+ <div class="font-bold">
+ {{ activity.name }} <span v-if="activity.status === TaskStatusEnum.SKIP">銆愯烦杩囥��</span>
+ </div>
+ <!-- 淇℃伅锛氭椂闂� -->
+ <div
+ v-if="activity.status !== TaskStatusEnum.NOT_START"
+ class="text-#a5a5a5 text-13px mt-1 ml-auto"
+ >
+ {{ getApprovalNodeTime(activity) }}
+ </div>
+ </div>
+ <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
+ <el-button
+ type="primary"
+ plain
+ size="small"
+ @click="handleChildProcess(activity)"
+ :disabled="!activity.processInstanceId"
+ >
+ 鏌ョ湅瀛愭祦绋�
+ </el-button>
+ </div>
+ <!-- 闇�瑕佽嚜瀹氫箟閫夋嫨瀹℃壒浜� -->
+ <div
+ class="flex flex-wrap gap2 items-center"
+ v-if="
+ isEmpty(activity.tasks) &&
+ ((CandidateStrategy.START_USER_SELECT === activity.candidateStrategy &&
+ isEmpty(activity.candidateUsers)) ||
+ (props.enableApproveUserSelect &&
+ CandidateStrategy.APPROVE_USER_SELECT === activity.candidateStrategy))
+ "
+ >
+ <!-- && activity.nodeType === NodeType.USER_TASK_NODE -->
+ <el-tooltip content="娣诲姞鐢ㄦ埛" placement="left">
+ <el-button
+ class="!px-6px"
+ @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
+ >
+ <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
+ </el-button>
+ </el-tooltip>
+ <div
+ v-for="(user, idx1) in customApproveUsers[activity.id]"
+ :key="idx1"
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+ >
+ <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ user.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ user.nickname }}
+ </div>
+ </div>
+ <div v-else class="flex items-center flex-wrap mt-1 gap2">
+ <!-- 鎯呭喌涓�锛氶亶鍘嗘瘡涓鎵硅妭鐐逛笅鐨勩�愯繘琛屼腑銆憈ask 浠诲姟 -->
+ <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
+ <div
+ class="position-relative flex flex-wrap gap2"
+ v-if="task.assigneeUser || task.ownerUser"
+ >
+ <!-- 淇℃伅锛氬ご鍍忔樀绉� -->
+ <div
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+ >
+ <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
+ <el-avatar
+ class="!m-5px"
+ :size="28"
+ v-if="task.assigneeUser?.avatar"
+ :src="task.assigneeUser?.avatar"
+ />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ task.assigneeUser?.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ task.assigneeUser?.nickname }}
+ </template>
+ <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
+ <el-avatar
+ class="!m-5px"
+ :size="28"
+ v-if="task.ownerUser?.avatar"
+ :src="task.ownerUser?.avatar"
+ />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ task.ownerUser?.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ task.ownerUser?.nickname }}
+ </template>
+ <!-- 淇℃伅锛氫换鍔� ICON -->
+ <div
+ v-if="props.showStatusIcon && onlyStatusIconShow.includes(task.status)"
+ class="position-absolute top-19px left-23px rounded-full flex items-center p-1px border-2 border-white border-solid"
+ :style="{ backgroundColor: statusIconMap2[task.status]?.color }"
+ >
+ <Icon :size="11" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
+ </div>
+ </div>
+ </div>
+ <teleport defer :to="`#activity-task-${activity.id}-${index}`">
+ <div
+ v-if="
+ task.reason &&
+ [NodeType.USER_TASK_NODE, NodeType.END_EVENT_NODE].includes(activity.nodeType)
+ "
+ class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+ >
+ <!-- TODO lesan锛氳繖閲屽鏋滄槸鍔炵悊锛岄渶瑕佹槸鍔炵悊鎰忚 -->
+ 瀹℃壒鎰忚锛歿{ task.reason }}
+ </div>
+ <div
+ v-if="task.signPicUrl && activity.nodeType === NodeType.USER_TASK_NODE"
+ class="text-#a5a5a5 text-13px mt-1 w-full bg-#f8f8fa p2 rounded-md"
+ >
+ 绛惧悕锛�
+ <el-image
+ class="w-90px h-40px ml-5px"
+ :src="task.signPicUrl"
+ :preview-src-list="[task.signPicUrl]"
+ />
+ </div>
+ </teleport>
+ </div>
+ <!-- 鎯呭喌浜岋細閬嶅巻姣忎釜瀹℃壒鑺傜偣涓嬬殑銆愬�欓�夌殑銆憈ask 浠诲姟銆備緥濡傝锛�1锛変緷娆″鎵癸紝2锛夋湭鏉ョ殑瀹℃壒浠诲姟绛� -->
+ <div
+ v-for="(user, idx1) in activity.candidateUsers"
+ :key="idx1"
+ class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+ >
+ <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+ <el-avatar class="!m-5px" :size="28" v-else>
+ {{ user.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ user.nickname }}
+
+ <!-- 淇℃伅锛氫换鍔� ICON -->
+ <div
+ v-if="props.showStatusIcon"
+ class="position-absolute top-20px left-24px rounded-full flex items-center p-1px border-2 border-white border-solid"
+ :style="{ backgroundColor: statusIconMap2['-1']?.color }"
+ >
+ <Icon :size="11" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+
+ <!-- 鐢ㄦ埛閫夋嫨寮圭獥 -->
+ <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+</template>
+
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import { NodeType, CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { isEmpty } from '@/utils/is'
+import { Check, Close, Loading, Clock, Minus, Delete, ArrowDown } from '@element-plus/icons-vue'
+import starterSvg from '@/assets/svgs/bpm/starter.svg'
+import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
+import copySvg from '@/assets/svgs/bpm/copy.svg'
+import conditionSvg from '@/assets/svgs/bpm/condition.svg'
+import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
+import finishSvg from '@/assets/svgs/bpm/finish.svg'
+import transactorSvg from '@/assets/svgs/bpm/transactor.svg'
+import childProcessSvg from '@/assets/svgs/bpm/child-process.svg'
+
+defineOptions({ name: 'BpmProcessInstanceTimeline' })
+const props = withDefaults(
+ defineProps<{
+ activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 瀹℃壒鑺傜偣淇℃伅
+ showStatusIcon?: boolean // 鏄惁鏄剧ず澶村儚鍙充笅瑙掔姸鎬佸浘鏍�
+ enableApproveUserSelect?: boolean // 鏄惁寮�鍚鎵逛汉鑷�夊姛鑳�
+ }>(),
+ {
+ showStatusIcon: true, // 榛樿鍊间负 true
+ enableApproveUserSelect: false // 榛樿鍊间负 false
+ }
+)
+const { push } = useRouter() // 璺敱
+
+// 瀹℃壒鑺傜偣
+const statusIconMap2 = {
+ // 璺宠繃
+ '-2': { color: '#cccccc', icon: 'ep:arrow-down' },
+ // 鏈紑濮�
+ '-1': { color: '#909398', icon: 'ep-clock' },
+ // 寰呭鎵�
+ '0': { color: '#00b32a', icon: 'ep:loading' },
+ // 瀹℃壒涓�
+ '1': { color: '#448ef7', icon: 'ep:loading' },
+ // 瀹℃壒閫氳繃
+ '2': { color: '#00b32a', icon: 'ep:circle-check-filled' },
+ // 瀹℃壒涓嶉�氳繃
+ '3': { color: '#f46b6c', icon: 'fa-solid:times-circle' },
+ // 鍙栨秷
+ '4': { color: '#cccccc', icon: 'ep:delete-filled' },
+ // 閫�鍥�
+ '5': { color: '#f46b6c', icon: 'ep:remove-filled' },
+ // 濮旀淳涓�
+ '6': { color: '#448ef7', icon: 'ep:loading' },
+ // 瀹℃壒閫氳繃涓�
+ '7': { color: '#00b32a', icon: 'ep:circle-check-filled' }
+}
+
+const statusIconMap = {
+ // 璺宠繃
+ '-2': { color: '#909398', icon: ArrowDown },
+ // 瀹℃壒鏈紑濮�
+ '-1': { color: '#909398', icon: Clock },
+ '0': { color: '#00b32a', icon: Clock },
+ // 瀹℃壒涓�
+ '1': { color: '#448ef7', icon: Loading },
+ // 瀹℃壒閫氳繃
+ '2': { color: '#00b32a', icon: Check },
+ // 瀹℃壒涓嶉�氳繃
+ '3': { color: '#f46b6c', icon: Close },
+ // 宸插彇娑�
+ '4': { color: '#cccccc', icon: Delete },
+ // 閫�鍥�
+ '5': { color: '#f46b6c', icon: Minus },
+ // 濮旀淳涓�
+ '6': { color: '#448ef7', icon: Loading },
+ // 瀹℃壒閫氳繃涓�
+ '7': { color: '#00b32a', icon: Check }
+}
+
+const nodeTypeSvgMap = {
+ // 缁撴潫鑺傜偣
+ [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
+ // 鍙戣捣浜鸿妭鐐�
+ [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
+ // 瀹℃壒浜鸿妭鐐�
+ [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
+ // 鍔炵悊浜鸿妭鐐�
+ [NodeType.TRANSACTOR_NODE]: { color: '#ff943e', svg: transactorSvg },
+ // 鎶勯�佷汉鑺傜偣
+ [NodeType.COPY_TASK_NODE]: { color: '#3296fb', svg: copySvg },
+ // 鏉′欢鍒嗘敮鑺傜偣
+ [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
+ // 骞惰鍒嗘敮鑺傜偣
+ [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg },
+ // 瀛愭祦绋嬭妭鐐�
+ [NodeType.CHILD_PROCESS_NODE]: { color: '#14bb83', svg: childProcessSvg }
+}
+
+// 鍙湁鍙湁鐘舵�佹槸 -1銆�0銆�1 鎵嶅睍绀哄ご鍍忓彸灏忚鐘舵�佸皬icon
+const onlyStatusIconShow = [-1, 0, 1]
+
+// timeline鏃堕棿绾夸笂icon鍥炬爣
+const getApprovalNodeImg = (nodeType: NodeType) => {
+ return nodeTypeSvgMap[nodeType]?.svg
+}
+
+const getApprovalNodeIcon = (taskStatus: number, nodeType: NodeType) => {
+ if (taskStatus == TaskStatusEnum.NOT_START) {
+ return statusIconMap[taskStatus]?.icon
+ }
+
+ if (
+ nodeType === NodeType.START_USER_NODE ||
+ nodeType === NodeType.USER_TASK_NODE ||
+ nodeType === NodeType.TRANSACTOR_NODE ||
+ nodeType === NodeType.CHILD_PROCESS_NODE ||
+ nodeType === NodeType.END_EVENT_NODE
+ ) {
+ return statusIconMap[taskStatus]?.icon
+ }
+}
+
+const getApprovalNodeColor = (taskStatus: number) => {
+ return statusIconMap[taskStatus]?.color
+}
+
+const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
+ if (node.nodeType === NodeType.START_USER_NODE && node.startTime) {
+ return `${formatDate(node.startTime)}`
+ }
+ if (node.endTime) {
+ return `${formatDate(node.endTime)}`
+ }
+ if (node.startTime) {
+ return `${formatDate(node.startTime)}`
+ }
+}
+
+// 閫夋嫨鑷畾涔夊鎵逛汉
+const userSelectFormRef = ref()
+const handleSelectUser = (activityId, selectedList) => {
+ userSelectFormRef.value.open(activityId, selectedList)
+}
+const emit = defineEmits<{
+ selectUserConfirm: [id: any, userList: any[]]
+}>()
+const customApproveUsers: any = ref({}) // key锛歛ctivityId锛寁alue锛氱敤鎴峰垪琛�
+// 閫夋嫨瀹屾垚
+const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
+ customApproveUsers.value[activityId] = userList || []
+ emit('selectUserConfirm', activityId, userList)
+}
+
+/** 璺宠浆瀛愭祦绋� */
+const handleChildProcess = (activity: any) => {
+ if (!activity.processInstanceId) {
+ return
+ }
+ push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: activity.processInstanceId
+ }
+ })
+}
+
+/** 璁剧疆鑷畾涔夊鎵逛汉 */
+const setCustomApproveUsers = (activityId: string, users: any[]) => {
+ customApproveUsers.value[activityId] = users || []
+}
+
+/** 鎵归噺璁剧疆澶氫釜鑺傜偣鐨勮嚜瀹氫箟瀹℃壒浜� */
+const batchSetCustomApproveUsers = (data: Record<string, any[]>) => {
+ Object.keys(data).forEach((activityId) => {
+ customApproveUsers.value[activityId] = data[activityId] || []
+ })
+}
+
+// 鏆撮湶鏂规硶缁欑埗缁勪欢
+defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers })
+</script>
diff --git a/src/views/bpm/processInstance/detail/SignDialog.vue b/src/views/bpm/processInstance/detail/SignDialog.vue
new file mode 100644
index 0000000..744a355
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/SignDialog.vue
@@ -0,0 +1,50 @@
+<template>
+ <el-dialog v-model="signDialogVisible" title="绛惧悕" width="935">
+ <div class="position-relative">
+ <Vue3Signature class="b b-solid b-gray" ref="signature" w="900px" h="400px" />
+ <el-button
+ class="pos-absolute bottom-20px right-10px"
+ type="primary"
+ text
+ size="small"
+ @click="signature.clear()"
+ >
+ <Icon icon="ep:delete" class="mr-5px" />
+ 娓呴櫎
+ </el-button>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="signDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="submit"> 鎻愪氦 </el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup lang="ts">
+import Vue3Signature from 'vue3-signature'
+import * as FileApi from '@/api/infra/file'
+import download from '@/utils/download'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const signDialogVisible = ref(false)
+const signature = ref()
+
+const open = async () => {
+ signDialogVisible.value = true
+}
+defineExpose({ open })
+
+const emits = defineEmits(['success'])
+const submit = async () => {
+ message.success('绛惧悕涓婁紶涓绋嶇瓑銆傘�傘��')
+ const res = await FileApi.updateFile({
+ file: download.base64ToFile(signature.value.save('image/png'), '绛惧悕')
+ })
+ emits('success', res.data)
+ signDialogVisible.value = false
+}
+</script>
+
+<style scoped></style>
diff --git a/src/views/bpm/processInstance/detail/index.vue b/src/views/bpm/processInstance/detail/index.vue
new file mode 100644
index 0000000..aec1473
--- /dev/null
+++ b/src/views/bpm/processInstance/detail/index.vue
@@ -0,0 +1,363 @@
+<template>
+ <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }" class="position-relative">
+ <div class="processInstance-wrap-main">
+ <el-scrollbar>
+ <img
+ class="position-absolute right-20px"
+ width="150"
+ :src="auditIconsMap[processInstance.status]"
+ alt=""
+ />
+ <div class="flex">
+ <div class="text-#878c93 h-15px">缂栧彿锛歿{ id }}</div>
+ <Icon icon="ep:printer" class="ml-15px cursor-pointer" @click="handlePrint" />
+ </div>
+ <el-divider class="!my-8px" />
+ <div class="flex items-center gap-5 mb-10px h-40px">
+ <div class="text-26px font-bold mb-5px">{{ processInstance.name }}</div>
+ <dict-tag
+ v-if="processInstance.status"
+ :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS"
+ :value="processInstance.status"
+ />
+ </div>
+
+ <div class="flex items-center gap-5 mb-10px text-13px h-35px">
+ <div
+ class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600"
+ >
+ <el-avatar
+ :size="28"
+ v-if="processInstance?.startUser?.avatar"
+ :src="processInstance?.startUser?.avatar"
+ />
+ <el-avatar :size="28" v-else-if="processInstance?.startUser?.nickname">
+ {{ processInstance?.startUser?.nickname.substring(0, 1) }}
+ </el-avatar>
+ {{ processInstance?.startUser?.nickname }}
+ </div>
+ <div class="text-#878c93"> {{ formatDate(processInstance.startTime) }} 鎻愪氦 </div>
+ </div>
+
+ <el-tabs v-model="activeTab">
+ <!-- 琛ㄥ崟淇℃伅 -->
+ <el-tab-pane label="瀹℃壒璇︽儏" name="form">
+ <div class="form-scroll-area">
+ <el-scrollbar>
+ <el-row>
+ <el-col :span="17" class="!flex !flex-col formCol">
+ <!-- 琛ㄥ崟淇℃伅 -->
+ <div
+ v-loading="processInstanceLoading"
+ class="form-box flex flex-col mb-30px flex-1"
+ >
+ <!-- 鎯呭喌涓�锛氭祦绋嬭〃鍗� -->
+ <el-col v-if="processDefinition?.formType === BpmModelFormType.NORMAL">
+ <form-create
+ v-model="detailForm.value"
+ v-model:api="fApi"
+ :option="detailForm.option"
+ :rule="detailForm.rule"
+ />
+ </el-col>
+ <!-- 鎯呭喌浜岋細涓氬姟琛ㄥ崟 -->
+ <div v-if="processDefinition?.formType === BpmModelFormType.CUSTOM">
+ <BusinessFormComponent :id="processInstance.businessKey" />
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="7">
+ <!-- 瀹℃壒璁板綍鏃堕棿绾� -->
+ <ProcessInstanceTimeline :activity-nodes="activityNodes" />
+ </el-col>
+ </el-row>
+ </el-scrollbar>
+ </div>
+ </el-tab-pane>
+
+ <!-- 娴佺▼鍥� -->
+ <el-tab-pane label="娴佺▼鍥�" name="diagram">
+ <div class="form-scroll-area">
+ <ProcessInstanceSimpleViewer
+ v-show="
+ processDefinition.modelType && processDefinition.modelType === BpmModelType.SIMPLE
+ "
+ :loading="processInstanceLoading"
+ :model-view="processModelView"
+ />
+ <ProcessInstanceBpmnViewer
+ v-show="
+ processDefinition.modelType && processDefinition.modelType === BpmModelType.BPMN
+ "
+ :loading="processInstanceLoading"
+ :model-view="processModelView"
+ />
+ </div>
+ </el-tab-pane>
+
+ <!-- 娴佽浆璁板綍 -->
+ <el-tab-pane label="娴佽浆璁板綍" name="record">
+ <div class="form-scroll-area">
+ <el-scrollbar>
+ <ProcessInstanceTaskList :loading="processInstanceLoading" :id="id" />
+ </el-scrollbar>
+ </div>
+ </el-tab-pane>
+
+ <!-- 娴佽浆璇勮 TODO 寰呭紑鍙� -->
+ <el-tab-pane label="娴佽浆璇勮" name="comment" v-if="false">
+ <div class="form-scroll-area">
+ <el-scrollbar> 娴佽浆璇勮 </el-scrollbar>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+
+ <div class="b-t-solid border-t-1px border-[var(--el-border-color)]">
+ <!-- 鎿嶄綔鏍忔寜閽� -->
+ <ProcessInstanceOperationButton
+ ref="operationButtonRef"
+ :process-instance="processInstance"
+ :process-definition="processDefinition"
+ :userOptions="userOptions"
+ :normal-form="detailForm"
+ :normal-form-api="fApi"
+ :writable-fields="writableFields"
+ @success="refresh"
+ />
+ </div>
+ </el-scrollbar>
+ </div>
+ </ContentWrap>
+
+ <!-- 鎵撳嵃棰勮寮圭獥 -->
+ <PrintDialog ref="printRef" />
+</template>
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { BpmModelType, BpmModelFormType } from '@/utils/constants'
+import { setConfAndFields2 } from '@/utils/formCreate'
+import { registerComponent } from '@/utils/routerHelper'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as UserApi from '@/api/system/user'
+import ProcessInstanceBpmnViewer from './ProcessInstanceBpmnViewer.vue'
+import ProcessInstanceSimpleViewer from './ProcessInstanceSimpleViewer.vue'
+import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
+import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
+import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
+import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import runningSvg from '@/assets/svgs/bpm/running.svg'
+import approveSvg from '@/assets/svgs/bpm/approve.svg'
+import rejectSvg from '@/assets/svgs/bpm/reject.svg'
+import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
+import PrintDialog from './PrintDialog.vue'
+
+defineOptions({ name: 'BpmProcessInstanceDetail' })
+const props = defineProps<{
+ id: string // 娴佺▼瀹炰緥鐨勭紪鍙�
+ taskId?: string // 浠诲姟缂栧彿
+ activityId?: string //娴佺▼娲诲姩缂栧彿锛岀敤浜庢妱閫佹煡鐪�
+}>()
+const message = useMessage() // 娑堟伅寮圭獥
+const processInstanceLoading = ref(false) // 娴佺▼瀹炰緥鐨勫姞杞戒腑
+const processInstance = ref<any>({}) // 娴佺▼瀹炰緥
+const processDefinition = ref<any>({}) // 娴佺▼瀹氫箟
+const processModelView = ref<any>({}) // 娴佺▼妯″瀷瑙嗗浘
+const operationButtonRef = ref() // 鎿嶄綔鎸夐挳缁勪欢 ref
+const auditIconsMap = {
+ [TaskStatusEnum.RUNNING]: runningSvg,
+ [TaskStatusEnum.APPROVE]: approveSvg,
+ [TaskStatusEnum.REJECT]: rejectSvg,
+ [TaskStatusEnum.CANCEL]: cancelSvg
+}
+
+// ========== 鐢宠淇℃伅 ==========
+const fApi = ref<ApiAttrs>() //
+const detailForm = ref({
+ rule: [],
+ option: {},
+ value: {}
+}) // 娴佺▼瀹炰緥鐨勮〃鍗曡鎯�
+
+const writableFields: Array<string> = [] // 琛ㄥ崟鍙互缂栬緫鐨勫瓧娈�
+
+/** 鑾峰緱璇︽儏 */
+const getDetail = () => {
+ // 鑾峰緱瀹℃壒璇︽儏
+ getApprovalDetail()
+ // 鑾峰緱娴佺▼妯″瀷瑙嗗浘
+ getProcessModelView()
+}
+
+/** 鍔犺浇娴佺▼瀹炰緥 */
+const BusinessFormComponent = ref<any>(null) // 寮傛缁勪欢
+/** 鑾峰彇瀹℃壒璇︽儏 */
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 瀹℃壒鑺傜偣淇℃伅
+const getApprovalDetail = async () => {
+ processInstanceLoading.value = true
+ try {
+ const param = {
+ processInstanceId: props.id,
+ activityId: props.activityId,
+ taskId: props.taskId
+ }
+ const data = await ProcessInstanceApi.getApprovalDetail(param)
+ if (!data) {
+ message.error('鏌ヨ涓嶅埌瀹℃壒璇︽儏淇℃伅锛�')
+ return
+ }
+ if (!data.processDefinition || !data.processInstance) {
+ message.error('鏌ヨ涓嶅埌娴佺▼淇℃伅锛�')
+ return
+ }
+ processInstance.value = data.processInstance
+ processDefinition.value = data.processDefinition
+
+ // 璁剧疆琛ㄥ崟淇℃伅
+ if (processDefinition.value.formType === BpmModelFormType.NORMAL) {
+ // 鑾峰彇琛ㄥ崟瀛楁鏉冮檺
+ const formFieldsPermission = data.formFieldsPermission
+ // 娓呯┖鍙紪杈戝瓧娈典负绌�
+ writableFields.splice(0)
+ if (detailForm.value.rule?.length > 0) {
+ // 閬垮厤鍒锋柊 form-create 鏄剧ず涓嶄簡
+ detailForm.value.value = processInstance.value.formVariables
+ } else {
+ setConfAndFields2(
+ detailForm,
+ processDefinition.value.formConf,
+ processDefinition.value.formFields,
+ processInstance.value.formVariables
+ )
+ }
+ nextTick().then(() => {
+ fApi.value?.btn.show(false)
+ fApi.value?.resetBtn.show(false)
+ //@ts-ignore
+ fApi.value?.disabled(true)
+ // 璁剧疆琛ㄥ崟瀛楁鏉冮檺
+ if (formFieldsPermission) {
+ Object.keys(data.formFieldsPermission).forEach((item) => {
+ setFieldPermission(item, formFieldsPermission[item])
+ })
+ }
+ })
+ } else {
+ // 娉ㄦ剰锛歞ata.processDefinition.formCustomViewPath 鏄粍浠剁殑鍏ㄨ矾寰勶紝渚嬪璇达細/crm/contract/detail/index.vue
+ BusinessFormComponent.value = registerComponent(data.processDefinition.formCustomViewPath)
+ }
+
+ // 鑾峰彇瀹℃壒鑺傜偣锛屾樉绀� Timeline 鐨勬暟鎹�
+ activityNodes.value = data.activityNodes
+
+ // 鑾峰彇寰呭姙浠诲姟鏄剧ず鎿嶄綔鎸夐挳
+ operationButtonRef.value?.loadTodoTask(data.todoTask)
+ } finally {
+ processInstanceLoading.value = false
+ }
+}
+
+/** 鑾峰彇娴佺▼妯″瀷瑙嗗浘*/
+const getProcessModelView = async () => {
+ if (BpmModelType.BPMN === processDefinition.value?.modelType) {
+ // 閲嶇疆锛岃В鍐� BPMN 娴佺▼鍥惧埛鏂颁笉浼氶噸鏂版覆鏌撻棶棰�
+ processModelView.value = {
+ bpmnXml: ''
+ }
+ }
+ const data = await ProcessInstanceApi.getProcessInstanceBpmnModelView(props.id)
+ if (data) {
+ processModelView.value = data
+ }
+}
+
+/** 璁剧疆琛ㄥ崟鏉冮檺 */
+const setFieldPermission = (field: string, permission: string) => {
+ if (permission === FieldPermissionType.READ) {
+ //@ts-ignore
+ fApi.value?.disabled(true, field)
+ }
+ if (permission === FieldPermissionType.WRITE) {
+ //@ts-ignore
+ fApi.value?.disabled(false, field)
+ // 鍔犲叆鍙互缂栬緫鐨勫瓧娈�
+ writableFields.push(field)
+ }
+ if (permission === FieldPermissionType.NONE) {
+ //@ts-ignore
+ fApi.value?.hidden(true, field)
+ }
+}
+
+/** 鎿嶄綔鎴愬姛鍚庡埛鏂� */
+const refresh = () => {
+ // 閲嶆柊鑾峰彇璇︽儏
+ getDetail()
+}
+
+/** 澶勭悊鎵撳嵃 */
+const printRef = ref()
+const handlePrint = async () => {
+ printRef.value.open(props.id)
+}
+
+/** 褰撳墠鐨� Tab */
+const activeTab = ref('form')
+
+/** 鍒濆鍖� */
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+onMounted(async () => {
+ getDetail()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
+
+<style lang="scss" scoped>
+$wrap-padding-height: 20px;
+$wrap-margin-height: 15px;
+$button-height: 51px;
+$process-header-height: 194px;
+
+.processInstance-wrap-main {
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+ );
+ max-height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px
+ );
+ overflow: auto;
+
+ .form-scroll-area {
+ display: flex;
+ height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+ $process-header-height - 40px
+ );
+ max-height: calc(
+ 100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 35px -
+ $process-header-height - 40px
+ );
+ overflow: auto;
+ flex-direction: column;
+
+ :deep(.box-card) {
+ height: 100%;
+ flex: 1;
+
+ .el-card__body {
+ height: 100%;
+ padding: 0;
+ }
+ }
+ }
+}
+
+.form-box {
+ :deep(.el-card) {
+ border: none;
+ }
+}
+</style>
diff --git a/src/views/bpm/processInstance/index.vue b/src/views/bpm/processInstance/index.vue
new file mode 100644
index 0000000..1a8ef89
--- /dev/null
+++ b/src/views/bpm/processInstance/index.vue
@@ -0,0 +1,338 @@
+<template>
+ <doc-alert title="娴佺▼鍙戣捣銆佸彇娑堛�侀噸鏂板彂璧�" url="https://doc.iocoder.cn/bpm/process-instance/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ </el-form-item>
+
+ <el-form-item label="" prop="category" class="absolute right-[300px]">
+ <el-select
+ v-model="queryParams.category"
+ placeholder="璇烽�夋嫨娴佺▼鍒嗙被"
+ clearable
+ class="!w-155px"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="category in categoryList"
+ :key="category.code"
+ :label="category.name"
+ :value="category.code"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="" prop="status" class="absolute right-[130px]">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娴佺▼鐘舵��"
+ clearable
+ class="!w-155px"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <!-- 楂樼骇绛涢�� -->
+ <el-form-item class="absolute right-0">
+ <el-popover
+ :visible="showPopover"
+ persistent
+ :width="400"
+ :show-arrow="false"
+ placement="bottom-end"
+ >
+ <template #reference>
+ <el-button @click="showPopover = !showPopover">
+ <Icon icon="ep:plus" class="mr-5px" />楂樼骇绛涢��
+ </el-button>
+ </template>
+ <el-form-item
+ label="鎵�灞炴祦绋�"
+ class="font-bold"
+ label-position="top"
+ prop="processDefinitionKey"
+ >
+ <el-select
+ v-model="queryParams.processDefinitionKey"
+ placeholder="璇烽�夋嫨娴佺▼瀹氫箟"
+ clearable
+ class="!w-390px"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="item in processDefinitionList"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戣捣鏃堕棿" class="font-bold" label-position="top" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item class="font-bold" label-position="top">
+ <div class="flex justify-end w-full">
+ <el-button @click="resetQuery">娓呯┖</el-button>
+ <el-button @click="showPopover = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="handleQuery">纭</el-button>
+ </div>
+ </el-form-item>
+ </el-popover>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="娴佺▼鍚嶇О" align="center" prop="name" min-width="200px" fixed="left" />
+ <el-table-column label="鎽樿" prop="summary" width="180" fixed="left">
+ <template #default="scope">
+ <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+ <div v-for="(item, index) in scope.row.summary" :key="index">
+ <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="娴佺▼鍒嗙被"
+ align="center"
+ prop="categoryName"
+ min-width="100"
+ fixed="left"
+ />
+ <el-table-column label="娴佺▼鐘舵��" prop="status" min-width="200">
+ <template #default="scope">
+ <!-- 瀹℃壒涓姸鎬� -->
+ <template
+ v-if="
+ scope.row.status === BpmProcessInstanceStatus.RUNNING && scope.row.tasks?.length > 0
+ "
+ >
+ <!-- 鍗曚汉瀹℃壒 -->
+ <template v-if="scope.row.tasks.length === 1">
+ <span>
+ <el-button link type="primary" @click="handleDetail(scope.row)">
+ {{ scope.row.tasks[0].assigneeUser?.nickname }}
+ </el-button>
+ ({{ scope.row.tasks[0].name }}) 瀹℃壒涓�
+ </span>
+ </template>
+ <!-- 澶氫汉瀹℃壒 -->
+ <template v-else>
+ <span>
+ <el-button link type="primary" @click="handleDetail(scope.row)">
+ {{ scope.row.tasks[0].assigneeUser?.nickname }}
+ </el-button>
+ 绛� {{ scope.row.tasks.length }} 浜� ({{ scope.row.tasks[0].name }})瀹℃壒涓�
+ </span>
+ </template>
+ </template>
+ <!-- 闈炲鎵逛腑鐘舵�� -->
+ <template v-else>
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+ </template>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戣捣鏃堕棿"
+ align="center"
+ prop="startTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ label="缁撴潫鏃堕棿"
+ align="center"
+ prop="endTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="180">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ v-hasPermi="['bpm:process-instance:cancel']"
+ @click="handleDetail(scope.row)"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ v-if="scope.row.status === 1"
+ v-hasPermi="['bpm:process-instance:query']"
+ @click="handleCancel(scope.row)"
+ >
+ 鍙栨秷
+ </el-button>
+ <el-button link type="primary" v-else @click="handleCreate(scope.row)">
+ 閲嶆柊鍙戣捣
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ElMessageBox } from 'element-plus'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import { ProcessInstanceVO } from '@/api/bpm/processInstance'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { BpmProcessInstanceStatus } from '@/utils/constants'
+
+defineOptions({ name: 'BpmProcessInstanceMy' })
+
+const router = useRouter() // 璺敱
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const processDefinitionList = ref<any[]>([]) // 娴佺▼瀹氫箟鍒楄〃
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ processDefinitionKey: undefined,
+ category: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const categoryList = ref<CategoryVO[]>([]) // 娴佺▼鍒嗙被鍒楄〃
+const showPopover = ref(false) // 楂樼骇绛涢�夋槸鍚﹀睍绀�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessInstanceApi.getProcessInstanceMyPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍙戣捣娴佺▼鎿嶄綔 **/
+const handleCreate = async (row?: ProcessInstanceVO) => {
+ if (row?.id) {
+ const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
+ row.processDefinitionId
+ )
+ // 濡傛灉鏄�愪笟鍔¤〃鍗曘�戯紝璺宠浆鍒板搴旂殑鍙戣捣鐣岄潰
+ if (processDefinitionDetail.formType === 20) {
+ await router.push({
+ path: processDefinitionDetail.formCustomCreatePath,
+ query: {
+ id: row.businessKey
+ }
+ })
+ } else if (processDefinitionDetail.formType === 10) {
+ //濡傛灉鏄�愭祦绋嬭〃鍗曘�戯紝璺宠浆鍒版祦绋嬪彂璧风晫闈�
+ await router.push({
+ name: 'BpmProcessInstanceCreate',
+ query: { processInstanceId: row.id }
+ })
+ }
+ }
+}
+
+/** 鏌ョ湅璇︽儏 */
+const handleDetail = (row: ProcessInstanceVO) => {
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.id
+ }
+ })
+}
+
+/** 鍙栨秷鎸夐挳鎿嶄綔 */
+const handleCancel = async (row: ProcessInstanceVO) => {
+ // 浜屾纭
+ const { value } = await ElMessageBox.prompt('璇疯緭鍏ュ彇娑堝師鍥�', '鍙栨秷娴佺▼', {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 鍒ゆ柇闈炵┖锛屼笖闈炵┖鏍�
+ inputErrorMessage: '鍙栨秷鍘熷洜涓嶈兘涓虹┖'
+ })
+ // 鍙戣捣鍙栨秷
+ await ProcessInstanceApi.cancelProcessInstanceByStartUser(row.id, value)
+ message.success('鍙栨秷鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+}
+
+/** 婵�娲绘椂 **/
+onActivated(() => {
+ getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ categoryList.value = await CategoryApi.getCategorySimpleList()
+ // 鑾峰彇娴佺▼瀹氫箟鍒楄〃
+ processDefinitionList.value = await DefinitionApi.getSimpleProcessDefinitionList()
+})
+</script>
diff --git a/src/views/bpm/processInstance/manager/index.vue b/src/views/bpm/processInstance/manager/index.vue
new file mode 100644
index 0000000..21e3a9e
--- /dev/null
+++ b/src/views/bpm/processInstance/manager/index.vue
@@ -0,0 +1,259 @@
+<template>
+ <doc-alert title="宸ヤ綔娴佹墜鍐�" url="https://doc.iocoder.cn/bpm/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍙戣捣浜�" prop="startUserId">
+ <el-select v-model="queryParams.startUserId" placeholder="璇烽�夋嫨鍙戣捣浜�" class="!w-240px">
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娴佺▼鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎵�灞炴祦绋�" prop="processDefinitionId">
+ <el-input
+ v-model="queryParams.processDefinitionId"
+ placeholder="璇疯緭鍏ユ祦绋嬪畾涔夌殑缂栧彿"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娴佺▼鍒嗙被" prop="category">
+ <el-select
+ v-model="queryParams.category"
+ placeholder="璇烽�夋嫨娴佺▼鍒嗙被"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="category in categoryList"
+ :key="category.code"
+ :label="category.name"
+ :value="category.code"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娴佺▼鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娴佺▼鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戣捣鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="娴佺▼鍚嶇О" align="center" prop="name" min-width="200px" fixed="left" />
+ <el-table-column
+ label="娴佺▼鍒嗙被"
+ align="center"
+ prop="categoryName"
+ min-width="100"
+ fixed="left"
+ />
+ <el-table-column label="娴佺▼鍙戣捣浜�" align="center" prop="startUser.nickname" width="120" />
+ <el-table-column label="鍙戣捣閮ㄩ棬" align="center" prop="startUser.deptName" width="120" />
+ <el-table-column label="娴佺▼鐘舵��" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戣捣鏃堕棿"
+ align="center"
+ prop="startTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ label="缁撴潫鏃堕棿"
+ align="center"
+ prop="endTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="169">
+ <template #default="scope">
+ {{ scope.row.durationInMillis > 0 ? formatPast2(scope.row.durationInMillis) : '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="褰撳墠瀹℃壒浠诲姟" align="center" prop="tasks" min-width="120px">
+ <template #default="scope">
+ <el-button type="primary" v-for="task in scope.row.tasks" :key="task.id" link>
+ <span>{{ task.name }}</span>
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="娴佺▼缂栧彿" align="center" prop="id" min-width="320px" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="180">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ v-hasPermi="['bpm:process-instance:cancel']"
+ @click="handleDetail(scope.row)"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ v-if="scope.row.status === 1"
+ v-hasPermi="['bpm:process-instance:query']"
+ @click="handleCancel(scope.row)"
+ >
+ 鍙栨秷
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import { ElMessageBox } from 'element-plus'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CategoryApi } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import { cancelProcessInstanceByAdmin } from '@/api/bpm/processInstance'
+
+// 瀹冨拰銆愭垜鐨勬祦绋嬨�戠殑宸紓鏄紝璇ヨ彍鍗曞彲浠ョ湅鍏ㄩ儴鐨勬祦绋嬪疄渚�
+defineOptions({ name: 'BpmProcessInstanceManager' })
+
+const router = useRouter() // 璺敱
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ startUserId: undefined,
+ name: '',
+ processDefinitionId: undefined,
+ category: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const categoryList = ref([]) // 娴佺▼鍒嗙被鍒楄〃
+const userList = ref<any[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessInstanceApi.getProcessInstanceManagerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鏌ョ湅璇︽儏 */
+const handleDetail = (row) => {
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.id
+ }
+ })
+}
+
+/** 鍙栨秷鎸夐挳鎿嶄綔 */
+const handleCancel = async (row) => {
+ // 浜屾纭
+ const { value } = await ElMessageBox.prompt('璇疯緭鍏ュ彇娑堝師鍥�', '鍙栨秷娴佺▼', {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 鍒ゆ柇闈炵┖锛屼笖闈炵┖鏍�
+ inputErrorMessage: '鍙栨秷鍘熷洜涓嶈兘涓虹┖'
+ })
+ // 鍙戣捣鍙栨秷
+ await ProcessInstanceApi.cancelProcessInstanceByAdmin(row.id, value)
+ message.success('鍙栨秷鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+}
+
+/** 婵�娲绘椂 **/
+onActivated(() => {
+ getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ categoryList.value = await CategoryApi.getCategorySimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/bpm/processInstance/report/index.vue b/src/views/bpm/processInstance/report/index.vue
new file mode 100644
index 0000000..939c0bd
--- /dev/null
+++ b/src/views/bpm/processInstance/report/index.vue
@@ -0,0 +1,274 @@
+<template>
+ <doc-alert title="宸ヤ綔娴佹墜鍐�" url="https://doc.iocoder.cn/bpm/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍙戣捣浜�" prop="startUserId">
+ <el-select v-model="queryParams.startUserId" placeholder="璇烽�夋嫨鍙戣捣浜�" class="!w-240px">
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娴佺▼鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娴佺▼鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娴佺▼鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戣捣鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-date-picker
+ v-model="queryParams.endTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item
+ v-for="(item, index) in formFields"
+ :key="index"
+ :label="item.title"
+ :prop="item.field"
+ >
+ <!-- TODO @lesan锛氱洰鍓嶅彧鏀寔input绫诲瀷鐨勫瓧绗︿覆鎼滅储 -->
+ <el-input
+ :disabled="item.type !== 'input'"
+ v-model="queryParams.formFieldsParams[item.field]"
+ :placeholder="`璇疯緭鍏�${item.title}`"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" border :data="list">
+ <el-table-column label="娴佺▼鍚嶇О" align="center" prop="name" fixed="left" width="200" />
+ <el-table-column label="娴佺▼鍙戣捣浜�" align="center" prop="startUser.nickname" width="120" />
+ <el-table-column label="娴佺▼鐘舵��" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戣捣鏃堕棿"
+ align="center"
+ prop="startTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ label="缁撴潫鏃堕棿"
+ align="center"
+ prop="endTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ v-for="(item, index) in formFields"
+ :key="index"
+ :label="item.title"
+ :prop="item.field"
+ width="120"
+ >
+ <!-- TODO @lesan锛氬彲浠ユ牴鎹甪ormField鐨則ype杩涜灞曠ず鏂瑰紡鐨勬帶鍒讹紝鐜板湪鍏ㄩ儴浠ュ瓧绗︿覆 -->
+ <template #default="scope">
+ {{ scope.row.formVariables[item.field] ?? '' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="180">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ v-hasPermi="['bpm:process-instance:cancel']"
+ @click="handleDetail(scope.row)"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ v-if="scope.row.status === 1"
+ v-hasPermi="['bpm:process-instance:query']"
+ @click="handleCancel(scope.row)"
+ >
+ 鍙栨秷
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import * as UserApi from '@/api/system/user'
+import * as DefinitionApi from '@/api/bpm/definition'
+import { parseFormFields } from '@/components/FormCreate/src/utils'
+import { ElMessageBox } from 'element-plus'
+
+defineOptions({ name: 'BpmProcessInstanceReport' })
+
+const router = useRouter() // 璺敱
+const { query } = useRoute()
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const formFields = ref()
+const processDefinitionId = query.processDefinitionId as string
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ startUserId: undefined,
+ name: '',
+ processDefinitionKey: query.processDefinitionKey,
+ status: undefined,
+ createTime: [],
+ endTime: [],
+ formFieldsParams: {}
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const userList = ref<any[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessInstanceApi.getProcessInstanceManagerPage({
+ ...queryParams,
+ formFieldsParams: JSON.stringify(queryParams.formFieldsParams)
+ })
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鑾峰彇娴佺▼瀹氫箟 */
+const getProcessDefinition = async () => {
+ const processDefinition = await DefinitionApi.getProcessDefinition(processDefinitionId)
+ formFields.value = parseFormCreateFields(processDefinition.formFields)
+}
+
+/** 瑙f瀽琛ㄥ崟瀛楁 */
+const parseFormCreateFields = (formFields?: string[]) => {
+ const result: Array<Record<string, any>> = []
+ if (formFields) {
+ formFields.forEach((fieldStr: string) => {
+ parseFormFields(JSON.parse(fieldStr), result)
+ })
+ }
+ return result
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ queryParams.formFieldsParams = {}
+ handleQuery()
+}
+
+/** 鏌ョ湅璇︽儏 */
+const handleDetail = (row) => {
+ router.push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.id
+ }
+ })
+}
+
+/** 鍙栨秷鎸夐挳鎿嶄綔 */
+const handleCancel = async (row) => {
+ // 浜屾纭
+ const { value } = await ElMessageBox.prompt('璇疯緭鍏ュ彇娑堝師鍥�', '鍙栨秷娴佺▼', {
+ confirmButtonText: t('common.ok'),
+ cancelButtonText: t('common.cancel'),
+ inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 鍒ゆ柇闈炵┖锛屼笖闈炵┖鏍�
+ inputErrorMessage: '鍙栨秷鍘熷洜涓嶈兘涓虹┖'
+ })
+ // 鍙戣捣鍙栨秷
+ await ProcessInstanceApi.cancelProcessInstanceByAdmin(row.id, value)
+ message.success('鍙栨秷鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 鑾峰彇娴佺▼瀹氫箟锛岀敤浜� table column 鐨勫睍绀�
+ await getProcessDefinition()
+ // 鑾峰彇娴佺▼鍒楄〃
+ await getList()
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/bpm/processListener/ProcessListenerForm.vue b/src/views/bpm/processListener/ProcessListenerForm.vue
new file mode 100644
index 0000000..916998e
--- /dev/null
+++ b/src/views/bpm/processListener/ProcessListenerForm.vue
@@ -0,0 +1,162 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="110px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="type">
+ <el-select
+ v-model="formData.type"
+ placeholder="璇烽�夋嫨绫诲瀷"
+ @change="formData.event = undefined"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浜嬩欢" prop="event">
+ <el-select v-model="formData.event" placeholder="璇烽�夋嫨浜嬩欢">
+ <el-option
+ v-for="event in formData.type == 'execution'
+ ? ['寮�濮�', '缁撴潫']
+ : ['鍒涘缓', '鎸囨淳', '瀹屾垚', '鍒犻櫎', '鏇存柊', '瓒呮椂']"
+ :label="event"
+ :value="event"
+ :key="event"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍊肩被鍨�" prop="valueType">
+ <el-select v-model="formData.valueType" placeholder="璇烽�夋嫨鍊肩被鍨�">
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="绫昏矾寰�" prop="value" v-if="formData.type == 'class'">
+ <el-input v-model="formData.value" placeholder="璇疯緭鍏ョ被璺緞" />
+ </el-form-item>
+ <el-form-item label="琛ㄨ揪寮�" prop="value" v-else>
+ <el-input v-model="formData.value" placeholder="璇疯緭鍏ヨ〃杈惧紡" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** BPM 娴佺▼ 琛ㄥ崟 */
+defineOptions({ name: 'ProcessListenerForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ type: undefined,
+ status: undefined,
+ event: undefined,
+ valueType: undefined,
+ value: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ type: [{ required: true, message: '绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ event: [{ required: true, message: '鐩戝惉浜嬩欢涓嶈兘涓虹┖', trigger: 'blur' }],
+ valueType: [{ required: true, message: '鍊肩被鍨嬩笉鑳戒负绌�', trigger: 'change' }],
+ value: [{ required: true, message: '鍊间笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProcessListenerApi.getProcessListener(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProcessListenerVO
+ if (formType.value === 'create') {
+ await ProcessListenerApi.createProcessListener(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProcessListenerApi.updateProcessListener(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ type: undefined,
+ status: CommonStatusEnum.ENABLE,
+ event: undefined,
+ valueType: undefined,
+ value: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/bpm/processListener/index.vue b/src/views/bpm/processListener/index.vue
new file mode 100644
index 0000000..8b5c36e
--- /dev/null
+++ b/src/views/bpm/processListener/index.vue
@@ -0,0 +1,185 @@
+<template>
+ <doc-alert title="鎵ц鐩戝惉鍣ㄣ�佷换鍔$洃鍚櫒" url="https://doc.iocoder.cn/bpm/listener/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="85px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="type">
+ <el-select v-model="queryParams.type" placeholder="璇烽�夋嫨绫诲瀷" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_LISTENER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['bpm:process-listener:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_PROCESS_LISTENER_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜嬩欢" align="center" prop="event" />
+ <el-table-column label="鍊肩被鍨�" align="center" prop="valueType">
+ <template #default="scope">
+ <dict-tag
+ :type="DICT_TYPE.BPM_PROCESS_LISTENER_VALUE_TYPE"
+ :value="scope.row.valueType"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍊�" align="center" prop="value" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['bpm:process-listener:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['bpm:process-listener:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProcessListenerForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProcessListenerApi, ProcessListenerVO } from '@/api/bpm/processListener'
+import ProcessListenerForm from './ProcessListenerForm.vue'
+
+/** BPM 娴佺▼ 鍒楄〃 */
+defineOptions({ name: 'BpmProcessListener' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProcessListenerVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ type: undefined,
+ event: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProcessListenerApi.deleteProcessListener(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/simple/SimpleModelDesign.vue b/src/views/bpm/simple/SimpleModelDesign.vue
new file mode 100644
index 0000000..0323527
--- /dev/null
+++ b/src/views/bpm/simple/SimpleModelDesign.vue
@@ -0,0 +1,39 @@
+<template>
+ <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
+ <SimpleProcessDesigner
+ :model-form-id="modelFormId"
+ :model-form-type="modelFormType"
+ :start-user-ids="startUserIds"
+ :start-dept-ids="startDeptIds"
+ @success="handleSuccess"
+ ref="designerRef"
+ />
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/'
+
+defineOptions({
+ name: 'SimpleModelDesign'
+})
+
+defineProps<{
+ modelName?: string
+ modelFormId?: number
+ modelFormType?: number
+ startUserIds?: number[]
+ startDeptIds?: number[]
+}>()
+
+const emit = defineEmits(['success'])
+const designerRef = ref()
+
+// 淇敼鎴愬姛鍥炶皟
+const handleSuccess = (data?: any) => {
+ console.info('handleSuccess', data)
+ if (data) {
+ emit('success', data)
+ }
+}
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/bpm/task/copy/index.vue b/src/views/bpm/task/copy/index.vue
new file mode 100644
index 0000000..91cfaaf
--- /dev/null
+++ b/src/views/bpm/task/copy/index.vue
@@ -0,0 +1,161 @@
+<!-- 宸ヤ綔娴� - 鎶勯�佹垜鐨勬祦绋� -->
+<template>
+ <doc-alert
+ title="瀹℃壒杞姙銆佸娲俱�佹妱閫�"
+ url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
+ />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form ref="queryFormRef" :inline="true" class="-mb-15px" label-width="68px">
+ <el-form-item label="娴佺▼鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.processInstanceName"
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ祦绋嬪悕绉�"
+ />
+ </el-form-item>
+ <el-form-item label="鎶勯�佹椂闂�" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <!-- TODO 鑺嬭壙锛氬鍔犳憳瑕� -->
+ <el-table-column align="center" label="娴佺▼鍚�" prop="processInstanceName" min-width="180" />
+ <el-table-column label="鎽樿" prop="summary" min-width="180">
+ <template #default="scope">
+ <div class="flex flex-col" v-if="scope.row.summary && scope.row.summary.length > 0">
+ <div v-for="(item, index) in scope.row.summary" :key="index">
+ <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="娴佺▼鍙戣捣浜�"
+ prop="startUser.nickname"
+ min-width="100"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="娴佺▼鍙戣捣鏃堕棿"
+ prop="processInstanceStartTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎶勯�佽妭鐐�" prop="activityName" min-width="180" />
+ <el-table-column align="center" label="鎶勯�佷汉" min-width="100">
+ <template #default="scope"> {{ scope.row.createUser?.nickname || '绯荤粺' }} </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎶勯�佹剰瑙�" prop="reason" width="150" />
+ <el-table-column
+ align="center"
+ label="鎶勯�佹椂闂�"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column align="center" label="鎿嶄綔" fixed="right" width="80">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleAudit(scope.row)">璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+
+defineOptions({ name: 'BpmProcessInstanceCopy' })
+
+const { push } = useRouter() // 璺敱
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ processInstanceId: '',
+ processInstanceName: '',
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ浠诲姟鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProcessInstanceApi.getProcessInstanceCopyPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 澶勭悊瀹℃壒鎸夐挳 */
+const handleAudit = (row: any) => {
+ const query = {
+ id: row.processInstanceId,
+ activityId: undefined
+ }
+ if (row.activityId) {
+ query.activityId = row.activityId
+ }
+ push({
+ name: 'BpmProcessInstanceDetail',
+ query: query
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/task/done/index.vue b/src/views/bpm/task/done/index.vue
new file mode 100644
index 0000000..202cd0e
--- /dev/null
+++ b/src/views/bpm/task/done/index.vue
@@ -0,0 +1,282 @@
+<template>
+ <doc-alert title="瀹℃壒閫氳繃銆佷笉閫氳繃銆侀┏鍥�" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
+ <doc-alert title="瀹℃壒鍔犵銆佸噺绛�" url="https://doc.iocoder.cn/bpm/sign/" />
+ <doc-alert
+ title="瀹℃壒杞姙銆佸娲俱�佹妱閫�"
+ url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
+ />
+ <doc-alert title="瀹℃壒鍔犵銆佸噺绛�" url="https://doc.iocoder.cn/bpm/sign/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ </el-form-item>
+
+ <el-form-item label="" prop="category" :style="{ position: 'absolute', right: '300px' }">
+ <el-select
+ v-model="queryParams.category"
+ placeholder="璇烽�夋嫨娴佺▼鍒嗙被"
+ clearable
+ class="!w-155px"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="category in categoryList"
+ :key="category.code"
+ :label="category.name"
+ :value="category.code"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="" prop="status" :style="{ position: 'absolute', right: '130px' }">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨瀹℃壒鐘舵��"
+ clearable
+ class="!w-155px"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <!-- 楂樼骇绛涢�� -->
+ <el-form-item :style="{ position: 'absolute', right: '0px' }">
+ <el-popover
+ :visible="showPopover"
+ persistent
+ :width="400"
+ :show-arrow="false"
+ placement="bottom-end"
+ >
+ <template #reference>
+ <el-button @click="showPopover = !showPopover">
+ <Icon icon="ep:plus" class="mr-5px" />楂樼骇绛涢��
+ </el-button>
+ </template>
+ <el-form-item
+ label="鎵�灞炴祦绋�"
+ class="font-bold"
+ label-position="top"
+ prop="processDefinitionKey"
+ >
+ <el-select
+ v-model="queryParams.processDefinitionKey"
+ placeholder="璇烽�夋嫨娴佺▼瀹氫箟"
+ clearable
+ @change="handleQuery"
+ class="!w-390px"
+ >
+ <el-option
+ v-for="item in processDefinitionList"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戣捣鏃堕棿" class="bold-label" label-position="top" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item class="bold-label" label-position="top">
+ <el-button @click="handleQuery"> 纭</el-button>
+ <el-button @click="showPopover = false"> 鍙栨秷</el-button>
+ <el-button @click="resetQuery"> 娓呯┖</el-button>
+ </el-form-item>
+ </el-popover>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="娴佺▼" prop="processInstance.name" width="180" />
+ <el-table-column label="鎽樿" prop="processInstance.summary" width="180">
+ <template #default="scope">
+ <div
+ class="flex flex-col"
+ v-if="scope.row.processInstance.summary && scope.row.processInstance.summary.length > 0"
+ >
+ <div v-for="(item, index) in scope.row.processInstance.summary" :key="index">
+ <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍙戣捣浜�"
+ prop="processInstance.startUser.nickname"
+ width="100"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍙戣捣鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="褰撳墠浠诲姟" prop="name" width="180" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浠诲姟寮�濮嬫椂闂�"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浠诲姟缁撴潫鏃堕棿"
+ prop="endTime"
+ width="180"
+ />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹℃壒寤鸿" prop="reason" min-width="180" />
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="160">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="娴佺▼缂栧彿"
+ prop="processInstanceId"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column align="center" label="浠诲姟缂栧彿" prop="id" :show-overflow-tooltip="true" />
+ <el-table-column align="center" label="鎿嶄綔" fixed="right" width="130">
+ <template #default="scope">
+ <el-button link type="warning" @click="handleWithdraw(scope.row)">鎾ゅ洖</el-button>
+ <el-button link type="primary" @click="handleAudit(scope.row)">鍘嗗彶</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import * as DefinitionApi from '@/api/bpm/definition'
+
+defineOptions({ name: 'BpmDoneTask' })
+
+const { push } = useRouter() // 璺敱
+const message = useMessage()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const processDefinitionList = ref<any[]>([]) // 娴佺▼瀹氫箟鍒楄〃
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ category: undefined,
+ status: undefined,
+ processDefinitionKey: '',
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const categoryList = ref<CategoryVO[]>([]) // 娴佺▼鍒嗙被鍒楄〃
+const showPopover = ref(false) // 楂樼骇绛涢�夋槸鍚﹀睍绀�
+
+/** 鏌ヨ浠诲姟鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TaskApi.getTaskDonePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 澶勭悊瀹℃壒鎸夐挳 */
+const handleAudit = (row: any) => {
+ push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.processInstance.id,
+ taskId: row.id
+ }
+ })
+}
+
+/** 娴嬪洖鎸夐挳 */
+const handleWithdraw = (row: any) => {
+ TaskApi.withdrawTask(row.id).then(() => {
+ message.success('鎾ゅ洖鎴愬姛')
+ getList()
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ categoryList.value = await CategoryApi.getCategorySimpleList()
+ // 鑾峰彇娴佺▼瀹氫箟鍒楄〃
+ processDefinitionList.value = await DefinitionApi.getSimpleProcessDefinitionList()
+})
+</script>
diff --git a/src/views/bpm/task/manager/index.vue b/src/views/bpm/task/manager/index.vue
new file mode 100644
index 0000000..ad21748
--- /dev/null
+++ b/src/views/bpm/task/manager/index.vue
@@ -0,0 +1,166 @@
+<template>
+ <doc-alert title="宸ヤ綔娴佹墜鍐�" url="https://doc.iocoder.cn/bpm/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="浠诲姟鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="娴佺▼" prop="processInstance.name" width="180" />
+ <el-table-column
+ align="center"
+ label="鍙戣捣浜�"
+ prop="processInstance.startUser.nickname"
+ width="100"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍙戣捣鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="褰撳墠浠诲姟" prop="name" width="180" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浠诲姟寮�濮嬫椂闂�"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浠诲姟缁撴潫鏃堕棿"
+ prop="endTime"
+ width="180"
+ />
+ <el-table-column align="center" label="瀹℃壒浜�" prop="assigneeUser.nickname" width="100" />
+ <el-table-column align="center" label="瀹℃壒鐘舵��" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹℃壒寤鸿" prop="reason" min-width="180" />
+ <el-table-column align="center" label="鑰楁椂" prop="durationInMillis" width="160">
+ <template #default="scope">
+ {{ formatPast2(scope.row.durationInMillis) }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="娴佺▼缂栧彿" prop="processInstanceId" :show-overflow-tooltip="true" />
+ <el-table-column align="center" label="浠诲姟缂栧彿" prop="id" :show-overflow-tooltip="true" />
+ <el-table-column align="center" label="鎿嶄綔" fixed="right" width="80">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleAudit(scope.row)">鍘嗗彶</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, formatPast2 } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+
+// 瀹冨拰銆愬緟鍔炰换鍔°�戙�愬凡鍔炰换鍔°�戠殑宸紓鏄紝璇ヨ彍鍗曞彲浠ョ湅鍏ㄩ儴鐨勬祦绋嬩换鍔�
+defineOptions({ name: 'BpmManagerTask' })
+
+const { push } = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ浠诲姟鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TaskApi.getTaskManagerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 澶勭悊瀹℃壒鎸夐挳 */
+const handleAudit = (row: any) => {
+ push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.processInstance.id
+ }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/bpm/task/todo/index.vue b/src/views/bpm/task/todo/index.vue
new file mode 100644
index 0000000..2c72e82
--- /dev/null
+++ b/src/views/bpm/task/todo/index.vue
@@ -0,0 +1,236 @@
+<template>
+ <doc-alert title="瀹℃壒閫氳繃銆佷笉閫氳繃銆侀┏鍥�" url="https://doc.iocoder.cn/bpm/task-todo-done/" />
+ <doc-alert title="瀹℃壒鍔犵銆佸噺绛�" url="https://doc.iocoder.cn/bpm/sign/" />
+ <doc-alert
+ title="瀹℃壒杞姙銆佸娲俱�佹妱閫�"
+ url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/"
+ />
+ <doc-alert title="瀹℃壒鍔犵銆佸噺绛�" url="https://doc.iocoder.cn/bpm/sign/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ </el-form-item>
+ <el-form-item label="" prop="category" class="absolute right-130px">
+ <el-select
+ v-model="queryParams.category"
+ placeholder="璇烽�夋嫨娴佺▼鍒嗙被"
+ clearable
+ class="!w-155px"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="category in categoryList"
+ :key="category.code"
+ :label="category.name"
+ :value="category.code"
+ />
+ </el-select>
+ </el-form-item>
+ <!-- 楂樼骇绛涢�� -->
+ <el-form-item class="absolute right-0">
+ <el-popover
+ :visible="showPopover"
+ persistent
+ :width="400"
+ :show-arrow="false"
+ placement="bottom-end"
+ >
+ <template #reference>
+ <el-button @click="showPopover = !showPopover">
+ <Icon icon="ep:plus" class="mr-5px" />楂樼骇绛涢��
+ </el-button>
+ </template>
+ <el-form-item
+ label="鎵�灞炴祦绋�"
+ class="font-bold"
+ label-position="top"
+ prop="processDefinitionKey"
+ >
+ <el-select
+ v-model="queryParams.processDefinitionKey"
+ placeholder="璇烽�夋嫨娴佺▼瀹氫箟"
+ clearable
+ @change="handleQuery"
+ class="!w-390px"
+ >
+ <el-option
+ v-for="item in processDefinitionList"
+ :key="item.key"
+ :label="item.name"
+ :value="item.key"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戣捣鏃堕棿" class="font-bold" label-position="top" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="w-240px!"
+ />
+ </el-form-item>
+ <el-form-item class="font-bold" label-position="top">
+ <div class="flex justify-end w-full">
+ <el-button @click="resetQuery">娓呯┖</el-button>
+ <el-button @click="showPopover = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="handleQuery">纭</el-button>
+ </div>
+ </el-form-item>
+ </el-popover>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="娴佺▼" prop="processInstance.name" width="180" />
+ <el-table-column label="鎽樿" prop="processInstance.summary" width="180">
+ <template #default="scope">
+ <div
+ class="flex flex-col"
+ v-if="scope.row.processInstance.summary && scope.row.processInstance.summary.length > 0"
+ >
+ <div v-for="(item, index) in scope.row.processInstance.summary" :key="index">
+ <el-text type="info"> {{ item.key }} : {{ item.value }} </el-text>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍙戣捣浜�"
+ prop="processInstance.startUser.nickname"
+ width="100"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍙戣捣鏃堕棿"
+ prop="processInstance.createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="褰撳墠浠诲姟" prop="name" width="180" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浠诲姟鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ align="center"
+ label="娴佺▼缂栧彿"
+ prop="processInstanceId"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column align="center" label="浠诲姟缂栧彿" prop="id" :show-overflow-tooltip="true" />
+ <el-table-column align="center" label="鎿嶄綔" fixed="right" width="80">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleAudit(scope.row)">鍔炵悊</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as TaskApi from '@/api/bpm/task'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
+import * as DefinitionApi from '@/api/bpm/definition'
+
+defineOptions({ name: 'BpmTodoTask' })
+
+const { push } = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const processDefinitionList = ref<any[]>([]) // 娴佺▼瀹氫箟鍒楄〃
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ category: undefined,
+ processDefinitionKey: '',
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const categoryList = ref<CategoryVO[]>([]) // 娴佺▼鍒嗙被鍒楄〃
+const showPopover = ref(false) // 楂樼骇绛涢�夋槸鍚﹀睍绀�
+
+/** 鏌ヨ浠诲姟鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TaskApi.getTaskTodoPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 澶勭悊瀹℃壒鎸夐挳 */
+const handleAudit = (row: any) => {
+ push({
+ name: 'BpmProcessInstanceDetail',
+ query: {
+ id: row.processInstance.id,
+ taskId: row.id
+ }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ categoryList.value = await CategoryApi.getCategorySimpleList()
+ // 鑾峰彇娴佺▼瀹氫箟鍒楄〃
+ processDefinitionList.value = await DefinitionApi.getSimpleProcessDefinitionList()
+})
+</script>
diff --git a/src/views/crm/backlog/components/ClueFollowList.vue b/src/views/crm/backlog/components/ClueFollowList.vue
new file mode 100644
index 0000000..4ed37d4
--- /dev/null
+++ b/src/views/crm/backlog/components/ClueFollowList.vue
@@ -0,0 +1,153 @@
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl">鍒嗛厤缁欐垜鐨勭嚎绱�</div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鐘舵��" prop="followUpStatus">
+ <el-select
+ v-model="queryParams.followUpStatus"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in FOLLOWUP_STATUS"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="绾跨储鍚嶇О" align="center" prop="name" fixed="left" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="绾跨储鏉ユ簮" align="center" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐢佃瘽" align="center" prop="telephone" width="130" />
+ <el-table-column label="閭" align="center" prop="email" width="180" />
+ <el-table-column label="鍦板潃" align="center" prop="detailAddress" width="180" />
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ label="鏈�鍚庤窡杩涙椂闂�"
+ align="center"
+ prop="contactLastTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100" />
+ <el-table-column
+ label="鏇存柊鏃堕棿"
+ align="center"
+ prop="updateTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ClueApi from '@/api/crm/clue'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { FOLLOWUP_STATUS } from './common'
+
+defineOptions({ name: 'CrmClueFollowList' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ followUpStatus: false,
+ transformStatus: false
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ClueApi.getCluePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鎵撳紑绾跨储璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmClueDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/backlog/components/ContractAuditList.vue b/src/views/crm/backlog/components/ContractAuditList.vue
new file mode 100644
index 0000000..9c13237
--- /dev/null
+++ b/src/views/crm/backlog/components/ContractAuditList.vue
@@ -0,0 +1,247 @@
+<!-- 寰呭鏍稿悎鍚� -->
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl">寰呭鏍稿悎鍚�</div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍚堝悓鐘舵��" prop="auditStatus">
+ <el-select
+ v-model="queryParams.auditStatus"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in AUDIT_STATUS"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="鍚堝悓缂栧彿" prop="no" width="180" />
+ <el-table-column align="center" fixed="left" label="鍚堝悓鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍟嗘満鍚嶇О" prop="businessName" width="130">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openBusinessDetail(scope.row.businessId)"
+ >
+ {{ scope.row.businessName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍚堝悓閲戦锛堝厓锛�"
+ prop="totalPrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="涓嬪崟鏃堕棿"
+ prop="orderDate"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="鍚堝悓寮�濮嬫椂闂�"
+ prop="startTime"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="鍚堝悓缁撴潫鏃堕棿"
+ prop="endTime"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column align="center" label="瀹㈡埛绛剧害浜�" prop="contactName" width="130">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openContactDetail(scope.row.signContactId)"
+ >
+ {{ scope.row.signContactName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍏徃绛剧害浜�" prop="signUserName" width="130" />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ align="center"
+ label="宸插洖娆鹃噾棰濓紙鍏冿級"
+ prop="totalReceivablePrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="鏈洖娆鹃噾棰濓紙鍏冿級"
+ prop="totalReceivablePrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ >
+ <template #default="scope">
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.totalReceivablePrice) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="120" />
+ <el-table-column align="center" fixed="right" label="鍚堝悓鐘舵��" prop="auditStatus" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="90">
+ <template #default="scope">
+ <el-button
+ link
+ v-hasPermi="['crm:contract:update']"
+ type="primary"
+ @click="handleProcessDetail(scope.row)"
+ >
+ 鏌ョ湅瀹℃壒
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts" name="CheckContract">
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import * as ContractApi from '@/api/crm/contract'
+import { DICT_TYPE } from '@/utils/dict'
+import { AUDIT_STATUS } from './common'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: 1, // 鎴戣礋璐g殑
+ auditStatus: 10
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ContractApi.getContractPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ョ湅瀹℃壒 */
+const handleProcessDetail = (row: ContractApi.ContractVO) => {
+ push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 鎵撳紑鍚堝悓璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const openContactDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 鎵撳紑鍟嗘満璇︽儏 */
+const openBusinessDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped></style>
diff --git a/src/views/crm/backlog/components/ContractRemindList.vue b/src/views/crm/backlog/components/ContractRemindList.vue
new file mode 100644
index 0000000..0cacf35
--- /dev/null
+++ b/src/views/crm/backlog/components/ContractRemindList.vue
@@ -0,0 +1,246 @@
+<!-- 鍗冲皢鍒版湡鐨勫悎鍚� -->
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl"> 鍗冲皢鍒版湡鐨勫悎鍚� </div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒版湡鐘舵��" prop="expiryType">
+ <el-select
+ v-model="queryParams.expiryType"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in CONTRACT_EXPIRY_TYPE"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="鍚堝悓缂栧彿" prop="no" width="180" />
+ <el-table-column align="center" fixed="left" label="鍚堝悓鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍟嗘満鍚嶇О" prop="businessName" width="130">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openBusinessDetail(scope.row.businessId)"
+ >
+ {{ scope.row.businessName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍚堝悓閲戦锛堝厓锛�"
+ prop="totalPrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="涓嬪崟鏃堕棿"
+ prop="orderDate"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="鍚堝悓寮�濮嬫椂闂�"
+ prop="startTime"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="鍚堝悓缁撴潫鏃堕棿"
+ prop="endTime"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column align="center" label="瀹㈡埛绛剧害浜�" prop="contactName" width="130">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openContactDetail(scope.row.signContactId)"
+ >
+ {{ scope.row.signContactName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍏徃绛剧害浜�" prop="signUserName" width="130" />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ align="center"
+ label="宸插洖娆鹃噾棰濓紙鍏冿級"
+ prop="totalReceivablePrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="鏈洖娆鹃噾棰濓紙鍏冿級"
+ prop="totalReceivablePrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ >
+ <template #default="scope">
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.totalReceivablePrice) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="120" />
+ <el-table-column align="center" fixed="right" label="鍚堝悓鐘舵��" prop="auditStatus" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="90">
+ <template #default="scope">
+ <el-button
+ link
+ v-hasPermi="['crm:contract:update']"
+ type="primary"
+ @click="handleProcessDetail(scope.row)"
+ >
+ 鏌ョ湅瀹℃壒
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts" name="EndContract">
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import * as ContractApi from '@/api/crm/contract'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE } from '@/utils/dict'
+import { CONTRACT_EXPIRY_TYPE } from './common'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 鑷繁璐熻矗鐨�
+ expiryType: 1
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ContractApi.getContractPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ョ湅瀹℃壒 */
+const handleProcessDetail = (row: ContractApi.ContractVO) => {
+ push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 鎵撳紑鍚堝悓璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const openContactDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 鎵撳紑鍟嗘満璇︽儏 */
+const openBusinessDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/backlog/components/CustomerFollowList.vue b/src/views/crm/backlog/components/CustomerFollowList.vue
new file mode 100644
index 0000000..0f367a3
--- /dev/null
+++ b/src/views/crm/backlog/components/CustomerFollowList.vue
@@ -0,0 +1,170 @@
+<!-- 鍒嗛厤缁欐垜鐨勫鎴� -->
+<!-- WHERE followUpStatus = ? -->
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl">鍒嗛厤缁欐垜鐨勫鎴�</div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鐘舵��" prop="followUpStatus">
+ <el-select
+ v-model="queryParams.followUpStatus"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in FOLLOWUP_STATUS"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" fixed="left" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐢佃瘽" align="center" prop="telephone" width="130" />
+ <el-table-column label="閭" align="center" prop="email" width="180" />
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column align="center" label="閿佸畾鐘舵��" prop="lockStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愪氦鐘舵��" prop="dealStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column label="鍦板潃" align="center" prop="detailAddress" width="180" />
+ <el-table-column align="center" label="璺濈杩涘叆鍏捣澶╂暟" prop="poolDay" width="140">
+ <template #default="scope"> {{ scope.row.poolDay }} 澶�</template>
+ </el-table-column>
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import * as CustomerApi from '@/api/crm/customer'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { FOLLOWUP_STATUS } from './common'
+
+defineOptions({ name: 'CrmCustomerFollowList' })
+
+const { push } = useRouter()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: 1,
+ followUpStatus: false
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerApi.getCustomerPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/backlog/components/CustomerPutPoolRemindList.vue b/src/views/crm/backlog/components/CustomerPutPoolRemindList.vue
new file mode 100644
index 0000000..17f8df6
--- /dev/null
+++ b/src/views/crm/backlog/components/CustomerPutPoolRemindList.vue
@@ -0,0 +1,169 @@
+<!-- 寰呰繘鍏ュ叕娴风殑瀹㈡埛 -->
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl"> 寰呰繘鍏ュ叕娴风殑瀹㈡埛 </div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="褰掑睘" prop="sceneType">
+ <el-select
+ v-model="queryParams.sceneType"
+ class="!w-240px"
+ placeholder="褰掑睘"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in SCENE_TYPES"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" fixed="left" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐢佃瘽" align="center" prop="telephone" width="130" />
+ <el-table-column label="閭" align="center" prop="email" width="180" />
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column align="center" label="閿佸畾鐘舵��" prop="lockStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愪氦鐘舵��" prop="dealStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column label="鍦板潃" align="center" prop="detailAddress" width="180" />
+ <el-table-column align="center" label="璺濈杩涘叆鍏捣澶╂暟" prop="poolDay" width="140">
+ <template #default="scope"> {{ scope.row.poolDay }} 澶�</template>
+ </el-table-column>
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { SCENE_TYPES } from './common'
+
+defineOptions({ name: 'CrmCustomerPutPoolRemindList' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: 1, // 鎴戣礋璐g殑
+ pool: true // 鍥哄畾 鍏捣鍙傛暟涓� true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerApi.getPutPoolRemindCustomerPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style lang="scss"></style>
diff --git a/src/views/crm/backlog/components/CustomerTodayContactList.vue b/src/views/crm/backlog/components/CustomerTodayContactList.vue
new file mode 100644
index 0000000..87aa31d
--- /dev/null
+++ b/src/views/crm/backlog/components/CustomerTodayContactList.vue
@@ -0,0 +1,180 @@
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl"> 浠婃棩闇�鑱旂郴瀹㈡埛 </div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鐘舵��" prop="contactStatus">
+ <el-select
+ v-model="queryParams.contactStatus"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in CONTACT_STATUS"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="褰掑睘" prop="sceneType">
+ <el-select
+ v-model="queryParams.sceneType"
+ class="!w-240px"
+ placeholder="褰掑睘"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in SCENE_TYPES"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" fixed="left" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐢佃瘽" align="center" prop="telephone" width="130" />
+ <el-table-column label="閭" align="center" prop="email" width="180" />
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column align="center" label="閿佸畾鐘舵��" prop="lockStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愪氦鐘舵��" prop="dealStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column label="鍦板潃" align="center" prop="detailAddress" width="180" />
+ <el-table-column align="center" label="璺濈杩涘叆鍏捣澶╂暟" prop="poolDay" width="140">
+ <template #default="scope"> {{ scope.row.poolDay }} 澶�</template>
+ </el-table-column>
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { CONTACT_STATUS, SCENE_TYPES } from './common'
+
+defineOptions({ name: 'CrmCustomerTodayContactList' })
+
+const { push } = useRouter()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ contactStatus: 1,
+ sceneType: 1,
+ pool: null // 鏄惁鍏捣鏁版嵁
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerApi.getCustomerPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style lang="scss"></style>
diff --git a/src/views/crm/backlog/components/ReceivableAuditList.vue b/src/views/crm/backlog/components/ReceivableAuditList.vue
new file mode 100644
index 0000000..2831d45
--- /dev/null
+++ b/src/views/crm/backlog/components/ReceivableAuditList.vue
@@ -0,0 +1,201 @@
+<!-- 寰呭鏍稿洖娆� -->
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl"> 寰呭鏍稿洖娆� </div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚堝悓鐘舵��" prop="auditStatus">
+ <el-select
+ v-model="queryParams.auditStatus"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in AUDIT_STATUS"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column align="center" fixed="left" label="鍥炴缂栧彿" prop="no" width="180">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.no }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍚堝悓缂栧彿" prop="contractNo" width="180">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openContractDetail(scope.row.contractId)"
+ >
+ {{ scope.row.contract.no }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="鍥炴鏃ユ湡"
+ prop="returnTime"
+ width="150px"
+ />
+ <el-table-column
+ align="center"
+ label="鍥炴閲戦(鍏�)"
+ prop="price"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column align="center" label="鍥炴鏂瑰紡" prop="returnType" width="130px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ align="center"
+ label="鍚堝悓閲戦锛堝厓锛�"
+ prop="contract.totalPrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="120" />
+ <el-table-column align="center" fixed="right" label="鍥炴鐘舵��" prop="auditStatus" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="180px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:receivable:update']"
+ link
+ type="primary"
+ @click="handleProcessDetail(scope.row)"
+ >
+ 鏌ョ湅瀹℃壒
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import * as ReceivableApi from '@/api/crm/receivable'
+import { AUDIT_STATUS } from './common'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'CrmReceivableAuditList' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ auditStatus: 10
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ReceivableApi.getReceivablePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ョ湅瀹℃壒 */
+const handleProcessDetail = (row: ReceivableApi.ReceivableVO) => {
+ push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 鎵撳紑鍥炴璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmReceivableDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鎵撳紑鍚堝悓璇︽儏 */
+const openContractDetail = (id: number) => {
+ push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/backlog/components/ReceivablePlanRemindList.vue b/src/views/crm/backlog/components/ReceivablePlanRemindList.vue
new file mode 100644
index 0000000..9a3cf0c
--- /dev/null
+++ b/src/views/crm/backlog/components/ReceivablePlanRemindList.vue
@@ -0,0 +1,220 @@
+<!-- 寰呭洖娆炬彁閱� -->
+<template>
+ <ContentWrap>
+ <div class="pb-5 text-xl">寰呭洖娆炬彁閱�</div>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍚堝悓鐘舵��" prop="remindType">
+ <el-select
+ v-model="queryParams.remindType"
+ class="!w-240px"
+ placeholder="鐘舵��"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(option, index) in RECEIVABLE_REMIND_TYPE"
+ :label="option.label"
+ :value="option.value"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="customerName" width="150">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍚堝悓缂栧彿" prop="contractNo" width="200px" />
+ <el-table-column align="center" label="鏈熸暟" prop="period">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.period }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="璁″垝鍥炴閲戦锛堝厓锛�"
+ prop="price"
+ width="160"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="璁″垝鍥炴鏃ユ湡"
+ prop="returnTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎻愬墠鍑犲ぉ鎻愰啋" prop="remindDays" width="150" />
+ <el-table-column
+ align="center"
+ label="鎻愰啋鏃ユ湡"
+ prop="remindTime"
+ width="180px"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column align="center" label="鍥炴鏂瑰紡" prop="returnType" width="130px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column
+ align="center"
+ label="瀹為檯鍥炴閲戦锛堝厓锛�"
+ prop="receivable.price"
+ width="160"
+ >
+ <template #default="scope">
+ <el-text v-if="scope.row.receivable">
+ {{ erpPriceInputFormatter(scope.row.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="瀹為檯鍥炴鏃ユ湡"
+ prop="receivable.returnTime"
+ width="180px"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="瀹為檯鍥炴閲戦锛堝厓锛�"
+ prop="receivable.price"
+ width="160"
+ >
+ <template #default="scope">
+ <el-text v-if="scope.row.receivable">
+ {{ erpPriceInputFormatter(scope.row.price - scope.row.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(scope.row.price) }}</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="180px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:receivable:create']"
+ link
+ type="success"
+ @click="openReceivableForm(scope.row)"
+ :disabled="scope.row.receivableId"
+ >
+ 鍒涘缓鍥炴
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ReceivableForm ref="receivableFormRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import { RECEIVABLE_REMIND_TYPE } from './common'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
+
+defineOptions({ name: 'ReceivablePlanRemindList' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ remindType: 1
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ReceivablePlanApi.getReceivablePlanPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鍒涘缓鍥炴鎿嶄綔 */
+const receivableFormRef = ref()
+const openReceivableForm = (row: ReceivablePlanApi.ReceivablePlanVO) => {
+ receivableFormRef.value.open('create', undefined, row)
+}
+
+/** 鎵撳紑璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmReceivablePlanDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ await getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+})
+</script>
diff --git a/src/views/crm/backlog/components/common.ts b/src/views/crm/backlog/components/common.ts
new file mode 100644
index 0000000..9ff6bfc
--- /dev/null
+++ b/src/views/crm/backlog/components/common.ts
@@ -0,0 +1,39 @@
+/** 璺熻繘鐘舵�� */
+export const FOLLOWUP_STATUS = [
+ { label: '寰呰窡杩�', value: false },
+ { label: '宸茶窡杩�', value: true }
+]
+
+/** 褰掑睘鑼冨洿 */
+export const SCENE_TYPES = [
+ { label: '鎴戣礋璐g殑', value: 1 },
+ { label: '鎴戝弬涓庣殑', value: 2 },
+ { label: '涓嬪睘璐熻矗鐨�', value: 3 }
+]
+
+/** 鑱旂郴鐘舵�� */
+export const CONTACT_STATUS = [
+ { label: '浠婃棩闇�鑱旂郴', value: 1 },
+ { label: '宸查�炬湡', value: 2 },
+ { label: '宸茶仈绯�', value: 3 }
+]
+
+/** 瀹℃壒鐘舵�� */
+export const AUDIT_STATUS = [
+ { label: '寰呭鎵�', value: 10 },
+ { label: '瀹℃牳閫氳繃', value: 20 },
+ { label: '瀹℃牳涓嶉�氳繃', value: 30 }
+]
+
+/** 鍥炴鎻愰啋绫诲瀷 */
+export const RECEIVABLE_REMIND_TYPE = [
+ { label: '寰呭洖娆�', value: 1 },
+ { label: '宸查�炬湡', value: 2 },
+ { label: '宸插洖娆�', value: 3 }
+]
+
+/** 鍚堝悓杩囨湡鐘舵�� */
+export const CONTRACT_EXPIRY_TYPE = [
+ { label: '鍗冲皢杩囨湡', value: 1 },
+ { label: '宸茶繃鏈�', value: 2 }
+]
diff --git a/src/views/crm/backlog/index.vue b/src/views/crm/backlog/index.vue
new file mode 100644
index 0000000..49a1d4c
--- /dev/null
+++ b/src/views/crm/backlog/index.vue
@@ -0,0 +1,177 @@
+<template>
+ <doc-alert title="銆愰�氱敤銆戣窡杩涜褰曘�佸緟鍔炰簨椤�" url="https://doc.iocoder.cn/crm/follow-up/" />
+
+ <el-row :gutter="20">
+ <el-col :span="4" class="min-w-[200px]">
+ <div class="side-item-list">
+ <div
+ v-for="(item, index) in leftSides"
+ :key="index"
+ :class="leftMenu == item.menu ? 'side-item-select' : 'side-item-default'"
+ class="side-item"
+ @click="sideClick(item)"
+ >
+ {{ item.name }}
+ <el-badge v-if="item.count > 0" :max="99" :value="item.count" />
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="20" :xs="24">
+ <CustomerTodayContactList v-if="leftMenu === 'customerTodayContact'" />
+ <ClueFollowList v-if="leftMenu === 'clueFollow'" />
+ <ContractAuditList v-if="leftMenu === 'contractAudit'" />
+ <ReceivableAuditList v-if="leftMenu === 'receivableAudit'" />
+ <ContractRemindList v-if="leftMenu === 'contractRemind'" />
+ <CustomerFollowList v-if="leftMenu === 'customerFollow'" />
+ <CustomerPutPoolRemindList v-if="leftMenu === 'customerPutPoolRemind'" />
+ <ReceivablePlanRemindList v-if="leftMenu === 'receivablePlanRemind'" />
+ </el-col>
+ </el-row>
+</template>
+
+<script lang="ts" setup>
+import CustomerFollowList from './components/CustomerFollowList.vue'
+import CustomerTodayContactList from './components/CustomerTodayContactList.vue'
+import CustomerPutPoolRemindList from './components/CustomerPutPoolRemindList.vue'
+import ClueFollowList from './components/ClueFollowList.vue'
+import ContractAuditList from './components/ContractAuditList.vue'
+import ContractRemindList from './components/ContractRemindList.vue'
+import ReceivablePlanRemindList from './components/ReceivablePlanRemindList.vue'
+import ReceivableAuditList from './components/ReceivableAuditList.vue'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ClueApi from '@/api/crm/clue'
+import * as ContractApi from '@/api/crm/contract'
+import * as ReceivableApi from '@/api/crm/receivable'
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+
+defineOptions({ name: 'CrmBacklog' })
+
+const leftMenu = ref('customerTodayContact')
+
+const clueFollowCount = ref(0)
+const customerFollowCount = ref(0)
+const customerPutPoolRemindCount = ref(0)
+const customerTodayContactCount = ref(0)
+const contractAuditCount = ref(0)
+const contractRemindCount = ref(0)
+const receivableAuditCount = ref(0)
+const receivablePlanRemindCount = ref(0)
+
+const leftSides = ref([
+ {
+ name: '浠婃棩闇�鑱旂郴瀹㈡埛',
+ menu: 'customerTodayContact',
+ count: customerTodayContactCount
+ },
+ {
+ name: '鍒嗛厤缁欐垜鐨勭嚎绱�',
+ menu: 'clueFollow',
+ count: clueFollowCount
+ },
+ {
+ name: '鍒嗛厤缁欐垜鐨勫鎴�',
+ menu: 'customerFollow',
+ count: customerFollowCount
+ },
+ {
+ name: '寰呰繘鍏ュ叕娴风殑瀹㈡埛',
+ menu: 'customerPutPoolRemind',
+ count: customerPutPoolRemindCount
+ },
+ {
+ name: '寰呭鏍稿悎鍚�',
+ menu: 'contractAudit',
+ count: contractAuditCount
+ },
+ {
+ name: '寰呭鏍稿洖娆�',
+ menu: 'receivableAudit',
+ count: receivableAuditCount
+ },
+ {
+ name: '寰呭洖娆炬彁閱�',
+ menu: 'receivablePlanRemind',
+ count: receivablePlanRemindCount
+ },
+ {
+ name: '鍗冲皢鍒版湡鐨勫悎鍚�',
+ menu: 'contractRemind',
+ count: contractRemindCount
+ }
+])
+
+/** 渚ц竟鐐瑰嚮 */
+const sideClick = (item: any) => {
+ leftMenu.value = item.menu
+}
+
+const getCount = () => {
+ CustomerApi.getTodayContactCustomerCount().then(
+ (count) => (customerTodayContactCount.value = count)
+ )
+ CustomerApi.getPutPoolRemindCustomerCount().then(
+ (count) => (customerPutPoolRemindCount.value = count)
+ )
+ CustomerApi.getFollowCustomerCount().then((count) => (customerFollowCount.value = count))
+ ClueApi.getFollowClueCount().then((count) => (clueFollowCount.value = count))
+ ContractApi.getAuditContractCount().then((count) => (contractAuditCount.value = count))
+ ContractApi.getRemindContractCount().then((count) => (contractRemindCount.value = count))
+ ReceivableApi.getAuditReceivableCount().then((count) => (receivableAuditCount.value = count))
+ ReceivablePlanApi.getReceivablePlanRemindCount().then(
+ (count) => (receivablePlanRemindCount.value = count)
+ )
+}
+
+/** 婵�娲绘椂 */
+onActivated(async () => {
+ getCount()
+})
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ getCount()
+})
+</script>
+
+<style lang="scss" scoped>
+.side-item-list {
+ top: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ font-size: 14px;
+ background-color: var(--el-bg-color);
+ border: 1px solid var(--el-border-color);
+ border-radius: 5px;
+
+ .side-item {
+ position: relative;
+ height: 50px;
+ padding: 0 20px;
+ line-height: 50px;
+ cursor: pointer;
+ }
+}
+
+.side-item-default {
+ color: var(--el-text-color-primary);
+ border-right: 2px solid transparent;
+}
+
+.side-item-select {
+ color: var(--el-color-primary);
+ background-color: var(--el-color-primary-light-9);
+ border-right: 2px solid var(--el-color-primary);
+}
+
+.el-badge :deep(.el-badge__content) {
+ top: 0;
+ border: none;
+}
+
+.el-badge {
+ position: absolute;
+ top: 0;
+ right: 15px;
+}
+</style>
diff --git a/src/views/crm/business/BusinessForm.vue b/src/views/crm/business/BusinessForm.vue
new file mode 100644
index 0000000..4af1abe
--- /dev/null
+++ b/src/views/crm/business/BusinessForm.vue
@@ -0,0 +1,287 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1280">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="鍟嗘満鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ晢鏈哄悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ :disabled="formData.customerDefault"
+ v-model="formData.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="鍟嗘満鐘舵�佺粍" prop="statusTypeId">
+ <el-select
+ v-model="formData.statusTypeId"
+ placeholder="璇烽�夋嫨鍟嗘満鐘舵�佺粍"
+ clearable
+ class="w-1/1"
+ :disabled="formType !== 'create'"
+ >
+ <el-option
+ v-for="item in statusTypeList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="棰勮鎴愪氦鏃ユ湡" prop="dealTime">
+ <el-date-picker
+ v-model="formData.dealTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨棰勮鎴愪氦鏃ユ湡"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="浜у搧娓呭崟" name="product">
+ <BusinessProductForm
+ ref="productFormRef"
+ :products="formData.products"
+ :disabled="disabled"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="浜у搧鎬婚噾棰�" prop="totalProductPrice">
+ <el-input
+ disabled
+ v-model="formData.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏁村崟鎶樻墸锛�%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ placeholder="璇疯緭鍏ユ暣鍗曟姌鎵�"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎶樻墸鍚庨噾棰�" prop="price">
+ <el-input
+ disabled
+ v-model="formData.totalPrice"
+ placeholder="璇疯緭鍏ュ晢鏈洪噾棰�"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import * as BusinessStatusApi from '@/api/crm/business/status'
+import * as CustomerApi from '@/api/crm/customer'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import BusinessProductForm from './components/BusinessProductForm.vue'
+import { erpPriceMultiply, erpPriceInputFormatter } from '@/utils'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ customerId: undefined,
+ ownerUserId: undefined,
+ statusTypeId: undefined,
+ dealTime: undefined,
+ discountPercent: 0,
+ totalProductPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined,
+ products: [],
+ contactId: undefined,
+ customerDefault: false
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍟嗘満鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }],
+ statusTypeId: [{ required: true, message: '鍟嗘満鐘舵�佺粍涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const statusTypeList = ref([]) // 鍟嗘満鐘舵�佺被鍨嬪垪琛�
+const customerList = ref([]) // 瀹㈡埛鍒楄〃鐨勬暟鎹�
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('product')
+const productFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ const totalProductPrice = val.products.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null
+ ? erpPriceMultiply(totalProductPrice, val.discountPercent / 100.0)
+ : 0
+ const totalPrice = totalProductPrice - discountPrice
+ // 璧嬪��
+ formData.value.totalProductPrice = totalProductPrice
+ formData.value.totalPrice = totalPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, customerId?: number, contactId?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await BusinessApi.getBusiness(id)
+ } finally {
+ formLoading.value = false
+ }
+ } else {
+ if (customerId) {
+ formData.value.customerId = customerId
+ formData.value.customerDefault = true // 榛樿瀹㈡埛鐨勯�夋嫨锛屼笉鍏佽鍙�
+ }
+ // 鑷姩鍏宠仈 contactId 鑱旂郴浜虹紪鍙�
+ if (contactId) {
+ formData.value.contactId = contactId
+ }
+ }
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鍔犺浇鍟嗘満鐘舵�佺被鍨嬪垪琛�
+ statusTypeList.value = await BusinessStatusApi.getBusinessStatusTypeSimpleList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ await productFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as BusinessApi.BusinessVO
+ if (formType.value === 'create') {
+ await BusinessApi.createBusiness(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await BusinessApi.updateBusiness(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ customerId: undefined,
+ ownerUserId: undefined,
+ statusTypeId: undefined,
+ dealTime: undefined,
+ discountPercent: 0,
+ totalProductPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined,
+ products: [],
+ contactId: undefined,
+ customerDefault: false
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/business/BusinessUpdateStatusForm.vue b/src/views/crm/business/BusinessUpdateStatusForm.vue
new file mode 100644
index 0000000..4f2f761
--- /dev/null
+++ b/src/views/crm/business/BusinessUpdateStatusForm.vue
@@ -0,0 +1,108 @@
+<template>
+ <Dialog title="鍙樻洿鍟嗘満鐘舵��" v-model="dialogVisible" width="400">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍟嗘満闃舵" prop="status">
+ <el-select v-model="formData.status" placeholder="璇烽�夋嫨鍟嗘満闃舵" class="w-1/1">
+ <el-option
+ v-for="item in statusList"
+ :key="item.id"
+ :label="item.name + '(璧㈠崟鐜囷細' + item.percent + '%)'"
+ :value="item.id"
+ />
+ <el-option
+ v-for="item in BusinessStatusApi.DEFAULT_STATUSES"
+ :key="item.endStatus"
+ :label="item.name + '(璧㈠崟鐜囷細' + item.percent + '%)'"
+ :value="-item.endStatus"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import * as BusinessStatusApi from '@/api/crm/business/status'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref({
+ id: undefined,
+ statusId: undefined,
+ endStatus: undefined,
+ status: undefined
+})
+const formRules = reactive({
+ status: [{ required: true, message: '鍟嗘満闃舵涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const statusList = ref([]) // 鍟嗘満鐘舵�佸垪琛�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (business: BusinessApi.BusinessVO) => {
+ dialogVisible.value = true
+ resetForm()
+ formData.value = {
+ id: business.id,
+ statusId: business.statusId,
+ endStatus: business.endStatus,
+ status: business.endStatus != null ? -business.endStatus : business.statusId
+ }
+ // 鍔犺浇鐘舵�佸垪琛�
+ formLoading.value = true
+ try {
+ statusList.value = await BusinessStatusApi.getBusinessStatusSimpleList(business.statusTypeId)
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await BusinessApi.updateBusinessStatus({
+ id: formData.value.id,
+ statusId: formData.value.status > 0 ? formData.value.status : undefined,
+ endStatus: formData.value.status < 0 ? -formData.value.status : undefined
+ })
+ message.success('鏇存柊鍟嗘満鐘舵�佹垚鍔�')
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ statusId: undefined,
+ endStatus: undefined,
+ status: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/business/components/BusinessList.vue b/src/views/crm/business/components/BusinessList.vue
new file mode 100644
index 0000000..f990606
--- /dev/null
+++ b/src/views/crm/business/components/BusinessList.vue
@@ -0,0 +1,186 @@
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row justify="end">
+ <el-button @click="openForm">
+ <Icon class="mr-5px" icon="ep:opportunity" />
+ 鍒涘缓鍟嗘満
+ </el-button>
+ <el-button
+ @click="openBusinessModal"
+ v-hasPermi="['crm:contact:create-business']"
+ v-if="queryParams.contactId"
+ >
+ <Icon class="mr-5px" icon="ep:circle-plus" />鍏宠仈
+ </el-button>
+ <el-button
+ @click="deleteContactBusinessList"
+ v-hasPermi="['crm:contact:delete-business']"
+ v-if="queryParams.contactId"
+ >
+ <Icon class="mr-5px" icon="ep:remove" />瑙i櫎鍏宠仈
+ </el-button>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table
+ ref="businessRef"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column type="selection" width="55" v-if="queryParams.contactId" />
+ <el-table-column label="鍟嗘満鍚嶇О" fixed="left" align="center" prop="name">
+ <template #default="scope">
+ <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍟嗘満閲戦"
+ align="center"
+ prop="price"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="瀹㈡埛鍚嶇О" align="center" prop="customerName" />
+ <el-table-column label="鍟嗘満缁�" align="center" prop="statusTypeName" />
+ <el-table-column label="鍟嗘満闃舵" align="center" prop="statusName" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <BusinessForm ref="formRef" @success="getList" />
+ <!-- 鍏宠仈鍟嗘満閫夋嫨寮规 -->
+ <BusinessListModal
+ ref="businessModalRef"
+ :customer-id="props.customerId"
+ @success="createContactBusinessList"
+ />
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import * as ContactApi from '@/api/crm/contact'
+import BusinessForm from './../BusinessForm.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import BusinessListModal from './BusinessListModal.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+const message = useMessage() // 娑堟伅
+
+defineOptions({ name: 'CrmBusinessList' })
+const props = defineProps<{
+ bizType: number // 涓氬姟绫诲瀷
+ bizId: number // 涓氬姟缂栧彿
+ customerId?: number // 鍏宠仈鑱旂郴浜轰笌鍟嗘満鏃讹紝闇�瑕佷紶鍏� customerId 杩涜绛涢��
+ contactId?: number // 鐗规畩锛氳仈绯讳汉缂栧彿锛涘湪銆愯仈绯讳汉銆戣鎯呬腑锛屽彲浠ヤ紶閫掕仈绯讳汉缂栧彿锛岄粯璁ゆ柊寤虹殑鍟嗘満鍏宠仈鍒拌鑱旂郴浜�
+}>()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ customerId: undefined as unknown, // 鍏佽 undefined + number
+ contactId: undefined as unknown // 鍏佽 undefined + number
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 缃┖鍙傛暟
+ queryParams.customerId = undefined
+ queryParams.contactId = undefined
+ // 鎵ц鏌ヨ
+ let data = { list: [], total: 0 }
+ switch (props.bizType) {
+ case BizTypeEnum.CRM_CUSTOMER:
+ queryParams.customerId = props.bizId
+ data = await BusinessApi.getBusinessPageByCustomer(queryParams)
+ break
+ case BizTypeEnum.CRM_CONTACT:
+ queryParams.contactId = props.bizId
+ data = await BusinessApi.getBusinessPageByContact(queryParams)
+ break
+ default:
+ return
+ }
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 娣诲姞鎿嶄綔 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open('create', null, props.customerId, props.contactId)
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 鎵撳紑鑱旂郴浜轰笌鍟嗘満鐨勫叧鑱斿脊绐� */
+const businessModalRef = ref()
+const openBusinessModal = () => {
+ businessModalRef.value.open()
+}
+const createContactBusinessList = async (businessIds: number[]) => {
+ const data = {
+ contactId: props.bizId,
+ businessIds: businessIds
+ } as ContactApi.ContactBusinessReqVO
+ businessRef.value.getSelectionRows().forEach((row: BusinessApi.BusinessVO) => {
+ data.businessIds.push(row.id)
+ })
+ await ContactApi.createContactBusinessList(data)
+ // 鍒锋柊鍒楄〃
+ message.success('鍏宠仈鍟嗘満鎴愬姛')
+ handleQuery()
+}
+
+/** 瑙i櫎鑱旂郴浜轰笌鍟嗘満鐨勫叧鑱� */
+const businessRef = ref()
+const deleteContactBusinessList = async () => {
+ const data = {
+ contactId: props.bizId,
+ businessIds: businessRef.value.getSelectionRows().map((row: BusinessApi.BusinessVO) => row.id)
+ } as ContactApi.ContactBusinessReqVO
+ if (data.businessIds.length === 0) {
+ return message.error('鏈�夋嫨鍟嗘満')
+ }
+ await ContactApi.deleteContactBusinessList(data)
+ // 鍒锋柊鍒楄〃
+ message.success('鍙栧叧鍟嗘満鎴愬姛')
+ handleQuery()
+}
+
+/** 鐩戝惉鎵撳紑鐨� bizId + bizType锛屼粠鑰屽姞杞芥渶鏂扮殑鍒楄〃 */
+watch(
+ () => [props.bizId, props.bizType],
+ () => {
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+</script>
diff --git a/src/views/crm/business/components/BusinessListModal.vue b/src/views/crm/business/components/BusinessListModal.vue
new file mode 100644
index 0000000..3c21f06
--- /dev/null
+++ b/src/views/crm/business/components/BusinessListModal.vue
@@ -0,0 +1,156 @@
+<template>
+ <Dialog title="鍏宠仈鍟嗘満" v-model="dialogVisible">
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍟嗘満鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ晢鏈哄悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" @click="openForm()" v-hasPermi="['crm:business:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table
+ v-loading="loading"
+ ref="businessRef"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="鍟嗘満鍚嶇О" fixed="left" align="center" prop="name">
+ <template #default="scope">
+ <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍟嗘満閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="瀹㈡埛鍚嶇О" align="center" prop="customerName" />
+ <el-table-column label="鍟嗘満缁�" align="center" prop="statusTypeName" />
+ <el-table-column label="鍟嗘満闃舵" align="center" prop="statusName" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <BusinessForm ref="formRef" @success="getList" />
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import BusinessForm from '../BusinessForm.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const props = defineProps<{
+ customerId: number
+}>()
+defineOptions({ name: 'BusinessListModal' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ customerId: props.customerId
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ queryParams.customerId = props.customerId // 瑙e喅 props.customerId 娌℃洿鏂板埌 queryParams 涓婄殑闂
+ await getList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BusinessApi.getBusinessPageByCustomer(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞鎿嶄綔 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open('create')
+}
+
+/** 鍏宠仈鍟嗘満鎻愪氦 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const businessRef = ref()
+const submitForm = async () => {
+ const businessIds = businessRef.value
+ .getSelectionRows()
+ .map((row: BusinessApi.BusinessVO) => row.id)
+ if (businessIds.length === 0) {
+ return message.error('鏈�夋嫨鍟嗘満')
+ }
+ dialogVisible.value = false
+ emit('success', businessIds, businessRef.value.getSelectionRows())
+}
+
+/** 鎵撳紑鍟嗘満璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+</script>
diff --git a/src/views/crm/business/components/BusinessProductForm.vue b/src/views/crm/business/components/BusinessProductForm.vue
new file mode 100644
index 0000000..fbba065
--- /dev/null
+++ b/src/views/crm/business/components/BusinessProductForm.vue
@@ -0,0 +1,183 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productNo" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浠锋牸锛堝厓锛�" min-width="120">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍞环锛堝厓锛�" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.businessPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.businessPrice"
+ controls-position="right"
+ :min="0.001"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堣" prop="totalPrice" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import * as ProductApi from '@/api/crm/product'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+ products: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ businessPrice: [{ required: true, message: '鍚堝悓浠锋牸涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductApi.ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 鍒濆鍖栬缃骇鍝侀」 */
+watch(
+ () => props.products,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ if (item.businessPrice != null && item.count != null) {
+ item.totalPrice = erpPriceMultiply(item.businessPrice, item.count)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnit: undefined, // 浜у搧鍗曚綅
+ productNo: undefined, // 浜у搧鏉$爜
+ productPrice: undefined, // 浜у搧浠锋牸
+ businessPrice: undefined,
+ count: 1
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnit = product.unit
+ row.productNo = product.no
+ row.productPrice = product.price
+ row.businessPrice = product.price
+ }
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+})
+</script>
diff --git a/src/views/crm/business/detail/BusinessDetailsHeader.vue b/src/views/crm/business/detail/BusinessDetailsHeader.vue
new file mode 100644
index 0000000..50d1efe
--- /dev/null
+++ b/src/views/crm/business/detail/BusinessDetailsHeader.vue
@@ -0,0 +1,37 @@
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ business.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ business.customerName }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗘満閲戦锛堝厓锛�">
+ {{ erpPriceInputFormatter(business.totalPrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘満缁�">{{ business.statusTypeName }}</el-descriptions-item>
+ <el-descriptions-item label="璐熻矗浜�">{{ business.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(business.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as BusinessApi from '@/api/crm/business'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { business } = defineProps<{ business: BusinessApi.BusinessVO }>()
+</script>
diff --git a/src/views/crm/business/detail/BusinessDetailsInfo.vue b/src/views/crm/business/detail/BusinessDetailsInfo.vue
new file mode 100644
index 0000000..a2c9ce1
--- /dev/null
+++ b/src/views/crm/business/detail/BusinessDetailsInfo.vue
@@ -0,0 +1,61 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="鍟嗘満濮撳悕">{{ business.name }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ business.customerName }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗘満閲戦锛堝厓锛�">
+ {{ erpPriceInputFormatter(business.totalPrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="棰勮鎴愪氦鏃ユ湡">
+ {{ formatDate(business.dealTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬫鑱旂郴鏃堕棿">
+ {{ formatDate(business.contactNextTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘満鐘舵�佺粍">
+ {{ business.statusTypeName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘満闃舵">{{ business.statusName }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ business.remark }}</el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">{{ business.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涙椂闂�">
+ {{ formatDate(business.contactLastTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ business.creatorName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(business.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(business.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { business } = defineProps<{
+ business: BusinessApi.BusinessVO
+}>()
+
+// 灞曠ず鐨勬姌鍙犻潰鏉�
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>
diff --git a/src/views/crm/business/detail/BusinessProductList.vue b/src/views/crm/business/detail/BusinessProductList.vue
new file mode 100644
index 0000000..9a31665
--- /dev/null
+++ b/src/views/crm/business/detail/BusinessProductList.vue
@@ -0,0 +1,66 @@
+<template>
+ <ContentWrap>
+ <el-table :data="business.products" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column
+ align="center"
+ label="浜у搧鍚嶇О"
+ fixed="left"
+ prop="productName"
+ min-width="160"
+ >
+ <template #default="scope">
+ {{ scope.row.productName }}
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鏉$爜" align="center" prop="productNo" min-width="120" />
+ <el-table-column align="center" label="浜у搧鍗曚綅" prop="productUnit" min-width="160">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="浜у搧浠锋牸锛堝厓锛�"
+ align="center"
+ prop="productPrice"
+ min-width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍟嗘満浠锋牸锛堝厓锛�"
+ align="center"
+ prop="businessPrice"
+ min-width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="鏁伴噺"
+ prop="count"
+ min-width="100px"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚堣閲戦锛堝厓锛�"
+ align="center"
+ prop="totalPrice"
+ min-width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ <el-row class="mt-10px" justify="end">
+ <el-col :span="3"> 鏁村崟鎶樻墸锛歿{ erpPriceInputFormatter(business.discountPercent) }}% </el-col>
+ <el-col :span="4">
+ 浜у搧鎬婚噾棰濓細{{ erpPriceInputFormatter(business.totalProductPrice) }} 鍏�
+ </el-col>
+ </el-row>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as BusinessApi from '@/api/crm/business'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const { business } = defineProps<{
+ business: BusinessApi.BusinessVO
+}>()
+</script>
diff --git a/src/views/crm/business/detail/index.vue b/src/views/crm/business/detail/index.vue
new file mode 100644
index 0000000..dbab819
--- /dev/null
+++ b/src/views/crm/business/detail/index.vue
@@ -0,0 +1,146 @@
+<template>
+ <BusinessDetailsHeader v-loading="loading" :business="business">
+ <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', business.id)">
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="permissionListRef?.validateWrite"
+ :disabled="business.endStatus"
+ type="success"
+ @click="openStatusForm()"
+ >
+ 鍙樻洿鍟嗘満鐘舵��
+ </el-button>
+ <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
+ 杞Щ
+ </el-button>
+ </BusinessDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璺熻繘璁板綍">
+ <FollowUpList :biz-id="businessId" :biz-type="BizTypeEnum.CRM_BUSINESS" />
+ </el-tab-pane>
+ <el-tab-pane label="璇︾粏璧勬枡">
+ <BusinessDetailsInfo :business="business" />
+ </el-tab-pane>
+ <el-tab-pane label="鑱旂郴浜�" lazy>
+ <ContactList
+ :biz-id="business.id!"
+ :biz-type="BizTypeEnum.CRM_BUSINESS"
+ :business-id="business.id"
+ :customer-id="business.customerId"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="浜у搧">
+ <BusinessProductList :business="business" />
+ </el-tab-pane>
+ <el-tab-pane label="鍚堝悓" lazy>
+ <ContractList :biz-id="business.id!" :biz-type="BizTypeEnum.CRM_BUSINESS" />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="business.id!"
+ :biz-type="BizTypeEnum.CRM_BUSINESS"
+ :show-action="true"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BusinessForm ref="formRef" @success="getBusiness" />
+ <BusinessUpdateStatusForm ref="statusFormRef" @success="getBusiness" />
+ <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_BUSINESS" @success="close" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as BusinessApi from '@/api/crm/business'
+import BusinessDetailsHeader from './BusinessDetailsHeader.vue'
+import BusinessDetailsInfo from './BusinessDetailsInfo.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 鍥㈤槦鎴愬憳鍒楄〃锛堟潈闄愶級
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogVO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import BusinessForm from '@/views/crm/business/BusinessForm.vue'
+import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+import ContactList from '@/views/crm/contact/components/ContactList.vue'
+import BusinessUpdateStatusForm from '@/views/crm/business/BusinessUpdateStatusForm.vue'
+import ContractList from '@/views/crm/contract/components/ContractList.vue'
+import BusinessProductList from '@/views/crm/business/detail/BusinessProductList.vue'
+
+defineOptions({ name: 'CrmBusinessDetail' })
+
+const message = useMessage()
+
+const businessId = ref(0) // 绾跨储缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const business = ref<BusinessApi.BusinessVO>({} as BusinessApi.BusinessVO) // 鍟嗘満璇︽儏
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 鑾峰彇璇︽儏 */
+const getBusiness = async () => {
+ loading.value = true
+ try {
+ business.value = await BusinessApi.getBusiness(businessId.value)
+ await getOperateLog(businessId.value)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 缂栬緫 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍙樻洿鍟嗘満鐘舵�� */
+const statusFormRef = ref()
+const openStatusForm = () => {
+ statusFormRef.value.open(business.value)
+}
+
+/** 鑱旂郴浜鸿浆绉� */
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 鑱旂郴浜鸿浆绉昏〃鍗� ref
+const transfer = () => {
+ transferFormRef.value?.open(business.value.id)
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async (contactId: number) => {
+ if (!contactId) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_BUSINESS,
+ bizId: contactId
+ })
+ logList.value = data.list
+}
+
+/** 鍏抽棴绐楀彛 */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+const close = () => {
+ delView(unref(currentRoute))
+}
+
+/** 鍒濆鍖� */
+const { params } = useRoute()
+onMounted(async () => {
+ if (!params.id) {
+ message.warning('鍙傛暟閿欒锛屽晢鏈轰笉鑳戒负绌猴紒')
+ close()
+ return
+ }
+ businessId.value = params.id as unknown as number
+ await getBusiness()
+})
+</script>
diff --git a/src/views/crm/business/index.vue b/src/views/crm/business/index.vue
new file mode 100644
index 0000000..84e447c
--- /dev/null
+++ b/src/views/crm/business/index.vue
@@ -0,0 +1,275 @@
+<template>
+ <doc-alert title="銆愬晢鏈恒�戝晢鏈虹鐞嗐�佸晢鏈虹姸鎬�" url="https://doc.iocoder.cn/crm/business/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍟嗘満鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ晢鏈哄悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['crm:business:create']" type="primary" @click="openForm('create')">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:business:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="鎴戝弬涓庣殑" name="2" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="鍟嗘満鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="erpPriceTableColumnFormatter"
+ align="center"
+ label="鍟嗘満閲戦锛堝厓锛�"
+ prop="totalPrice"
+ width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="棰勮鎴愪氦鏃ユ湡"
+ prop="dealTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column
+ align="center"
+ fixed="right"
+ label="鍟嗘満鐘舵�佺粍"
+ prop="statusTypeName"
+ width="140"
+ />
+ <el-table-column
+ align="center"
+ fixed="right"
+ label="鍟嗘満闃舵"
+ prop="statusName"
+ width="120"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="130px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:business:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:business:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BusinessForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as BusinessApi from '@/api/crm/business'
+import BusinessForm from './BusinessForm.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { TabsPaneContext } from 'element-plus'
+
+defineOptions({ name: 'CrmBusiness' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ name: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BusinessApi.getBusinessPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName
+ handleQuery()
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await BusinessApi.deleteBusiness(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await BusinessApi.exportBusiness(queryParams)
+ download.excel(data, '鍟嗘満.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/business/status/BusinessStatusForm.vue b/src/views/crm/business/status/BusinessStatusForm.vue
new file mode 100644
index 0000000..d6a4d6f
--- /dev/null
+++ b/src/views/crm/business/status/BusinessStatusForm.vue
@@ -0,0 +1,194 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鐘舵�佺粍鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ姸鎬佺粍鍚�" />
+ </el-form-item>
+ <el-form-item label="搴旂敤閮ㄩ棬" prop="deptIds">
+ <template #label>
+ <Tooltip message="涓嶉�夋嫨閮ㄩ棬鏃讹紝榛樿鍏ㄥ叕鍙哥敓鏁�" title="搴旂敤閮ㄩ棬" />
+ </template>
+ <el-tree
+ ref="treeRef"
+ :data="deptList"
+ :props="defaultProps"
+ :check-strictly="!checkStrictly"
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ show-checkbox
+ />
+ </el-form-item>
+ <el-form-item label="闃舵璁剧疆" prop="statuses">
+ <el-table
+ border
+ style="width: 100%"
+ :data="formData.statuses.concat(BusinessStatusApi.DEFAULT_STATUSES)"
+ >
+ <el-table-column align="center" label="闃舵" width="70">
+ <template #default="scope">
+ <el-text v-if="!scope.row.defaultStatus">闃舵 {{ scope.$index + 1 }}</el-text>
+ <el-text v-else>缁撴潫</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="闃舵鍚嶇О" width="160" prop="name">
+ <template #default="{ row }">
+ <el-input v-if="!row.endStatus" v-model="row.name" placeholder="璇疯緭鍏ョ姸鎬佸悕绉�" />
+ <el-text v-else>{{ row.name }}</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column width="140" align="center" label="璧㈠崟鐜囷紙%锛�" prop="percent">
+ <template #default="{ row }">
+ <el-input-number
+ v-if="!row.endStatus"
+ v-model="row.percent"
+ placeholder="璇疯緭鍏ヨ耽鍗曠巼"
+ controls-position="right"
+ :min="0"
+ :max="100"
+ :precision="2"
+ class="!w-1/1"
+ />
+ <el-text v-else>{{ row.percent }}</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="110" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="!scope.row.endStatus"
+ link
+ type="primary"
+ @click="addStatus(scope.$index)"
+ >
+ 娣诲姞
+ </el-button>
+ <el-button
+ v-if="!scope.row.endStatus"
+ link
+ type="danger"
+ @click="deleteStatusArea(scope.$index)"
+ :disabled="formData.statuses.length <= 1"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as BusinessStatusApi from '@/api/crm/business/status'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭粍锛歝reate - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ deptIds: [],
+ statuses: []
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鐘舵�佺粍鍚嶄笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const deptList = ref<Tree[]>([]) // 鏍戝舰缁撴瀯
+const treeRef = ref() // 鑿滃崟鏍戠粍浠� Ref
+const checkStrictly = ref(true) // 鏄惁涓ユ牸妯″紡锛屽嵆鐖跺瓙涓嶅叧鑱�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await BusinessStatusApi.getBusinessStatus(id)
+ treeRef.value.setCheckedKeys(formData.value.deptIds)
+ if (formData.value.statuses.length == 0) {
+ addStatus()
+ }
+ } finally {
+ formLoading.value = false
+ }
+ } else {
+ addStatus()
+ }
+ // 鍔犺浇閮ㄩ棬鏍�
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as BusinessStatusApi.BusinessStatusTypeVO
+ data.deptIds = treeRef.value.getCheckedKeys(false)
+ if (formType.value === 'create') {
+ await BusinessStatusApi.createBusinessStatus(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await BusinessStatusApi.updateBusinessStatus(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ checkStrictly.value = true
+ formData.value = {
+ id: undefined,
+ name: '',
+ deptIds: [],
+ statuses: []
+ }
+ treeRef.value?.setCheckedNodes([])
+ formRef.value?.resetFields()
+}
+
+/** 娣诲姞鐘舵�� */
+const addStatus = () => {
+ const data = formData.value
+ data.statuses.push({
+ name: '',
+ percent: undefined
+ })
+}
+
+/** 鍒犻櫎鐘舵�� */
+const deleteStatusArea = (index: number) => {
+ const data = formData.value
+ data.statuses.splice(index, 1)
+}
+</script>
diff --git a/src/views/crm/business/status/index.vue b/src/views/crm/business/status/index.vue
new file mode 100644
index 0000000..ef51488
--- /dev/null
+++ b/src/views/crm/business/status/index.vue
@@ -0,0 +1,150 @@
+<template>
+ <doc-alert title="銆愬晢鏈恒�戝晢鏈虹鐞嗐�佸晢鏈虹姸鎬�" url="https://doc.iocoder.cn/crm/business/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['crm:business-status:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鐘舵�佺粍鍚�" align="center" prop="name" />
+ <el-table-column label="搴旂敤閮ㄩ棬" align="center" prop="deptNames">
+ <template #default="scope">
+ <span v-if="scope.row?.deptNames?.length > 0">
+ {{ scope.row.deptNames.join(' ') }}
+ </span>
+ <span v-else>鍏ㄥ叕鍙�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creator" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['crm:business-status:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['crm:business-status:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BusinessStatusForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as BusinessStatusApi from '@/api/crm/business/status'
+import BusinessStatusForm from './BusinessStatusForm.vue'
+import { deleteBusinessStatus } from '@/api/crm/business/status'
+
+defineOptions({ name: 'CrmBusinessStatus' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BusinessStatusApi.getBusinessStatusPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await BusinessStatusApi.deleteBusinessStatus(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/clue/ClueForm.vue b/src/views/crm/clue/ClueForm.vue
new file mode 100644
index 0000000..3562c1c
--- /dev/null
+++ b/src/views/crm/clue/ClueForm.vue
@@ -0,0 +1,260 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="绾跨储鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ嚎绱㈠悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鏉ユ簮" prop="source">
+ <el-select v-model="formData.source" placeholder="璇烽�夋嫨瀹㈡埛鏉ユ簮" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢佃瘽" prop="telephone">
+ <el-input v-model="formData.telephone" placeholder="璇疯緭鍏ョ數璇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="formData.email" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="寰俊" prop="wechat">
+ <el-input v-model="formData.wechat" placeholder="璇疯緭鍏ュ井淇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="QQ" prop="qq">
+ <el-input v-model="formData.qq" placeholder="璇疯緭鍏� QQ" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛琛屼笟" prop="industryId">
+ <el-select v-model="formData.industryId" placeholder="璇烽�夋嫨瀹㈡埛琛屼笟" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛绾у埆" prop="level">
+ <el-select v-model="formData.level" placeholder="璇烽�夋嫨瀹㈡埛绾у埆" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="areaId">
+ <el-cascader
+ v-model="formData.areaId"
+ :options="areaList"
+ :props="defaultProps"
+ class="w-1/1"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍩庡競"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇︾粏鍦板潃" prop="detailAddress">
+ <el-input v-model="formData.detailAddress" placeholder="璇疯緭鍏ヨ缁嗗湴鍧�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="涓嬫鑱旂郴鏃堕棿" prop="contactNextTime">
+ <el-date-picker
+ v-model="formData.contactNextTime"
+ placeholder="閫夋嫨涓嬫鑱旂郴鏃堕棿"
+ type="datetime"
+ value-format="x"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as ClueApi from '@/api/crm/clue'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const areaList = ref([]) // 鍦板尯鍒楄〃
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ contactNextTime: undefined,
+ ownerUserId: 0,
+ mobile: undefined,
+ telephone: undefined,
+ qq: undefined,
+ wechat: undefined,
+ email: undefined,
+ areaId: undefined,
+ detailAddress: undefined,
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ remark: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '绾跨储鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ClueApi.getClue(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鍦板尯鍒楄〃
+ areaList.value = await AreaApi.getAreaTree()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ClueApi.ClueVO
+ if (formType.value === 'create') {
+ await ClueApi.createClue(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ClueApi.updateClue(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ contactNextTime: undefined,
+ ownerUserId: 0,
+ mobile: undefined,
+ telephone: undefined,
+ qq: undefined,
+ wechat: undefined,
+ email: undefined,
+ areaId: undefined,
+ detailAddress: undefined,
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ remark: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/clue/detail/ClueDetailsHeader.vue b/src/views/crm/clue/detail/ClueDetailsHeader.vue
new file mode 100644
index 0000000..41552c7
--- /dev/null
+++ b/src/views/crm/clue/detail/ClueDetailsHeader.vue
@@ -0,0 +1,43 @@
+<template>
+ <div v-loading="loading">
+ <div class="flex items-start justify-between">
+ <div>
+ <!-- 宸︿笂锛氱嚎绱㈠熀鏈俊鎭� -->
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ clue.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="绾跨储鏉ユ簮">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満"> {{ clue.mobile }} </el-descriptions-item>
+ <el-descriptions-item label="璐熻矗浜�">
+ {{ clue.ownerUserName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(clue.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as ClueApi from '@/api/crm/clue'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'CrmClueDetailsHeader' })
+defineProps<{
+ clue: ClueApi.ClueVO // 绾跨储淇℃伅
+ loading: boolean // 鍔犺浇涓�
+}>()
+</script>
diff --git a/src/views/crm/clue/detail/ClueDetailsInfo.vue b/src/views/crm/clue/detail/ClueDetailsInfo.vue
new file mode 100644
index 0000000..5a1d01f
--- /dev/null
+++ b/src/views/crm/clue/detail/ClueDetailsInfo.vue
@@ -0,0 +1,72 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames" class="">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="绾跨储鍚嶇О">
+ {{ clue.name }}
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鏉ユ簮">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="clue.source" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満">{{ clue.mobile }}</el-descriptions-item>
+ <el-descriptions-item label="鐢佃瘽">{{ clue.telephone }}</el-descriptions-item>
+ <el-descriptions-item label="閭">{{ clue.email }}</el-descriptions-item>
+ <el-descriptions-item label="鍦板潃">
+ {{ clue.areaName }} {{ clue.detailAddress }}
+ </el-descriptions-item>
+ <el-descriptions-item label="QQ">{{ clue.qq }}</el-descriptions-item>
+ <el-descriptions-item label="寰俊">{{ clue.wechat }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛琛屼笟">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="clue.industryId" />
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛绾у埆">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="clue.level" />
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬫鑱旂郴鏃堕棿">
+ {{ formatDate(clue.contactNextTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ clue.remark }}</el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">{{ clue.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涜褰�">
+ {{ clue.contactLastContent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涙椂闂�">
+ {{ formatDate(clue.contactLastTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ clue.creatorName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(clue.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(clue.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ClueApi from '@/api/crm/clue'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'CrmClueDetailsInfo' })
+const { clue } = defineProps<{
+ clue: ClueApi.ClueVO // 绾跨储鏄庣粏
+}>()
+
+const activeNames = ref(['basicInfo', 'systemInfo']) // 灞曠ず鐨勬姌鍙犻潰鏉�
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/crm/clue/detail/index.vue b/src/views/crm/clue/detail/index.vue
new file mode 100644
index 0000000..4c211e6
--- /dev/null
+++ b/src/views/crm/clue/detail/index.vue
@@ -0,0 +1,130 @@
+<template>
+ <ClueDetailsHeader :clue="clue" :loading="loading">
+ <el-button
+ v-if="permissionListRef?.validateWrite"
+ v-hasPermi="['crm:clue:update']"
+ type="primary"
+ @click="openForm"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
+ 杞Щ
+ </el-button>
+ <el-button
+ v-if="permissionListRef?.validateOwnerUser && !clue.transformStatus"
+ type="success"
+ @click="handleTransform"
+ >
+ 杞寲涓哄鎴�
+ </el-button>
+ <el-button v-else disabled type="success">宸茶浆鍖栧鎴�</el-button>
+ </ClueDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璺熻繘璁板綍">
+ <FollowUpList :biz-id="clueId" :biz-type="BizTypeEnum.CRM_CLUE" />
+ </el-tab-pane>
+ <el-tab-pane label="鍩烘湰淇℃伅">
+ <ClueDetailsInfo :clue="clue" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="clue.id!"
+ :biz-type="BizTypeEnum.CRM_CLUE"
+ :show-action="true"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ClueForm ref="formRef" @success="getClue" />
+ <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CLUE" @success="close" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ClueApi from '@/api/crm/clue'
+import ClueForm from '@/views/crm/clue/ClueForm.vue'
+import ClueDetailsHeader from './ClueDetailsHeader.vue' // 绾跨储鏄庣粏 - 澶撮儴
+import ClueDetailsInfo from './ClueDetailsInfo.vue' // 绾跨储鏄庣粏 - 璇︾粏淇℃伅
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 鍥㈤槦鎴愬憳鍒楄〃锛堟潈闄愶級
+import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import type { OperateLogVO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+
+defineOptions({ name: 'CrmClueDetail' })
+
+const clueId = ref(0) // 绾跨储缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const message = useMessage() // 娑堟伅寮圭獥
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 鑾峰彇璇︽儏 */
+const clue = ref<ClueApi.ClueVO>({} as ClueApi.ClueVO) // 绾跨储璇︽儏
+const getClue = async () => {
+ loading.value = true
+ try {
+ clue.value = await ClueApi.getClue(clueId.value)
+ await getOperateLog()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 缂栬緫绾跨储 */
+const formRef = ref<InstanceType<typeof ClueForm>>() // 绾跨储琛ㄥ崟 Ref
+const openForm = () => {
+ formRef.value?.open('update', clueId.value)
+}
+
+/** 绾跨储杞Щ */
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 绾跨储杞Щ琛ㄥ崟 ref
+const transfer = () => {
+ transferFormRef.value?.open(clueId.value)
+}
+
+/** 杞寲涓哄鎴� */
+const handleTransform = async () => {
+ await message.confirm(`纭畾灏嗐��${clue.value.name}銆戣浆鍖栦负瀹㈡埛鍚楋紵`)
+ await ClueApi.transformClue(clueId.value)
+ message.success(`杞寲瀹㈡埛銆�${clue.value.name}銆戞垚鍔焋)
+ await getClue()
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async () => {
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_CLUE,
+ bizId: clueId.value
+ })
+ logList.value = data.list
+}
+
+const close = () => {
+ delView(unref(currentRoute))
+}
+
+/** 鍒濆鍖� */
+const { params } = useRoute()
+onMounted(() => {
+ if (!params.id) {
+ message.warning('鍙傛暟閿欒锛岀嚎绱笉鑳戒负绌猴紒')
+ close()
+ return
+ }
+ clueId.value = params.id as unknown as number
+ getClue()
+})
+</script>
diff --git a/src/views/crm/clue/index.vue b/src/views/crm/clue/index.vue
new file mode 100644
index 0000000..f90d497
--- /dev/null
+++ b/src/views/crm/clue/index.vue
@@ -0,0 +1,270 @@
+<template>
+ <doc-alert title="銆愮嚎绱€�戠嚎绱㈢鐞�" url="https://doc.iocoder.cn/crm/clue/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="绾跨储鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ョ嚎绱㈠悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="杞寲鐘舵��" prop="transformStatus">
+ <el-select v-model="queryParams.transformStatus" class="!w-240px">
+ <el-option :value="false" label="鏈浆鍖�" />
+ <el-option :value="true" label="宸茶浆鍖�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙�" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢佃瘽" prop="telephone">
+ <el-input
+ v-model="queryParams.telephone"
+ placeholder="璇疯緭鍏ョ數璇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:clue:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['crm:clue:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="鎴戝弬涓庣殑" name="2" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="绾跨储鍚嶇О" align="center" prop="name" fixed="left" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="绾跨储鏉ユ簮" align="center" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐢佃瘽" align="center" prop="telephone" width="130" />
+ <el-table-column label="閭" align="center" prop="email" width="180" />
+ <el-table-column label="鍦板潃" align="center" prop="detailAddress" width="180" />
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ label="鏈�鍚庤窡杩涙椂闂�"
+ align="center"
+ prop="contactLastTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100" />
+ <el-table-column
+ label="鏇存柊鏃堕棿"
+ align="center"
+ prop="updateTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column label="鎿嶄綔" align="center" min-width="110" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['crm:clue:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['crm:clue:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ClueForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ClueApi from '@/api/crm/clue'
+import ClueForm from './ClueForm.vue'
+import { TabsPaneContext } from 'element-plus'
+
+defineOptions({ name: 'CrmClue' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ name: null,
+ telephone: null,
+ mobile: null,
+ transformStatus: false
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ClueApi.getCluePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName
+ handleQuery()
+}
+
+/** 鎵撳紑绾跨储璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmClueDetail', params: { id } })
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ClueApi.deleteClue(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ClueApi.exportClue(queryParams)
+ download.excel(data, '绾跨储.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/contact/ContactForm.vue b/src/views/crm/contact/ContactForm.vue
new file mode 100644
index 0000000..81813e6
--- /dev/null
+++ b/src/views/crm/contact/ContactForm.vue
@@ -0,0 +1,311 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴浜哄鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ鍚�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ :disabled="formData.customerDefault"
+ v-model="formData.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢佃瘽" prop="telephone">
+ <el-input v-model="formData.telephone" placeholder="璇疯緭鍏ョ數璇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="formData.email" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="寰俊" prop="wechat">
+ <el-input v-model="formData.wechat" placeholder="璇疯緭鍏ュ井淇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="QQ" prop="qq">
+ <el-input v-model="formData.qq" placeholder="璇疯緭鍏� QQ" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鑱屼綅" prop="post">
+ <el-input v-model="formData.post" placeholder="璇疯緭鍏ヨ亴浣�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏抽敭鍐崇瓥浜�" prop="master" style="width: 400px">
+ <el-radio-group v-model="formData.master">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select v-model="formData.sex" placeholder="璇烽�夋嫨" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐩村睘涓婄骇" prop="parentId">
+ <el-select v-model="formData.parentId" placeholder="璇烽�夋嫨鐩村睘涓婄骇" class="w-1/1">
+ <el-option
+ v-for="item in contactList"
+ :key="item.id"
+ :disabled="item.id == formData.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="areaId">
+ <el-cascader
+ v-model="formData.areaId"
+ :options="areaList"
+ :props="defaultProps"
+ class="w-1/1"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍩庡競"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇︾粏鍦板潃" prop="detailAddress">
+ <el-input v-model="formData.detailAddress" placeholder="璇疯緭鍏ヨ缁嗗湴鍧�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="涓嬫鑱旂郴鏃堕棿" prop="contactNextTime">
+ <el-date-picker
+ v-model="formData.contactNextTime"
+ placeholder="閫夋嫨涓嬫鑱旂郴鏃堕棿"
+ type="datetime"
+ value-format="x"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as ContactApi from '@/api/crm/contact'
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import * as UserApi from '@/api/system/user'
+import * as CustomerApi from '@/api/crm/customer'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import { useUserStore } from '@/store/modules/user'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const areaList = ref([]) // 鍦板尯鍒楄〃
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ customerId: undefined,
+ contactNextTime: undefined,
+ ownerUserId: 0,
+ mobile: undefined,
+ telephone: undefined,
+ qq: undefined,
+ wechat: undefined,
+ email: undefined,
+ areaId: undefined,
+ detailAddress: undefined,
+ sex: undefined,
+ master: false,
+ post: undefined,
+ parentId: undefined,
+ remark: undefined,
+ businessId: undefined,
+ customerDefault: false
+})
+const formRules = reactive({
+ name: [{ required: true, message: '濮撳悕涓嶈兘涓虹┖', trigger: 'blur' }],
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const contactList = ref<ContactApi.ContactVO[]>([]) // 鑱旂郴浜哄垪琛�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, customerId?: number, businessId?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ContactApi.getContact(id)
+ } finally {
+ formLoading.value = false
+ }
+ } else {
+ if (customerId) {
+ formData.value.customerId = customerId
+ formData.value.customerDefault = true // 榛樿瀹㈡埛鐨勯�夋嫨锛屼笉鍏佽鍙�
+ }
+ // 鑷姩鍏宠仈 businessId 鍟嗘満缂栧彿
+ if (businessId) {
+ formData.value.businessId = businessId
+ }
+ }
+ // 鑾峰緱鑱旂郴浜哄垪琛�
+ contactList.value = await ContactApi.getSimpleContactList()
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鑾峰緱鍦板尯鍒楄〃
+ areaList.value = await AreaApi.getAreaTree()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ContactApi.ContactVO
+ if (formType.value === 'create') {
+ await ContactApi.createContact(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ContactApi.updateContact(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ customerId: undefined,
+ contactNextTime: undefined,
+ ownerUserId: 0,
+ mobile: undefined,
+ telephone: undefined,
+ qq: undefined,
+ wechat: undefined,
+ email: undefined,
+ areaId: undefined,
+ detailAddress: undefined,
+ sex: undefined,
+ master: false,
+ post: undefined,
+ parentId: undefined,
+ remark: undefined,
+ businessId: undefined,
+ customerDefault: false
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/contact/components/ContactList.vue b/src/views/crm/contact/components/ContactList.vue
new file mode 100644
index 0000000..1c12ca8
--- /dev/null
+++ b/src/views/crm/contact/components/ContactList.vue
@@ -0,0 +1,185 @@
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row justify="end">
+ <el-button @click="openForm">
+ <Icon class="mr-5px" icon="system-uicons:contacts" />
+ 鍒涘缓鑱旂郴浜�
+ </el-button>
+ <el-button
+ v-if="queryParams.businessId"
+ v-hasPermi="['crm:contact:create-business']"
+ @click="openBusinessModal"
+ >
+ <Icon class="mr-5px" icon="ep:circle-plus" />
+ 鍏宠仈
+ </el-button>
+ <el-button
+ v-if="queryParams.businessId"
+ v-hasPermi="['crm:contact:delete-business']"
+ @click="deleteContactBusinessList"
+ >
+ <Icon class="mr-5px" icon="ep:remove" />
+ 瑙i櫎鍏宠仈
+ </el-button>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table
+ ref="contactRef"
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ >
+ <el-table-column v-if="queryParams.businessId" type="selection" width="55" />
+ <el-table-column align="center" fixed="left" label="濮撳悕" prop="name">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵嬫満鍙�" prop="mobile" />
+ <el-table-column align="center" label="鑱屼綅" prop="post" />
+ <el-table-column align="center" label="鐩村睘涓婄骇" prop="parentName" />
+ <el-table-column align="center" label="鏄惁鍏抽敭鍐崇瓥浜�" min-width="100" prop="master">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <ContactForm ref="formRef" @success="getList" />
+ <!-- 鍏宠仈鍟嗘満閫夋嫨寮规 -->
+ <ContactListModal
+ v-if="customerId"
+ ref="contactModalRef"
+ :customer-id="customerId"
+ @success="createContactBusinessList"
+ />
+</template>
+<script lang="ts" setup>
+import * as ContactApi from '@/api/crm/contact'
+import ContactForm from './../ContactForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { BizTypeEnum } from '@/api/crm/permission'
+import ContactListModal from './ContactListModal.vue'
+
+defineOptions({ name: 'CrmContactList' })
+const props = defineProps<{
+ bizType: number // 涓氬姟绫诲瀷
+ bizId: number // 涓氬姟缂栧彿
+ customerId?: number // 鐗规畩锛氬鎴风紪鍙凤紱鍦ㄣ�愬晢鏈恒�戣鎯呬腑锛屽彲浠ヤ紶閫掑鎴风紪鍙凤紝榛樿鏂板缓鐨勮仈绯讳汉鍏宠仈鍒拌瀹㈡埛
+ businessId?: number // 鐗规畩锛氬晢鏈虹紪鍙凤紱鍦ㄣ�愬晢鏈恒�戣鎯呬腑锛屽彲浠ヤ紶閫掑晢鏈虹紪鍙凤紝榛樿鏂板缓鐨勮仈绯讳汉鍏宠仈鍒拌鍟嗘満
+}>()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ customerId: undefined as unknown, // 鍏佽 undefined + number
+ businessId: undefined as unknown // 鍏佽 undefined + number
+})
+const message = useMessage()
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 缃┖鍙傛暟
+ queryParams.customerId = undefined
+ // 鎵ц鏌ヨ
+ let data = { list: [], total: 0 }
+ switch (props.bizType) {
+ case BizTypeEnum.CRM_CUSTOMER:
+ queryParams.customerId = props.bizId
+ data = await ContactApi.getContactPageByCustomer(queryParams)
+ break
+ case BizTypeEnum.CRM_BUSINESS:
+ queryParams.businessId = props.bizId
+ data = await ContactApi.getContactPageByBusiness(queryParams)
+ break
+ default:
+ return
+ }
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 娣诲姞鎿嶄綔 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open('create', undefined, props.customerId, props.businessId)
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 鎵撳紑鑱旂郴浜轰笌鍟嗘満鐨勫叧鑱斿脊绐� */
+const contactModalRef = ref()
+const openBusinessModal = () => {
+ contactModalRef.value.open()
+}
+const createContactBusinessList = async (contactIds: number[]) => {
+ const data = {
+ businessId: props.bizId,
+ contactIds: contactIds
+ } as ContactApi.ContactBusiness2ReqVO
+ contactRef.value.getSelectionRows().forEach((row: ContactApi.ContactVO) => {
+ data.contactIds.push(row.id)
+ })
+ await ContactApi.createContactBusinessList2(data)
+ // 鍒锋柊鍒楄〃
+ message.success('鍏宠仈鑱旂郴浜烘垚鍔�')
+ handleQuery()
+}
+
+/** 瑙i櫎鑱旂郴浜轰笌鍟嗘満鐨勫叧鑱� */
+const contactRef = ref()
+const deleteContactBusinessList = async () => {
+ const data = {
+ businessId: props.bizId,
+ contactIds: contactRef.value.getSelectionRows().map((row: ContactApi.ContactVO) => row.id)
+ } as ContactApi.ContactBusiness2ReqVO
+ if (data.contactIds.length === 0) {
+ return message.error('鏈�夋嫨鑱旂郴浜�')
+ }
+ await ContactApi.deleteContactBusinessList2(data)
+ // 鍒锋柊鍒楄〃
+ message.success('鍙栧叧鑱旂郴浜烘垚鍔�')
+ handleQuery()
+}
+
+/** 鐩戝惉鎵撳紑鐨� bizId + bizType锛屼粠鑰屽姞杞芥渶鏂扮殑鍒楄〃 */
+watch(
+ () => [props.bizId, props.bizType],
+ () => {
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+</script>
diff --git a/src/views/crm/contact/components/ContactListModal.vue b/src/views/crm/contact/components/ContactListModal.vue
new file mode 100644
index 0000000..8b655c1
--- /dev/null
+++ b/src/views/crm/contact/components/ContactListModal.vue
@@ -0,0 +1,160 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍏宠仈鑱旂郴浜�">
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="90px"
+ >
+ <el-form-item label="鑱旂郴浜哄悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ仈绯讳汉鍚嶇О"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['crm:business:create']" type="primary" @click="openForm()">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table
+ ref="contactRef"
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column align="center" fixed="left" label="濮撳悕" prop="name">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵嬫満鍙�" prop="mobile" />
+ <el-table-column align="center" label="鑱屼綅" prop="post" />
+ <el-table-column align="center" label="鐩村睘涓婄骇" prop="parentName" />
+ <el-table-column align="center" label="鏄惁鍏抽敭鍐崇瓥浜�" min-width="100" prop="master">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <ContactForm ref="formRef" @success="getList" />
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as ContactApi from '@/api/crm/contact'
+import ContactForm from '../ContactForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const props = defineProps<{
+ customerId: number
+}>()
+defineOptions({ name: 'ContactListModal' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ customerId: props.customerId
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ queryParams.customerId = props.customerId // 瑙e喅 props.customerId 娌℃洿鏂板埌 queryParams 涓婄殑闂
+ await getList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ContactApi.getContactPageByCustomer(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞鎿嶄綔 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open('create')
+}
+
+/** 鍏宠仈鑱旂郴浜烘彁浜� */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const contactRef = ref()
+const submitForm = async () => {
+ const contactIds = contactRef.value.getSelectionRows().map((row: ContactApi.ContactVO) => row.id)
+ if (contactIds.length === 0) {
+ return message.error('鏈�夋嫨鑱旂郴浜�')
+ }
+ dialogVisible.value = false
+ emit('success', contactIds, contactRef.value.getSelectionRows())
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+</script>
diff --git a/src/views/crm/contact/detail/ContactDetailsHeader.vue b/src/views/crm/contact/detail/ContactDetailsHeader.vue
new file mode 100644
index 0000000..12fb3bc
--- /dev/null
+++ b/src/views/crm/contact/detail/ContactDetailsHeader.vue
@@ -0,0 +1,33 @@
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ contact.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ contact.customerName }}</el-descriptions-item>
+ <el-descriptions-item label="鑱屽姟">{{ contact.post }}</el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満">{{ contact.mobile }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(contact.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ContactApi from '@/api/crm/contact'
+import { formatDate } from '@/utils/formatTime'
+
+const { contact } = defineProps<{ contact: ContactApi.ContactVO }>()
+</script>
diff --git a/src/views/crm/contact/detail/ContactDetailsInfo.vue b/src/views/crm/contact/detail/ContactDetailsInfo.vue
new file mode 100644
index 0000000..9e8bfff
--- /dev/null
+++ b/src/views/crm/contact/detail/ContactDetailsInfo.vue
@@ -0,0 +1,69 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="濮撳悕">{{ contact.name }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ contact.customerName }}</el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満">{{ contact.mobile }}</el-descriptions-item>
+ <el-descriptions-item label="鐢佃瘽">{{ contact.telephone }}</el-descriptions-item>
+ <el-descriptions-item label="閭">{{ contact.email }}</el-descriptions-item>
+ <el-descriptions-item label="QQ">{{ contact.qq }}</el-descriptions-item>
+ <el-descriptions-item label="寰俊">{{ contact.wechat }}</el-descriptions-item>
+ <el-descriptions-item label="鍦板潃">
+ {{ contact.areaName }} {{ contact.detailAddress }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鑱屽姟">{{ contact.post }}</el-descriptions-item>
+ <el-descriptions-item label="鐩村睘涓婄骇">{{ contact.parentName }}</el-descriptions-item>
+ <el-descriptions-item label="鍏抽敭鍐崇瓥浜�">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="contact.master" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎬у埆">
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="contact.sex" />
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬫鑱旂郴鏃堕棿">
+ {{ formatDate(contact.contactNextTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ contact.remark }}</el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">{{ contact.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涜褰�">
+ {{ contact.contactLastContent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涙椂闂�">
+ {{ formatDate(contact.contactLastTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ contact.creatorName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(contact.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(contact.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ContactApi from '@/api/crm/contact'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+
+const { contact } = defineProps<{
+ contact: ContactApi.ContactVO
+}>()
+
+// 灞曠ず鐨勬姌鍙犻潰鏉�
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>
diff --git a/src/views/crm/contact/detail/index.vue b/src/views/crm/contact/detail/index.vue
new file mode 100644
index 0000000..7989d56
--- /dev/null
+++ b/src/views/crm/contact/detail/index.vue
@@ -0,0 +1,121 @@
+<template>
+ <ContactDetailsHeader v-loading="loading" :contact="contact">
+ <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', contact.id)">
+ 缂栬緫
+ </el-button>
+ <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
+ 杞Щ
+ </el-button>
+ </ContactDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璺熻繘璁板綍">
+ <FollowUpList :biz-id="contactId" :biz-type="BizTypeEnum.CRM_CONTACT" />
+ </el-tab-pane>
+ <el-tab-pane label="璇︾粏璧勬枡">
+ <ContactDetailsInfo :contact="contact" />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="contact.id!"
+ :biz-type="BizTypeEnum.CRM_CONTACT"
+ :show-action="true"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍟嗘満" lazy>
+ <BusinessList
+ :biz-id="contact.id!"
+ :biz-type="BizTypeEnum.CRM_CONTACT"
+ :contact-id="contact.id"
+ :customer-id="contact.customerId"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ContactForm ref="formRef" @success="getContact" />
+ <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CONTACT" @success="close" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ContactApi from '@/api/crm/contact'
+import ContactDetailsHeader from '@/views/crm/contact/detail/ContactDetailsHeader.vue'
+import ContactDetailsInfo from '@/views/crm/contact/detail/ContactDetailsInfo.vue'
+import BusinessList from '@/views/crm/business/components/BusinessList.vue' // 鍟嗘満鍒楄〃
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 鍥㈤槦鎴愬憳鍒楄〃锛堟潈闄愶級
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogVO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import ContactForm from '@/views/crm/contact/ContactForm.vue'
+import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+
+defineOptions({ name: 'CrmContactDetail' })
+
+const message = useMessage()
+
+const contactId = ref(0) // 绾跨储缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const contact = ref<ContactApi.ContactVO>({} as ContactApi.ContactVO) // 鑱旂郴浜鸿鎯�
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 鑾峰彇璇︽儏 */
+const getContact = async () => {
+ loading.value = true
+ try {
+ contact.value = await ContactApi.getContact(contactId.value)
+ await getOperateLog(contactId.value)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 缂栬緫 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鑱旂郴浜鸿浆绉� */
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 鑱旂郴浜鸿浆绉昏〃鍗� ref
+const transfer = () => {
+ transferFormRef.value?.open(contact.value.id)
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async (contactId: number) => {
+ if (!contactId) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_CONTACT,
+ bizId: contactId
+ })
+ logList.value = data.list
+}
+
+/** 鍏抽棴绐楀彛 */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+const close = () => {
+ delView(unref(currentRoute))
+}
+
+/** 鍒濆鍖� */
+const { params } = useRoute()
+onMounted(async () => {
+ if (!params.id) {
+ message.warning('鍙傛暟閿欒锛岃仈绯讳汉涓嶈兘涓虹┖锛�')
+ close()
+ return
+ }
+ contactId.value = params.id as unknown as number
+ await getContact()
+})
+</script>
diff --git a/src/views/crm/contact/index.vue b/src/views/crm/contact/index.vue
new file mode 100644
index 0000000..ec26f1e
--- /dev/null
+++ b/src/views/crm/contact/index.vue
@@ -0,0 +1,332 @@
+<template>
+ <doc-alert title="銆愬鎴枫�戝鎴风鐞嗐�佸叕娴峰鎴�" url="https://doc.iocoder.cn/crm/customer/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ class="!w-240px"
+ clearable
+ lable-key="name"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ value-key="id"
+ @keyup.enter="handleQuery"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="濮撳悕" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ鍚�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙�" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ墜鏈哄彿"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢佃瘽" prop="telephone">
+ <el-input
+ v-model="queryParams.telephone"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ョ數璇�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="寰俊" prop="wechat">
+ <el-input
+ v-model="queryParams.wechat"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ井淇�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢靛瓙閭" prop="email">
+ <el-input
+ v-model="queryParams.email"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ョ數瀛愰偖绠�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['crm:contact:create']" type="primary" @click="openForm('create')">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:contact:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="鎴戝弬涓庣殑" name="2" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="鑱旂郴浜哄鍚�" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵嬫満" prop="mobile" width="120" />
+ <el-table-column align="center" label="鐢佃瘽" prop="telephone" width="130" />
+ <el-table-column align="center" label="閭" prop="email" width="180" />
+ <el-table-column align="center" label="鑱屼綅" prop="post" width="120" />
+ <el-table-column align="center" label="鍦板潃" prop="detailAddress" width="120" />
+ <el-table-column align="center" label="鍏抽敭鍐崇瓥浜�" prop="master" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐩村睘涓婄骇" prop="parentName" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.parentId)">
+ {{ scope.row.parentName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍦板潃" align="center" prop="detailAddress" width="180" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎬у埆" prop="sex">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="120" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="200">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:contact:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:contact:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ContactForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ContactApi from '@/api/crm/contact'
+import ContactForm from './ContactForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import * as CustomerApi from '@/api/crm/customer'
+import { TabsPaneContext } from 'element-plus'
+
+defineOptions({ name: 'CrmContact' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ mobile: undefined,
+ telephone: undefined,
+ email: undefined,
+ customerId: undefined,
+ name: undefined,
+ wechat: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ContactApi.getContactPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ContactApi.deleteContact(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ContactApi.exportContact(queryParams)
+ download.excel(data, '鑱旂郴浜�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+})
+</script>
diff --git a/src/views/crm/contract/ContractForm.vue b/src/views/crm/contract/ContractForm.vue
new file mode 100644
index 0000000..db2eec9
--- /dev/null
+++ b/src/views/crm/contract/ContractForm.vue
@@ -0,0 +1,369 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="1280">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ >
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="鍚堝悓缂栧彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍚堝悓鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悎鍚屽悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="w-1/1"
+ @change="handleCustomerChange"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍟嗘満鍚嶇О" prop="businessId">
+ <el-select
+ @change="handleBusinessChange"
+ :disabled="!formData.customerId"
+ v-model="formData.businessId"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in getBusinessOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="涓嬪崟鏃ユ湡" prop="orderDate">
+ <el-date-picker
+ v-model="formData.orderDate"
+ placeholder="閫夋嫨涓嬪崟鏃ユ湡"
+ type="date"
+ value-format="x"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-date-picker
+ v-model="formData.startTime"
+ placeholder="閫夋嫨寮�濮嬫椂闂�"
+ type="date"
+ value-format="x"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-date-picker
+ v-model="formData.endTime"
+ placeholder="閫夋嫨缁撴潫鏃堕棿"
+ type="date"
+ value-format="x"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="鍏徃绛剧害浜�" prop="signUserId">
+ <el-select v-model="formData.signUserId" class="w-1/1">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛绛剧害浜�" prop="signContactId">
+ <el-select
+ v-model="formData.signContactId"
+ :disabled="!formData.customerId"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in getContactOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="浜у搧娓呭崟" name="product">
+ <ContractProductForm
+ ref="productFormRef"
+ :products="formData.products"
+ :disabled="disabled"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="浜у搧鎬婚噾棰�" prop="totalProductPrice">
+ <el-input
+ disabled
+ v-model="formData.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏁村崟鎶樻墸锛�%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ placeholder="璇疯緭鍏ユ暣鍗曟姌鎵�"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎶樻墸鍚庨噾棰�" prop="totalPrice">
+ <el-input
+ disabled
+ v-model="formData.totalPrice"
+ placeholder="璇疯緭鍏ュ晢鏈洪噾棰�"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">淇濆瓨</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
+import * as UserApi from '@/api/system/user'
+import * as ContactApi from '@/api/crm/contact'
+import * as BusinessApi from '@/api/crm/business'
+import { erpPriceMultiply, erpPriceInputFormatter } from '@/utils'
+import { useUserStore } from '@/store/modules/user'
+import ContractProductForm from '@/views/crm/contract/components/ContractProductForm.vue'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ no: undefined,
+ name: undefined,
+ customerId: undefined,
+ businessId: undefined,
+ orderDate: undefined,
+ startTime: undefined,
+ endTime: undefined,
+ signUserId: undefined,
+ signContactId: undefined,
+ ownerUserId: undefined,
+ discountPercent: 0,
+ totalProductPrice: undefined,
+ remark: undefined,
+ products: []
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚堝悓鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ orderDate: [{ required: true, message: '涓嬪崟鏃ユ湡涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const customerList = ref([]) // 瀹㈡埛鍒楄〃鐨勬暟鎹�
+const businessList = ref<BusinessApi.BusinessVO[]>([])
+const contactList = ref<ContactApi.ContactVO[]>([])
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('product')
+const productFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ const totalProductPrice = val.products.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null
+ ? erpPriceMultiply(totalProductPrice, val.discountPercent / 100.0)
+ : 0
+ const totalPrice = totalProductPrice - discountPrice
+ // 璧嬪��
+ formData.value.totalProductPrice = totalProductPrice
+ formData.value.totalPrice = totalPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ContractApi.getContract(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+ // 鑾峰彇鑱旂郴浜�
+ contactList.value = await ContactApi.getSimpleContactList()
+ // 鑾峰緱鍟嗘満鍒楄〃
+ businessList.value = await BusinessApi.getSimpleBusinessList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ productFormRef.value.validate()
+ try {
+ const data = unref(formData.value) as unknown as ContractApi.ContractVO
+ if (formType.value === 'create') {
+ await ContractApi.createContract(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ContractApi.updateContract(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ no: undefined,
+ name: undefined,
+ customerId: undefined,
+ businessId: undefined,
+ orderDate: undefined,
+ startTime: undefined,
+ endTime: undefined,
+ signUserId: undefined,
+ signContactId: undefined,
+ ownerUserId: undefined,
+ discountPercent: 0,
+ totalProductPrice: undefined,
+ remark: undefined,
+ products: []
+ }
+ formRef.value?.resetFields()
+}
+
+/** 澶勭悊鍒囨崲瀹㈡埛 */
+const handleCustomerChange = () => {
+ formData.value.businessId = undefined
+ formData.value.signContactId = undefined
+ formData.value.products = []
+}
+
+/** 澶勭悊鍟嗘満鍙樺寲 */
+const handleBusinessChange = async (businessId: number) => {
+ const business = await BusinessApi.getBusiness(businessId)
+ business.products.forEach((item) => {
+ item.contractPrice = item.businessPrice
+ })
+ formData.value.products = business.products
+}
+
+/** 鍔ㄦ�佽幏鍙栧鎴疯仈绯讳汉 */
+const getContactOptions = computed(() =>
+ contactList.value.filter((item) => item.customerId == formData.value.customerId)
+)
+/** 鍔ㄦ�佽幏鍙栧晢鏈� */
+const getBusinessOptions = computed(() =>
+ businessList.value.filter((item) => item.customerId == formData.value.customerId)
+)
+</script>
diff --git a/src/views/crm/contract/components/ContractList.vue b/src/views/crm/contract/components/ContractList.vue
new file mode 100644
index 0000000..f693c9a
--- /dev/null
+++ b/src/views/crm/contract/components/ContractList.vue
@@ -0,0 +1,136 @@
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row justify="end">
+ <el-button @click="openForm">
+ <Icon class="mr-5px" icon="clarity:contract-line" />
+ 鍒涘缓鍚堝悓
+ </el-button>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚堝悓鍚嶇О" fixed="left" align="center" prop="name">
+ <template #default="scope">
+ <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堝悓缂栧彿" align="center" prop="no" />
+ <el-table-column label="瀹㈡埛鍚嶇О" align="center" prop="customerName" />
+ <el-table-column
+ label="鍚堝悓閲戦锛堝厓锛�"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="寮�濮嬫椂闂�"
+ align="center"
+ prop="startTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column
+ label="缁撴潫鏃堕棿"
+ align="center"
+ prop="endTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column align="center" label="鐘舵��" prop="auditStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <ContractForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import * as ContractApi from '@/api/crm/contract'
+import ContractForm from './../ContractForm.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import { dateFormatter } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'CrmContractList' })
+const props = defineProps<{
+ bizType: number // 涓氬姟绫诲瀷
+ bizId: number // 涓氬姟缂栧彿
+}>()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ customerId: undefined as unknown // 鍏佽 undefined + number
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 缃┖鍙傛暟
+ queryParams.customerId = undefined
+ // 鎵ц鏌ヨ
+ let data = { list: [], total: 0 }
+ switch (props.bizType) {
+ case BizTypeEnum.CRM_CUSTOMER:
+ queryParams.customerId = props.bizId
+ data = await ContractApi.getContractPageByCustomer(queryParams)
+ break
+ case BizTypeEnum.CRM_BUSINESS:
+ queryParams.businessId = props.bizId
+ data = await ContractApi.getContractPageByBusiness(queryParams)
+ break
+ default:
+ return
+ }
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 娣诲姞 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open('create')
+}
+
+/** 鎵撳紑鍚堝悓璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 鐩戝惉鎵撳紑鐨� bizId + bizType锛屼粠鑰屽姞杞芥渶鏂扮殑鍒楄〃 */
+watch(
+ () => [props.bizId, props.bizType],
+ () => {
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+</script>
diff --git a/src/views/crm/contract/components/ContractProductForm.vue b/src/views/crm/contract/components/ContractProductForm.vue
new file mode 100644
index 0000000..c33b996
--- /dev/null
+++ b/src/views/crm/contract/components/ContractProductForm.vue
@@ -0,0 +1,183 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productNo" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浠锋牸锛堝厓锛�" min-width="120">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍞环锛堝厓锛�" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.contractPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.contractPrice"
+ controls-position="right"
+ :min="0.001"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堣" prop="totalPrice" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import * as ProductApi from '@/api/crm/product'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+ products: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ contractPrice: [{ required: true, message: '鍚堝悓浠锋牸涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductApi.ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 鍒濆鍖栬缃骇鍝侀」 */
+watch(
+ () => props.products,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ if (item.contractPrice != null && item.count != null) {
+ item.totalPrice = erpPriceMultiply(item.contractPrice, item.count)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnit: undefined, // 浜у搧鍗曚綅
+ productNo: undefined, // 浜у搧鏉$爜
+ productPrice: undefined, // 浜у搧浠锋牸
+ contractPrice: undefined,
+ count: 1
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnit = product.unit
+ row.productNo = product.no
+ row.productPrice = product.price
+ row.contractPrice = product.price
+ }
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+})
+</script>
diff --git a/src/views/crm/contract/config/index.vue b/src/views/crm/contract/config/index.vue
new file mode 100644
index 0000000..c592123
--- /dev/null
+++ b/src/views/crm/contract/config/index.vue
@@ -0,0 +1,103 @@
+<template>
+ <doc-alert title="銆愬悎鍚屻�戝悎鍚岀鐞嗐�佸悎鍚屾彁閱�" url="https://doc.iocoder.cn/crm/contract/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="160px"
+ v-loading="formLoading"
+ >
+ <el-card shadow="never">
+ <!-- 鎿嶄綔 -->
+ <template #header>
+ <div class="flex items-center justify-between">
+ <CardTitle title="鍚堝悓閰嶇疆璁剧疆" />
+ <el-button type="primary" @click="onSubmit" v-hasPermi="['crm:contract-config:update']">
+ 淇濆瓨
+ </el-button>
+ </div>
+ </template>
+ <!-- 琛ㄥ崟 -->
+ <el-form-item label="鎻愬墠鎻愰啋璁剧疆" prop="notifyEnabled">
+ <el-radio-group
+ v-model="formData.notifyEnabled"
+ @change="changeNotifyEnable"
+ class="ml-4"
+ >
+ <el-radio :value="false" size="large">涓嶆彁閱�</el-radio>
+ <el-radio :value="true" size="large">鎻愰啋</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <div v-if="formData.notifyEnabled">
+ <el-form-item>
+ 鎻愬墠 <el-input-number class="mx-2" v-model="formData.notifyDays" /> 澶╂彁閱�
+ </el-form-item>
+ </div>
+ </el-card>
+ </el-form>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ContractConfigApi from '@/api/crm/contract/config'
+import { CardTitle } from '@/components/Card'
+
+defineOptions({ name: 'CrmContractConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const formLoading = ref(false)
+const formData = ref({
+ notifyEnabled: false,
+ notifyDays: undefined
+})
+const formRules = reactive({})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鑾峰彇閰嶇疆 */
+const getConfig = async () => {
+ try {
+ formLoading.value = true
+ const data = await ContractConfigApi.getContractConfig()
+ if (data === null) {
+ return
+ }
+ formData.value = data
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鎻愪氦閰嶇疆 */
+const onSubmit = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as ContractConfigApi.ContractConfigVO
+ await ContractConfigApi.saveContractConfig(data)
+ message.success(t('common.updateSuccess'))
+ await getConfig()
+ formLoading.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鏇存敼鎻愬墠鎻愰啋璁剧疆 */
+const changeNotifyEnable = () => {
+ if (!formData.value.notifyEnabled) {
+ formData.value.notifyDays = undefined
+ }
+}
+
+onMounted(() => {
+ getConfig()
+})
+</script>
diff --git a/src/views/crm/contract/detail/ContractDetailsHeader.vue b/src/views/crm/contract/detail/ContractDetailsHeader.vue
new file mode 100644
index 0000000..9cfbfc7
--- /dev/null
+++ b/src/views/crm/contract/detail/ContractDetailsHeader.vue
@@ -0,0 +1,45 @@
+<!-- 鍚堝悓璇︽儏澶撮儴缁勪欢-->
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ contract.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">
+ {{ contract.customerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓閲戦锛堝厓锛�">
+ {{ erpPriceInputFormatter(contract.totalPrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬪崟鏃堕棿">
+ {{ formatDate(contract.orderDate) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥炴閲戦锛堝厓锛�">
+ {{ erpPriceInputFormatter(contract.totalReceivablePrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璐熻矗浜�">
+ {{ contract.ownerUserName }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ContractApi from '@/api/crm/contract'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+defineOptions({ name: 'ContractDetailsHeader' })
+defineProps<{ contract: ContractApi.ContractVO }>()
+</script>
diff --git a/src/views/crm/contract/detail/ContractDetailsInfo.vue b/src/views/crm/contract/detail/ContractDetailsInfo.vue
new file mode 100644
index 0000000..73aa144
--- /dev/null
+++ b/src/views/crm/contract/detail/ContractDetailsInfo.vue
@@ -0,0 +1,76 @@
+<!-- 鍚堝悓璇︽儏缁勪欢 -->
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-collapse-item name="contractInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="鍚堝悓缂栧彿">{{ contract.no }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓鍚嶇О">{{ contract.name }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ contract.customerName }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗘満鍚嶇О">{{ contract.businessName }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓閲戦锛堝厓锛�">
+ {{ erpPriceInputFormatter(contract.totalPrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬪崟鏃堕棿">
+ {{ formatDate(contract.orderDate) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓寮�濮嬫椂闂�">
+ {{ formatDate(contract.startTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓缁撴潫鏃堕棿">
+ {{ formatDate(contract.endTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛绛剧害浜�">
+ {{ contract.signContactName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍏徃绛剧害浜�">
+ {{ contract.signUserName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">
+ {{ contract.remark }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓鐘舵��">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="contract.auditStatus" />
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">{{ contract.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涙椂闂�">
+ {{ formatDate(contract.contactLastTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ contract.creatorName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(contract.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(contract.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ContractApi from '@/api/crm/contract'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceInputFormatter } from '@/utils'
+
+defineOptions({ name: 'ContractDetailsInfo' })
+defineProps<{
+ contract: ContractApi.ContractVO
+}>()
+
+// 灞曠ず鐨勬姌鍙犻潰鏉�
+const activeNames = ref(['contractInfo', 'systemInfo'])
+</script>
diff --git a/src/views/crm/contract/detail/ContractProductList.vue b/src/views/crm/contract/detail/ContractProductList.vue
new file mode 100644
index 0000000..ea23d17
--- /dev/null
+++ b/src/views/crm/contract/detail/ContractProductList.vue
@@ -0,0 +1,66 @@
+<template>
+ <ContentWrap>
+ <el-table :data="contract.products" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column
+ align="center"
+ label="浜у搧鍚嶇О"
+ fixed="left"
+ prop="productName"
+ min-width="160"
+ >
+ <template #default="scope">
+ {{ scope.row.productName }}
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鏉$爜" align="center" prop="productNo" min-width="120" />
+ <el-table-column align="center" label="浜у搧鍗曚綅" prop="productUnit" min-width="160">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="row.productUnit" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="浜у搧浠锋牸锛堝厓锛�"
+ align="center"
+ prop="productPrice"
+ min-width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚堝悓浠锋牸锛堝厓锛�"
+ align="center"
+ prop="contractPrice"
+ min-width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="鏁伴噺"
+ prop="count"
+ min-width="100px"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚堣閲戦锛堝厓锛�"
+ align="center"
+ prop="totalPrice"
+ min-width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ <el-row class="mt-10px" justify="end">
+ <el-col :span="3"> 鏁村崟鎶樻墸锛歿{ erpPriceInputFormatter(contract.discountPercent) }}% </el-col>
+ <el-col :span="4">
+ 浜у搧鎬婚噾棰濓細{{ erpPriceInputFormatter(contract.totalProductPrice) }} 鍏�
+ </el-col>
+ </el-row>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ContractApi from '@/api/crm/contract'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+const { contract } = defineProps<{
+ contract: ContractApi.ContractVO
+}>()
+</script>
diff --git a/src/views/crm/contract/detail/index.vue b/src/views/crm/contract/detail/index.vue
new file mode 100644
index 0000000..9d5e14c
--- /dev/null
+++ b/src/views/crm/contract/detail/index.vue
@@ -0,0 +1,139 @@
+<!-- 鍚堝悓璇︽儏椤甸潰缁勪欢-->
+<template>
+ <ContractDetailsHeader v-loading="loading" :contract="contract">
+ <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', contract.id)">
+ 缂栬緫
+ </el-button>
+ <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transferContract">
+ 杞Щ
+ </el-button>
+ </ContractDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璺熻繘璁板綍">
+ <FollowUpList :biz-id="contract.id" :biz-type="BizTypeEnum.CRM_CONTRACT" />
+ </el-tab-pane>
+ <el-tab-pane label="鍩烘湰淇℃伅">
+ <ContractDetailsInfo :contract="contract" />
+ </el-tab-pane>
+ <el-tab-pane label="浜у搧">
+ <ContractProductList :contract="contract" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥炴">
+ <ReceivablePlanList
+ :contract-id="contract.id!"
+ :customer-id="contract.customerId"
+ @create-receivable="createReceivable"
+ />
+ <ReceivableList
+ ref="receivableListRef"
+ :contract-id="contract.id!"
+ :customer-id="contract.customerId"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="contract.id!"
+ :biz-type="BizTypeEnum.CRM_CONTRACT"
+ :show-action="true"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ContractForm ref="formRef" @success="getContractData" />
+ <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CONTRACT" @success="close" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { OperateLogVO } from '@/api/system/operatelog'
+import * as ContractApi from '@/api/crm/contract'
+import ContractDetailsInfo from './ContractDetailsInfo.vue'
+import ContractDetailsHeader from './ContractDetailsHeader.vue'
+import ContractProductList from './ContractProductList.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import ContractForm from '@/views/crm/contract/ContractForm.vue'
+import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue'
+import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue'
+
+defineOptions({ name: 'CrmContractDetail' })
+const props = defineProps<{ id?: number }>()
+
+const route = useRoute()
+const message = useMessage()
+const contractId = ref(0) // 缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const contract = ref<ContractApi.ContractVO>({} as ContractApi.ContractVO) // 璇︽儏
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 缂栬緫 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鑾峰彇璇︽儏 */
+const getContractData = async () => {
+ loading.value = true
+ try {
+ contract.value = await ContractApi.getContract(contractId.value)
+ await getOperateLog(contractId.value)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async (contractId: number) => {
+ if (!contractId) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_CONTRACT,
+ bizId: contractId
+ })
+ logList.value = data.list
+}
+
+/** 浠庡洖娆捐鍒掑垱寤哄洖娆� */
+const receivableListRef = ref<InstanceType<typeof ReceivableList>>() // 鍥炴鍒楄〃 Ref
+const createReceivable = (planData: any) => {
+ receivableListRef.value?.createReceivable(planData)
+}
+
+/** 杞Щ */
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 鍚堝悓杞Щ琛ㄥ崟 ref
+const transferContract = () => {
+ transferFormRef.value?.open(contract.value.id)
+}
+
+/** 鍏抽棴 */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+const close = () => {
+ delView(unref(currentRoute))
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ const id = props.id || route.params.id
+ if (!id) {
+ message.warning('鍙傛暟閿欒锛屽悎鍚屼笉鑳戒负绌猴紒')
+ close()
+ return
+ }
+ contractId.value = id as unknown as number
+ await getContractData()
+})
+</script>
diff --git a/src/views/crm/contract/index.vue b/src/views/crm/contract/index.vue
new file mode 100644
index 0000000..0c9d728
--- /dev/null
+++ b/src/views/crm/contract/index.vue
@@ -0,0 +1,398 @@
+<template>
+ <doc-alert title="銆愬悎鍚屻�戝悎鍚岀鐞嗐�佸悎鍚屾彁閱�" url="https://doc.iocoder.cn/crm/contract/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍚堝悓缂栧彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ悎鍚岀紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍚堝悓鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ悎鍚屽悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ class="!w-240px"
+ clearable
+ lable-key="name"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ value-key="id"
+ @keyup.enter="handleQuery"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['crm:contract:create']" type="primary" @click="openForm('create')">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:contract:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="鎴戝弬涓庣殑" name="2" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="鍚堝悓缂栧彿" prop="no" width="180" />
+ <el-table-column align="center" fixed="left" label="鍚堝悓鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍟嗘満鍚嶇О" prop="businessName" width="130">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openBusinessDetail(scope.row.businessId)"
+ >
+ {{ scope.row.businessName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍚堝悓閲戦锛堝厓锛�"
+ prop="totalPrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="涓嬪崟鏃堕棿"
+ prop="orderDate"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="鍚堝悓寮�濮嬫椂闂�"
+ prop="startTime"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="鍚堝悓缁撴潫鏃堕棿"
+ prop="endTime"
+ width="120"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column align="center" label="瀹㈡埛绛剧害浜�" prop="contactName" width="130">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openContactDetail(scope.row.signContactId)"
+ >
+ {{ scope.row.signContactName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍏徃绛剧害浜�" prop="signUserName" width="130" />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ align="center"
+ label="宸插洖娆鹃噾棰濓紙鍏冿級"
+ prop="totalReceivablePrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ align="center"
+ label="鏈洖娆鹃噾棰濓紙鍏冿級"
+ prop="totalReceivablePrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ >
+ <template #default="scope">
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.totalReceivablePrice) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="120" />
+ <el-table-column align="center" fixed="right" label="鍚堝悓鐘舵��" prop="auditStatus" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="250">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.auditStatus === 0"
+ v-hasPermi="['crm:contract:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="scope.row.auditStatus === 0"
+ v-hasPermi="['crm:contract:update']"
+ link
+ type="primary"
+ @click="handleSubmit(scope.row)"
+ >
+ 鎻愪氦瀹℃牳
+ </el-button>
+ <el-button
+ v-else
+ link
+ v-hasPermi="['crm:contract:update']"
+ type="primary"
+ @click="handleProcessDetail(scope.row)"
+ >
+ 鏌ョ湅瀹℃壒
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:contract:query']"
+ link
+ type="primary"
+ @click="openDetail(scope.row.id)"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:contract:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ContractForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ContractApi from '@/api/crm/contract'
+import ContractForm from './ContractForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import * as CustomerApi from '@/api/crm/customer'
+import { TabsPaneContext } from 'element-plus'
+
+defineOptions({ name: 'CrmContract' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ name: null,
+ customerId: null,
+ orderDate: [],
+ no: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName
+ handleQuery()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ContractApi.getContractPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ContractApi.deleteContract(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ContractApi.exportContract(queryParams)
+ download.excel(data, '鍚堝悓.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鎻愪氦瀹℃牳 **/
+const handleSubmit = async (row: ContractApi.ContractVO) => {
+ await message.confirm(`鎮ㄧ‘瀹氭彁浜ゃ��${row.name}銆戝鏍稿悧锛焋)
+ await ContractApi.submitContract(row.id)
+ message.success('鎻愪氦瀹℃牳鎴愬姛锛�')
+ await getList()
+}
+
+/** 鏌ョ湅瀹℃壒 */
+const handleProcessDetail = (row: ContractApi.ContractVO) => {
+ push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 鎵撳紑鍚堝悓璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const openContactDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 鎵撳紑鍟嗘満璇︽儏 */
+const openBusinessDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+})
+</script>
diff --git a/src/views/crm/customer/CustomerForm.vue b/src/views/crm/customer/CustomerForm.vue
new file mode 100644
index 0000000..9a904d2
--- /dev/null
+++ b/src/views/crm/customer/CustomerForm.vue
@@ -0,0 +1,260 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ鎴峰悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鏉ユ簮" prop="source">
+ <el-select v-model="formData.source" placeholder="璇烽�夋嫨瀹㈡埛鏉ユ簮" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢佃瘽" prop="telephone">
+ <el-input v-model="formData.telephone" placeholder="璇疯緭鍏ョ數璇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="formData.email" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="寰俊" prop="wechat">
+ <el-input v-model="formData.wechat" placeholder="璇疯緭鍏ュ井淇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="QQ" prop="qq">
+ <el-input v-model="formData.qq" placeholder="璇疯緭鍏� QQ" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛琛屼笟" prop="industryId">
+ <el-select v-model="formData.industryId" placeholder="璇烽�夋嫨瀹㈡埛琛屼笟" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛绾у埆" prop="level">
+ <el-select v-model="formData.level" placeholder="璇烽�夋嫨瀹㈡埛绾у埆" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="areaId">
+ <el-cascader
+ v-model="formData.areaId"
+ :options="areaList"
+ :props="defaultProps"
+ class="w-1/1"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍩庡競"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇︾粏鍦板潃" prop="detailAddress">
+ <el-input v-model="formData.detailAddress" placeholder="璇疯緭鍏ヨ缁嗗湴鍧�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="涓嬫鑱旂郴鏃堕棿" prop="contactNextTime">
+ <el-date-picker
+ v-model="formData.contactNextTime"
+ placeholder="閫夋嫨涓嬫鑱旂郴鏃堕棿"
+ type="datetime"
+ value-format="x"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as CustomerApi from '@/api/crm/customer'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const areaList = ref([]) // 鍦板尯鍒楄〃
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ contactNextTime: undefined,
+ ownerUserId: 0,
+ mobile: undefined,
+ telephone: undefined,
+ qq: undefined,
+ wechat: undefined,
+ email: undefined,
+ areaId: undefined,
+ detailAddress: undefined,
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ remark: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '瀹㈡埛鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await CustomerApi.getCustomer(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鍦板尯鍒楄〃
+ areaList.value = await AreaApi.getAreaTree()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as CustomerApi.CustomerVO
+ if (formType.value === 'create') {
+ await CustomerApi.createCustomer(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await CustomerApi.updateCustomer(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ contactNextTime: undefined,
+ ownerUserId: 0,
+ mobile: undefined,
+ telephone: undefined,
+ qq: undefined,
+ wechat: undefined,
+ email: undefined,
+ areaId: undefined,
+ detailAddress: undefined,
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ remark: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/customer/CustomerImportForm.vue b/src/views/crm/customer/CustomerImportForm.vue
new file mode 100644
index 0000000..d5f2f13
--- /dev/null
+++ b/src/views/crm/customer/CustomerImportForm.vue
@@ -0,0 +1,158 @@
+<!-- 瀹㈡埛瀵煎叆绐楀彛 -->
+<template>
+ <Dialog v-model="dialogVisible" title="瀹㈡埛瀵煎叆" width="400">
+ <div class="flex items-center my-10px">
+ <span class="mr-10px">璐熻矗浜�</span>
+ <el-select v-model="ownerUserId" class="!w-240px" clearable>
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </div>
+ <el-upload
+ ref="uploadRef"
+ v-model:file-list="fileList"
+ :auto-upload="false"
+ :disabled="formLoading"
+ :limit="1"
+ :on-exceed="handleExceed"
+ accept=".xlsx, .xls"
+ action="none"
+ drag
+ >
+ <Icon icon="ep:upload" />
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <div class="el-upload__tip">
+ <el-checkbox v-model="updateSupport" />
+ 鏄惁鏇存柊宸茬粡瀛樺湪鐨勫鎴锋暟鎹紙鈥滃鎴峰悕绉扳�濋噸澶嶏級
+ </div>
+ <span>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆�</span>
+ <el-link
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ type="primary"
+ @click="importTemplate"
+ >
+ 涓嬭浇妯℃澘
+ </el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
+import download from '@/utils/download'
+import type { UploadUserFile } from 'element-plus'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'CrmCustomerImportForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const uploadRef = ref()
+const fileList = ref<UploadUserFile[]>([]) // 鏂囦欢鍒楄〃
+const updateSupport = ref(false) // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勫鎴锋暟鎹�
+const ownerUserId = ref<undefined | number>() // 璐熻矗浜虹紪鍙�
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ await resetForm()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ ownerUserId.value = useUserStore().getUser.id
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ if (fileList.value.length == 0) {
+ message.error('璇蜂笂浼犳枃浠�')
+ return
+ }
+
+ formLoading.value = true
+ try {
+ const formData = new FormData()
+ formData.append('updateSupport', String(updateSupport.value))
+ formData.append('file', fileList.value[0].raw as Blob)
+ formData.append('ownerUserId', String(ownerUserId.value))
+ const res = await CustomerApi.handleImport(formData)
+ submitFormSuccess(res)
+ } catch {
+ submitFormError()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鏂囦欢涓婁紶鎴愬姛 */
+const emits = defineEmits(['success'])
+const submitFormSuccess = (response: any) => {
+ if (response.code !== 0) {
+ message.error(response.msg)
+ formLoading.value = false
+ return
+ }
+ // 鎷兼帴鎻愮ず璇�
+ const data = response.data
+ let text = '涓婁紶鎴愬姛鏁伴噺锛�' + data.createCustomerNames.length + ';'
+ for (let customerName of data.createCustomerNames) {
+ text += '< ' + customerName + ' >'
+ }
+ text += '鏇存柊鎴愬姛鏁伴噺锛�' + data.updateCustomerNames.length + ';'
+ for (const customerName of data.updateCustomerNames) {
+ text += '< ' + customerName + ' >'
+ }
+ text += '鏇存柊澶辫触鏁伴噺锛�' + Object.keys(data.failureCustomerNames).length + ';'
+ for (const customerName in data.failureCustomerNames) {
+ text += '< ' + customerName + ': ' + data.failureCustomerNames[customerName] + ' >'
+ }
+ message.alert(text)
+ formLoading.value = false
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emits('success')
+}
+
+/** 涓婁紶閿欒鎻愮ず */
+const submitFormError = (): void => {
+ message.error('涓婁紶澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒')
+ resetForm()
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ // 閲嶇疆涓婁紶鐘舵�佸拰鏂囦欢
+ fileList.value = []
+ updateSupport.value = false
+ ownerUserId.value = undefined
+ await nextTick()
+ uploadRef.value?.clearFiles()
+}
+
+/** 鏂囦欢鏁拌秴鍑烘彁绀� */
+const handleExceed = (): void => {
+ message.error('鏈�澶氬彧鑳戒笂浼犱竴涓枃浠讹紒')
+}
+
+/** 涓嬭浇妯℃澘鎿嶄綔 */
+const importTemplate = async () => {
+ const res = await CustomerApi.importCustomerTemplate()
+ download.excel(res, '瀹㈡埛瀵煎叆妯$増.xls')
+}
+</script>
diff --git a/src/views/crm/customer/detail/CustomerDetailsHeader.vue b/src/views/crm/customer/detail/CustomerDetailsHeader.vue
new file mode 100644
index 0000000..514ec61
--- /dev/null
+++ b/src/views/crm/customer/detail/CustomerDetailsHeader.vue
@@ -0,0 +1,43 @@
+<template>
+ <div v-loading="loading">
+ <div class="flex items-start justify-between">
+ <div>
+ <!-- 宸︿笂锛氬鎴峰熀鏈俊鎭� -->
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ customer.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="瀹㈡埛绾у埆">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎴愪氦鐘舵��">
+ {{ customer.dealStatus ? '宸叉垚浜�' : '鏈垚浜�' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璐熻矗浜�">{{ customer.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(customer.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as CustomerApi from '@/api/crm/customer'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'CrmCustomerDetailsHeader' })
+defineProps<{
+ customer: CustomerApi.CustomerVO // 瀹㈡埛淇℃伅
+ loading: boolean // 鍔犺浇涓�
+}>()
+</script>
diff --git a/src/views/crm/customer/detail/CustomerDetailsInfo.vue b/src/views/crm/customer/detail/CustomerDetailsInfo.vue
new file mode 100644
index 0000000..d9ea62a
--- /dev/null
+++ b/src/views/crm/customer/detail/CustomerDetailsInfo.vue
@@ -0,0 +1,72 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames" class="">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">
+ {{ customer.name }}
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鏉ユ簮">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="customer.source" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満">{{ customer.mobile }}</el-descriptions-item>
+ <el-descriptions-item label="鐢佃瘽">{{ customer.telephone }}</el-descriptions-item>
+ <el-descriptions-item label="閭">{{ customer.email }}</el-descriptions-item>
+ <el-descriptions-item label="鍦板潃">
+ {{ customer.areaName }} {{ customer.detailAddress }}
+ </el-descriptions-item>
+ <el-descriptions-item label="QQ">{{ customer.qq }}</el-descriptions-item>
+ <el-descriptions-item label="寰俊">{{ customer.wechat }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛琛屼笟">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="customer.industryId" />
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛绾у埆">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬫鑱旂郴鏃堕棿">
+ {{ formatDate(customer.contactNextTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ customer.remark }}</el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">{{ customer.ownerUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涜褰�">
+ {{ customer.contactLastContent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庤窡杩涙椂闂�">
+ {{ formatDate(customer.contactLastTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label=""> </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{ customer.creatorName }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(customer.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(customer.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'CrmCustomerDetailsInfo' })
+const { customer } = defineProps<{
+ customer: CustomerApi.CustomerVO // 瀹㈡埛鏄庣粏
+}>()
+
+const activeNames = ref(['basicInfo', 'systemInfo']) // 灞曠ず鐨勬姌鍙犻潰鏉�
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/crm/customer/detail/index.vue b/src/views/crm/customer/detail/index.vue
new file mode 100644
index 0000000..aff3676
--- /dev/null
+++ b/src/views/crm/customer/detail/index.vue
@@ -0,0 +1,230 @@
+<template>
+ <CustomerDetailsHeader :customer="customer" :loading="loading">
+ <el-button
+ v-if="permissionListRef?.validateWrite"
+ v-hasPermi="['crm:customer:update']"
+ type="primary"
+ @click="openForm"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button v-if="permissionListRef?.validateOwnerUser" type="primary" @click="transfer">
+ 杞Щ
+ </el-button>
+ <el-button v-if="permissionListRef?.validateWrite" @click="handleUpdateDealStatus">
+ 鏇存敼鎴愪氦鐘舵��
+ </el-button>
+ <el-button
+ v-if="customer.lockStatus && permissionListRef?.validateOwnerUser"
+ @click="handleUnlock"
+ >
+ 瑙i攣
+ </el-button>
+ <el-button
+ v-if="!customer.lockStatus && permissionListRef?.validateOwnerUser"
+ @click="handleLock"
+ >
+ 閿佸畾
+ </el-button>
+ <el-button v-if="!customer.ownerUserId" type="primary" @click="handleReceive"> 棰嗗彇</el-button>
+ <el-button v-if="!customer.ownerUserId" type="primary" @click="handleDistributeForm">
+ 鍒嗛厤
+ </el-button>
+ <el-button
+ v-if="customer.ownerUserId && permissionListRef?.validateOwnerUser"
+ @click="handlePutPool"
+ >
+ 鏀惧叆鍏捣
+ </el-button>
+ </CustomerDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璺熻繘璁板綍">
+ <FollowUpList :biz-id="customerId" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
+ </el-tab-pane>
+ <el-tab-pane label="鍩烘湰淇℃伅">
+ <CustomerDetailsInfo :customer="customer" />
+ </el-tab-pane>
+ <el-tab-pane label="鑱旂郴浜�" lazy>
+ <ContactList
+ :biz-id="customer.id!"
+ :customer-id="customer.id!"
+ :biz-type="BizTypeEnum.CRM_CUSTOMER"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="customer.id!"
+ :biz-type="BizTypeEnum.CRM_CUSTOMER"
+ :show-action="!permissionListRef?.isPool || false"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍟嗘満" lazy>
+ <BusinessList
+ :biz-id="customer.id!"
+ :customer-id="customer.id!"
+ :biz-type="BizTypeEnum.CRM_CUSTOMER"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍚堝悓" lazy>
+ <ContractList :biz-id="customer.id!" :biz-type="BizTypeEnum.CRM_CUSTOMER" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥炴" lazy>
+ <ReceivablePlanList :customer-id="customer.id!" @create-receivable="createReceivable" />
+ <ReceivableList ref="receivableListRef" :customer-id="customer.id!" />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CustomerForm ref="formRef" @success="getCustomer" />
+ <CustomerDistributeForm ref="distributeForm" @success="getCustomer" />
+ <CrmTransferForm ref="transferFormRef" :biz-type="BizTypeEnum.CRM_CUSTOMER" @success="close" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as CustomerApi from '@/api/crm/customer'
+import CustomerForm from '@/views/crm/customer/CustomerForm.vue'
+import CustomerDetailsInfo from './CustomerDetailsInfo.vue' // 瀹㈡埛鏄庣粏 - 璇︾粏淇℃伅
+import CustomerDetailsHeader from './CustomerDetailsHeader.vue' // 瀹㈡埛鏄庣粏 - 澶撮儴
+import ContactList from '@/views/crm/contact/components/ContactList.vue' // 鑱旂郴浜哄垪琛�
+import ContractList from '@/views/crm/contract/components/ContractList.vue' // 鍚堝悓鍒楄〃
+import BusinessList from '@/views/crm/business/components/BusinessList.vue' // 鍟嗘満鍒楄〃
+import ReceivableList from '@/views/crm/receivable/components/ReceivableList.vue' // 鍥炴鍒楄〃
+import ReceivablePlanList from '@/views/crm/receivable/plan/components/ReceivablePlanList.vue' // 鍥炴璁″垝鍒楄〃
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 鍥㈤槦鎴愬憳鍒楄〃锛堟潈闄愶級
+import CrmTransferForm from '@/views/crm/permission/components/TransferForm.vue'
+import FollowUpList from '@/views/crm/followup/index.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import type { OperateLogVO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import CustomerDistributeForm from '@/views/crm/customer/pool/CustomerDistributeForm.vue'
+
+defineOptions({ name: 'CrmCustomerDetail' })
+
+const customerId = ref(0) // 瀹㈡埛缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const message = useMessage() // 娑堟伅寮圭獥
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { push, currentRoute } = useRouter() // 璺敱
+
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 鑾峰彇璇︽儏 */
+const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO) // 瀹㈡埛璇︽儏
+const getCustomer = async () => {
+ loading.value = true
+ try {
+ customer.value = await CustomerApi.getCustomer(customerId.value)
+ await getOperateLog()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 缂栬緫瀹㈡埛 */
+const formRef = ref<InstanceType<typeof CustomerForm>>() // 瀹㈡埛琛ㄥ崟 Ref
+const openForm = () => {
+ formRef.value?.open('update', customerId.value)
+}
+
+/** 鏇存柊鎴愪氦鐘舵�佹搷浣� */
+const handleUpdateDealStatus = async () => {
+ const dealStatus = !customer.value.dealStatus
+ try {
+ // 鏇存柊鐘舵�佺殑浜屾纭
+ await message.confirm(`纭畾鏇存柊鎴愪氦鐘舵�佷负銆�${dealStatus ? '宸叉垚浜�' : '鏈垚浜�'}銆戝悧锛焋)
+ // 鍙戣捣鏇存柊
+ await CustomerApi.updateCustomerDealStatus(customerId.value, dealStatus)
+ message.success(`鏇存柊鎴愪氦鐘舵�佹垚鍔焋)
+ // 鍒锋柊鏁版嵁
+ await getCustomer()
+ } catch {}
+}
+
+/** 瀹㈡埛杞Щ */
+const transferFormRef = ref<InstanceType<typeof CrmTransferForm>>() // 瀹㈡埛杞Щ琛ㄥ崟 ref
+const transfer = () => {
+ transferFormRef.value?.open(customerId.value)
+}
+
+/** 閿佸畾瀹㈡埛 */
+const handleLock = async () => {
+ await message.confirm(`纭畾閿佸畾瀹㈡埛銆�${customer.value.name}銆� 鍚楋紵`)
+ await CustomerApi.lockCustomer(unref(customerId.value), true)
+ message.success(`閿佸畾瀹㈡埛銆�${customer.value.name}銆戞垚鍔焋)
+ await getCustomer()
+}
+
+/** 瑙i攣瀹㈡埛 */
+const handleUnlock = async () => {
+ await message.confirm(`纭畾瑙i攣瀹㈡埛銆�${customer.value.name}銆� 鍚楋紵`)
+ await CustomerApi.lockCustomer(unref(customerId.value), false)
+ message.success(`瑙i攣瀹㈡埛銆�${customer.value.name}銆戞垚鍔焋)
+ await getCustomer()
+}
+
+/** 棰嗗彇瀹㈡埛 */
+const handleReceive = async () => {
+ await message.confirm(`纭畾棰嗗彇瀹㈡埛銆�${customer.value.name}銆� 鍚楋紵`)
+ await CustomerApi.receiveCustomer([unref(customerId.value)])
+ message.success(`棰嗗彇瀹㈡埛銆�${customer.value.name}銆戞垚鍔焋)
+ await getCustomer()
+}
+
+/** 鍒嗛厤瀹㈡埛 */
+const distributeForm = ref<InstanceType<typeof CustomerDistributeForm>>() // 鍒嗛厤瀹㈡埛琛ㄥ崟 Ref
+const handleDistributeForm = async () => {
+ distributeForm.value?.open(customerId.value)
+}
+
+/** 瀹㈡埛鏀惧叆鍏捣 */
+const handlePutPool = async () => {
+ await message.confirm(`纭畾灏嗗鎴枫��${customer.value.name}銆戞斁鍏ュ叕娴峰悧锛焋)
+ await CustomerApi.putCustomerPool(unref(customerId.value))
+ message.success(`瀹㈡埛銆�${customer.value.name}銆戞斁鍏ュ叕娴锋垚鍔焋)
+ // 鍔犺浇
+ close()
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async () => {
+ if (!customerId.value) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_CUSTOMER,
+ bizId: customerId.value
+ })
+ logList.value = data.list
+}
+
+/** 浠庡洖娆捐鍒掑垱寤哄洖娆� */
+const receivableListRef = ref<InstanceType<typeof ReceivableList>>() // 鍥炴鍒楄〃 Ref
+const createReceivable = (planData: any) => {
+ receivableListRef.value?.createReceivable(planData)
+}
+
+const close = () => {
+ delView(unref(currentRoute))
+ push({ name: 'CrmCustomer' })
+}
+
+/** 鍒濆鍖� */
+const { params } = useRoute()
+onMounted(() => {
+ if (!params.id) {
+ message.warning('鍙傛暟閿欒锛屽鎴蜂笉鑳戒负绌猴紒')
+ close()
+ return
+ }
+ customerId.value = params.id as unknown as number
+ getCustomer()
+})
+</script>
diff --git a/src/views/crm/customer/index.vue b/src/views/crm/customer/index.vue
new file mode 100644
index 0000000..86bddc0
--- /dev/null
+++ b/src/views/crm/customer/index.vue
@@ -0,0 +1,343 @@
+<template>
+ <doc-alert title="銆愬鎴枫�戝鎴风鐞嗐�佸叕娴峰鎴�" url="https://doc.iocoder.cn/crm/customer/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ墜鏈�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵�灞炶涓�" prop="industryId">
+ <el-select
+ v-model="queryParams.industryId"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鎵�灞炶涓�"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绾у埆" prop="level">
+ <el-select
+ v-model="queryParams.level"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨瀹㈡埛绾у埆"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鏉ユ簮" prop="source">
+ <el-select
+ v-model="queryParams.source"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨瀹㈡埛鏉ユ簮"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['crm:customer:create']" type="primary" @click="openForm('create')">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button v-hasPermi="['crm:customer:import']" plain type="warning" @click="handleImport">
+ <Icon icon="ep:upload" />
+ 瀵煎叆
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:customer:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="鎴戝弬涓庣殑" name="2" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵嬫満" prop="mobile" width="120" />
+ <el-table-column align="center" label="鐢佃瘽" prop="telephone" width="130" />
+ <el-table-column align="center" label="閭" prop="email" width="180" />
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column align="center" label="閿佸畾鐘舵��" prop="lockStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愪氦鐘舵��" prop="dealStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column align="center" label="鍦板潃" prop="detailAddress" width="180" />
+ <el-table-column align="center" label="璺濈杩涘叆鍏捣澶╂暟" prop="poolDay" width="140">
+ <template #default="scope"> {{ scope.row.poolDay }} 澶�</template>
+ </el-table-column>
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" min-width="150">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:customer:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:customer:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CustomerForm ref="formRef" @success="getList" />
+ <CustomerImportForm ref="importFormRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as CustomerApi from '@/api/crm/customer'
+import CustomerForm from './CustomerForm.vue'
+import CustomerImportForm from './CustomerImportForm.vue'
+import { TabsPaneContext } from 'element-plus'
+
+defineOptions({ name: 'CrmCustomer' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ name: '',
+ mobile: '',
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ pool: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName as string
+ handleQuery()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerApi.getCustomerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const { currentRoute, push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await CustomerApi.deleteCustomer(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+const importFormRef = ref<InstanceType<typeof CustomerImportForm>>()
+const handleImport = () => {
+ importFormRef.value?.open()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await CustomerApi.exportCustomer(queryParams)
+ download.excel(data, '瀹㈡埛.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鐩戝惉璺敱鍙樺寲鏇存柊鍒楄〃 */
+watch(
+ () => currentRoute.value,
+ () => {
+ getList()
+ }
+)
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue b/src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue
new file mode 100644
index 0000000..c7338a4
--- /dev/null
+++ b/src/views/crm/customer/limitConfig/CustomerLimitConfigForm.vue
@@ -0,0 +1,150 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="200px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="瑙勫垯閫傜敤浜虹兢" prop="userIds">
+ <el-select multiple filterable v-model="formData.userIds">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瑙勫垯閫傜敤閮ㄩ棬" prop="deptIds">
+ <el-tree-select
+ v-model="formData.deptIds"
+ :data="deptTree"
+ :props="defaultProps"
+ multiple
+ filterable
+ check-strictly
+ node-key="id"
+ placeholder="璇烽�夋嫨瑙勫垯閫傜敤閮ㄩ棬"
+ />
+ </el-form-item>
+ <el-form-item
+ :label="
+ formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT
+ ? '鎷ユ湁瀹㈡埛鏁颁笂闄�'
+ : '閿佸畾瀹㈡埛鏁颁笂闄�'
+ "
+ prop="maxCount"
+ >
+ <el-input-number v-model="formData.maxCount" placeholder="璇疯緭鍏ユ暟閲忎笂闄�" />
+ </el-form-item>
+ <el-form-item
+ label="鎴愪氦瀹㈡埛鏄惁鍗犵敤鎷ユ湁瀹㈡埛鏁�"
+ v-if="formData.type === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
+ prop="dealCountEnabled"
+ >
+ <el-switch v-model="formData.dealCountEnabled" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
+import * as DeptApi from '@/api/system/dept'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as UserApi from '@/api/system/user'
+import { cloneDeep } from 'lodash-es'
+import { LimitConfType } from '@/api/crm/customer/limitConfig'
+import { aw } from '../../../../../dist-prod/assets/index-9eac537b'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ type: LimitConfType.CUSTOMER_LOCK_LIMIT, // 缁欎釜榛樿鍊硷紝閬垮厤 IDE 鎶ラ敊
+ userIds: undefined,
+ deptIds: undefined,
+ maxCount: undefined,
+ dealCountEnabled: false
+})
+const formRules = reactive({
+ type: [{ required: true, message: '瑙勫垯绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ maxCount: [{ required: true, message: '鏁伴噺涓婇檺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const deptTree = ref() // 閮ㄩ棬鏍戝舰缁撴瀯
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, limitConfType: LimitConfType, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await CustomerLimitConfigApi.getCustomerLimitConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ } else {
+ formData.value.type = limitConfType
+ }
+ // 鑾峰緱閮ㄩ棬鏍�
+ deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
+ // 鑾峰緱鐢ㄦ埛
+ userOptions.value = await UserApi.getSimpleUserList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as CustomerLimitConfigApi.CustomerLimitConfigVO
+ if (formType.value === 'create') {
+ await CustomerLimitConfigApi.createCustomerLimitConfig(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await CustomerLimitConfigApi.updateCustomerLimitConfig(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ type: LimitConfType.CUSTOMER_LOCK_LIMIT,
+ userIds: undefined,
+ deptIds: undefined,
+ maxCount: undefined,
+ dealCountEnabled: false
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue b/src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue
new file mode 100644
index 0000000..f5c488c
--- /dev/null
+++ b/src/views/crm/customer/limitConfig/CustomerLimitConfigList.vue
@@ -0,0 +1,150 @@
+<template>
+ <el-button plain @click="handleQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 鍒锋柊 </el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['crm:customer-limit-config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ class="mt-4"
+ >
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="瑙勫垯閫傜敤浜虹兢"
+ align="center"
+ :formatter="(row) => row.users?.map((user: any) => user.nickname).join('锛�')"
+ />
+ <el-table-column
+ label="瑙勫垯閫傜敤閮ㄩ棬"
+ align="center"
+ :formatter="(row) => row.depts?.map((dept: any) => dept.name).join('锛�')"
+ />
+ <el-table-column
+ :label="
+ confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT ? '鎷ユ湁瀹㈡埛鏁颁笂闄�' : '閿佸畾瀹㈡埛鏁颁笂闄�'
+ "
+ align="center"
+ prop="maxCount"
+ />
+ <el-table-column
+ v-if="confType === LimitConfType.CUSTOMER_QUANTITY_LIMIT"
+ label="鎴愪氦瀹㈡埛鏄惁鍗犵敤鎷ユ湁瀹㈡埛鏁�"
+ align="center"
+ prop="dealCountEnabled"
+ min-width="100"
+ >
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealCountEnabled" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="110" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['crm:customer-limit-config:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['crm:customer-limit-config:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CustomerLimitConfigForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as CustomerLimitConfigApi from '@/api/crm/customer/limitConfig'
+import CustomerLimitConfigForm from './CustomerLimitConfigForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { LimitConfType } from '@/api/crm/customer/limitConfig'
+
+defineOptions({ name: 'CustomerLimitConfigList' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const { confType } = defineProps<{ confType: LimitConfType }>()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: confType
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerLimitConfigApi.getCustomerLimitConfigPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, confType, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await CustomerLimitConfigApi.deleteCustomerLimitConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/customer/limitConfig/index.vue b/src/views/crm/customer/limitConfig/index.vue
new file mode 100644
index 0000000..01f3ef6
--- /dev/null
+++ b/src/views/crm/customer/limitConfig/index.vue
@@ -0,0 +1,22 @@
+<template>
+ <doc-alert title="銆愬鎴枫�戝鎴风鐞嗐�佸叕娴峰鎴�" url="https://doc.iocoder.cn/crm/customer/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs>
+ <el-tab-pane label="鎷ユ湁瀹㈡埛鏁伴檺鍒�">
+ <CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_QUANTITY_LIMIT" />
+ </el-tab-pane>
+ <el-tab-pane label="閿佸畾瀹㈡埛鏁伴檺鍒�">
+ <CustomerLimitConfigList :confType="LimitConfType.CUSTOMER_LOCK_LIMIT" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import CustomerLimitConfigList from './CustomerLimitConfigList.vue'
+import { LimitConfType } from '@/api/crm/customer/limitConfig'
+
+defineOptions({ name: 'CrmCustomerLimitConfig' })
+</script>
diff --git a/src/views/crm/customer/pool/CustomerDistributeForm.vue b/src/views/crm/customer/pool/CustomerDistributeForm.vue
new file mode 100644
index 0000000..5fd80a1
--- /dev/null
+++ b/src/views/crm/customer/pool/CustomerDistributeForm.vue
@@ -0,0 +1,85 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍒嗛厤瀹㈡埛">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select v-model="formData.ownerUserId" class="w-1/1">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as CustomerApi from '@/api/crm/customer'
+import * as UserApi from '@/api/system/user'
+import { distributeCustomer } from '@/api/crm/customer'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const formData = ref({
+ id: undefined,
+ ownerUserId: undefined
+})
+const formRules = reactive({
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ formData.value.id = id
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await CustomerApi.distributeCustomer([formData.value.id], formData.value.ownerUserId)
+ message.success('鍒嗛厤瀹㈡埛鎴愬姛')
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ ownerUserId: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/customer/pool/index.vue b/src/views/crm/customer/pool/index.vue
new file mode 100644
index 0000000..eab90e0
--- /dev/null
+++ b/src/views/crm/customer/pool/index.vue
@@ -0,0 +1,270 @@
+<template>
+ <doc-alert title="銆愬鎴枫�戝鎴风鐞嗐�佸叕娴峰鎴�" url="https://doc.iocoder.cn/crm/customer/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ墜鏈�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵�灞炶涓�" prop="industryId">
+ <el-select
+ v-model="queryParams.industryId"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鎵�灞炶涓�"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绾у埆" prop="level">
+ <el-select
+ v-model="queryParams.level"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨瀹㈡埛绾у埆"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鏉ユ簮" prop="source">
+ <el-select
+ v-model="queryParams.source"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨瀹㈡埛鏉ユ簮"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery(undefined)">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:customer:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" fixed="left" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐢佃瘽" align="center" prop="telephone" width="130" />
+ <el-table-column label="閭" align="center" prop="email" width="180" />
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="135">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column align="center" label="鎴愪氦鐘舵��" prop="dealStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.dealStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鏈�鍚庤窡杩涜褰�" prop="contactLastContent" width="200" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as CustomerApi from '@/api/crm/customer'
+
+defineOptions({ name: 'CrmCustomerPool' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ mobile: '',
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ sceneType: undefined,
+ pool: true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerApi.getCustomerPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ queryParams.value = {
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ mobile: '',
+ industryId: undefined,
+ level: undefined,
+ source: undefined,
+ sceneType: undefined,
+ pool: true
+ }
+ handleQuery()
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const { currentRoute, push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await CustomerApi.exportCustomer(queryParams.value)
+ download.excel(data, '瀹㈡埛鍏捣.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鐩戝惉璺敱鍙樺寲鏇存柊鍒楄〃 */
+watch(
+ () => currentRoute.value,
+ () => {
+ getList()
+ }
+)
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/customer/poolConfig/index.vue b/src/views/crm/customer/poolConfig/index.vue
new file mode 100644
index 0000000..28d58a6
--- /dev/null
+++ b/src/views/crm/customer/poolConfig/index.vue
@@ -0,0 +1,136 @@
+<template>
+ <doc-alert title="銆愬鎴枫�戝鎴风鐞嗐�佸叕娴峰鎴�" url="https://doc.iocoder.cn/crm/customer/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="160px"
+ v-loading="formLoading"
+ >
+ <el-card shadow="never">
+ <!-- 鎿嶄綔 -->
+ <template #header>
+ <div class="flex items-center justify-between">
+ <CardTitle title="瀹㈡埛鍏捣瑙勫垯璁剧疆" />
+ <el-button
+ type="primary"
+ @click="onSubmit"
+ v-hasPermi="['crm:customer-pool-config:update']"
+ >
+ 淇濆瓨
+ </el-button>
+ </div>
+ </template>
+ <!-- 琛ㄥ崟 -->
+ <el-form-item label="瀹㈡埛鍏捣瑙勫垯璁剧疆" prop="enabled">
+ <el-radio-group v-model="formData.enabled" @change="changeEnable" class="ml-4">
+ <el-radio :value="false" size="large">涓嶅惎鐢�</el-radio>
+ <el-radio :value="true" size="large">鍚敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <div v-if="formData.enabled">
+ <el-form-item>
+ <el-input-number class="mr-2" v-model="formData.contactExpireDays" />
+ 澶╀笉璺熻繘鎴�
+ <el-input-number class="mx-2" v-model="formData.dealExpireDays" />
+ 澶╂湭鎴愪氦
+ </el-form-item>
+ <el-form-item label="鎻愬墠鎻愰啋璁剧疆" prop="notifyEnabled">
+ <el-radio-group
+ v-model="formData.notifyEnabled"
+ @change="changeNotifyEnable"
+ class="ml-4"
+ >
+ <el-radio :value="false" size="large">涓嶆彁閱�</el-radio>
+ <el-radio :value="true" size="large">鎻愰啋</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <div v-if="formData.notifyEnabled">
+ <el-form-item>
+ 鎻愬墠 <el-input-number class="mx-2" v-model="formData.notifyDays" /> 澶╂彁閱�
+ </el-form-item>
+ </div>
+ </div>
+ </el-card>
+ </el-form>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as CustomerPoolConfigApi from '@/api/crm/customer/poolConfig'
+import { CardTitle } from '@/components/Card'
+
+defineOptions({ name: 'CrmCustomerPoolConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const formLoading = ref(false)
+const formData = ref({
+ enabled: false,
+ contactExpireDays: undefined,
+ dealExpireDays: undefined,
+ notifyEnabled: false,
+ notifyDays: undefined
+})
+const formRules = reactive({
+ enabled: [{ required: true, message: '鏄惁鍚敤瀹㈡埛鍏捣涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鑾峰彇閰嶇疆 */
+const getConfig = async () => {
+ try {
+ formLoading.value = true
+ const data = await CustomerPoolConfigApi.getCustomerPoolConfig()
+ if (data === null) {
+ return
+ }
+ formData.value = data
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鎻愪氦閰嶇疆 */
+const onSubmit = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as CustomerPoolConfigApi.CustomerPoolConfigVO
+ await CustomerPoolConfigApi.saveCustomerPoolConfig(data)
+ message.success(t('common.updateSuccess'))
+ await getConfig()
+ formLoading.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鏇存敼瀹㈡埛鍏捣瑙勫垯璁剧疆 */
+const changeEnable = () => {
+ if (!formData.value.enabled) {
+ formData.value.contactExpireDays = undefined
+ formData.value.dealExpireDays = undefined
+ formData.value.notifyEnabled = false
+ formData.value.notifyDays = undefined
+ }
+}
+
+/** 鏇存敼鎻愬墠鎻愰啋璁剧疆 */
+const changeNotifyEnable = () => {
+ if (!formData.value.notifyEnabled) {
+ formData.value.notifyDays = undefined
+ }
+}
+
+onMounted(() => {
+ getConfig()
+})
+</script>
diff --git a/src/views/crm/followup/FollowUpRecordForm.vue b/src/views/crm/followup/FollowUpRecordForm.vue
new file mode 100644
index 0000000..eb626f0
--- /dev/null
+++ b/src/views/crm/followup/FollowUpRecordForm.vue
@@ -0,0 +1,188 @@
+<!-- 璺熻繘璁板綍鐨勬坊鍔犺〃鍗曞脊绐� -->
+<template>
+ <Dialog v-model="dialogVisible" title="娣诲姞璺熻繘璁板綍" width="50%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="璺熻繘绫诲瀷" prop="type">
+ <el-select v-model="formData.type" placeholder="璇烽�夋嫨璺熻繘绫诲瀷">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_FOLLOW_UP_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嬫鑱旂郴鏃堕棿" prop="nextTime">
+ <el-date-picker
+ v-model="formData.nextTime"
+ placeholder="閫夋嫨涓嬫鑱旂郴鏃堕棿"
+ type="date"
+ value-format="x"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璺熻繘鍐呭" prop="content">
+ <el-input v-model="formData.content" :rows="3" type="textarea" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍥剧墖" prop="picUrls">
+ <UploadImgs v-model="formData.picUrls" class="min-w-80px" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="闄勪欢" prop="fileUrls">
+ <UploadFile v-model="formData.fileUrls" class="min-w-80px" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
+ <el-form-item label="鍏宠仈鑱旂郴浜�" prop="contactIds">
+ <el-button @click="handleOpenContact">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 娣诲姞鑱旂郴浜�
+ </el-button>
+ <FollowUpRecordContactForm :contacts="formData.contacts" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" v-if="formData.bizType == BizTypeEnum.CRM_CUSTOMER">
+ <el-form-item label="鍏宠仈鍟嗘満" prop="businessIds">
+ <el-button @click="handleOpenBusiness">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 娣诲姞鍟嗘満
+ </el-button>
+ <FollowUpRecordBusinessForm :businesses="formData.businesses" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+
+ <!-- 寮圭獥 -->
+ <ContactListModal
+ ref="contactTableSelectRef"
+ :customer-id="formData.bizId"
+ @success="handleAddContact"
+ />
+ <BusinessListModal
+ ref="businessTableSelectRef"
+ :customer-id="formData.bizId"
+ @success="handleAddBusiness"
+ />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup'
+import { BizTypeEnum } from '@/api/crm/permission'
+import FollowUpRecordBusinessForm from './components/FollowUpRecordBusinessForm.vue'
+import FollowUpRecordContactForm from './components/FollowUpRecordContactForm.vue'
+import BusinessListModal from '@/views/crm/business/components/BusinessListModal.vue'
+import * as BusinessApi from '@/api/crm/business'
+import ContactListModal from '@/views/crm/contact/components/ContactListModal.vue'
+import * as ContactApi from '@/api/crm/contact'
+
+defineOptions({ name: 'FollowUpRecordForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ bizType: undefined,
+ bizId: undefined,
+ businesses: [],
+ contacts: []
+})
+const formRules = reactive({
+ type: [{ required: true, message: '璺熻繘绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ content: [{ required: true, message: '璺熻繘鍐呭涓嶈兘涓虹┖', trigger: 'blur' }],
+ nextTime: [{ required: true, message: '涓嬫鑱旂郴鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (bizType: number, bizId: number) => {
+ dialogVisible.value = true
+ resetForm()
+ formData.value.bizType = bizType
+ formData.value.bizId = bizId
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = {
+ ...formData.value,
+ contactIds: formData.value.contacts.map((item) => item.id),
+ businessIds: formData.value.businesses.map((item) => item.id)
+ } as unknown as FollowUpRecordVO
+ await FollowUpRecordApi.createFollowUpRecord(data)
+ message.success(t('common.createSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鍏宠仈鑱旂郴浜� */
+const contactTableSelectRef = ref<InstanceType<typeof ContactListModal>>()
+const handleOpenContact = () => {
+ contactTableSelectRef.value?.open()
+}
+const handleAddContact = (contactId: [], newContacts: ContactApi.ContactVO[]) => {
+ newContacts.forEach((contact) => {
+ if (!formData.value.contacts.some((item) => item.id === contact.id)) {
+ formData.value.contacts.push(contact)
+ }
+ })
+}
+
+/** 鍏宠仈鍟嗘満 */
+const businessTableSelectRef = ref<InstanceType<typeof BusinessListModal>>()
+const handleOpenBusiness = () => {
+ businessTableSelectRef.value?.open()
+}
+const handleAddBusiness = (businessId: [], newBusinesses: BusinessApi.BusinessVO[]) => {
+ newBusinesses.forEach((business) => {
+ if (!formData.value.businesses.some((item) => item.id === business.id)) {
+ formData.value.businesses.push(business)
+ }
+ })
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formRef.value?.resetFields()
+ formData.value = {
+ bizId: undefined,
+ bizType: undefined,
+ businesses: [],
+ contacts: []
+ }
+}
+</script>
diff --git a/src/views/crm/followup/components/FollowUpRecordBusinessForm.vue b/src/views/crm/followup/components/FollowUpRecordBusinessForm.vue
new file mode 100644
index 0000000..620b5fb
--- /dev/null
+++ b/src/views/crm/followup/components/FollowUpRecordBusinessForm.vue
@@ -0,0 +1,42 @@
+<template>
+ <el-table :data="formData" :show-overflow-tooltip="true" :stripe="true" height="120">
+ <el-table-column label="鍟嗘満鍚嶇О" fixed="left" align="center" prop="name" />
+ <el-table-column
+ label="鍟嗘満閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="瀹㈡埛鍚嶇О" align="center" prop="customerName" />
+ <el-table-column label="鍟嗘満缁�" align="center" prop="statusTypeName" />
+ <el-table-column label="鍟嗘満闃舵" align="center" prop="statusName" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="80">
+ <template #default="{ $index }">
+ <el-button link type="danger" @click="handleDelete($index)"> 绉婚櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+
+<script lang="ts" setup>
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+const props = defineProps<{
+ businesses: undefined
+}>()
+const formData = ref([])
+
+/** 鍒濆鍖栧晢鏈哄垪琛� */
+watch(
+ () => props.businesses,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+</script>
diff --git a/src/views/crm/followup/components/FollowUpRecordContactForm.vue b/src/views/crm/followup/components/FollowUpRecordContactForm.vue
new file mode 100644
index 0000000..b3b5d3a
--- /dev/null
+++ b/src/views/crm/followup/components/FollowUpRecordContactForm.vue
@@ -0,0 +1,47 @@
+<template>
+ <el-table :data="contacts" :show-overflow-tooltip="true" :stripe="true" height="150">
+ <el-table-column label="濮撳悕" fixed="left" align="center" prop="name">
+ <template #default="scope">
+ <el-link type="primary" :underline="false" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬫満鍙�" align="center" prop="mobile" />
+ <el-table-column label="鑱屼綅" align="center" prop="post" />
+ <el-table-column label="鐩村睘涓婄骇" align="center" prop="parentName" />
+ <el-table-column label="鏄惁鍏抽敭鍐崇瓥浜�" align="center" prop="master" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="130">
+ <template #default="scope">
+ <el-button link type="danger" @click="handleDelete(scope.row.id)"> 绉婚櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+
+const props = defineProps<{
+ contacts: undefined
+}>()
+const formData = ref([])
+
+/** 鍒濆鍖栬仈绯讳汉鍒楄〃 */
+watch(
+ () => props.contacts,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+</script>
diff --git a/src/views/crm/followup/index.vue b/src/views/crm/followup/index.vue
new file mode 100644
index 0000000..720af86
--- /dev/null
+++ b/src/views/crm/followup/index.vue
@@ -0,0 +1,207 @@
+<!-- 鏌愪釜璁板綍鐨勮窡杩涜褰曞垪琛紝鐩墠涓昏鐢ㄤ簬 CRM 瀹㈡埛銆佸晢鏈虹瓑璇︽儏鐣岄潰 -->
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row class="mb-10px" justify="end">
+ <el-button @click="openForm">
+ <Icon class="mr-5px" icon="ep:edit" />
+ 鍐欒窡杩�
+ </el-button>
+ </el-row>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璺熻繘浜�" prop="creatorName" />
+ <el-table-column align="center" label="璺熻繘绫诲瀷" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_FOLLOW_UP_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="璺熻繘鍐呭" prop="content" />
+ <el-table-column label="鍥剧墖" align="center">
+ <template #default="scope">
+ <div v-if="scope.row.picUrls && scope.row.picUrls.length > 0" class="flex">
+ <el-image
+ v-for="(url, index) in scope.row.picUrls"
+ :key="index"
+ :src="url"
+ :preview-src-list="scope.row.picUrls"
+ class="w-10 h-10 mr-1"
+ :initial-index="index"
+ fit="cover"
+ preview-teleported
+ />
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="闄勪欢" align="center">
+ <template #default="scope">
+ <div v-if="scope.row.fileUrls && scope.row.fileUrls.length > 0" class="flex flex-col">
+ <el-link
+ v-for="(url, index) in scope.row.fileUrls"
+ :key="index"
+ :href="url"
+ type="primary"
+ target="_blank"
+ download
+ >
+ {{ getFileName(url) }}
+ </el-link>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="nextTime"
+ width="180px"
+ />
+ <el-table-column
+ align="center"
+ label="鍏宠仈鑱旂郴浜�"
+ prop="contactIds"
+ v-if="bizType === BizTypeEnum.CRM_CUSTOMER"
+ >
+ <template #default="scope">
+ <el-link
+ v-for="contact in scope.row.contacts"
+ :key="`key-${contact.id}`"
+ :underline="false"
+ type="primary"
+ @click="openContactDetail(contact.id)"
+ class="ml-5px"
+ >
+ {{ contact.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍏宠仈鍟嗘満"
+ prop="businessIds"
+ v-if="bizType === BizTypeEnum.CRM_CUSTOMER"
+ >
+ <template #default="scope">
+ <el-link
+ v-for="business in scope.row.businesses"
+ :key="`key-${business.id}`"
+ :underline="false"
+ type="primary"
+ @click="openBusinessDetail(business.id)"
+ class="ml-5px"
+ >
+ {{ business.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button link type="danger" @click="handleDelete(scope.row.id)"> 鍒犻櫎 </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <FollowUpRecordForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { FollowUpRecordApi, FollowUpRecordVO } from '@/api/crm/followup'
+import FollowUpRecordForm from './FollowUpRecordForm.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+
+/** 璺熻繘璁板綍鍒楄〃 */
+defineOptions({ name: 'FollowUpRecord' })
+
+const getFileName = (url: string) => {
+ if (!url) {
+ return ''
+ }
+ return url.substring(url.lastIndexOf('/') + 1)
+}
+
+const props = defineProps<{
+ bizType: number
+ bizId: number
+}>()
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<FollowUpRecordVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ bizType: 0,
+ bizId: 0
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FollowUpRecordApi.getFollowUpRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref<InstanceType<typeof FollowUpRecordForm>>()
+const openForm = () => {
+ formRef.value?.open(props.bizType, props.bizId)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await FollowUpRecordApi.deleteFollowUpRecord(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵撳紑鑱旂郴浜鸿鎯� */
+const { push } = useRouter()
+const openContactDetail = (id: number) => {
+ push({ name: 'CrmContactDetail', params: { id } })
+}
+
+/** 鎵撳紑鍟嗘満璇︽儏 */
+const openBusinessDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+watch(
+ () => props.bizId,
+ () => {
+ queryParams.bizType = props.bizType
+ queryParams.bizId = props.bizId
+ getList()
+ }
+)
+</script>
diff --git a/src/views/crm/permission/components/PermissionForm.vue b/src/views/crm/permission/components/PermissionForm.vue
new file mode 100644
index 0000000..632a347
--- /dev/null
+++ b/src/views/crm/permission/components/PermissionForm.vue
@@ -0,0 +1,137 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="30%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item v-if="formType === 'create'" label="閫夋嫨浜哄憳" prop="userId">
+ <el-select v-model="formData.userId">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏉冮檺绾у埆" prop="level">
+ <el-radio-group v-model="formData.level">
+ <template
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PERMISSION_LEVEL)"
+ :key="dict.value"
+ >
+ <el-radio v-if="dict.value != PermissionLevelEnum.OWNER" :value="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ </template>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="formType === 'create' && formData.bizType === BizTypeEnum.CRM_CUSTOMER"
+ label="鍚屾椂娣诲姞鑷�"
+ >
+ <el-checkbox-group v-model="formData.toBizTypes">
+ <el-checkbox :value="BizTypeEnum.CRM_CONTACT">鑱旂郴浜�</el-checkbox>
+ <el-checkbox :value="BizTypeEnum.CRM_BUSINESS">鍟嗘満</el-checkbox>
+ <el-checkbox :value="BizTypeEnum.CRM_CONTRACT">鍚堝悓</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/system/user'
+import * as PermissionApi from '@/api/crm/permission'
+import { BizTypeEnum, PermissionLevelEnum } from '@/api/crm/permission'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'CrmPermissionForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const formData = ref<PermissionApi.PermissionVO>({} as PermissionApi.PermissionVO)
+const formRules = reactive({
+ userId: [{ required: true, message: '浜哄憳涓嶈兘涓虹┖', trigger: 'blur' }],
+ level: [{ required: true, message: '鏉冮檺绾у埆涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: 'create' | 'update', bizType: number, bizId: number, ids?: number[]) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type) + '鍥㈤槦鎴愬憳'
+ formType.value = type
+ resetForm(bizType, bizId)
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (ids) {
+ formData.value.ids = ids
+ }
+}
+/** 鎵撳紑淇敼鏉冮檺寮圭獥 */
+const open0 = async (
+ type: 'create' | 'update',
+ bizType: number,
+ bizId: number,
+ id: number,
+ level: number
+) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type) + '鍥㈤槦鎴愬憳'
+ formType.value = type
+ resetForm(bizType, bizId)
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ formData.value.level = level
+ formData.value.ids = [id]
+}
+defineExpose({ open, open0 }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value
+ if (formType.value === 'create') {
+ await PermissionApi.createPermission(unref(data))
+ message.success(t('common.createSuccess'))
+ } else {
+ await PermissionApi.updatePermission(unref(data))
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = (bizType: number, bizId: number) => {
+ formRef.value?.resetFields()
+ formData.value = {} as PermissionApi.PermissionVO
+ formData.value = { ...formData.value, bizType, bizId }
+}
+onMounted(async () => {
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/crm/permission/components/PermissionList.vue b/src/views/crm/permission/components/PermissionList.vue
new file mode 100644
index 0000000..39c7aab
--- /dev/null
+++ b/src/views/crm/permission/components/PermissionList.vue
@@ -0,0 +1,206 @@
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row v-if="showAction" justify="end">
+ <el-button v-if="validateOwnerUser" type="primary" @click="openForm">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button v-if="validateOwnerUser" @click="handleUpdate">
+ <Icon class="mr-5px" icon="ep:edit" />
+ 缂栬緫
+ </el-button>
+ <el-button v-if="validateOwnerUser" @click="handleDelete">
+ <Icon class="mr-5px" icon="ep:delete" />
+ 绉婚櫎
+ </el-button>
+ <el-button v-if="!validateOwnerUser && list.length > 0" type="danger" @click="handleQuit">
+ 閫�鍑哄洟闃�
+ </el-button>
+ </el-row>
+ <!-- 鍥㈤槦鎴愬憳灞曠ず -->
+ <el-table
+ ref="elTableRef"
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ class="mt-20px"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column align="center" label="濮撳悕" prop="nickname" />
+ <el-table-column align="center" label="閮ㄩ棬" prop="deptName" />
+ <el-table-column align="center" label="宀椾綅" prop="postNames" />
+ <el-table-column align="center" label="鏉冮檺绾у埆" prop="level">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.CRM_PERMISSION_LEVEL" :value="row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column :formatter="dateFormatter" align="center" label="鍔犲叆鏃堕棿" prop="createTime" />
+ </el-table>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CrmPermissionForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import { ElTable } from 'element-plus'
+import * as PermissionApi from '@/api/crm/permission'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import CrmPermissionForm from './PermissionForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+defineOptions({ name: 'CrmPermissionList' })
+
+const message = useMessage() // 娑堟伅
+
+const props = defineProps<{
+ bizType: number // 妯″潡绫诲瀷
+ bizId: number | undefined // 妯″潡鏁版嵁缂栧彿
+ showAction: boolean //鏄惁灞曠ず鎿嶄綔鎸夐挳
+}>()
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<PermissionApi.PermissionVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const formData = ref({
+ ownerUserId: 0
+})
+const userStore = useUserStoreWithOut() // 鐢ㄦ埛淇℃伅缂撳瓨
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PermissionApi.getPermissionList({
+ bizType: props.bizType,
+ bizId: props.bizId
+ })
+ list.value = data
+ const permission = list.value.find(
+ (item) =>
+ item.userId === userStore.getUser.id &&
+ item.level === PermissionApi.PermissionLevelEnum.OWNER
+ )
+ if (permission) {
+ formData.value.ownerUserId = userStore.getUser.id
+ }
+ } finally {
+ loading.value = false
+ }
+}
+const multipleSelection = ref<PermissionApi.PermissionVO[]>([]) // 閫夋嫨鐨勫洟闃熸垚鍛�
+const elTableRef = ref<InstanceType<typeof ElTable>>()
+const handleSelectionChange = (val: PermissionApi.PermissionVO[]) => {
+ if (val.findIndex((item) => item.level === PermissionApi.PermissionLevelEnum.OWNER) !== -1) {
+ message.warning('涓嶈兘閫夋嫨璐熻矗浜猴紒')
+ elTableRef.value?.clearSelection()
+ return
+ }
+ multipleSelection.value = val
+}
+
+/** 缂栬緫鍥㈤槦鎴愬憳 */
+const formRef = ref<InstanceType<typeof CrmPermissionForm>>() // 鏉冮檺琛ㄥ崟 Ref
+const handleUpdate = () => {
+ if (multipleSelection.value?.length === 0) {
+ message.warning('璇峰厛閫夋嫨鍥㈤槦鎴愬憳鍚庢搷浣滐紒')
+ return
+ }
+ if (multipleSelection.value?.length > 1) {
+ message.warning('缂栬緫鍥㈤槦鎴愬憳鏃跺彧鑳介�夋嫨涓�涓紒')
+ return
+ }
+ formRef.value?.open0(
+ 'update',
+ props.bizType,
+ props.bizId!,
+ multipleSelection.value[0].id!,
+ multipleSelection.value[0].level
+ )
+}
+
+/** 绉婚櫎鍥㈤槦鎴愬憳 */
+const handleDelete = async () => {
+ if (multipleSelection.value?.length === 0) {
+ message.warning('璇峰厛閫夋嫨鍥㈤槦鎴愬憳鍚庢搷浣滐紒')
+ return
+ }
+ await message.delConfirm()
+ const ids = multipleSelection.value?.map((item) => item.id) as unknown as number[]
+ await PermissionApi.deletePermissionBatch(ids)
+ message.success('绉婚櫎鍥㈤槦鎴愬憳鎴愬姛锛�')
+ await getList()
+}
+
+/** 娣诲姞鍥㈤槦鎴愬憳 */
+const openForm = () => {
+ formRef.value?.open('create', props.bizType, props.bizId!)
+}
+
+// 鏍¢獙璐熻矗浜烘潈闄愬拰缂栬緫鏉冮檺
+const validateOwnerUser = ref(false)
+const validateWrite = ref(false)
+const isPool = ref(false)
+watch(
+ list,
+ (newArr) => {
+ isPool.value = false
+ if (newArr?.length > 0) {
+ isPool.value = !list.value.some(
+ (item) => item.level === PermissionApi.PermissionLevelEnum.OWNER
+ )
+ validateOwnerUser.value = false
+ validateWrite.value = false
+ const userId = userStore.getUser?.id
+ list.value
+ .filter((item) => item.userId === userId)
+ .forEach((item) => {
+ if (item.level === PermissionApi.PermissionLevelEnum.OWNER) {
+ validateOwnerUser.value = true
+ validateWrite.value = true
+ } else if (item.level === PermissionApi.PermissionLevelEnum.WRITE) {
+ validateWrite.value = true
+ }
+ })
+ } else {
+ isPool.value = true
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+defineExpose({ openForm, validateOwnerUser, validateWrite, isPool })
+const emits = defineEmits<{
+ (e: 'quitTeam'): void
+}>()
+/** 閫�鍑哄洟闃� */
+const handleQuit = async () => {
+ const permission = list.value.find(
+ (item) =>
+ item.userId === userStore.getUser.id && item.level === PermissionApi.PermissionLevelEnum.OWNER
+ )
+ if (permission) {
+ message.warning('璐熻矗浜轰笉鑳介��鍑哄洟闃燂紒')
+ return
+ }
+ const userPermission = list.value.find((item) => item.userId === userStore.getUser.id)
+ if (!userPermission) {
+ return
+ }
+ await PermissionApi.deleteSelfPermission(userPermission.id!)
+ message.success('閫�鍑哄洟闃熸垚鍛樻垚鍔燂紒')
+ emits('quitTeam')
+}
+
+watch(
+ () => props.bizId,
+ (bizId) => {
+ if (!bizId) {
+ return
+ }
+ getList()
+ },
+ { immediate: true, deep: true }
+)
+</script>
diff --git a/src/views/crm/permission/components/TransferForm.vue b/src/views/crm/permission/components/TransferForm.vue
new file mode 100644
index 0000000..43d5af7
--- /dev/null
+++ b/src/views/crm/permission/components/TransferForm.vue
@@ -0,0 +1,162 @@
+<!-- 杞Щ鏁版嵁鐨勮〃鍗曞脊绐楋紝鐩墠涓昏鐢ㄤ簬 CRM 瀹㈡埛銆佸晢鏈虹瓑璇︽儏鐣岄潰 -->
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="30%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="150px"
+ >
+ <el-form-item label="閫夋嫨鏂拌礋璐d汉" prop="newOwnerUserId">
+ <el-select v-model="formData.newOwnerUserId">
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑰佽礋璐d汉">
+ <el-radio-group v-model="oldOwnerHandler" @change="handleOwnerChange">
+ <el-radio :value="false" size="large">绉婚櫎</el-radio>
+ <el-radio :value="true" size="large">鍔犲叆鍥㈤槦</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="oldOwnerHandler" label="鑰佽礋璐d汉鏉冮檺绾у埆" prop="oldOwnerPermissionLevel">
+ <el-radio-group v-model="formData.oldOwnerPermissionLevel">
+ <template
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PERMISSION_LEVEL)"
+ :key="dict.value"
+ >
+ <el-radio v-if="dict.value != PermissionLevelEnum.OWNER" :value="dict.value">
+ {{ dict.label }}
+ </el-radio>
+ </template>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="bizType === BizTypeEnum.CRM_CUSTOMER" label="鍚屾椂杞Щ">
+ <el-checkbox-group v-model="formData.toBizTypes">
+ <el-checkbox :value="BizTypeEnum.CRM_CONTACT">鑱旂郴浜�</el-checkbox>
+ <el-checkbox :value="BizTypeEnum.CRM_BUSINESS">鍟嗘満</el-checkbox>
+ <el-checkbox :value="BizTypeEnum.CRM_CONTRACT">鍚堝悓</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/system/user'
+import * as BusinessApi from '@/api/crm/business'
+import * as ClueApi from '@/api/crm/clue'
+import * as ContactApi from '@/api/crm/contact'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { BizTypeEnum, PermissionLevelEnum, TransferReqVO } from '@/api/crm/permission'
+
+defineOptions({ name: 'CrmTransferForm' })
+
+const props = defineProps<{
+ bizType: number
+}>()
+
+const message = useMessage() // 娑堟伅寮圭獥
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const oldOwnerHandler = ref(false) // 鑰佽礋璐d汉鐨勫鐞嗘柟寮�
+const formData = ref<TransferReqVO>({} as TransferReqVO)
+const formRules = reactive({
+ newOwnerUserId: [{ required: true, message: '鏂拌礋璐d汉涓嶈兘涓虹┖', trigger: 'blur' }],
+ oldOwnerPermissionLevel: [
+ { required: true, message: '鑰佽礋璐d汉鍔犲叆鍥㈤槦鍚庣殑鏉冮檺绾у埆涓嶈兘涓虹┖', trigger: 'blur' }
+ ]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (bizId: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = getDialogTitle()
+ resetForm()
+ formData.value.id = bizId
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+// 鑰佽礋璐d汉璐熻矗鏂瑰紡
+const handleOwnerChange = (val: boolean) => {
+ if (!val) {
+ // 绉婚櫎鐨勮瘽鎻愪氦涓嶅甫 oldOwnerPermissionLevel 鍙傛暟
+ formData.value.oldOwnerPermissionLevel = undefined
+ }
+}
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value
+ await transfer(unref(data))
+ message.success(dialogTitle.value + '鎴愬姛')
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+const transfer = async (data: TransferReqVO) => {
+ switch (props.bizType) {
+ case BizTypeEnum.CRM_CLUE:
+ return await ClueApi.transferClue(data)
+ case BizTypeEnum.CRM_CUSTOMER:
+ return await CustomerApi.transferCustomer(data)
+ case BizTypeEnum.CRM_CONTACT:
+ return await ContactApi.transferContact(data)
+ case BizTypeEnum.CRM_BUSINESS:
+ return await BusinessApi.transferBusiness(data)
+ case BizTypeEnum.CRM_CONTRACT:
+ return await ContractApi.transferContract(data)
+ default:
+ message.error('銆愯浆绉诲け璐ャ�戞病鏈夎浆绉绘帴鍙�')
+ throw new Error('銆愯浆绉诲け璐ャ�戞病鏈夎浆绉绘帴鍙�')
+ }
+}
+const getDialogTitle = () => {
+ switch (props.bizType) {
+ case BizTypeEnum.CRM_CLUE:
+ return '绾跨储杞Щ'
+ case BizTypeEnum.CRM_CUSTOMER:
+ return '瀹㈡埛杞Щ'
+ case BizTypeEnum.CRM_CONTACT:
+ return '鑱旂郴浜鸿浆绉�'
+ case BizTypeEnum.CRM_BUSINESS:
+ return '鍟嗘満杞Щ'
+ case BizTypeEnum.CRM_CONTRACT:
+ return '鍚堝悓杞Щ'
+ default:
+ return '杞Щ'
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formRef.value?.resetFields()
+ formData.value = {} as TransferReqVO
+}
+onMounted(async () => {
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/crm/product/ProductForm.vue b/src/views/crm/product/ProductForm.vue
new file mode 100644
index 0000000..1bc5aac
--- /dev/null
+++ b/src/views/crm/product/ProductForm.vue
@@ -0,0 +1,212 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ placeholder="璇烽�夋嫨璐熻矗浜�"
+ :disabled="formData.id"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧绫诲瀷" prop="categoryId">
+ <el-cascader
+ v-model="formData.categoryId"
+ :options="productCategoryList"
+ :props="defaultProps"
+ class="w-1/1"
+ clearable
+ placeholder="璇烽�夋嫨浜у搧绫诲瀷"
+ filterable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍗曚綅" prop="unit">
+ <el-select v-model="formData.unit" class="w-1/1" placeholder="璇烽�夋嫨鍗曚綅">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PRODUCT_UNIT)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧缂栫爜" prop="no">
+ <el-input v-model="formData.no" placeholder="璇疯緭鍏ヤ骇鍝佺紪鐮�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠锋牸" prop="price">
+ <el-input-number
+ v-model="formData.price"
+ placeholder="璇疯緭鍏ヤ环鏍�"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-full!"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧鎻忚堪" prop="description">
+ <el-input v-model="formData.description" placeholder="璇疯緭鍏ヤ骇鍝佹弿杩�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓婃灦鐘舵��" prop="status">
+ <el-select v-model="formData.status" placeholder="璇烽�夋嫨鐘舵��" class="w-1/1">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PRODUCT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as ProductApi from '@/api/crm/product'
+import * as ProductCategoryApi from '@/api/crm/product/category'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { getSimpleUserList, UserVO } from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'CrmProductForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const userId = useUserStore().getUser.id // 褰撳墠鐧诲綍鐨勭紪鍙�
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ no: undefined,
+ unit: undefined,
+ price: Number(undefined),
+ status: undefined,
+ categoryId: undefined,
+ description: undefined,
+ ownerUserId: -1
+})
+const formRules = reactive({
+ name: [{ required: true, message: '浜у搧鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ no: [{ required: true, message: '浜у搧缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }],
+ categoryId: [{ required: true, message: '浜у搧鍒嗙被ID涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }],
+ price: [{ required: true, message: '浠锋牸涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductApi.getProduct(id)
+ } finally {
+ formLoading.value = false
+ }
+ } else {
+ formData.value.ownerUserId = userId
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductApi.ProductVO
+ if (formType.value === 'create') {
+ await ProductApi.createProduct(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductApi.updateProduct(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ no: undefined,
+ unit: undefined,
+ price: Number(undefined),
+ status: undefined,
+ categoryId: undefined,
+ description: undefined,
+ ownerUserId: -1
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鍒濆鍖� */
+const productCategoryList = ref<any[]>([]) // 浜у搧鍒嗙被鏍�
+const userList = ref<UserVO[]>([]) // 绯荤粺鐢ㄦ埛
+onMounted(async () => {
+ // 浜у搧鍒嗙被鏍�
+ const data = await ProductCategoryApi.getProductCategoryList({})
+ productCategoryList.value = handleTree(data, 'id', 'parentId')
+ // 绯荤粺鐢ㄦ埛鍒楄〃
+ userList.value = await getSimpleUserList()
+})
+</script>
diff --git a/src/views/crm/product/category/ProductCategoryForm.vue b/src/views/crm/product/category/ProductCategoryForm.vue
new file mode 100644
index 0000000..0373fc3
--- /dev/null
+++ b/src/views/crm/product/category/ProductCategoryForm.vue
@@ -0,0 +1,110 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鐖剁骇鍒嗙被" prop="parentId">
+ <el-select v-model="formData.parentId" placeholder="璇烽�夋嫨涓婄骇鍒嗙被">
+ <el-option :key="0" label="椤剁骇鍒嗙被" :value="0" />
+ <el-option
+ v-for="item in productCategoryList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as ProductCategoryApi from '@/api/crm/product/category'
+
+defineOptions({ name: 'CrmProductCategoryForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ parentId: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ parentId: [{ required: true, message: '鐖剁骇鍒嗙被涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const productCategoryList = ref<any[]>([]) // 浜у搧鍒嗙被鏍�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductCategoryApi.getProductCategory(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鍒嗙被鏍�
+ productCategoryList.value = await ProductCategoryApi.getProductCategoryList({ parentId: 0 })
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductCategoryApi.ProductCategoryVO
+ if (formType.value === 'create') {
+ await ProductCategoryApi.createProductCategory(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductCategoryApi.updateProductCategory(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ parentId: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/crm/product/category/index.vue b/src/views/crm/product/category/index.vue
new file mode 100644
index 0000000..631c170
--- /dev/null
+++ b/src/views/crm/product/category/index.vue
@@ -0,0 +1,139 @@
+<template>
+ <doc-alert title="銆愪骇鍝併�戜骇鍝佺鐞嗐�佷骇鍝佸垎绫�" url="https://doc.iocoder.cn/crm/product/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['crm:product-category:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
+ <el-table-column label="鍒嗙被缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍒嗙被鍚嶇О" align="center" prop="name" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['crm:product-category:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['crm:product-category:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductCategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProductCategoryApi from '@/api/crm/product/category'
+import ProductCategoryForm from './ProductCategoryForm.vue'
+import { handleTree } from '@/utils/tree'
+
+defineOptions({ name: 'CrmProductCategory' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ name: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductCategoryApi.getProductCategoryList(queryParams)
+ list.value = handleTree(data, 'id', 'parentId')
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductCategoryApi.deleteProductCategory(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/product/detail/ProductDetailsHeader.vue b/src/views/crm/product/detail/ProductDetailsHeader.vue
new file mode 100644
index 0000000..11286d6
--- /dev/null
+++ b/src/views/crm/product/detail/ProductDetailsHeader.vue
@@ -0,0 +1,46 @@
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ product.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <el-button @click="openForm('update', product.id)" v-hasPermi="['crm:product:update']">
+ 缂栬緫
+ </el-button>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="浜у搧绫诲埆">{{ product.categoryName }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍗曚綅">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="product.unit" />
+ </el-descriptions-item>
+ <el-descriptions-item label="浜у搧浠锋牸">
+ {{ erpPriceInputFormatter(product.price) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item label="浜у搧缂栫爜">{{ product.no }}</el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductForm ref="formRef" @success="emit('refresh')" />
+</template>
+<script setup lang="ts">
+import ProductForm from '@/views/crm/product/ProductForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceInputFormatter } from '@/utils'
+import * as ProductApi from '@/api/crm/product'
+
+// 鎿嶄綔淇敼
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+const { product } = defineProps<{ product: ProductApi.ProductVO }>()
+</script>
diff --git a/src/views/crm/product/detail/ProductDetailsInfo.vue b/src/views/crm/product/detail/ProductDetailsInfo.vue
new file mode 100644
index 0000000..52a11e9
--- /dev/null
+++ b/src/views/crm/product/detail/ProductDetailsInfo.vue
@@ -0,0 +1,38 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ product.name }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧缂栫爜">{{ product.no }}</el-descriptions-item>
+ <el-descriptions-item label="浠锋牸">
+ {{ erpPriceInputFormatter(product.price) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item label="浜у搧鎻忚堪">{{ product.description }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧绫诲瀷">{{ product.categoryName }}</el-descriptions-item>
+ <el-descriptions-item label="鏄惁涓婁笅鏋�">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="product.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="product.unit" />
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import * as ProductApi from '@/api/crm/product'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { product } = defineProps<{
+ product: ProductApi.ProductVO
+}>()
+
+// 灞曠ず鐨勬姌鍙犻潰鏉�
+const activeNames = ref(['basicInfo'])
+</script>
diff --git a/src/views/crm/product/detail/index.vue b/src/views/crm/product/detail/index.vue
new file mode 100644
index 0000000..65fdd0f
--- /dev/null
+++ b/src/views/crm/product/detail/index.vue
@@ -0,0 +1,66 @@
+<template>
+ <ProductDetailsHeader :loading="loading" :product="product" @refresh="getProductData(id)" />
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璇︾粏璧勬枡">
+ <ProductDetailsInfo :product="product" />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { OperateLogVO } from '@/api/system/operatelog'
+import * as ProductApi from '@/api/crm/product'
+import ProductDetailsHeader from '@/views/crm/product/detail/ProductDetailsHeader.vue'
+import ProductDetailsInfo from '@/views/crm/product/detail/ProductDetailsInfo.vue'
+import { BizTypeEnum } from '@/api/crm/permission'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+
+defineOptions({ name: 'CrmProductDetail' })
+
+const route = useRoute()
+const message = useMessage()
+const id = route.params.id // 缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) // 璇︽儏
+
+/** 鑾峰彇璇︽儏 */
+const getProductData = async (id: number) => {
+ loading.value = true
+ try {
+ product.value = await ProductApi.getProduct(id)
+ await getOperateLog(id)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async (productId: number) => {
+ if (!productId) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_PRODUCT,
+ bizId: productId
+ })
+ logList.value = data.list
+}
+
+/** 鍒濆鍖� */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+onMounted(async () => {
+ if (!id) {
+ message.warning('鍙傛暟閿欒锛屼骇鍝佷笉鑳戒负绌猴紒')
+ delView(unref(currentRoute))
+ return
+ }
+ await getProductData(id)
+})
+</script>
diff --git a/src/views/crm/product/index.vue b/src/views/crm/product/index.vue
new file mode 100644
index 0000000..5d656df
--- /dev/null
+++ b/src/views/crm/product/index.vue
@@ -0,0 +1,230 @@
+<template>
+ <doc-alert title="銆愪骇鍝併�戜骇鍝佺鐞嗐�佷骇鍝佸垎绫�" url="https://doc.iocoder.cn/crm/product/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="浜у搧鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PRODUCT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 鎼滅储 </el-button>
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆 </el-button>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['crm:product:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['crm:product:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="浜у搧鍚嶇О" align="center" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧绫诲瀷" align="center" prop="categoryName" width="160" />
+ <el-table-column label="浜у搧鍗曚綅" align="center" prop="unit">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_UNIT" :value="scope.row.unit" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧缂栫爜" align="center" prop="no" />
+ <el-table-column
+ label="浠锋牸锛堝厓锛�"
+ align="center"
+ prop="price"
+ :formatter="erpPriceTableColumnFormatter"
+ width="100"
+ />
+ <el-table-column label="浜у搧鎻忚堪" align="center" prop="description" width="150" />
+ <el-table-column label="涓婃灦鐘舵��" align="center" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璐熻矗浜�" align="center" prop="ownerUserName" width="120" />
+ <el-table-column
+ label="鏇存柊鏃堕棿"
+ align="center"
+ prop="updateTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" width="120" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="160">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['crm:product:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['crm:product:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ProductApi from '@/api/crm/product'
+import ProductForm from './ProductForm.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'CrmProduct' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductApi.getProductPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鎵撳紑璇︽儏 */
+const { currentRoute, push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmProductDetail', params: { id } })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductApi.deleteProduct(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ProductApi.exportProduct(queryParams)
+ download.excel(data, '浜у搧.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 婵�娲绘椂 */
+onActivated(() => {
+ getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/crm/receivable/ReceivableForm.vue b/src/views/crm/receivable/ReceivableForm.vue
new file mode 100644
index 0000000..61fac08
--- /dev/null
+++ b/src/views/crm/receivable/ReceivableForm.vue
@@ -0,0 +1,294 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍥炴缂栧彿" prop="no">
+ <el-input v-model="formData.no" disabled placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ filterable
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ @change="handleCustomerChange"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍚堝悓鍚嶇О" prop="contractId">
+ <el-select
+ v-model="formData.contractId"
+ :disabled="formType !== 'create' || !formData.customerId"
+ class="w-1/1"
+ filterable
+ placeholder="璇烽�夋嫨鍚堝悓"
+ @change="handleContractChange"
+ >
+ <el-option
+ v-for="data in contractList"
+ :key="data.id"
+ :disabled="data.auditStatus !== 20"
+ :label="data.name"
+ :value="data.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍥炴鏈熸暟" prop="planId">
+ <el-select
+ v-model="formData.planId"
+ :disabled="formType !== 'create' || !formData.contractId"
+ class="!w-1/1"
+ placeholder="璇烽�夋嫨鍥炴鏈熸暟"
+ @change="handleReceivablePlanChange"
+ >
+ <el-option
+ v-for="data in receivablePlanList"
+ :key="data.id"
+ :disabled="data.receivableId"
+ :label="'绗� ' + data.period + ' 鏈�'"
+ :value="data.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍥炴鏂瑰紡" prop="returnType">
+ <el-select v-model="formData.returnType" class="w-1/1" placeholder="璇烽�夋嫨鍥炴鏂瑰紡">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍥炴閲戦" prop="price">
+ <el-input-number
+ v-model="formData.price"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ controls-position="right"
+ placeholder="璇疯緭鍏ュ洖娆鹃噾棰�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍥炴鏃ユ湡" prop="returnTime">
+ <el-date-picker
+ v-model="formData.returnTime"
+ placeholder="閫夋嫨鍥炴鏃ユ湡"
+ type="date"
+ value-format="x"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import * as ReceivableApi from '@/api/crm/receivable'
+import { ReceivableVO } from '@/api/crm/receivable'
+import * as UserApi from '@/api/system/user'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<ReceivableApi.ReceivableVO>({} as ReceivableApi.ReceivableVO)
+const formRules = reactive({
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ contractId: [{ required: true, message: '鍚堝悓涓嶈兘涓虹┖', trigger: 'blur' }],
+ returnTime: [{ required: true, message: '鍥炴鏃ユ湡涓嶈兘涓虹┖', trigger: 'blur' }],
+ price: [{ required: true, message: '鍥炴閲戦涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const contractList = ref<ContractApi.ContractVO[]>([]) // 鍚堝悓鍒楄〃
+const receivablePlanList = ref<ReceivablePlanApi.ReceivablePlanVO[]>([]) // 鍥炴璁″垝鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (
+ type: string,
+ id?: number,
+ receivablePlan?: ReceivablePlanApi.ReceivablePlanVO
+) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = (await ReceivableApi.getReceivable(id)) as ReceivableVO
+ formData.value = data
+ await handleCustomerChange(data.customerId!)
+ formData.value.contractId = data?.contract?.id
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+ // 浠庡洖娆捐鍒掑垱寤哄洖娆�
+ if (receivablePlan) {
+ formData.value.customerId = receivablePlan.customerId
+ await handleCustomerChange(receivablePlan.customerId)
+ formData.value.contractId = receivablePlan.contractId
+ await handleContractChange(receivablePlan.contractId)
+ if (receivablePlan.id) {
+ formData.value.planId = receivablePlan.id
+ formData.value.price = receivablePlan.price
+ formData.value.returnType = receivablePlan.returnType
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ReceivableApi.ReceivableVO
+ if (formType.value === 'create') {
+ await ReceivableApi.createReceivable(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ReceivableApi.updateReceivable(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {} as ReceivableApi.ReceivableVO
+ formRef.value?.resetFields()
+}
+
+/** 澶勭悊鍒囨崲瀹㈡埛 */
+const handleCustomerChange = async (customerId: number) => {
+ // 閲嶇疆鍚堝悓缂栧彿
+ formData.value.contractId = undefined
+ // 鑾峰緱鍚堝悓鍒楄〃
+ if (customerId) {
+ contractList.value = []
+ contractList.value = await ContractApi.getContractSimpleList(customerId)
+ }
+}
+
+/** 澶勭悊鍒囨崲鍚堝悓 */
+const handleContractChange = async (contractId: number) => {
+ // 閲嶇疆鍥炴璁″垝缂栧彿
+ formData.value.planId = undefined
+ if (contractId) {
+ // 鑾峰緱鍥炴璁″垝鍒楄〃
+ receivablePlanList.value = []
+ receivablePlanList.value = await ReceivablePlanApi.getReceivablePlanSimpleList(
+ formData.value.customerId!,
+ contractId
+ )
+ // 璁剧疆閲戦
+ const contract = contractList.value.find((item) => item.id === contractId)
+ if (contract) {
+ formData.value.price = contract.totalPrice - contract.totalReceivablePrice
+ }
+ }
+}
+
+/** 澶勭悊鍒囨崲鍥炴璁″垝 */
+const handleReceivablePlanChange = (planId: number) => {
+ if (!planId) {
+ return
+ }
+ const receivablePlan = receivablePlanList.value.find((item) => item.id === planId)
+ if (!receivablePlan) {
+ return
+ }
+ formData.value.price = receivablePlan.price
+ formData.value.returnType = receivablePlan.returnType
+}
+</script>
diff --git a/src/views/crm/receivable/components/ReceivableList.vue b/src/views/crm/receivable/components/ReceivableList.vue
new file mode 100644
index 0000000..67287ea
--- /dev/null
+++ b/src/views/crm/receivable/components/ReceivableList.vue
@@ -0,0 +1,164 @@
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row justify="end">
+ <el-button @click="openForm('create')">
+ <Icon class="mr-5px" icon="icon-park:income-one" />
+ 鍒涘缓鍥炴
+ </el-button>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="鍥炴缂栧彿" prop="no" />
+ <el-table-column align="center" label="瀹㈡埛" prop="customerName" />
+ <el-table-column align="center" label="鍚堝悓" prop="contract.no" />
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="鍥炴鏃ユ湡"
+ prop="returnTime"
+ width="150px"
+ />
+ <el-table-column align="center" label="鍥炴鏂瑰紡" prop="returnType" width="130px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="鍥炴閲戦(鍏�)"
+ prop="price"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" />
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="130px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:receivable:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <ReceivableForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import * as ReceivableApi from '@/api/crm/receivable'
+import ReceivableForm from './../ReceivableForm.vue'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'CrmReceivableList' })
+const props = defineProps<{
+ customerId?: number // 瀹㈡埛缂栧彿
+ contractId?: number // 鍚堝悓缂栧彿
+}>()
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ customerId: undefined as unknown, // 鍏佽 undefined + number
+ contractId: undefined as unknown // 鍏佽 undefined + number
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ if (props.customerId && !props.contractId) {
+ queryParams.customerId = props.customerId
+ } else if (props.customerId && props.contractId) {
+ // 濡傛灉鏄悎鍚岀殑璇濆鎴风紪鍙蜂篃闇�瑕佸甫涓婂洜涓烘潈闄愬熀浜庡鎴�
+ queryParams.customerId = props.customerId
+ queryParams.contractId = props.contractId
+ }
+ const data = await ReceivableApi.getReceivablePageByCustomer(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ // 缃┖鍙傛暟
+ queryParams.customerId = undefined
+ queryParams.contractId = undefined
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id, {
+ customerId: props.customerId,
+ contractId: props.contractId
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ReceivableApi.deleteReceivable(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 浠庡洖娆捐鍒掑垱寤哄洖娆� */
+const createReceivable = (planData: any) => {
+ const data = planData as unknown as ReceivablePlanApi.ReceivablePlanVO
+ formRef.value.open('create', undefined, data)
+}
+defineExpose({ createReceivable })
+
+/** 鐩戝惉鎵撳紑鐨� customerId + contractId锛屼粠鑰屽姞杞芥渶鏂扮殑鍒楄〃 */
+watch(
+ () => [props.customerId, props.contractId],
+ (newVal) => {
+ // 淇濊瘉鑷冲皯瀹㈡埛缂栧彿鏈夊��
+ if (!newVal[0]) {
+ return
+ }
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+</script>
diff --git a/src/views/crm/receivable/detail/ReceivableDetailsHeader.vue b/src/views/crm/receivable/detail/ReceivableDetailsHeader.vue
new file mode 100644
index 0000000..62201de
--- /dev/null
+++ b/src/views/crm/receivable/detail/ReceivableDetailsHeader.vue
@@ -0,0 +1,43 @@
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ receivable.no }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">
+ {{ receivable.customerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓閲戦">
+ {{ erpPriceInputFormatter(receivable.contract?.totalPrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥炴鏃ユ湡">
+ {{ formatDate(receivable.returnTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥炴閲戦">
+ {{ erpPriceInputFormatter(receivable.price) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璐熻矗浜�">
+ {{ receivable.ownerUserName }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ReceivableApi from '@/api/crm/receivable'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivable } = defineProps<{ receivable: ReceivableApi.ReceivableVO }>()
+</script>
diff --git a/src/views/crm/receivable/detail/ReceivableDetailsInfo.vue b/src/views/crm/receivable/detail/ReceivableDetailsInfo.vue
new file mode 100644
index 0000000..003029f
--- /dev/null
+++ b/src/views/crm/receivable/detail/ReceivableDetailsInfo.vue
@@ -0,0 +1,62 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="鍥炴缂栧彿">{{ receivable.no }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">
+ {{ receivable.customerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓缂栧彿">
+ {{ receivable.contract?.no }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥炴鏃ユ湡">
+ {{ formatDate(receivable.returnTime, 'YYYY-MM-DD') }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥炴閲戦">
+ {{ erpPriceInputFormatter(receivable.price) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥炴鏂瑰紡">
+ <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="receivable.returnType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ receivable.remark }}</el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">
+ {{ receivable.ownerUserName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">
+ {{ receivable.creatorName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(receivable.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(receivable.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ReceivableApi from '@/api/crm/receivable'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivable } = defineProps<{
+ receivable: ReceivableApi.ReceivableVO
+}>()
+
+// 灞曠ず鐨勬姌鍙犻潰鏉�
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>
diff --git a/src/views/crm/receivable/detail/index.vue b/src/views/crm/receivable/detail/index.vue
new file mode 100644
index 0000000..3603572
--- /dev/null
+++ b/src/views/crm/receivable/detail/index.vue
@@ -0,0 +1,100 @@
+<template>
+ <ReceivableDetailsHeader v-loading="loading" :receivable="receivable">
+ <el-button v-if="permissionListRef?.validateWrite" @click="openForm('update', receivable.id)">
+ 缂栬緫
+ </el-button>
+ </ReceivableDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璇︾粏璧勬枡">
+ <ReceivableDetailsInfo :receivable="receivable" />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="receivable.id!"
+ :biz-type="BizTypeEnum.CRM_RECEIVABLE"
+ :show-action="true"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ReceivableForm ref="formRef" @success="getReceivable(receivable.id)" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ReceivableApi from '@/api/crm/receivable'
+import ReceivableDetailsHeader from './ReceivableDetailsHeader.vue'
+import ReceivableDetailsInfo from './ReceivableDetailsInfo.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 鍥㈤槦鎴愬憳鍒楄〃锛堟潈闄愶級
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogVO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
+
+defineOptions({ name: 'CrmReceivablePlanDetail' })
+const props = defineProps<{ id?: number }>()
+
+const route = useRoute()
+const message = useMessage()
+const receivableId = ref(0) // 鍥炴缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const receivable = ref<ReceivableApi.ReceivableVO>({} as ReceivableApi.ReceivableVO) // 鍥炴璇︽儏
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 鑾峰彇璇︽儏 */
+const getReceivable = async (id: number) => {
+ loading.value = true
+ try {
+ receivable.value = await ReceivableApi.getReceivable(id)
+ await getOperateLog(id)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 缂栬緫 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async (receivableId: number) => {
+ if (!receivableId) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_RECEIVABLE,
+ bizId: receivableId
+ })
+ logList.value = data.list
+}
+
+/** 鍏抽棴绐楀彛 */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+const close = () => {
+ delView(unref(currentRoute))
+}
+
+/** 鍒濆鍖� */
+const { params } = useRoute()
+onMounted(async () => {
+ const id = props.id || route.params.id
+ if (!id) {
+ message.warning('鍙傛暟閿欒锛屽洖娆句笉鑳戒负绌猴紒')
+ close()
+ return
+ }
+ receivableId.value = id
+ await getReceivable(receivableId.value)
+})
+</script>
diff --git a/src/views/crm/receivable/index.vue b/src/views/crm/receivable/index.vue
new file mode 100644
index 0000000..6928942
--- /dev/null
+++ b/src/views/crm/receivable/index.vue
@@ -0,0 +1,335 @@
+<template>
+ <doc-alert title="銆愬洖娆俱�戝洖娆剧鐞嗐�佸洖娆捐鍒�" url="https://doc.iocoder.cn/crm/receivable/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍥炴缂栧彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ洖娆剧紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ class="!w-240px"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ @keyup.enter="handleQuery"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="鎴戝弬涓庣殑" name="2" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="鍥炴缂栧彿" prop="no" width="180">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.no }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍚堝悓缂栧彿" prop="contractNo" width="180">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openContractDetail(scope.row.contractId)"
+ >
+ {{ scope.row.contract.no }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="鍥炴鏃ユ湡"
+ prop="returnTime"
+ width="150px"
+ />
+ <el-table-column
+ align="center"
+ label="鍥炴閲戦(鍏�)"
+ prop="price"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column align="center" label="鍥炴鏂瑰紡" prop="returnType" width="130px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ align="center"
+ label="鍚堝悓閲戦锛堝厓锛�"
+ prop="contract.totalPrice"
+ width="140"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="120" />
+ <el-table-column align="center" fixed="right" label="鍥炴鐘舵��" prop="auditStatus" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_AUDIT_STATUS" :value="scope.row.auditStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="180px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:receivable:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="scope.row.auditStatus === 0"
+ v-hasPermi="['crm:receivable:update']"
+ link
+ type="primary"
+ @click="handleSubmit(scope.row)"
+ >
+ 鎻愪氦瀹℃牳
+ </el-button>
+ <el-button
+ v-else
+ v-hasPermi="['crm:receivable:update']"
+ link
+ type="primary"
+ @click="handleProcessDetail(scope.row)"
+ >
+ 鏌ョ湅瀹℃壒
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ReceivableForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ReceivableApi from '@/api/crm/receivable'
+import ReceivableForm from './ReceivableForm.vue'
+import * as CustomerApi from '@/api/crm/customer'
+import { TabsPaneContext } from 'element-plus'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'Receivable' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ no: undefined,
+ customerId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName
+ handleQuery()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ReceivableApi.getReceivablePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ReceivableApi.deleteReceivable(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎻愪氦瀹℃牳 **/
+const handleSubmit = async (row: ReceivableApi.ReceivableVO) => {
+ await message.confirm(`鎮ㄧ‘瀹氭彁浜ょ紪鍙蜂负銆�${row.no}銆戠殑鍥炴瀹℃牳鍚楋紵`)
+ await ReceivableApi.submitReceivable(row.id)
+ message.success('鎻愪氦瀹℃牳鎴愬姛锛�')
+ await getList()
+}
+
+/** 鏌ョ湅瀹℃壒 */
+const handleProcessDetail = (row: ReceivableApi.ReceivableVO) => {
+ push({ name: 'BpmProcessInstanceDetail', query: { id: row.processInstanceId } })
+}
+
+/** 鎵撳紑鍥炴璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmReceivableDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鎵撳紑鍚堝悓璇︽儏 */
+const openContractDetail = (id: number) => {
+ push({ name: 'CrmContractDetail', params: { id } })
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ReceivableApi.exportReceivable(queryParams)
+ download.excel(data, '鍥炴.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+})
+</script>
diff --git a/src/views/crm/receivable/plan/ReceivablePlanForm.vue b/src/views/crm/receivable/plan/ReceivablePlanForm.vue
new file mode 100644
index 0000000..f321592
--- /dev/null
+++ b/src/views/crm/receivable/plan/ReceivablePlanForm.vue
@@ -0,0 +1,240 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="110px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="杩樻鏈熸暟" prop="period">
+ <el-input v-model="formData.period" disabled placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="ownerUserId">
+ <el-select
+ v-model="formData.ownerUserId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ :disabled="formType !== 'create'"
+ class="w-1/1"
+ filterable
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ @change="handleCustomerChange"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍚堝悓鍚嶇О" prop="contractId">
+ <el-select
+ v-model="formData.contractId"
+ :disabled="formType !== 'create' || !formData.customerId"
+ class="w-1/1"
+ filterable
+ placeholder="璇烽�夋嫨鍚堝悓"
+ >
+ <el-option
+ v-for="data in contractList"
+ :key="data.id"
+ :label="data.name"
+ :value="data.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="璁″垝鍥炴閲戦" prop="price">
+ <el-input-number
+ v-model="formData.price"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ controls-position="right"
+ placeholder="璇疯緭鍏ヨ鍒掑洖娆鹃噾棰�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁″垝鍥炴鏃ユ湡" prop="returnTime">
+ <el-date-picker
+ v-model="formData.returnTime"
+ placeholder="閫夋嫨璁″垝鍥炴鏃ユ湡"
+ type="date"
+ value-format="x"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎻愬墠鍑犲ぉ鎻愰啋" prop="remindDays">
+ <el-input-number
+ v-model="formData.remindDays"
+ class="!w-100%"
+ controls-position="right"
+ placeholder="璇疯緭鍏ユ彁鍓嶅嚑澶╂彁閱�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍥炴鏂瑰紡" prop="returnType">
+ <el-select v-model="formData.returnType" class="w-1/1" placeholder="璇烽�夋嫨鍥炴鏂瑰紡">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import * as UserApi from '@/api/system/user'
+import * as CustomerApi from '@/api/crm/customer'
+import * as ContractApi from '@/api/crm/contract'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { cloneDeep } from 'lodash-es'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const userOptions = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<ReceivablePlanApi.ReceivablePlanVO>({} as ReceivablePlanApi.ReceivablePlanVO)
+const formRules = reactive({
+ price: [{ required: true, message: '璁″垝鍥炴閲戦涓嶈兘涓虹┖', trigger: 'blur' }],
+ returnTime: [{ required: true, message: '璁″垝鍥炴鏃ユ湡涓嶈兘涓虹┖', trigger: 'blur' }],
+ customerId: [{ required: true, message: '瀹㈡埛缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ contractId: [{ required: true, message: '鍚堝悓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ ownerUserId: [{ required: true, message: '璐熻矗浜轰笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const contractList = ref<ContractApi.ContractVO[]>([]) // 鍚堝悓鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, customerId?: number, contractId?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = await ReceivablePlanApi.getReceivablePlan(id)
+ formData.value = cloneDeep(data)
+ await handleCustomerChange(data.customerId!)
+ formData.value.contractId = data?.contractId
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userOptions.value = await UserApi.getSimpleUserList()
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 榛樿鏂板缓鏃堕�変腑鑷繁
+ if (formType.value === 'create') {
+ formData.value.ownerUserId = useUserStore().getUser.id
+ }
+ // 璁剧疆 customerId 鍜� contractId 榛樿鍊�
+ if (customerId) {
+ formData.value.customerId = customerId
+ await handleCustomerChange(customerId)
+ }
+ if (contractId) {
+ formData.value.contractId = contractId
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ReceivablePlanApi.ReceivablePlanVO
+ if (formType.value === 'create') {
+ await ReceivablePlanApi.createReceivablePlan(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ReceivablePlanApi.updateReceivablePlan(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {} as ReceivablePlanApi.ReceivablePlanVO
+ formRef.value?.resetFields()
+}
+
+/** 澶勭悊鍒囨崲瀹㈡埛 */
+const handleCustomerChange = async (customerId: number) => {
+ // 閲嶇疆鍚堝悓缂栧彿
+ formData.value.contractId = undefined
+ // 鑾峰緱鍚堝悓鍒楄〃
+ if (customerId) {
+ contractList.value = []
+ contractList.value = await ContractApi.getContractSimpleList(customerId)
+ }
+}
+</script>
diff --git a/src/views/crm/receivable/plan/components/ReceivablePlanList.vue b/src/views/crm/receivable/plan/components/ReceivablePlanList.vue
new file mode 100644
index 0000000..3b80526
--- /dev/null
+++ b/src/views/crm/receivable/plan/components/ReceivablePlanList.vue
@@ -0,0 +1,173 @@
+<template>
+ <!-- 鎿嶄綔鏍� -->
+ <el-row justify="end">
+ <el-button @click="openForm('create', undefined)">
+ <Icon class="mr-5px" icon="icon-park:income" />
+ 鍒涘缓鍥炴璁″垝
+ </el-button>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap class="mt-10px">
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="瀹㈡埛鍚嶇О" prop="customerName" width="150px" />
+ <el-table-column align="center" label="鍚堝悓缂栧彿" prop="contractNo" width="200px" />
+ <el-table-column align="center" label="鏈熸暟" prop="period" />
+ <el-table-column
+ align="center"
+ label="璁″垝鍥炴(鍏�)"
+ prop="price"
+ width="120"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="璁″垝鍥炴鏃ユ湡"
+ prop="returnTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎻愬墠鍑犲ぉ鎻愰啋" prop="remindDays" width="150" />
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="鎻愰啋鏃ユ湡"
+ prop="remindTime"
+ width="180px"
+ />
+ <el-table-column label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="200px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:receivable:create']"
+ link
+ type="primary"
+ @click="createReceivable(scope.row)"
+ :disabled="scope.row.receivableId"
+ >
+ 鍒涘缓鍥炴
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable-plan:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable-plan:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <ReceivableForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import ReceivableForm from './../ReceivablePlanForm.vue'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'CrmReceivablePlanList' })
+const props = defineProps<{
+ customerId?: number // 瀹㈡埛缂栧彿
+ contractId?: number // 鍚堝悓缂栧彿
+}>()
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ customerId: undefined as unknown, // 鍏佽 undefined + number
+ contractId: undefined as unknown // 鍏佽 undefined + number
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ if (props.customerId && !props.contractId) {
+ queryParams.customerId = props.customerId
+ } else if (props.customerId && props.contractId) {
+ // 濡傛灉鏄悎鍚岀殑璇濆鎴风紪鍙蜂篃闇�瑕佸甫涓婂洜涓烘潈闄愬熀浜庡鎴�
+ queryParams.customerId = props.customerId
+ queryParams.contractId = props.contractId
+ }
+ const data = await ReceivablePlanApi.getReceivablePlanPageByCustomer(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ // 缃┖鍙傛暟
+ queryParams.customerId = undefined
+ queryParams.contractId = undefined
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id, props.customerId, props.contractId)
+}
+
+/** 鍒涘缓鍥炴 */
+const emits = defineEmits<{
+ (e: 'createReceivable', v: ReceivablePlanApi.ReceivablePlanVO)
+}>()
+const createReceivable = (row: ReceivablePlanApi.ReceivablePlanVO) => {
+ emits('createReceivable', row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ReceivablePlanApi.deleteReceivablePlan(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鐩戝惉鎵撳紑鐨� customerId + contractId锛屼粠鑰屽姞杞芥渶鏂扮殑鍒楄〃 */
+watch(
+ () => [props.customerId, props.contractId],
+ (newVal) => {
+ // 淇濊瘉鑷冲皯瀹㈡埛缂栧彿鏈夊��
+ if (!newVal[0]) {
+ return
+ }
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+</script>
diff --git a/src/views/crm/receivable/plan/detail/ReceivablePlanDetailsHeader.vue b/src/views/crm/receivable/plan/detail/ReceivablePlanDetailsHeader.vue
new file mode 100644
index 0000000..b0e0044
--- /dev/null
+++ b/src/views/crm/receivable/plan/detail/ReceivablePlanDetailsHeader.vue
@@ -0,0 +1,44 @@
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">绗� {{ receivablePlan.period }} 鏈�</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <slot></slot>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="vertical">
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">
+ {{ receivablePlan.customerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓缂栧彿">{{ receivablePlan.contractNo }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝鍥炴閲戦">
+ {{ erpPriceInputFormatter(receivablePlan.price) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁″垝鍥炴鏃ユ湡">
+ {{ formatDate(receivablePlan.returnTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹為檯鍥炴閲戦">
+ <el-text v-if="receivablePlan.receivable">
+ {{ erpPriceInputFormatter(receivablePlan.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivablePlan } = defineProps<{ receivablePlan: ReceivablePlanApi.ReceivablePlanVO }>()
+</script>
diff --git a/src/views/crm/receivable/plan/detail/ReceivablePlanDetailsInfo.vue b/src/views/crm/receivable/plan/detail/ReceivablePlanDetailsInfo.vue
new file mode 100644
index 0000000..c25259b
--- /dev/null
+++ b/src/views/crm/receivable/plan/detail/ReceivablePlanDetailsInfo.vue
@@ -0,0 +1,83 @@
+<template>
+ <ContentWrap>
+ <el-collapse v-model="activeNames">
+ <el-collapse-item name="basicInfo">
+ <template #title>
+ <span class="text-base font-bold">鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="鏈熸暟">{{ receivablePlan.period }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">
+ {{ receivablePlan.customerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓缂栧彿">
+ {{ receivablePlan.contractNo }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁″垝鍥炴閲戦">
+ {{ erpPriceInputFormatter(receivablePlan.price) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁″垝鍥炴鏃ユ湡">
+ {{ formatDate(receivablePlan.returnTime, 'YYYY-MM-DD') }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁″垝鍥炴鏂瑰紡">
+ <dict-tag
+ :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE"
+ :value="receivablePlan.returnType"
+ />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎻愬墠鍑犲ぉ鎻愰啋">
+ {{ receivablePlan.remindDays }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ receivablePlan.remark }}</el-descriptions-item>
+ <el-descriptions-item label="瀹為檯鍥炴閲戦">
+ <el-text v-if="receivablePlan.receivable">
+ {{ erpPriceInputFormatter(receivablePlan.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈洖娆鹃噾棰�">
+ <el-text v-if="receivablePlan.receivable">
+ {{ erpPriceInputFormatter(receivablePlan.price - receivablePlan.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(receivablePlan.price) }}</el-text>
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹為檯鍥炴鏃ユ湡">
+ {{ formatDate(receivablePlan.receivable?.returnTime, 'YYYY-MM-DD') }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ <el-collapse-item name="systemInfo">
+ <template #title>
+ <span class="text-base font-bold">绯荤粺淇℃伅</span>
+ </template>
+ <el-descriptions :column="4">
+ <el-descriptions-item label="璐熻矗浜�">
+ {{ receivablePlan.ownerUserName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">
+ {{ receivablePlan.creatorName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(receivablePlan.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(receivablePlan.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-collapse-item>
+ </el-collapse>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { erpPriceInputFormatter } from '@/utils'
+
+const { receivablePlan } = defineProps<{
+ receivablePlan: ReceivablePlanApi.ReceivablePlanVO
+}>()
+
+// 灞曠ず鐨勬姌鍙犻潰鏉�
+const activeNames = ref(['basicInfo', 'systemInfo'])
+</script>
diff --git a/src/views/crm/receivable/plan/detail/index.vue b/src/views/crm/receivable/plan/detail/index.vue
new file mode 100644
index 0000000..fba8694
--- /dev/null
+++ b/src/views/crm/receivable/plan/detail/index.vue
@@ -0,0 +1,103 @@
+<template>
+ <ReceivablePlanDetailsHeader v-loading="loading" :receivable-plan="receivablePlan">
+ <el-button
+ v-if="permissionListRef?.validateWrite"
+ @click="openForm('update', receivablePlan.id)"
+ >
+ 缂栬緫
+ </el-button>
+ </ReceivablePlanDetailsHeader>
+ <el-col>
+ <el-tabs>
+ <el-tab-pane label="璇︾粏璧勬枡">
+ <ReceivablePlanDetailsInfo :receivable-plan="receivablePlan" />
+ </el-tab-pane>
+ <el-tab-pane label="鎿嶄綔鏃ュ織">
+ <OperateLogV2 :log-list="logList" />
+ </el-tab-pane>
+ <el-tab-pane label="鍥㈤槦鎴愬憳">
+ <PermissionList
+ ref="permissionListRef"
+ :biz-id="receivablePlan.id!"
+ :biz-type="BizTypeEnum.CRM_RECEIVABLE_PLAN"
+ :show-action="true"
+ @quit-team="close"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ReceivablePlanForm ref="formRef" @success="getReceivablePlan(receivablePlan.id)" />
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import ReceivablePlanDetailsHeader from './ReceivablePlanDetailsHeader.vue'
+import ReceivablePlanDetailsInfo from './ReceivablePlanDetailsInfo.vue'
+import PermissionList from '@/views/crm/permission/components/PermissionList.vue' // 鍥㈤槦鎴愬憳鍒楄〃锛堟潈闄愶級
+import { BizTypeEnum } from '@/api/crm/permission'
+import { OperateLogVO } from '@/api/system/operatelog'
+import { getOperateLogPage } from '@/api/crm/operateLog'
+import ReceivablePlanForm from '@/views/crm/receivable/plan/ReceivablePlanForm.vue'
+
+defineOptions({ name: 'CrmReceivablePlanDetail' })
+
+const message = useMessage()
+
+const receivablePlanId = ref(0) // 鍥炴璁″垝缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const receivablePlan = ref<ReceivablePlanApi.ReceivablePlanVO>(
+ {} as ReceivablePlanApi.ReceivablePlanVO
+) // 鍥炴璁″垝璇︽儏
+const permissionListRef = ref<InstanceType<typeof PermissionList>>() // 鍥㈤槦鎴愬憳鍒楄〃 Ref
+
+/** 鑾峰彇璇︽儏 */
+const getReceivablePlan = async (id: number) => {
+ loading.value = true
+ try {
+ receivablePlan.value = await ReceivablePlanApi.getReceivablePlan(id)
+ await getOperateLog(id)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 缂栬緫 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鑾峰彇鎿嶄綔鏃ュ織 */
+const logList = ref<OperateLogVO[]>([]) // 鎿嶄綔鏃ュ織鍒楄〃
+const getOperateLog = async (receivablePlanId: number) => {
+ if (!receivablePlanId) {
+ return
+ }
+ const data = await getOperateLogPage({
+ bizType: BizTypeEnum.CRM_RECEIVABLE_PLAN,
+ bizId: receivablePlanId
+ })
+ logList.value = data.list
+}
+
+/** 鍏抽棴绐楀彛 */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter() // 璺敱
+const close = () => {
+ delView(unref(currentRoute))
+}
+
+/** 鍒濆鍖� */
+const { params } = useRoute()
+onMounted(async () => {
+ if (!params.id) {
+ message.warning('鍙傛暟閿欒锛屽洖娆捐鍒掍笉鑳戒负绌猴紒')
+ close()
+ return
+ }
+ receivablePlanId.value = params.id as unknown as number
+ await getReceivablePlan(receivablePlanId.value)
+})
+</script>
diff --git a/src/views/crm/receivable/plan/index.vue b/src/views/crm/receivable/plan/index.vue
new file mode 100644
index 0000000..43abe15
--- /dev/null
+++ b/src/views/crm/receivable/plan/index.vue
@@ -0,0 +1,335 @@
+<template>
+ <doc-alert title="銆愬洖娆俱�戝洖娆剧鐞嗐�佸洖娆捐鍒�" url="https://doc.iocoder.cn/crm/receivable/" />
+ <doc-alert title="銆愰�氱敤銆戞暟鎹潈闄�" url="https://doc.iocoder.cn/crm/permission/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ class="!w-240px"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ @keyup.enter="handleQuery"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍚堝悓缂栧彿" prop="contractNo">
+ <el-input
+ v-model="queryParams.contractNo"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ悎鍚岀紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable-plan:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable-plan:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="activeName" @tab-click="handleTabClick">
+ <el-tab-pane label="鎴戣礋璐g殑" name="1" />
+ <el-tab-pane label="涓嬪睘璐熻矗鐨�" name="3" />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="customerName" width="150">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍚堝悓缂栧彿" prop="contractNo" width="200px" />
+ <el-table-column align="center" label="鏈熸暟" prop="period">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.period }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="璁″垝鍥炴閲戦锛堝厓锛�"
+ prop="price"
+ width="160"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ :formatter="dateFormatter2"
+ align="center"
+ label="璁″垝鍥炴鏃ユ湡"
+ prop="returnTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎻愬墠鍑犲ぉ鎻愰啋" prop="remindDays" width="150" />
+ <el-table-column
+ align="center"
+ label="鎻愰啋鏃ユ湡"
+ prop="remindTime"
+ width="180px"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column align="center" label="鍥炴鏂瑰紡" prop="returnType" width="130px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_RECEIVABLE_RETURN_TYPE" :value="scope.row.returnType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column label="璐熻矗浜�" prop="ownerUserName" width="120" />
+ <el-table-column
+ align="center"
+ label="瀹為檯鍥炴閲戦锛堝厓锛�"
+ prop="receivable.price"
+ width="160"
+ >
+ <template #default="scope">
+ <el-text v-if="scope.row.receivable">
+ {{ erpPriceInputFormatter(scope.row.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(0) }}</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ label="瀹為檯鍥炴鏃ユ湡"
+ prop="receivable.returnTime"
+ width="180px"
+ :formatter="dateFormatter2"
+ />
+ <el-table-column
+ align="center"
+ label="瀹為檯鍥炴閲戦锛堝厓锛�"
+ prop="receivable.price"
+ width="160"
+ >
+ <template #default="scope">
+ <el-text v-if="scope.row.receivable">
+ {{ erpPriceInputFormatter(scope.row.price - scope.row.receivable.price) }}
+ </el-text>
+ <el-text v-else>{{ erpPriceInputFormatter(scope.row.price) }}</el-text>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="180px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['crm:receivable:create']"
+ link
+ type="success"
+ @click="openReceivableForm(scope.row)"
+ :disabled="scope.row.receivableId"
+ >
+ 鍒涘缓鍥炴
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable-plan:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['crm:receivable-plan:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ReceivablePlanForm ref="formRef" @success="getList" />
+ <ReceivableForm ref="receivableFormRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ReceivablePlanApi from '@/api/crm/receivable/plan'
+import ReceivablePlanForm from './ReceivablePlanForm.vue'
+import * as CustomerApi from '@/api/crm/customer'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { TabsPaneContext } from 'element-plus'
+import ReceivableForm from '@/views/crm/receivable/ReceivableForm.vue'
+
+defineOptions({ name: 'ReceivablePlan' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ sceneType: '1', // 榛樿鍜� activeName 鐩哥瓑
+ customerId: undefined,
+ contractNo: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const activeName = ref('1') // 鍒楄〃 tab
+const customerList = ref<CustomerApi.CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+
+/** tab 鍒囨崲 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.sceneType = tab.paneName
+ handleQuery()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ReceivablePlanApi.getReceivablePlanPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒涘缓鍥炴鎿嶄綔 */
+const receivableFormRef = ref()
+const openReceivableForm = (row: ReceivablePlanApi.ReceivablePlanVO) => {
+ receivableFormRef.value.open('create', undefined, row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ReceivablePlanApi.deleteReceivablePlan(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ReceivablePlanApi.exportReceivablePlan(queryParams)
+ download.excel(data, '鍥炴璁″垝.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鎵撳紑璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmReceivablePlanDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerConversionStat.vue b/src/views/crm/statistics/customer/components/CustomerConversionStat.vue
new file mode 100644
index 0000000..4f5c50c
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerConversionStat.vue
@@ -0,0 +1,170 @@
+<!-- 瀹㈡埛杞寲鐜囧垎鏋� -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" fixed="left" />
+ <el-table-column
+ label="瀹㈡埛鍚嶇О"
+ align="center"
+ prop="customerName"
+ min-width="200"
+ fixed="left"
+ />
+ <el-table-column label="鍚堝悓鍚嶇О" align="center" prop="contractName" min-width="200" />
+ <el-table-column
+ label="鍚堝悓鎬婚噾棰�"
+ align="center"
+ prop="totalPrice"
+ min-width="200"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍥炴閲戦"
+ align="center"
+ prop="receivablePrice"
+ min-width="200"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璐熻矗浜�" align="center" prop="ownerUserName" min-width="200" />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorUserName" min-width="200" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ min-width="200"
+ />
+ <el-table-column
+ label="涓嬪崟鏃ユ湡"
+ align="center"
+ prop="orderDate"
+ :formatter="dateFormatter"
+ min-width="200"
+ fixed="right"
+ />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsCustomerSummaryByDateRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { dateFormatter } from '@/utils/formatTime'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+
+defineOptions({ name: 'CustomerConversionStat' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsCustomerSummaryByDateRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 40, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '瀹㈡埛杞寲鐜�',
+ type: 'line',
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '瀹㈡埛杞寲鐜囧垎鏋�' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: {
+ type: 'value',
+ name: '杞寲鐜�(%)'
+ },
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const customerCount = await StatisticsCustomerApi.getCustomerSummaryByDate(props.queryParams)
+ const contractSummary = await StatisticsCustomerApi.getContractSummary(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = customerCount.map(
+ (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = customerCount.map(
+ (item: CrmStatisticsCustomerSummaryByDateRespVO) => {
+ return {
+ name: item.time,
+ value: item.customerCreateCount
+ ? ((item.customerDealCount / item.customerCreateCount) * 100).toFixed(2)
+ : 0
+ }
+ }
+ )
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = contractSummary
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerDealCycleByArea.vue b/src/views/crm/statistics/customer/components/CustomerDealCycleByArea.vue
new file mode 100644
index 0000000..9aa6d5e
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerDealCycleByArea.vue
@@ -0,0 +1,153 @@
+<!-- 鎴愪氦鍛ㄦ湡鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" />
+ <el-table-column label="鍖哄煙" align="center" prop="areaName" min-width="200" />
+ <el-table-column
+ label="鎴愪氦鍛ㄦ湡(澶�)"
+ align="center"
+ prop="customerDealCycle"
+ min-width="200"
+ />
+ <el-table-column label="鎴愪氦瀹㈡埛鏁�" align="center" prop="customerDealCount" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsCustomerDealCycleByAreaRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerDealCycleByArea' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsCustomerDealCycleByAreaRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 40, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '鎴愪氦鍛ㄦ湡(澶�)',
+ type: 'bar',
+ data: [],
+ yAxisIndex: 0
+ },
+ {
+ name: '鎴愪氦瀹㈡埛鏁�',
+ type: 'bar',
+ data: [],
+ yAxisIndex: 1
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鎴愪氦鍛ㄦ湡鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鎴愪氦鍛ㄦ湡(澶�)',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '鎴愪氦瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鍖哄煙',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const customerDealCycleByArea = (
+ await StatisticsCustomerApi.getCustomerDealCycleByArea(props.queryParams)
+ ).map((s: CrmStatisticsCustomerDealCycleByAreaRespVO) => {
+ return {
+ areaName: s.areaName,
+ customerDealCycle: s.customerDealCycle,
+ customerDealCount: s.customerDealCount
+ }
+ })
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = customerDealCycleByArea.map(
+ (s: CrmStatisticsCustomerDealCycleByAreaRespVO) => s.areaName
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = customerDealCycleByArea.map(
+ (s: CrmStatisticsCustomerDealCycleByAreaRespVO) => s.customerDealCycle
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = customerDealCycleByArea.map(
+ (s: CrmStatisticsCustomerDealCycleByAreaRespVO) => s.customerDealCount
+ )
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = customerDealCycleByArea
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue b/src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue
new file mode 100644
index 0000000..74558d1
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerDealCycleByProduct.vue
@@ -0,0 +1,153 @@
+<!-- 鎴愪氦鍛ㄦ湡鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" />
+ <el-table-column label="浜у搧鍚嶇О" align="center" prop="productName" min-width="200" />
+ <el-table-column
+ label="鎴愪氦鍛ㄦ湡(澶�)"
+ align="center"
+ prop="customerDealCycle"
+ min-width="200"
+ />
+ <el-table-column label="鎴愪氦瀹㈡埛鏁�" align="center" prop="customerDealCount" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsCustomerDealCycleByProductRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerDealCycleByProduct' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsCustomerDealCycleByProductRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 40, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '鎴愪氦鍛ㄦ湡(澶�)',
+ type: 'bar',
+ data: [],
+ yAxisIndex: 0
+ },
+ {
+ name: '鎴愪氦瀹㈡埛鏁�',
+ type: 'bar',
+ data: [],
+ yAxisIndex: 1
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鎴愪氦鍛ㄦ湡鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鎴愪氦鍛ㄦ湡(澶�)',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '鎴愪氦瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '浜у搧鍚嶇О',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const customerDealCycleByProduct = (
+ await StatisticsCustomerApi.getCustomerDealCycleByProduct(props.queryParams)
+ ).map((s: CrmStatisticsCustomerDealCycleByProductRespVO) => {
+ return {
+ productName: s.productName ?? '鏈煡',
+ customerDealCycle: s.customerDealCount,
+ customerDealCount: s.customerDealCount
+ }
+ })
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = customerDealCycleByProduct.map(
+ (s: CrmStatisticsCustomerDealCycleByProductRespVO) => s.productName
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = customerDealCycleByProduct.map(
+ (s: CrmStatisticsCustomerDealCycleByProductRespVO) => s.customerDealCycle
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = customerDealCycleByProduct.map(
+ (s: CrmStatisticsCustomerDealCycleByProductRespVO) => s.customerDealCount
+ )
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = customerDealCycleByProduct
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue b/src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue
new file mode 100644
index 0000000..e3d877e
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerDealCycleByUser.vue
@@ -0,0 +1,154 @@
+<!-- 鎴愪氦鍛ㄦ湡鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" />
+ <el-table-column label="鏃ユ湡" align="center" prop="ownerUserName" min-width="200" />
+ <el-table-column
+ label="鎴愪氦鍛ㄦ湡(澶�)"
+ align="center"
+ prop="customerDealCycle"
+ min-width="200"
+ />
+ <el-table-column label="鎴愪氦瀹㈡埛鏁�" align="center" prop="customerDealCount" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsCustomerDealCycleByDateRespVO,
+ CrmStatisticsCustomerSummaryByDateRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerDealCycleByUser' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsCustomerDealCycleByDateRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 40, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '鎴愪氦鍛ㄦ湡(澶�)',
+ type: 'bar',
+ data: [],
+ yAxisIndex: 0
+ },
+ {
+ name: '鎴愪氦瀹㈡埛鏁�',
+ type: 'bar',
+ data: [],
+ yAxisIndex: 1
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鎴愪氦鍛ㄦ湡鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鎴愪氦鍛ㄦ湡(澶�)',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '鎴愪氦瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const customerDealCycleByDate = await StatisticsCustomerApi.getCustomerDealCycleByDate(
+ props.queryParams
+ )
+ const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
+ props.queryParams
+ )
+ const customerDealCycleByUser = await StatisticsCustomerApi.getCustomerDealCycleByUser(
+ props.queryParams
+ )
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = customerDealCycleByDate.map(
+ (s: CrmStatisticsCustomerDealCycleByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = customerDealCycleByDate.map(
+ (s: CrmStatisticsCustomerDealCycleByDateRespVO) => s.customerDealCycle
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = customerSummaryByDate.map(
+ (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerDealCount
+ )
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = customerDealCycleByUser
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerFollowUpSummary.vue b/src/views/crm/statistics/customer/components/CustomerFollowUpSummary.vue
new file mode 100644
index 0000000..eeb0ff0
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerFollowUpSummary.vue
@@ -0,0 +1,156 @@
+<!-- 瀹㈡埛璺熻繘娆℃暟鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" />
+ <el-table-column label="鍛樺伐濮撳悕" align="center" prop="ownerUserName" min-width="200" />
+ <el-table-column label="璺熻繘娆℃暟" align="right" prop="followUpRecordCount" min-width="200" />
+ <el-table-column
+ label="璺熻繘瀹㈡埛鏁�"
+ align="right"
+ prop="followUpCustomerCount"
+ min-width="200"
+ />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsFollowUpSummaryByDateRespVO,
+ CrmStatisticsFollowUpSummaryByUserRespVO
+} from '@/api/crm/statistics/customer'
+import Echart from '@/components/Echart/src/Echart.vue'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerFollowupSummary' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsFollowUpSummaryByUserRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 30, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '璺熻繘瀹㈡埛鏁�',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: []
+ },
+ {
+ name: '璺熻繘娆℃暟',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '瀹㈡埛璺熻繘娆℃暟鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '璺熻繘瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '璺熻繘娆℃暟',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ axisTick: {
+ alignWithLabel: true
+ },
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const followUpSummaryByDate = await StatisticsCustomerApi.getFollowUpSummaryByDate(
+ props.queryParams
+ )
+ const followUpSummaryByUser = await StatisticsCustomerApi.getFollowUpSummaryByUser(
+ props.queryParams
+ )
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = followUpSummaryByDate.map(
+ (s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = followUpSummaryByDate.map(
+ (s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.followUpCustomerCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = followUpSummaryByDate.map(
+ (s: CrmStatisticsFollowUpSummaryByDateRespVO) => s.followUpRecordCount
+ )
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = followUpSummaryByUser
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerFollowUpType.vue b/src/views/crm/statistics/customer/components/CustomerFollowUpType.vue
new file mode 100644
index 0000000..3d8d873
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerFollowUpType.vue
@@ -0,0 +1,120 @@
+<!-- 瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" />
+ <el-table-column label="璺熻繘鏂瑰紡" align="center" prop="followUpType" min-width="200">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_FOLLOW_UP_TYPE" :value="scope.row.followUpType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="涓暟" align="center" prop="followUpRecordCount" min-width="200" />
+ <el-table-column label="鍗犳瘮(%)" align="center" prop="portion" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsFollowUpSummaryByTypeRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { sumBy } from 'lodash-es'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { erpCalculatePercentage } from '@/utils'
+
+defineOptions({ name: 'CustomerFollowupType' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsFollowUpSummaryByTypeRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 楗煎浘閰嶇疆 */
+const echartsOption = reactive<EChartsOption>({
+ title: {
+ text: '瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽',
+ left: 'center'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{b} : {c}% '
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '璺熻繘鏂瑰紡',
+ type: 'pie',
+ radius: '50%',
+ data: [],
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+ ]
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const followUpSummaryByType = await StatisticsCustomerApi.getFollowUpSummaryByType(
+ props.queryParams
+ )
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = followUpSummaryByType.map(
+ (row: CrmStatisticsFollowUpSummaryByTypeRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_FOLLOW_UP_TYPE, row.followUpType),
+ value: row.followUpRecordCount
+ }
+ }
+ )
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ const totalCount = sumBy(followUpSummaryByType, 'followUpRecordCount')
+ list.value = followUpSummaryByType.map((row: CrmStatisticsFollowUpSummaryByTypeRespVO) => {
+ return {
+ ...row,
+ portion: erpCalculatePercentage(row.followUpRecordCount, totalCount)
+ }
+ })
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerPoolSummary.vue b/src/views/crm/statistics/customer/components/CustomerPoolSummary.vue
new file mode 100644
index 0000000..5f0606a
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerPoolSummary.vue
@@ -0,0 +1,154 @@
+<!-- 瀹㈡埛鎬婚噺缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" fixed="left" />
+ <el-table-column label="鍛樺伐濮撳悕" prop="ownerUserName" min-width="100" fixed="left" />
+ <el-table-column
+ label="杩涘叆鍏捣瀹㈡埛鏁�"
+ align="right"
+ prop="customerPutCount"
+ min-width="200"
+ />
+ <el-table-column
+ label="鍏捣棰嗗彇瀹㈡埛鏁�"
+ align="right"
+ prop="customerTakeCount"
+ min-width="200"
+ />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsPoolSummaryByDateRespVO,
+ CrmStatisticsPoolSummaryByUserRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+
+defineOptions({ name: 'CustomerPoolSummary' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsPoolSummaryByUserRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 40, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '杩涘叆鍏捣瀹㈡埛鏁�',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: []
+ },
+ {
+ name: '鍏捣棰嗗彇瀹㈡埛鏁�',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鍏捣瀹㈡埛鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '杩涘叆鍏捣瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '鍏捣棰嗗彇瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const poolSummaryByDate = await StatisticsCustomerApi.getPoolSummaryByDate(props.queryParams)
+ const poolSummaryByUser = await StatisticsCustomerApi.getPoolSummaryByUser(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = poolSummaryByDate.map(
+ (s: CrmStatisticsPoolSummaryByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = poolSummaryByDate.map(
+ (s: CrmStatisticsPoolSummaryByDateRespVO) => s.customerPutCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = poolSummaryByDate.map(
+ (s: CrmStatisticsPoolSummaryByDateRespVO) => s.customerTakeCount
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = poolSummaryByUser
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/components/CustomerSummary.vue b/src/views/crm/statistics/customer/components/CustomerSummary.vue
new file mode 100644
index 0000000..d1429c2
--- /dev/null
+++ b/src/views/crm/statistics/customer/components/CustomerSummary.vue
@@ -0,0 +1,183 @@
+<!-- 瀹㈡埛鎬婚噺缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="搴忓彿" align="center" type="index" width="80" fixed="left" />
+ <el-table-column label="鍛樺伐濮撳悕" prop="ownerUserName" min-width="100" fixed="left" />
+ <el-table-column
+ label="鏂板瀹㈡埛鏁�"
+ align="right"
+ prop="customerCreateCount"
+ min-width="200"
+ />
+ <el-table-column label="鎴愪氦瀹㈡埛鏁�" align="right" prop="customerDealCount" min-width="200" />
+ <el-table-column label="瀹㈡埛鎴愪氦鐜�(%)" align="right" min-width="200">
+ <template #default="scope">
+ {{ erpCalculatePercentage(scope.row.customerDealCount, scope.row.customerCreateCount) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍚堝悓鎬婚噾棰�"
+ align="right"
+ prop="contractPrice"
+ min-width="200"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍥炴閲戦"
+ align="right"
+ prop="receivablePrice"
+ min-width="200"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈洖娆鹃噾棰�" align="right" min-width="200">
+ <template #default="scope">
+ {{ erpCalculatePercentage(scope.row.receivablePrice, scope.row.contractPrice) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍥炴瀹屾垚鐜�(%)" align="right" min-width="200" fixed="right">
+ <template #default="scope">
+ {{ erpCalculatePercentage(scope.row.receivablePrice, scope.row.contractPrice) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import {
+ StatisticsCustomerApi,
+ CrmStatisticsCustomerSummaryByDateRespVO,
+ CrmStatisticsCustomerSummaryByUserRespVO
+} from '@/api/crm/statistics/customer'
+import { EChartsOption } from 'echarts'
+import { erpCalculatePercentage, erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'CustomerSummary' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticsCustomerSummaryByUserRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 30, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '鏂板瀹㈡埛鏁�',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: []
+ },
+ {
+ name: '鎴愪氦瀹㈡埛鏁�',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '瀹㈡埛鎬婚噺鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鏂板瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '鎴愪氦瀹㈡埛鏁�',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const customerSummaryByDate = await StatisticsCustomerApi.getCustomerSummaryByDate(
+ props.queryParams
+ )
+ const customerSummaryByUser = await StatisticsCustomerApi.getCustomerSummaryByUser(
+ props.queryParams
+ )
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = customerSummaryByDate.map(
+ (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = customerSummaryByDate.map(
+ (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerCreateCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = customerSummaryByDate.map(
+ (s: CrmStatisticsCustomerSummaryByDateRespVO) => s.customerDealCount
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = customerSummaryByUser
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/customer/index.vue b/src/views/crm/statistics/customer/index.vue
new file mode 100644
index 0000000..207dc35
--- /dev/null
+++ b/src/views/crm/statistics/customer/index.vue
@@ -0,0 +1,214 @@
+<!-- 鏁版嵁缁熻 - 鍛樺伐瀹㈡埛鍒嗘瀽 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鏃堕棿鑼冨洿" prop="orderDate">
+ <el-date-picker
+ v-model="queryParams.times"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ :shortcuts="defaultShortcuts"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鏃堕棿闂撮殧" prop="interval">
+ <el-select
+ v-model="queryParams.interval"
+ class="!w-240px"
+ placeholder="闂撮殧绫诲瀷"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select
+ v-model="queryParams.deptId"
+ :data="deptList"
+ :props="defaultProps"
+ check-strictly
+ class="!w-240px"
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ @change="(queryParams.userId = undefined), handleQuery()"
+ />
+ </el-form-item>
+ <el-form-item label="鍛樺伐" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ class="!w-240px"
+ clearable
+ placeholder="鍛樺伐"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(user, index) in userListByDeptId"
+ :key="index"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鏌ヨ
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 瀹㈡埛缁熻 -->
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <!-- 瀹㈡埛鎬婚噺鍒嗘瀽 -->
+ <el-tab-pane label="瀹㈡埛鎬婚噺鍒嗘瀽" lazy name="customerSummary">
+ <CustomerSummary ref="customerSummaryRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 瀹㈡埛璺熻繘娆℃暟鍒嗘瀽 -->
+ <el-tab-pane label="瀹㈡埛璺熻繘娆℃暟鍒嗘瀽" lazy name="followUpSummary">
+ <CustomerFollowUpSummary ref="followUpSummaryRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽 -->
+ <el-tab-pane label="瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽" lazy name="followUpType">
+ <CustomerFollowUpType ref="followUpTypeRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 瀹㈡埛杞寲鐜囧垎鏋� -->
+ <el-tab-pane label="瀹㈡埛杞寲鐜囧垎鏋�" lazy name="conversionStat">
+ <CustomerConversionStat ref="conversionStatRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 鍏捣瀹㈡埛鍒嗘瀽 -->
+ <el-tab-pane label="鍏捣瀹㈡埛鍒嗘瀽" lazy name="poolSummary">
+ <CustomerPoolSummary ref="customerPoolSummaryRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 鎴愪氦鍛ㄦ湡鍒嗘瀽 -->
+ <el-tab-pane label="鍛樺伐瀹㈡埛鎴愪氦鍛ㄦ湡鍒嗘瀽" lazy name="dealCycleByUser">
+ <CustomerDealCycleByUser ref="dealCycleByUserRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <el-tab-pane label="鍦板尯瀹㈡埛鎴愪氦鍛ㄦ湡鍒嗘瀽" lazy name="dealCycleByArea">
+ <CustomerDealCycleByArea ref="dealCycleByAreaRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <el-tab-pane label="浜у搧瀹㈡埛鎴愪氦鍛ㄦ湡鍒嗘瀽" lazy name="dealCycleByProduct">
+ <CustomerDealCycleByProduct ref="dealCycleByProductRef" :query-params="queryParams" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import CustomerConversionStat from './components/CustomerConversionStat.vue'
+import CustomerDealCycleByUser from './components/CustomerDealCycleByUser.vue'
+import CustomerDealCycleByArea from './components/CustomerDealCycleByArea.vue'
+import CustomerDealCycleByProduct from './components/CustomerDealCycleByProduct.vue'
+import CustomerFollowUpSummary from './components/CustomerFollowUpSummary.vue'
+import CustomerFollowUpType from './components/CustomerFollowUpType.vue'
+import CustomerSummary from './components/CustomerSummary.vue'
+import CustomerPoolSummary from './components/CustomerPoolSummary.vue'
+
+defineOptions({ name: 'CrmStatisticsCustomer' })
+
+const queryParams = reactive({
+ interval: 2, // WEEK, 鍛�
+ deptId: useUserStore().getUser.deptId,
+ userId: undefined,
+ times: [
+ // 榛樿鏄剧ず鏈�杩戜竴鍛ㄧ殑鏁版嵁
+ formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+ formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+ ]
+})
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const deptList = ref<Tree[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯
+const userList = ref<UserApi.UserVO[]>([]) // 鍏ㄩ噺鐢ㄦ埛娓呭崟
+
+/** 鏍规嵁閫夋嫨鐨勯儴闂ㄧ瓫閫夊憳宸ユ竻鍗� */
+const userListByDeptId = computed(() =>
+ queryParams.deptId
+ ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
+ : []
+)
+
+const activeTab = ref('customerSummary') // 娲昏穬鏍囩
+const customerSummaryRef = ref() // 1. 瀹㈡埛鎬婚噺鍒嗘瀽
+const followUpSummaryRef = ref() // 2. 瀹㈡埛璺熻繘娆℃暟鍒嗘瀽
+const followUpTypeRef = ref() // 3. 瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽
+const conversionStatRef = ref() // 4. 瀹㈡埛杞寲鐜囧垎鏋�
+const customerPoolSummaryRef = ref() // 5. 瀹㈡埛鍏捣鍒嗘瀽
+const dealCycleByUserRef = ref() // 6. 鎴愪氦鍛ㄦ湡鍒嗘瀽(鎸夊憳宸�)
+const dealCycleByAreaRef = ref() // 7. 鎴愪氦鍛ㄦ湡鍒嗘瀽(鎸夊湴鍖�)
+const dealCycleByProductRef = ref() // 8. 鎴愪氦鍛ㄦ湡鍒嗘瀽(鎸変骇鍝�)
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ switch (activeTab.value) {
+ case 'customerSummary': // 瀹㈡埛鎬婚噺鍒嗘瀽
+ customerSummaryRef.value?.loadData?.()
+ break
+ case 'followUpSummary': // 瀹㈡埛璺熻繘娆℃暟鍒嗘瀽
+ followUpSummaryRef.value?.loadData?.()
+ break
+ case 'followUpType': // 瀹㈡埛璺熻繘鏂瑰紡鍒嗘瀽
+ followUpTypeRef.value?.loadData?.()
+ break
+ case 'conversionStat': // 瀹㈡埛杞寲鐜囧垎鏋�
+ conversionStatRef.value?.loadData?.()
+ break
+ case 'poolSummary': // 鍏捣瀹㈡埛鍒嗘瀽
+ customerPoolSummaryRef.value?.loadData?.()
+ break
+ case 'dealCycleByUser': // 鎴愪氦鍛ㄦ湡鍒嗘瀽
+ dealCycleByUserRef.value?.loadData?.()
+ break
+ case 'dealCycleByArea': // 鎴愪氦鍛ㄦ湡鍒嗘瀽
+ dealCycleByAreaRef.value?.loadData?.()
+ break
+ case 'dealCycleByProduct': // 鎴愪氦鍛ㄦ湡鍒嗘瀽
+ dealCycleByProductRef.value?.loadData?.()
+ break
+ }
+}
+
+/** 褰� activeTab 鏀瑰彉鏃讹紝鍒锋柊褰撳墠娲诲姩鐨� tab */
+watch(activeTab, () => {
+ handleQuery()
+})
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+ userList.value = handleTree(await UserApi.getSimpleUserList())
+})
+</script>
diff --git a/src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue b/src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue
new file mode 100644
index 0000000..541d6fc
--- /dev/null
+++ b/src/views/crm/statistics/funnel/components/BusinessInversionRateSummary.vue
@@ -0,0 +1,307 @@
+<!-- 瀹㈡埛鎬婚噺缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card class="mt-16px" shadow="never">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" fixed="left" label="搴忓彿" type="index" width="80" />
+ <el-table-column align="center" fixed="left" label="鍟嗘満鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="erpPriceTableColumnFormatter"
+ align="center"
+ label="鍟嗘満閲戦锛堝厓锛�"
+ prop="totalPrice"
+ width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="棰勮鎴愪氦鏃ユ湡"
+ prop="dealTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column
+ align="center"
+ fixed="right"
+ label="鍟嗘満鐘舵�佺粍"
+ prop="statusTypeName"
+ width="140"
+ />
+ <el-table-column
+ align="center"
+ fixed="right"
+ label="鍟嗘満闃舵"
+ prop="statusName"
+ width="120"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams0.pageSize"
+ v-model:page="queryParams0.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import {
+ CrmStatisticsBusinessInversionRateSummaryByDateRespVO,
+ StatisticFunnelApi
+} from '@/api/crm/statistics/funnel'
+import { EChartsOption } from 'echarts'
+import { erpCalculatePercentage, erpPriceTableColumnFormatter } from '@/utils'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'BusinessSummary' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+const queryParams0 = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0)
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 queryParams0 */
+watch(
+ () => props.queryParams,
+ (data) => {
+ if (!data) {
+ return
+ }
+ const newObj = { ...queryParams0, ...data }
+ Object.assign(queryParams0, newObj)
+ },
+ {
+ immediate: true
+ }
+)
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ color: ['#6ca2ff', '#6ac9d7', '#ff7474'],
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ // 鍧愭爣杞存寚绀哄櫒锛屽潗鏍囪酱瑙﹀彂鏈夋晥
+ type: 'shadow' // 榛樿涓虹洿绾匡紝鍙�変负锛�'line' | 'shadow'
+ }
+ },
+ legend: {
+ data: ['璧㈠崟杞寲鐜�', '鍟嗘満鎬绘暟', '璧㈠崟鍟嗘満鏁�'],
+ bottom: '0px',
+ itemWidth: 14
+ },
+ grid: {
+ top: '40px',
+ left: '40px',
+ right: '40px',
+ bottom: '40px',
+ containLabel: true,
+ borderColor: '#fff'
+ },
+ xAxis: [
+ {
+ type: 'category',
+ data: [],
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: { width: 0 }
+ },
+ axisLabel: {
+ color: '#BDBDBD'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: { color: '#BDBDBD' }
+ },
+ splitLine: {
+ show: false
+ }
+ }
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ name: '璧㈠崟杞寲鐜�',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: { width: 0 }
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: { color: '#BDBDBD' }
+ },
+ splitLine: {
+ show: false
+ }
+ },
+ {
+ type: 'value',
+ name: '鍟嗘満鏁�',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: { width: 0 }
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}涓�'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: { color: '#BDBDBD' }
+ },
+ splitLine: {
+ show: false
+ }
+ }
+ ],
+ series: [
+ {
+ name: '璧㈠崟杞寲鐜�',
+ type: 'line',
+ yAxisIndex: 0,
+ data: []
+ },
+ {
+ name: '鍟嗘満鎬绘暟',
+ type: 'bar',
+ yAxisIndex: 1,
+ barWidth: 15,
+ data: []
+ },
+ {
+ name: '璧㈠崟鍟嗘満鏁�',
+ type: 'bar',
+ yAxisIndex: 1,
+ barWidth: 15,
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const businessSummaryByDate = await StatisticFunnelApi.getBusinessInversionRateSummaryByDate(
+ props.queryParams
+ )
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis[0] && echartsOption.xAxis[0]['data']) {
+ echartsOption.xAxis[0]['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) =>
+ erpCalculatePercentage(s.businessWinCount, s.businessCount)
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) => s.businessCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
+ echartsOption.series[2]['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessInversionRateSummaryByDateRespVO) => s.businessWinCount
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ await getList()
+}
+/** 鑾峰彇鍟嗘満鍒楄〃 */
+const getList = async () => {
+ const data = await StatisticFunnelApi.getBusinessPageByDate(props.queryParams)
+ list.value = data.list
+ total.value = data.total
+}
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/funnel/components/BusinessSummary.vue b/src/views/crm/statistics/funnel/components/BusinessSummary.vue
new file mode 100644
index 0000000..942a712
--- /dev/null
+++ b/src/views/crm/statistics/funnel/components/BusinessSummary.vue
@@ -0,0 +1,259 @@
+<!-- 瀹㈡埛鎬婚噺缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card class="mt-16px" shadow="never">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" fixed="left" label="搴忓彿" type="index" width="80" />
+ <el-table-column align="center" fixed="left" label="鍟嗘満鍚嶇О" prop="name" width="160">
+ <template #default="scope">
+ <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+ {{ scope.row.name }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="left" label="瀹㈡埛鍚嶇О" prop="customerName" width="120">
+ <template #default="scope">
+ <el-link
+ :underline="false"
+ type="primary"
+ @click="openCustomerDetail(scope.row.customerId)"
+ >
+ {{ scope.row.customerName }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="erpPriceTableColumnFormatter"
+ align="center"
+ label="鍟嗘満閲戦锛堝厓锛�"
+ prop="totalPrice"
+ width="140"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="棰勮鎴愪氦鏃ユ湡"
+ prop="dealTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="澶囨敞" prop="remark" width="200" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="涓嬫鑱旂郴鏃堕棿"
+ prop="contactNextTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="璐熻矗浜�" prop="ownerUserName" width="100px" />
+ <el-table-column align="center" label="鎵�灞為儴闂�" prop="ownerUserDeptName" width="100px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏈�鍚庤窡杩涙椂闂�"
+ prop="contactLastTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鍒涘缓浜�" prop="creatorName" width="100px" />
+ <el-table-column
+ align="center"
+ fixed="right"
+ label="鍟嗘満鐘舵�佺粍"
+ prop="statusTypeName"
+ width="140"
+ />
+ <el-table-column
+ align="center"
+ fixed="right"
+ label="鍟嗘満闃舵"
+ prop="statusName"
+ width="120"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams0.pageSize"
+ v-model:page="queryParams0.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import {
+ CrmStatisticsBusinessSummaryByDateRespVO,
+ StatisticFunnelApi
+} from '@/api/crm/statistics/funnel'
+import { EChartsOption } from 'echarts'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'BusinessSummary' })
+
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+const queryParams0 = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0)
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 queryParams0 */
+watch(
+ () => props.queryParams,
+ (data) => {
+ if (!data) {
+ return
+ }
+ const newObj = { ...queryParams0, ...data }
+ Object.assign(queryParams0, newObj)
+ },
+ {
+ immediate: true
+ }
+)
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 30,
+ right: 30, // 璁� X 杞村彸渚ф樉绀哄畬鏁�
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '鏂板鍟嗘満鏁伴噺',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: []
+ },
+ {
+ name: '鏂板鍟嗘満閲戦',
+ type: 'bar',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鏂板鍟嗘満鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鏂板鍟嗘満鏁伴噺',
+ min: 0,
+ minInterval: 1 // 鏄剧ず鏁存暟鍒诲害
+ },
+ {
+ type: 'value',
+ name: '鏂板鍟嗘満閲戦',
+ min: 0,
+ minInterval: 1, // 鏄剧ず鏁存暟鍒诲害
+ splitLine: {
+ lineStyle: {
+ type: 'dotted', // 鍙充晶缃戞牸绾胯櫄鍖�, 鍑忓皯娣蜂贡
+ opacity: 0.7
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏁版嵁骞跺~鍏呭浘琛� */
+const fetchAndFill = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ const businessSummaryByDate = await StatisticFunnelApi.getBusinessSummaryByDate(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessSummaryByDateRespVO) => s.time
+ )
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessSummaryByDateRespVO) => s.businessCreateCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = businessSummaryByDate.map(
+ (s: CrmStatisticsBusinessSummaryByDateRespVO) => s.totalPrice
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ await getList()
+}
+/** 鑾峰彇鍟嗘満鍒楄〃 */
+const getList = async () => {
+ const data = await StatisticFunnelApi.getBusinessPageByDate(props.queryParams)
+ list.value = data.list
+ total.value = data.total
+}
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'CrmBusinessDetail', params: { id } })
+}
+
+/** 鎵撳紑瀹㈡埛璇︽儏 */
+const openCustomerDetail = (id: number) => {
+ push({ name: 'CrmCustomerDetail', params: { id } })
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ try {
+ await fetchAndFill()
+ } finally {
+ loading.value = false
+ }
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/funnel/components/FunnelBusiness.vue b/src/views/crm/statistics/funnel/components/FunnelBusiness.vue
new file mode 100644
index 0000000..c4e4bf6
--- /dev/null
+++ b/src/views/crm/statistics/funnel/components/FunnelBusiness.vue
@@ -0,0 +1,149 @@
+<!-- 閿�鍞紡鏂楀垎鏋� -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-row>
+ <el-col :span="24">
+ <el-button-group class="mb-10px">
+ <el-button type="primary" @click="handleActive(true)">瀹㈡埛瑙嗚</el-button>
+ <el-button type="primary" @click="handleActive(false)">鍔ㄦ�佽瑙�</el-button>
+ </el-button-group>
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card class="mt-16px" shadow="never">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="搴忓彿" type="index" width="80" />
+ <el-table-column align="center" label="闃舵" prop="endStatus" width="200">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_BUSINESS_END_STATUS_TYPE" :value="scope.row.endStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍟嗘満鏁�" min-width="200" prop="businessCount" />
+ <el-table-column align="center" label="鍟嗘満鎬婚噾棰�(鍏�)" min-width="200" prop="totalPrice" />
+ </el-table>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { CrmStatisticFunnelRespVO, StatisticFunnelApi } from '@/api/crm/statistics/funnel'
+import { EChartsOption } from 'echarts'
+import { DICT_TYPE } from '@/utils/dict'
+
+defineOptions({ name: 'FunnelBusiness' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const active = ref(true)
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticFunnelRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 閿�鍞紡鏂� */
+const echartsOption = reactive<EChartsOption>({
+ title: {
+ text: '閿�鍞紡鏂�'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b}'
+ },
+ toolbox: {
+ feature: {
+ dataView: { readOnly: false },
+ restore: {},
+ saveAsImage: {}
+ }
+ },
+ legend: {
+ data: ['瀹㈡埛', '鍟嗘満', '璧㈠崟']
+ },
+ series: [
+ {
+ name: '閿�鍞紡鏂�',
+ type: 'funnel',
+ left: '10%',
+ top: 60,
+ bottom: 60,
+ width: '80%',
+ min: 0,
+ max: 100,
+ minSize: '0%',
+ maxSize: '100%',
+ sort: 'descending',
+ gap: 2,
+ label: {
+ show: true,
+ position: 'inside'
+ },
+ labelLine: {
+ length: 10,
+ lineStyle: {
+ width: 1,
+ type: 'solid'
+ }
+ },
+ itemStyle: {
+ borderColor: '#fff',
+ borderWidth: 1
+ },
+ emphasis: {
+ label: {
+ fontSize: 20
+ }
+ },
+ data: [
+ { value: 60, name: '瀹㈡埛-0涓�' },
+ { value: 40, name: '鍟嗘満-0涓�' },
+ { value: 20, name: '璧㈠崟-0涓�' }
+ ]
+ }
+ ]
+}) as EChartsOption
+
+const handleActive = async (val: boolean) => {
+ active.value = val
+ await loadData()
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ loading.value = true
+ // 1. 鍔犺浇婕忔枟鏁版嵁
+ const data = (await StatisticFunnelApi.getFunnelSummary(
+ props.queryParams
+ )) as CrmStatisticFunnelRespVO
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (
+ !!data &&
+ echartsOption.series &&
+ echartsOption.series[0] &&
+ echartsOption.series[0]['data']
+ ) {
+ // tips锛氬啓姝� value 鍊兼槸涓轰簡淇濇寔婕忔枟椤哄簭涓嶅彉
+ const list: { value: number; name: string }[] = []
+ if (active.value) {
+ list.push({ value: 60, name: `瀹㈡埛-${data.customerCount || 0}涓猔 })
+ list.push({ value: 40, name: `鍟嗘満-${data.businessCount || 0}涓猔 })
+ list.push({ value: 20, name: `璧㈠崟-${data.businessWinCount || 0}涓猔 })
+ } else {
+ list.push({ value: data.customerCount || 0, name: `瀹㈡埛-${data.customerCount || 0}涓猔 })
+ list.push({ value: data.businessCount || 0, name: `鍟嗘満-${data.businessCount || 0}涓猔 })
+ list.push({ value: data.businessWinCount || 0, name: `璧㈠崟-${data.businessWinCount || 0}涓猔 })
+ }
+
+ echartsOption.series[0]['data'] = list
+ }
+ // 2.2 鑾峰彇鍟嗘満缁撴潫鐘舵�佺粺璁�
+ list.value = await StatisticFunnelApi.getBusinessSummaryByEndStatus(props.queryParams)
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/funnel/index.vue b/src/views/crm/statistics/funnel/index.vue
new file mode 100644
index 0000000..804cb49
--- /dev/null
+++ b/src/views/crm/statistics/funnel/index.vue
@@ -0,0 +1,171 @@
+<!-- 鏁版嵁缁熻 - 瀹㈡埛鐢诲儚 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鏃堕棿鑼冨洿" prop="orderDate">
+ <el-date-picker
+ v-model="queryParams.times"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ :shortcuts="defaultShortcuts"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鏃堕棿闂撮殧" prop="interval">
+ <el-select
+ v-model="queryParams.interval"
+ class="!w-240px"
+ placeholder="闂撮殧绫诲瀷"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select
+ v-model="queryParams.deptId"
+ :data="deptList"
+ :props="defaultProps"
+ check-strictly
+ class="!w-240px"
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ @change="(queryParams.userId = undefined), handleQuery()"
+ />
+ </el-form-item>
+ <el-form-item label="鍛樺伐" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ class="!w-240px"
+ clearable
+ placeholder="鍛樺伐"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="(user, index) in userListByDeptId"
+ :key="index"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鏌ヨ
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 瀹㈡埛缁熻 -->
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="閿�鍞紡鏂楀垎鏋�" lazy name="funnelRef">
+ <FunnelBusiness ref="funnelRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <el-tab-pane label="鏂板鍟嗘満鍒嗘瀽" lazy name="businessSummaryRef">
+ <BusinessSummary ref="businessSummaryRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <el-tab-pane label="鍟嗘満杞寲鐜囧垎鏋�" lazy name="businessInversionRateSummaryRef">
+ <BusinessInversionRateSummary
+ ref="businessInversionRateSummaryRef"
+ :query-params="queryParams"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import FunnelBusiness from './components/FunnelBusiness.vue'
+import BusinessSummary from './components/BusinessSummary.vue'
+import BusinessInversionRateSummary from './components/BusinessInversionRateSummary.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'CrmStatisticsFunnel' })
+
+const queryParams = reactive({
+ interval: 2, // WEEK, 鍛�
+ deptId: useUserStore().getUser.deptId,
+ userId: undefined,
+ times: [
+ // 榛樿鏄剧ず鏈�杩戜竴鍛ㄧ殑鏁版嵁
+ formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+ formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+ ]
+})
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const deptList = ref<Tree[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯
+const userList = ref<UserApi.UserVO[]>([]) // 鍏ㄩ噺鐢ㄦ埛娓呭崟
+
+/** 鏍规嵁閫夋嫨鐨勯儴闂ㄧ瓫閫夊憳宸ユ竻鍗� */
+const userListByDeptId = computed(() =>
+ queryParams.deptId
+ ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
+ : []
+)
+
+const activeTab = ref('funnelRef') // 娲昏穬鏍囩
+const funnelRef = ref() // 閿�鍞紡鏂�
+const businessSummaryRef = ref() // 鏂板鍟嗘満鍒嗘瀽
+const businessInversionRateSummaryRef = ref() // 鍟嗘満杞寲鐜囧垎鏋�
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ switch (activeTab.value) {
+ case 'funnelRef':
+ funnelRef.value?.loadData?.()
+ break
+ case 'businessSummaryRef':
+ businessSummaryRef.value?.loadData?.()
+ break
+ case 'businessInversionRateSummaryRef':
+ businessInversionRateSummaryRef.value?.loadData?.()
+ break
+ }
+}
+
+/** 褰� activeTab 鏀瑰彉鏃讹紝鍒锋柊褰撳墠娲诲姩鐨� tab */
+watch(activeTab, () => {
+ handleQuery()
+})
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+ userList.value = handleTree(await UserApi.getSimpleUserList())
+})
+</script>
diff --git a/src/views/crm/statistics/performance/components/ContractCountPerformance.vue b/src/views/crm/statistics/performance/components/ContractCountPerformance.vue
new file mode 100644
index 0000000..fa5a897
--- /dev/null
+++ b/src/views/crm/statistics/performance/components/ContractCountPerformance.vue
@@ -0,0 +1,236 @@
+<!-- 鍛樺伐涓氱哗缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="tableData">
+ <el-table-column
+ v-for="item in columnsData"
+ :key="item.prop"
+ :label="item.label"
+ :prop="item.prop"
+ align="center"
+ >
+ <template #default="scope">
+ {{ scope.row[item.prop] }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { EChartsOption } from 'echarts'
+import {
+ StatisticsPerformanceApi,
+ StatisticsPerformanceRespVO
+} from '@/api/crm/statistics/performance'
+
+defineOptions({ name: 'ContractCountPerformance' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsPerformanceRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '褰撴湀鍚堝悓鏁伴噺锛堜釜锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '涓婃湀鍚堝悓鏁伴噺锛堜釜锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '鍘诲勾鍚屾湀鍚堝悓鏁伴噺锛堜釜锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '鐜瘮澧為暱鐜囷紙%锛�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: []
+ },
+ {
+ name: '鍚屾瘮澧為暱鐜囷紙%锛�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '瀹㈡埛鎬婚噺鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鏁伴噺锛堜釜锛�',
+ axisTick: {
+ show: false
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6'
+ }
+ }
+ },
+ {
+ type: 'value',
+ name: '',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: {
+ width: 0
+ }
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6'
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const performanceList = await StatisticsPerformanceApi.getContractCountPerformance(
+ props.queryParams
+ )
+
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.currentMonthCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.lastMonthCount
+ )
+ echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
+ s.lastMonthCount !== 0
+ ? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
+ : 'NULL'
+ )
+ }
+ if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
+ echartsOption.series[2]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.lastYearCount
+ )
+ echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
+ s.lastYearCount !== 0
+ ? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
+ : 'NULL'
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = performanceList
+ convertListData()
+ loading.value = false
+}
+
+// 鍒濆鍖栨暟鎹�
+const columnsData = reactive([])
+const tableData = reactive([
+ { title: '褰撴湀鍚堝悓鏁伴噺缁熻锛堜釜锛�' },
+ { title: '涓婃湀鍚堝悓鏁伴噺缁熻锛堜釜锛�' },
+ { title: '鍘诲勾褰撴湀鍚堝悓鏁伴噺缁熻锛堜釜锛�' },
+ { title: '鐜瘮澧為暱鐜囷紙%锛�' },
+ { title: '鍚屾瘮澧為暱鐜囷紙%锛�' }
+])
+
+// 瀹氫箟 convertListData 鏂规硶锛屾暟鎹鍒楄浆缃紝灞曠ず姣忔湀鏁版嵁
+const convertListData = () => {
+ const columnObj = { label: '鏃ユ湡', prop: 'title' }
+ columnsData.splice(0, columnsData.length) //娓呯┖鏁扮粍
+ columnsData.push(columnObj)
+
+ list.value.forEach((item, index) => {
+ const columnObj = { label: item.time, prop: 'prop' + index }
+ columnsData.push(columnObj)
+ tableData[0]['prop' + index] = item.currentMonthCount
+ tableData[1]['prop' + index] = item.lastMonthCount
+ tableData[2]['prop' + index] = item.lastYearCount
+ tableData[3]['prop' + index] =
+ item.lastMonthCount !== 0
+ ? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
+ : 'NULL'
+ tableData[4]['prop' + index] =
+ item.lastYearCount !== 0
+ ? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
+ : 'NULL'
+ })
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/performance/components/ContractPricePerformance.vue b/src/views/crm/statistics/performance/components/ContractPricePerformance.vue
new file mode 100644
index 0000000..dd52d9f
--- /dev/null
+++ b/src/views/crm/statistics/performance/components/ContractPricePerformance.vue
@@ -0,0 +1,236 @@
+<!-- 鍛樺伐涓氱哗缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="tableData">
+ <el-table-column
+ v-for="item in columnsData"
+ :key="item.prop"
+ :label="item.label"
+ :prop="item.prop"
+ align="center"
+ >
+ <template #default="scope">
+ {{ scope.row[item.prop] }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { EChartsOption } from 'echarts'
+import {
+ StatisticsPerformanceApi,
+ StatisticsPerformanceRespVO
+} from '@/api/crm/statistics/performance'
+
+defineOptions({ name: 'ContractPricePerformance' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsPerformanceRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '褰撴湀鍚堝悓閲戦锛堝厓锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '涓婃湀鍚堝悓閲戦锛堝厓锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '鍘诲勾鍚屾湀鍚堝悓閲戦锛堝厓锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '鐜瘮澧為暱鐜囷紙%锛�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: []
+ },
+ {
+ name: '鍚屾瘮澧為暱鐜囷紙%锛�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '瀹㈡埛鎬婚噺鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '閲戦锛堝厓锛�',
+ axisTick: {
+ show: false
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6'
+ }
+ }
+ },
+ {
+ type: 'value',
+ name: '',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: {
+ width: 0
+ }
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6'
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const performanceList = await StatisticsPerformanceApi.getContractPricePerformance(
+ props.queryParams
+ )
+
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.currentMonthCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.lastMonthCount
+ )
+ echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
+ s.lastMonthCount !== 0
+ ? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
+ : 'NULL'
+ )
+ }
+ if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
+ echartsOption.series[2]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.lastYearCount
+ )
+ echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
+ s.lastYearCount !== 0
+ ? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
+ : 'NULL'
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = performanceList
+ convertListData()
+ loading.value = false
+}
+
+// 鍒濆鍖栨暟鎹�
+const columnsData = reactive([])
+const tableData = reactive([
+ { title: '褰撴湀鍚堝悓閲戦缁熻锛堝厓锛�' },
+ { title: '涓婃湀鍚堝悓閲戦缁熻锛堝厓锛�' },
+ { title: '鍘诲勾褰撴湀鍚堝悓閲戦缁熻锛堝厓锛�' },
+ { title: '鐜瘮澧為暱鐜囷紙%锛�' },
+ { title: '鍚屾瘮澧為暱鐜囷紙%锛�' }
+])
+
+// 瀹氫箟 init 鏂规硶
+const convertListData = () => {
+ const columnObj = { label: '鏃ユ湡', prop: 'title' }
+ columnsData.splice(0, columnsData.length) //娓呯┖鏁扮粍
+ columnsData.push(columnObj)
+
+ list.value.forEach((item, index) => {
+ const columnObj = { label: item.time, prop: 'prop' + index }
+ columnsData.push(columnObj)
+ tableData[0]['prop' + index] = item.currentMonthCount
+ tableData[1]['prop' + index] = item.lastMonthCount
+ tableData[2]['prop' + index] = item.lastYearCount
+ tableData[3]['prop' + index] =
+ item.lastMonthCount !== 0
+ ? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
+ : 'NULL'
+ tableData[4]['prop' + index] =
+ item.lastYearCount !== 0
+ ? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
+ : 'NULL'
+ })
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/performance/components/ReceivablePricePerformance.vue b/src/views/crm/statistics/performance/components/ReceivablePricePerformance.vue
new file mode 100644
index 0000000..169f074
--- /dev/null
+++ b/src/views/crm/statistics/performance/components/ReceivablePricePerformance.vue
@@ -0,0 +1,236 @@
+<!-- 鍛樺伐涓氱哗缁熻 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="tableData">
+ <el-table-column
+ v-for="item in columnsData"
+ :key="item.prop"
+ :label="item.label"
+ :prop="item.prop"
+ align="center"
+ >
+ <template #default="scope">
+ {{ scope.row[item.prop] }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { EChartsOption } from 'echarts'
+import {
+ StatisticsPerformanceApi,
+ StatisticsPerformanceRespVO
+} from '@/api/crm/statistics/performance'
+
+defineOptions({ name: 'ContractPricePerformance' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsPerformanceRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細绾靛悜 */
+const echartsOption = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {},
+ series: [
+ {
+ name: '褰撴湀鍥炴閲戦锛堝厓锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '涓婃湀鍥炴閲戦锛堝厓锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '鍘诲勾鍚屾湀鍥炴閲戦锛堝厓锛�',
+ type: 'line',
+ data: []
+ },
+ {
+ name: '鐜瘮澧為暱鐜囷紙%锛�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: []
+ },
+ {
+ name: '鍚屾瘮澧為暱鐜囷紙%锛�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: []
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ xAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '瀹㈡埛鎬婚噺鍒嗘瀽' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '閲戦锛堝厓锛�',
+ axisTick: {
+ show: false
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6'
+ }
+ }
+ },
+ {
+ type: 'value',
+ name: '',
+ axisTick: {
+ alignWithLabel: true,
+ lineStyle: {
+ width: 0
+ }
+ },
+ axisLabel: {
+ color: '#BDBDBD',
+ formatter: '{value}%'
+ },
+ /** 鍧愭爣杞磋酱绾跨浉鍏宠缃� */
+ axisLine: {
+ lineStyle: {
+ color: '#BDBDBD'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#e6e6e6'
+ }
+ }
+ }
+ ],
+ xAxis: {
+ type: 'category',
+ name: '鏃ユ湡',
+ data: []
+ }
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const performanceList = await StatisticsPerformanceApi.getReceivablePricePerformance(
+ props.queryParams
+ )
+
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.xAxis && echartsOption.xAxis['data']) {
+ echartsOption.xAxis['data'] = performanceList.map((s: StatisticsPerformanceRespVO) => s.time)
+ }
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.currentMonthCount
+ )
+ }
+ if (echartsOption.series && echartsOption.series[1] && echartsOption.series[1]['data']) {
+ echartsOption.series[1]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.lastMonthCount
+ )
+ echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
+ s.lastMonthCount !== 0
+ ? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
+ : 'NULL'
+ )
+ }
+ if (echartsOption.series && echartsOption.series[2] && echartsOption.series[1]['data']) {
+ echartsOption.series[2]['data'] = performanceList.map(
+ (s: StatisticsPerformanceRespVO) => s.lastYearCount
+ )
+ echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
+ s.lastYearCount !== 0
+ ? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
+ : 'NULL'
+ )
+ }
+
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = performanceList
+ convertListData()
+ loading.value = false
+}
+
+// 鍒濆鍖栨暟鎹�
+const columnsData = reactive([])
+const tableData = reactive([
+ { title: '褰撴湀鍥炴閲戦缁熻锛堝厓锛�' },
+ { title: '涓婃湀鍥炴閲戦缁熻锛堝厓锛�' },
+ { title: '鍘诲勾褰撴湀鍥炴閲戦缁熻锛堝厓锛�' },
+ { title: '鐜瘮澧為暱鐜囷紙%锛�' },
+ { title: '鍚屾瘮澧為暱鐜囷紙%锛�' }
+])
+
+// 瀹氫箟 init 鏂规硶
+const convertListData = () => {
+ const columnObj = { label: '鏃ユ湡', prop: 'title' }
+ columnsData.splice(0, columnsData.length) //娓呯┖鏁扮粍
+ columnsData.push(columnObj)
+
+ list.value.forEach((item, index) => {
+ const columnObj = { label: item.time, prop: 'prop' + index }
+ columnsData.push(columnObj)
+ tableData[0]['prop' + index] = item.currentMonthCount
+ tableData[1]['prop' + index] = item.lastMonthCount
+ tableData[2]['prop' + index] = item.lastYearCount
+ tableData[3]['prop' + index] =
+ item.lastMonthCount !== 0
+ ? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
+ : 'NULL'
+ tableData[4]['prop' + index] =
+ item.lastYearCount !== 0
+ ? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
+ : 'NULL'
+ })
+}
+
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/performance/index.vue b/src/views/crm/statistics/performance/index.vue
new file mode 100644
index 0000000..822afec
--- /dev/null
+++ b/src/views/crm/statistics/performance/index.vue
@@ -0,0 +1,146 @@
+<!-- 鏁版嵁缁熻 - 鍛樺伐涓氱哗鍒嗘瀽 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閫夋嫨骞翠唤" prop="orderDate">
+ <el-date-picker
+ v-model="queryParams.times[0]"
+ class="!w-240px"
+ type="year"
+ value-format="YYYY"
+ :default-time="[new Date().getFullYear()]"
+ />
+ </el-form-item>
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select
+ v-model="queryParams.deptId"
+ class="!w-240px"
+ :data="deptList"
+ :props="defaultProps"
+ check-strictly
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ @change="queryParams.userId = undefined"
+ />
+ </el-form-item>
+ <el-form-item label="鍛樺伐" prop="userId">
+ <el-select v-model="queryParams.userId" class="!w-240px" placeholder="鍛樺伐" clearable>
+ <el-option
+ v-for="(user, index) in userListByDeptId"
+ :label="user.nickname"
+ :value="user.id"
+ :key="index"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 鎼滅储 </el-button>
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆 </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍛樺伐涓氱哗缁熻 -->
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <!-- 鍛樺伐鍚堝悓缁熻 -->
+ <el-tab-pane label="鍛樺伐鍚堝悓鏁伴噺缁熻" name="ContractCountPerformance" lazy>
+ <ContractCountPerformance :query-params="queryParams" ref="ContractCountPerformanceRef" />
+ </el-tab-pane>
+ <!-- 鍛樺伐鍚堝悓閲戦缁熻 -->
+ <el-tab-pane label="鍛樺伐鍚堝悓閲戦缁熻" name="ContractPricePerformance" lazy>
+ <ContractPricePerformance :query-params="queryParams" ref="ContractPricePerformanceRef" />
+ </el-tab-pane>
+ <!-- 鍛樺伐鍥炴閲戦缁熻 -->
+ <el-tab-pane label="鍛樺伐鍥炴閲戦缁熻" name="ReceivablePricePerformance" lazy>
+ <ReceivablePricePerformance
+ :query-params="queryParams"
+ ref="ReceivablePricePerformanceRef"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { beginOfDay, endOfDay, formatDate } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import ContractCountPerformance from './components/ContractCountPerformance.vue'
+import ContractPricePerformance from './components/ContractPricePerformance.vue'
+import ReceivablePricePerformance from './components/ReceivablePricePerformance.vue'
+
+defineOptions({ name: 'CrmStatisticsCustomer' })
+
+const queryParams = reactive({
+ deptId: useUserStore().getUser.deptId,
+ userId: undefined,
+ times: [
+ formatDate(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
+ formatDate(endOfDay(new Date(new Date().getFullYear(), 11, 31)))
+ ]
+})
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const deptList = ref<Tree[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯
+const userList = ref<UserApi.UserVO[]>([]) // 鍏ㄩ噺鐢ㄦ埛娓呭崟
+// 鏍规嵁閫夋嫨鐨勯儴闂ㄧ瓫閫夊憳宸ユ竻鍗�
+const userListByDeptId = computed(() =>
+ queryParams.deptId
+ ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
+ : []
+)
+
+// 娲昏穬鏍囩
+const activeTab = ref('ContractCountPerformance')
+const ContractCountPerformanceRef = ref() // 鍛樺伐鍚堝悓鏁伴噺缁熻
+const ContractPricePerformanceRef = ref() // 鍛樺伐鍚堝悓閲戦缁熻
+const ReceivablePricePerformanceRef = ref() // 鍛樺伐鍥炴閲戦缁熻
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ // 浠� queryParams.times[0] 涓幏鍙栧埌浜嗗勾浠�
+ const selectYear = parseInt(queryParams.times[0])
+ queryParams.times[0] = formatDate(beginOfDay(new Date(selectYear, 0, 1)))
+ queryParams.times[1] = formatDate(endOfDay(new Date(selectYear, 11, 31)))
+
+ // 鎵ц鏌ヨ
+ switch (activeTab.value) {
+ case 'ContractCountPerformance':
+ ContractCountPerformanceRef.value?.loadData?.()
+ break
+ case 'ContractPricePerformance':
+ ContractPricePerformanceRef.value?.loadData?.()
+ break
+ case 'ReceivablePricePerformance':
+ ReceivablePricePerformanceRef.value?.loadData?.()
+ break
+ }
+}
+
+// 褰� activeTab 鏀瑰彉鏃讹紝鍒锋柊褰撳墠娲诲姩鐨� tab
+watch(activeTab, () => {
+ handleQuery()
+})
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+// 鍔犺浇閮ㄩ棬鏍�
+onMounted(async () => {
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+ userList.value = handleTree(await UserApi.getSimpleUserList())
+})
+</script>
diff --git a/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue
new file mode 100644
index 0000000..513936c
--- /dev/null
+++ b/src/views/crm/statistics/portrait/components/PortraitCustomerArea.vue
@@ -0,0 +1,147 @@
+<!-- 瀹㈡埛鍩庡競鍒嗗竷 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-col>
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption2" />
+ </el-skeleton>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { EChartsOption } from 'echarts'
+import china from '@/assets/map/json/china.json'
+import echarts from '@/plugins/echarts'
+import {
+ CrmStatisticCustomerAreaRespVO,
+ StatisticsPortraitApi
+} from '@/api/crm/statistics/portrait'
+import { areaReplace } from '@/utils'
+
+defineOptions({ name: 'PortraitCustomerArea' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+// 娉ㄥ唽鍦板浘
+echarts?.registerMap('china', china as any)
+
+const loading = ref(false) // 鍔犺浇涓�
+const areaStatisticsList = ref<CrmStatisticCustomerAreaRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鍦板浘閰嶇疆锛堝叏閮ㄥ鎴凤級 */
+const echartsOption = reactive<EChartsOption>({
+ title: {
+ text: '鍏ㄩ儴瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item',
+ showDelay: 0,
+ transitionDuration: 0.2
+ },
+ visualMap: {
+ text: ['楂�', '浣�'],
+ realtime: false,
+ calculable: true,
+ top: 'middle',
+ inRange: {
+ color: ['#fff', '#3b82f6']
+ }
+ },
+ series: [
+ {
+ name: '瀹㈡埛鍦板煙鍒嗗竷',
+ type: 'map',
+ map: 'china',
+ roam: false,
+ selectedMode: false,
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鍦板浘閰嶇疆锛堟垚浜ゅ鎴凤級 */
+const echartsOption2 = reactive<EChartsOption>({
+ title: {
+ text: '鎴愪氦瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item',
+ showDelay: 0,
+ transitionDuration: 0.2
+ },
+ visualMap: {
+ text: ['楂�', '浣�'],
+ realtime: false,
+ calculable: true,
+ top: 'middle',
+ inRange: {
+ color: ['#fff', '#3b82f6']
+ }
+ },
+ series: [
+ {
+ name: '瀹㈡埛鍦板煙鍒嗗竷',
+ type: 'map',
+ map: 'china',
+ roam: false,
+ selectedMode: false,
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const areaList = await StatisticsPortraitApi.getCustomerArea(props.queryParams)
+ areaStatisticsList.value = areaList.map((item: CrmStatisticCustomerAreaRespVO) => {
+ return {
+ ...item,
+ areaName: areaReplace(item.areaName)
+ }
+ })
+ buildLeftMap()
+ buildRightMap()
+ loading.value = false
+}
+defineExpose({ loadData })
+
+const buildLeftMap = () => {
+ let min = 0
+ let max = 0
+ echartsOption.series![0].data = areaStatisticsList.value.map((item) => {
+ min = Math.min(min, item.customerCount || 0)
+ max = Math.max(max, item.customerCount || 0)
+ return { ...item, name: item.areaName, value: item.customerCount || 0 }
+ })
+ echartsOption.visualMap!['min'] = min
+ echartsOption.visualMap!['max'] = max
+}
+
+const buildRightMap = () => {
+ let min = 0
+ let max = 0
+ echartsOption2.series![0].data = areaStatisticsList.value.map((item) => {
+ min = Math.min(min, item.dealCount || 0)
+ max = Math.max(max, item.dealCount || 0)
+ return { ...item, name: item.areaName, value: item.dealCount || 0 }
+ })
+ echartsOption2.visualMap!['min'] = min
+ echartsOption2.visualMap!['max'] = max
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue
new file mode 100644
index 0000000..d426993
--- /dev/null
+++ b/src/views/crm/statistics/portrait/components/PortraitCustomerIndustry.vue
@@ -0,0 +1,198 @@
+<!-- 瀹㈡埛琛屼笟鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-col>
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption2" />
+ </el-skeleton>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card class="mt-16px" shadow="never">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="搴忓彿" type="index" width="80" />
+ <el-table-column align="center" label="瀹㈡埛琛屼笟" prop="industryId" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="scope.row.industryId" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛涓暟" min-width="200" prop="customerCount" />
+ <el-table-column align="center" label="鎴愪氦涓暟" min-width="200" prop="dealCount" />
+ <el-table-column align="center" label="琛屼笟鍗犳瘮(%)" min-width="200" prop="industryPortion" />
+ <el-table-column align="center" label="鎴愪氦鍗犳瘮(%)" min-width="200" prop="dealPortion" />
+ </el-table>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import {
+ CrmStatisticCustomerIndustryRespVO,
+ StatisticsPortraitApi
+} from '@/api/crm/statistics/portrait'
+import { EChartsOption } from 'echarts'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { erpCalculatePercentage, getSumValue } from '@/utils'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'PortraitCustomerIndustry' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticCustomerIndustryRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 楗煎浘閰嶇疆锛堝叏閮ㄥ鎴凤級 */
+const echartsOption = reactive<EChartsOption>({
+ title: {
+ text: '鍏ㄩ儴瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鍏ㄩ儴瀹㈡埛' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '鍏ㄩ儴瀹㈡埛',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 楗煎浘閰嶇疆锛堟垚浜ゅ鎴凤級 */
+const echartsOption2 = reactive<EChartsOption>({
+ title: {
+ text: '鎴愪氦瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鎴愪氦瀹㈡埛' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '鎴愪氦瀹㈡埛',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const industryList = await StatisticsPortraitApi.getCustomerIndustry(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
+ value: r.customerCount
+ }
+ })
+ }
+ // 2.2 鏇存柊 Echarts2 鏁版嵁
+ if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
+ echartsOption2.series[0]['data'] = industryList.map((r: CrmStatisticCustomerIndustryRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
+ value: r.dealCount
+ }
+ })
+ }
+ // 3. 璁$畻姣斾緥
+ calculateProportion(industryList)
+ list.value = industryList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 璁$畻姣斾緥 */
+const calculateProportion = (sourceList: CrmStatisticCustomerIndustryRespVO[]) => {
+ if (isEmpty(sourceList)) {
+ return
+ }
+ // 杩欓噷绫诲瀷涓㈠け浜嗘墍浠ラ噸鏂版悶涓彉閲�
+ const list = sourceList as unknown as CrmStatisticCustomerIndustryRespVO[]
+ const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
+ const sumDealCount = getSumValue(list.map((item) => item.dealCount))
+ list.forEach((item) => {
+ item.industryPortion =
+ item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount)
+ item.dealPortion =
+ item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount)
+ })
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue
new file mode 100644
index 0000000..653feef
--- /dev/null
+++ b/src/views/crm/statistics/portrait/components/PortraitCustomerLevel.vue
@@ -0,0 +1,198 @@
+<!-- 瀹㈡埛鏉ユ簮鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-col>
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption2" />
+ </el-skeleton>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card class="mt-16px" shadow="never">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="搴忓彿" type="index" width="80" />
+ <el-table-column align="center" label="瀹㈡埛绾у埆" prop="level" width="200">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛涓暟" min-width="200" prop="customerCount" />
+ <el-table-column align="center" label="鎴愪氦涓暟" min-width="200" prop="dealCount" />
+ <el-table-column align="center" label="绾у埆鍗犳瘮(%)" min-width="200" prop="levelPortion" />
+ <el-table-column align="center" label="鎴愪氦鍗犳瘮(%)" min-width="200" prop="dealPortion" />
+ </el-table>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import {
+ CrmStatisticCustomerLevelRespVO,
+ StatisticsPortraitApi
+} from '@/api/crm/statistics/portrait'
+import { EChartsOption } from 'echarts'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { erpCalculatePercentage, getSumValue } from '@/utils'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'PortraitCustomerLevel' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticCustomerLevelRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 楗煎浘閰嶇疆锛堝叏閮ㄥ鎴凤級 */
+const echartsOption = reactive<EChartsOption>({
+ title: {
+ text: '鍏ㄩ儴瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鍏ㄩ儴瀹㈡埛' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '鍏ㄩ儴瀹㈡埛',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 楗煎浘閰嶇疆锛堟垚浜ゅ鎴凤級 */
+const echartsOption2 = reactive<EChartsOption>({
+ title: {
+ text: '鎴愪氦瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鎴愪氦瀹㈡埛' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '鎴愪氦瀹㈡埛',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const levelList = await StatisticsPortraitApi.getCustomerLevel(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
+ value: r.customerCount
+ }
+ })
+ }
+ // 2.2 鏇存柊 Echarts2 鏁版嵁
+ if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
+ echartsOption2.series[0]['data'] = levelList.map((r: CrmStatisticCustomerLevelRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
+ value: r.dealCount
+ }
+ })
+ }
+ // 3. 璁$畻姣斾緥
+ calculateProportion(levelList)
+ list.value = levelList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 璁$畻姣斾緥 */
+const calculateProportion = (levelList: CrmStatisticCustomerLevelRespVO[]) => {
+ if (isEmpty(levelList)) {
+ return
+ }
+ // 杩欓噷绫诲瀷涓㈠け浜嗘墍浠ラ噸鏂版悶涓彉閲�
+ const list = levelList as unknown as CrmStatisticCustomerLevelRespVO[]
+ const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
+ const sumDealCount = getSumValue(list.map((item) => item.dealCount))
+ list.forEach((item) => {
+ item.levelPortion =
+ item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount)
+ item.dealPortion =
+ item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount)
+ })
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue b/src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue
new file mode 100644
index 0000000..ade6445
--- /dev/null
+++ b/src/views/crm/statistics/portrait/components/PortraitCustomerSource.vue
@@ -0,0 +1,198 @@
+<!-- 瀹㈡埛鏉ユ簮鍒嗘瀽 -->
+<template>
+ <!-- Echarts鍥� -->
+ <el-card shadow="never">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-col>
+ <el-col :span="12">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption2" />
+ </el-skeleton>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 缁熻鍒楄〃 -->
+ <el-card class="mt-16px" shadow="never">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="搴忓彿" type="index" width="80" />
+ <el-table-column align="center" label="瀹㈡埛鏉ユ簮" prop="source" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="scope.row.source" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛涓暟" min-width="200" prop="customerCount" />
+ <el-table-column align="center" label="鎴愪氦涓暟" min-width="200" prop="dealCount" />
+ <el-table-column align="center" label="鏉ユ簮鍗犳瘮(%)" min-width="200" prop="sourcePortion" />
+ <el-table-column align="center" label="鎴愪氦鍗犳瘮(%)" min-width="200" prop="dealPortion" />
+ </el-table>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import {
+ CrmStatisticCustomerSourceRespVO,
+ StatisticsPortraitApi
+} from '@/api/crm/statistics/portrait'
+import { EChartsOption } from 'echarts'
+import { DICT_TYPE, getDictLabel } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
+import { erpCalculatePercentage, getSumValue } from '@/utils'
+
+defineOptions({ name: 'PortraitCustomerSource' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<CrmStatisticCustomerSourceRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 楗煎浘閰嶇疆锛堝叏閮ㄥ鎴凤級 */
+const echartsOption = reactive<EChartsOption>({
+ title: {
+ text: '鍏ㄩ儴瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鍏ㄩ儴瀹㈡埛' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '鍏ㄩ儴瀹㈡埛',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 楗煎浘閰嶇疆锛堟垚浜ゅ鎴凤級 */
+const echartsOption2 = reactive<EChartsOption>({
+ title: {
+ text: '鎴愪氦瀹㈡埛',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: { show: true, name: '鎴愪氦瀹㈡埛' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ series: [
+ {
+ name: '鎴愪氦瀹㈡埛',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const loadData = async () => {
+ // 1. 鍔犺浇缁熻鏁版嵁
+ loading.value = true
+ const sourceList = await StatisticsPortraitApi.getCustomerSource(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.series && echartsOption.series[0] && echartsOption.series[0]['data']) {
+ echartsOption.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
+ value: r.customerCount
+ }
+ })
+ }
+ // 2.2 鏇存柊 Echarts2 鏁版嵁
+ if (echartsOption2.series && echartsOption2.series[0] && echartsOption2.series[0]['data']) {
+ echartsOption2.series[0]['data'] = sourceList.map((r: CrmStatisticCustomerSourceRespVO) => {
+ return {
+ name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
+ value: r.dealCount
+ }
+ })
+ }
+ // 3. 璁$畻姣斾緥
+ calculateProportion(sourceList)
+ list.value = sourceList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 璁$畻姣斾緥 */
+const calculateProportion = (sourceList: CrmStatisticCustomerSourceRespVO[]) => {
+ if (isEmpty(sourceList)) {
+ return
+ }
+ // 杩欓噷绫诲瀷涓㈠け浜嗘墍浠ラ噸鏂版悶涓彉閲�
+ const list = sourceList as unknown as CrmStatisticCustomerSourceRespVO[]
+ const sumCustomerCount = getSumValue(list.map((item) => item.customerCount))
+ const sumDealCount = getSumValue(list.map((item) => item.dealCount))
+ list.forEach((item) => {
+ item.sourcePortion =
+ item.customerCount === 0 ? 0 : erpCalculatePercentage(item.customerCount, sumCustomerCount)
+ item.dealPortion =
+ item.dealCount === 0 ? 0 : erpCalculatePercentage(item.dealCount, sumDealCount)
+ })
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/portrait/index.vue b/src/views/crm/statistics/portrait/index.vue
new file mode 100644
index 0000000..71807e1
--- /dev/null
+++ b/src/views/crm/statistics/portrait/index.vue
@@ -0,0 +1,156 @@
+<!-- 鏁版嵁缁熻 - 瀹㈡埛鐢诲儚 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鏃堕棿鑼冨洿" prop="orderDate">
+ <el-date-picker
+ v-model="queryParams.times"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ :shortcuts="defaultShortcuts"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select
+ v-model="queryParams.deptId"
+ :data="deptList"
+ :props="defaultProps"
+ check-strictly
+ class="!w-240px"
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ @change="queryParams.userId = undefined"
+ />
+ </el-form-item>
+ <el-form-item label="鍛樺伐" prop="userId">
+ <el-select v-model="queryParams.userId" class="!w-240px" clearable placeholder="鍛樺伐">
+ <el-option
+ v-for="(user, index) in userListByDeptId"
+ :key="index"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 瀹㈡埛缁熻 -->
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <!-- 鍩庡競鍒嗗竷鍒嗘瀽 -->
+ <el-tab-pane label="鍩庡競鍒嗗竷鍒嗘瀽" lazy name="areaRef">
+ <PortraitCustomerArea ref="areaRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 瀹㈡埛绾у埆鍒嗘瀽 -->
+ <el-tab-pane label="瀹㈡埛绾у埆鍒嗘瀽" lazy name="levelRef">
+ <PortraitCustomerLevel ref="levelRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 瀹㈡埛鏉ユ簮鍒嗘瀽 -->
+ <el-tab-pane label="瀹㈡埛鏉ユ簮鍒嗘瀽" lazy name="sourceRef">
+ <PortraitCustomerSource ref="sourceRef" :query-params="queryParams" />
+ </el-tab-pane>
+ <!-- 瀹㈡埛琛屼笟鍒嗘瀽 -->
+ <el-tab-pane label="瀹㈡埛琛屼笟鍒嗘瀽" lazy name="industryRef">
+ <PortraitCustomerIndustry ref="industryRef" :query-params="queryParams" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { useUserStore } from '@/store/modules/user'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { defaultProps, handleTree } from '@/utils/tree'
+import PortraitCustomerArea from './components/PortraitCustomerArea.vue'
+import PortraitCustomerIndustry from './components/PortraitCustomerIndustry.vue'
+import PortraitCustomerSource from './components/PortraitCustomerSource.vue'
+import PortraitCustomerLevel from './components/PortraitCustomerLevel.vue'
+
+defineOptions({ name: 'CrmStatisticsPortrait' })
+
+const queryParams = reactive({
+ deptId: useUserStore().getUser.deptId,
+ userId: undefined,
+ times: [
+ // 榛樿鏄剧ず鏈�杩戜竴鍛ㄧ殑鏁版嵁
+ formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+ formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+ ]
+})
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const deptList = ref<Tree[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯
+const userList = ref<UserApi.UserVO[]>([]) // 鍏ㄩ噺鐢ㄦ埛娓呭崟
+
+/** 鏍规嵁閫夋嫨鐨勯儴闂ㄧ瓫閫夊憳宸ユ竻鍗� */
+const userListByDeptId = computed(() =>
+ queryParams.deptId
+ ? userList.value.filter((u: UserApi.UserVO) => u.deptId === queryParams.deptId)
+ : []
+)
+
+const activeTab = ref('areaRef') // 娲昏穬鏍囩
+const areaRef = ref() // 瀹㈡埛鍦板尯鍒嗗竷
+const levelRef = ref() // 瀹㈡埛绾у埆
+const sourceRef = ref() // 瀹㈡埛鏉ユ簮
+const industryRef = ref() // 瀹㈡埛琛屼笟
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ switch (activeTab.value) {
+ case 'areaRef':
+ areaRef.value?.loadData?.()
+ break
+ case 'levelRef':
+ levelRef.value?.loadData?.()
+ break
+ case 'sourceRef':
+ sourceRef.value?.loadData?.()
+ break
+ case 'industryRef':
+ industryRef.value?.loadData?.()
+ break
+ }
+}
+
+/** 褰� activeTab 鏀瑰彉鏃讹紝鍒锋柊褰撳墠娲诲姩鐨� tab */
+watch(activeTab, () => {
+ handleQuery()
+})
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+ userList.value = handleTree(await UserApi.getSimpleUserList())
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/ContactCountRank.vue b/src/views/crm/statistics/rank/components/ContactCountRank.vue
new file mode 100644
index 0000000..5edc118
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/ContactCountRank.vue
@@ -0,0 +1,98 @@
+<!-- 鏂板鑱旂郴浜烘暟鎺掕 -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column label="鏂板鑱旂郴浜烘暟锛堜釜锛�" align="center" prop="count" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'ContactCountRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '鏂板鑱旂郴浜烘暟鎺掕',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鏂板鑱旂郴浜烘暟鎺掕' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '鏂板鑱旂郴浜烘暟锛堜釜锛�'
+ },
+ yAxis: {
+ type: 'category',
+ name: '鍒涘缓浜�'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏂板鑱旂郴浜烘暟鎺掕 */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getContactsCountRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/ContractCountRank.vue b/src/views/crm/statistics/rank/components/ContractCountRank.vue
new file mode 100644
index 0000000..fc50a6d
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/ContractCountRank.vue
@@ -0,0 +1,98 @@
+<!-- 绛剧害鍚堝悓鎺掕 -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="绛捐浜�" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column label="绛剧害鍚堝悓鏁帮紙涓級" align="center" prop="count" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'ContractCountRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '绛剧害鍚堝悓鎺掕',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '绛剧害鍚堝悓鎺掕' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '绛剧害鍚堝悓鏁帮紙涓級'
+ },
+ yAxis: {
+ type: 'category',
+ name: '绛捐浜�'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇绛剧害鍚堝悓鎺掕 */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getContractCountRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/ContractPriceRank.vue b/src/views/crm/statistics/rank/components/ContractPriceRank.vue
new file mode 100644
index 0000000..b69ebd2
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/ContractPriceRank.vue
@@ -0,0 +1,105 @@
+<!-- 鍚堝悓閲戦鎺掕 -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="绛捐浜�" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column
+ label="鍚堝悓閲戦锛堝厓锛�"
+ align="center"
+ prop="count"
+ min-width="200"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'ContractPriceRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '鍚堝悓閲戦鎺掕',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鍚堝悓閲戦鎺掕' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '鍚堝悓閲戦锛堝厓锛�'
+ },
+ yAxis: {
+ type: 'category',
+ name: '绛捐浜�'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鍚堝悓閲戦鎺掕 */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getContractPriceRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/CustomerCountRank.vue b/src/views/crm/statistics/rank/components/CustomerCountRank.vue
new file mode 100644
index 0000000..b66a681
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/CustomerCountRank.vue
@@ -0,0 +1,98 @@
+<!-- 鏂板瀹㈡埛鏁版帓琛� -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column label="鏂板瀹㈡埛鏁帮紙涓級" align="center" prop="count" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'CustomerCountRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '鏂板瀹㈡埛鏁版帓琛�',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鏂板瀹㈡埛鏁版帓琛�' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '鏂板瀹㈡埛鏁帮紙涓級'
+ },
+ yAxis: {
+ type: 'category',
+ name: '鍒涘缓浜�'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鏂板瀹㈡埛鏁版帓琛� */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getCustomerCountRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/FollowCountRank.vue b/src/views/crm/statistics/rank/components/FollowCountRank.vue
new file mode 100644
index 0000000..43352ab
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/FollowCountRank.vue
@@ -0,0 +1,98 @@
+<!-- 璺熻繘娆℃暟鎺掕 -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="鍛樺伐" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column label="璺熻繘娆℃暟锛堟锛�" align="center" prop="count" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'FollowCountRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '璺熻繘娆℃暟鎺掕',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '璺熻繘娆℃暟鎺掕' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '璺熻繘娆℃暟锛堟锛�'
+ },
+ yAxis: {
+ type: 'category',
+ name: '鍛樺伐'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇璺熻繘娆℃暟鎺掕 */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getFollowCountRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/FollowCustomerCountRank.vue b/src/views/crm/statistics/rank/components/FollowCustomerCountRank.vue
new file mode 100644
index 0000000..92a2205
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/FollowCustomerCountRank.vue
@@ -0,0 +1,98 @@
+<!-- 璺熻繘瀹㈡埛鏁版帓琛� -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="鍛樺伐" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column label="璺熻繘瀹㈡埛鏁帮紙涓級" align="center" prop="count" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'FollowCustomerCountRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '璺熻繘瀹㈡埛鏁版帓琛�',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '璺熻繘瀹㈡埛鏁版帓琛�' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '璺熻繘瀹㈡埛鏁帮紙涓級'
+ },
+ yAxis: {
+ type: 'category',
+ name: '鍛樺伐'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇璺熻繘瀹㈡埛鏁版帓琛� */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getFollowCustomerCountRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/ProductSalesRank.vue b/src/views/crm/statistics/rank/components/ProductSalesRank.vue
new file mode 100644
index 0000000..e2a02b7
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/ProductSalesRank.vue
@@ -0,0 +1,98 @@
+<!-- 浜у搧閿�閲忔帓琛� -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="鍛樺伐" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column label="浜у搧閿�閲�" align="center" prop="count" min-width="200" />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+
+defineOptions({ name: 'ProductSalesRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '浜у搧閿�閲忔帓琛�',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '浜у搧閿�閲忔帓琛�' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '浜у搧閿�閲�'
+ },
+ yAxis: {
+ type: 'category',
+ name: '鍛樺伐'
+ }
+}) as EChartsOption
+
+/** 鑾峰彇浜у搧閿�閲忔帓琛� */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getProductSalesRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/components/ReceivablePriceRank.vue b/src/views/crm/statistics/rank/components/ReceivablePriceRank.vue
new file mode 100644
index 0000000..06d7d9f
--- /dev/null
+++ b/src/views/crm/statistics/rank/components/ReceivablePriceRank.vue
@@ -0,0 +1,106 @@
+<!-- 鍥炴閲戦鎺掕 -->
+<template>
+ <!-- 鏌辩姸鍥� -->
+ <el-card shadow="never">
+ <el-skeleton :loading="loading" animated>
+ <Echart :height="500" :options="echartsOption" />
+ </el-skeleton>
+ </el-card>
+
+ <!-- 鎺掕鍒楄〃 -->
+ <el-card shadow="never" class="mt-16px">
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃鎺掑悕" align="center" type="index" width="80" />
+ <el-table-column label="绛捐浜�" align="center" prop="nickname" min-width="200" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="deptName" min-width="200" />
+ <el-table-column
+ label="鍥炴閲戦锛堝厓锛�"
+ align="center"
+ prop="count"
+ min-width="200"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ </el-card>
+</template>
+<script setup lang="ts">
+import { StatisticsRankApi, StatisticsRankRespVO } from '@/api/crm/statistics/rank'
+import { EChartsOption } from 'echarts'
+import { clone } from 'lodash-es'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+defineOptions({ name: 'ReceivablePriceRank' })
+const props = defineProps<{ queryParams: any }>() // 鎼滅储鍙傛暟
+
+const loading = ref(false) // 鍔犺浇涓�
+const list = ref<StatisticsRankRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌辩姸鍥鹃厤缃細妯悜 */
+const echartsOption = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['nickname', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ {
+ name: '鍥炴閲戦鎺掕',
+ type: 'bar'
+ }
+ ],
+ toolbox: {
+ feature: {
+ dataZoom: {
+ yAxisIndex: false // 鏁版嵁鍖哄煙缂╂斁锛歒 杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鍥炴閲戦鎺掕' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'value',
+ name: '鍥炴閲戦锛堝厓锛�'
+ },
+ yAxis: {
+ type: 'category',
+ name: '绛捐浜�',
+ nameGap: 30
+ }
+}) as EChartsOption
+
+/** 鑾峰彇鍥炴閲戦鎺掕 */
+const loadData = async () => {
+ // 1. 鍔犺浇鎺掕鏁版嵁
+ loading.value = true
+ const rankingList = await StatisticsRankApi.getReceivablePriceRank(props.queryParams)
+ // 2.1 鏇存柊 Echarts 鏁版嵁
+ if (echartsOption.dataset && echartsOption.dataset['source']) {
+ echartsOption.dataset['source'] = clone(rankingList).reverse()
+ }
+ // 2.2 鏇存柊鍒楄〃鏁版嵁
+ list.value = rankingList
+ loading.value = false
+}
+defineExpose({ loadData })
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/crm/statistics/rank/index.vue b/src/views/crm/statistics/rank/index.vue
new file mode 100644
index 0000000..98340cc
--- /dev/null
+++ b/src/views/crm/statistics/rank/index.vue
@@ -0,0 +1,163 @@
+<!-- BI 鎺掕鐗� -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏃堕棿鑼冨洿" prop="orderDate">
+ <el-date-picker
+ v-model="queryParams.times"
+ :shortcuts="defaultShortcuts"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ />
+ </el-form-item>
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select
+ v-model="queryParams.deptId"
+ :data="deptList"
+ :props="defaultProps"
+ check-strictly
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鎺掕鏁版嵁 -->
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <!-- 鍚堝悓閲戦鎺掕 -->
+ <el-tab-pane label="鍚堝悓閲戦鎺掕" name="contractPriceRank" lazy>
+ <ContractPriceRank :query-params="queryParams" ref="contractPriceRankRef" />
+ </el-tab-pane>
+ <!-- 鍥炴閲戦鎺掕 -->
+ <el-tab-pane label="鍥炴閲戦鎺掕" name="receivablePriceRank" lazy>
+ <ReceivablePriceRank :query-params="queryParams" ref="receivablePriceRankRef" />
+ </el-tab-pane>
+ <!-- 绛剧害鍚堝悓鎺掕 -->
+ <el-tab-pane label="绛剧害鍚堝悓鎺掕" name="contractCountRank" lazy>
+ <ContractCountRank :query-params="queryParams" ref="contractCountRankRef" />
+ </el-tab-pane>
+ <!-- 浜у搧閿�閲忔帓琛� -->
+ <el-tab-pane label="浜у搧閿�閲忔帓琛�" name="productSalesRank" lazy>
+ <ProductSalesRank :query-params="queryParams" ref="productSalesRankRef" />
+ </el-tab-pane>
+ <!-- 鏂板瀹㈡埛鏁版帓琛� -->
+ <el-tab-pane label="鏂板瀹㈡埛鏁版帓琛�" name="customerCountRank" lazy>
+ <CustomerCountRank :query-params="queryParams" ref="customerCountRankRef" />
+ </el-tab-pane>
+ <!-- 鏂板鑱旂郴浜烘暟鎺掕 -->
+ <el-tab-pane label="鏂板鑱旂郴浜烘暟鎺掕" name="contactCountRank" lazy>
+ <ContactCountRank :query-params="queryParams" ref="contactCountRankRef" />
+ </el-tab-pane>
+ <!-- 璺熻繘娆℃暟鎺掕 -->
+ <el-tab-pane label="璺熻繘娆℃暟鎺掕" name="followCountRank" lazy>
+ <FollowCountRank :query-params="queryParams" ref="followCountRankRef" />
+ </el-tab-pane>
+ <!-- 璺熻繘瀹㈡埛鏁版帓琛� -->
+ <el-tab-pane label="璺熻繘瀹㈡埛鏁版帓琛�" name="followCustomerCountRank" lazy>
+ <FollowCustomerCountRank :query-params="queryParams" ref="followCustomerCountRankRef" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+<script lang="ts" setup>
+import ContractPriceRank from './components/ContractPriceRank.vue'
+import ReceivablePriceRank from './components/ReceivablePriceRank.vue'
+import ContractCountRank from './components/ContractCountRank.vue'
+import ProductSalesRank from './components/ProductSalesRank.vue'
+import CustomerCountRank from './components/CustomerCountRank.vue'
+import ContactCountRank from './components/ContactCountRank.vue'
+import FollowCountRank from './components/FollowCountRank.vue'
+import FollowCustomerCountRank from './components/FollowCustomerCountRank.vue'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { useUserStore } from '@/store/modules/user'
+
+defineOptions({ name: 'CrmStatisticsRank' })
+
+const queryParams = reactive({
+ deptId: useUserStore().getUser.deptId,
+ times: [
+ // 榛樿鏄剧ず鏈�杩戜竴鍛ㄧ殑鏁版嵁
+ formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+ formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+ ]
+})
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const deptList = ref<Tree[]>([]) // 鏍戝舰缁撴瀯
+const activeTab = ref('contractPriceRank')
+const contractPriceRankRef = ref() // ContractPriceRank 缁勪欢鐨勫紩鐢�
+const receivablePriceRankRef = ref() // ReceivablePriceRank 缁勪欢鐨勫紩鐢�
+const contractCountRankRef = ref() // ContractCountRank 缁勪欢鐨勫紩鐢�
+const productSalesRankRef = ref() // ProductSalesRank 缁勪欢鐨勫紩鐢�
+const customerCountRankRef = ref() // CustomerCountRank 缁勪欢鐨勫紩鐢�
+const contactCountRankRef = ref() // ContactCountRank 缁勪欢鐨勫紩鐢�
+const followCountRankRef = ref() // FollowCountRank 缁勪欢鐨勫紩鐢�
+const followCustomerCountRankRef = ref() // FollowCustomerCountRank 缁勪欢鐨勫紩鐢�
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ switch (activeTab.value) {
+ case 'contractPriceRank': // 鍚堝悓閲戦鎺掕
+ contractPriceRankRef.value?.loadData?.()
+ break
+ case 'receivablePriceRank': // 鍥炴閲戦鎺掕
+ receivablePriceRankRef.value?.loadData?.()
+ break
+ case 'contractCountRank': // 绛剧害鍚堝悓鎺掕
+ contractCountRankRef.value?.loadData?.()
+ break
+ case 'productSalesRank': // 浜у搧閿�閲忔帓琛�
+ productSalesRankRef.value?.loadData?.()
+ break
+ case 'customerCountRank': // 鏂板瀹㈡埛鏁版帓琛�
+ customerCountRankRef.value?.loadData?.()
+ break
+ case 'contactCountRank': // 鏂板鑱旂郴浜烘暟鎺掕
+ contactCountRankRef.value?.loadData?.()
+ break
+ case 'followCountRank': // 璺熻繘娆℃暟鎺掕
+ followCountRankRef.value?.loadData?.()
+ break
+ case 'followCustomerCountRank': // 璺熻繘瀹㈡埛鏁版帓琛�
+ followCustomerCountRankRef.value?.loadData?.()
+ break
+ }
+}
+
+// 褰� activeTab 鏀瑰彉鏃讹紝鍒锋柊褰撳墠娲诲姩鐨� tab
+watch(activeTab, () => {
+ handleQuery()
+})
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+// 鍔犺浇閮ㄩ棬鏍�
+onMounted(async () => {
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+})
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/erp/finance/account/AccountForm.vue b/src/views/erp/finance/account/AccountForm.vue
new file mode 100644
index 0000000..bde4bd2
--- /dev/null
+++ b/src/views/erp/finance/account/AccountForm.vue
@@ -0,0 +1,124 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="缂栫爜" prop="no">
+ <el-input v-model="formData.no" placeholder="璇疯緭鍏ョ紪鐮�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input v-model="formData.sort" placeholder="璇疯緭鍏ユ帓搴�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 缁撶畻 琛ㄥ崟 */
+defineOptions({ name: 'AccountForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ no: undefined,
+ remark: undefined,
+ status: undefined,
+ sort: undefined,
+ defaultStatus: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await AccountApi.getAccount(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as AccountVO
+ if (formType.value === 'create') {
+ await AccountApi.createAccount(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await AccountApi.updateAccount(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ no: undefined,
+ remark: undefined,
+ status: undefined,
+ sort: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/finance/account/index.vue b/src/views/erp/finance/account/index.vue
new file mode 100644
index 0000000..8d85ef3
--- /dev/null
+++ b/src/views/erp/finance/account/index.vue
@@ -0,0 +1,235 @@
+<template>
+ <doc-alert
+ title="銆愯储鍔°�戦噰璐粯娆俱�侀攢鍞敹娆�"
+ url="https://doc.iocoder.cn/sale/finance-payment-receipt/"
+ />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缂栫爜" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ョ紪鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:account:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:account:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="缂栫爜" align="center" prop="no" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鏄惁榛樿" align="center" prop="defaultStatus">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.defaultStatus"
+ :active-value="true"
+ :inactive-value="false"
+ @change="handleDefaultStatusChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:account:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:account:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <AccountForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import AccountForm from './AccountForm.vue'
+
+/** ERP 缁撶畻璐︽埛 鍒楄〃 */
+defineOptions({ name: 'ErpAccount' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<AccountVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ remark: undefined,
+ status: undefined,
+ name: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await AccountApi.getAccountPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await AccountApi.deleteAccount(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼榛樿鐘舵�� */
+const handleDefaultStatusChange = async (row: WarehouseVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.defaultStatus ? '璁剧疆' : '鍙栨秷'
+ await message.confirm('纭瑕�' + text + '"' + row.name + '"榛樿鍚�?')
+ // 鍙戣捣淇敼鐘舵��
+ await AccountApi.updateAccountDefaultStatus(row.id, row.defaultStatus)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch (e) {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.defaultStatus = !row.defaultStatus
+ }
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await AccountApi.exportAccount(queryParams)
+ download.excel(data, 'ERP 缁撶畻璐︽埛.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/erp/finance/payment/FinancePaymentForm.vue b/src/views/erp/finance/payment/FinancePaymentForm.vue
new file mode 100644
index 0000000..3da2e6e
--- /dev/null
+++ b/src/views/erp/finance/payment/FinancePaymentForm.vue
@@ -0,0 +1,278 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浠樻鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠樻鏃堕棿" prop="paymentTime">
+ <el-date-picker
+ v-model="formData.paymentTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨浠樻鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="formData.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璐㈠姟浜哄憳" prop="financeUserId">
+ <el-select
+ v-model="formData.financeUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨璐㈠姟浜哄憳"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="閲囪喘鍏ュ簱銆侀��璐у崟" name="item">
+ <FinancePaymentItemForm
+ ref="itemFormRef"
+ :supplier-id="formData.supplierId"
+ :items="formData.items"
+ :disabled="disabled"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浠樻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍚堣浠樻" prop="totalPrice">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬閲戦" prop="discountPrice">
+ <el-input-number
+ v-model="formData.discountPrice"
+ controls-position="right"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犻噾棰�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹為檯浠樻">
+ <el-input
+ disabled
+ v-model="formData.paymentPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { FinancePaymentApi, FinancePaymentVO } from '@/api/erp/finance/payment'
+import FinancePaymentItemForm from './components/FinancePaymentItemForm.vue'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import * as UserApi from '@/api/system/user'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 浠樻鍗曡〃鍗� */
+defineOptions({ name: 'FinancePaymentForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ financeUserId: undefined,
+ paymentTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ totalPrice: 0,
+ discountPrice: 0,
+ paymentPrice: 0,
+ items: [],
+ no: undefined // 璁㈠崟鍗曞彿锛屽悗绔繑鍥�
+})
+const formRules = reactive({
+ supplierId: [{ required: true, message: '渚涘簲鍟嗕笉鑳戒负绌�', trigger: 'blur' }],
+ paymentTime: [{ required: true, message: '璁㈠崟鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.paymentPrice, 0)
+ formData.value.totalPrice = totalPrice
+ formData.value.paymentPrice = totalPrice - val.discountPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await FinancePaymentApi.getFinancePayment(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇渚涘簲鍟嗗垪琛�
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as FinancePaymentVO
+ if (formType.value === 'create') {
+ await FinancePaymentApi.createFinancePayment(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await FinancePaymentApi.updateFinancePayment(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ financeUserId: undefined,
+ paymentTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ totalPrice: 0,
+ discountPrice: 0,
+ paymentPrice: 0,
+ items: [],
+ no: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/finance/payment/components/FinancePaymentItemForm.vue b/src/views/erp/finance/payment/components/FinancePaymentItemForm.vue
new file mode 100644
index 0000000..ea0e085
--- /dev/null
+++ b/src/views/erp/finance/payment/components/FinancePaymentItemForm.vue
@@ -0,0 +1,182 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="閲囪喘鍗曟嵁缂栧彿" min-width="200">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.bizNo" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴斾粯閲戦" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="宸蹭粯閲戦" prop="paidPrice" fixed="right" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.paidPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈浠樻" prop="paymentPrice" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.paymentPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.paymentPrice"
+ controls-position="right"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleOpenPurchaseIn" round>+ 娣诲姞閲囪喘鍏ュ簱鍗�</el-button>
+ <el-button @click="handleOpenPurchaseReturn" round>+ 娣诲姞閲囪喘閫�璐у崟</el-button>
+ </el-row>
+
+ <!-- 鍙粯娆剧殑銆愰噰璐叆搴撳崟銆戝垪琛� -->
+ <PurchaseInPaymentEnableList
+ ref="purchaseInPaymentEnableListRef"
+ @success="handleAddPurchaseIn"
+ />
+ <!-- 鍙粯娆剧殑銆愰噰璐叆搴撳崟銆戝垪琛� -->
+ <PurchaseReturnRefundEnableList
+ ref="purchaseReturnRefundEnableListRef"
+ @success="handleAddPurchaseReturn"
+ />
+</template>
+<script setup lang="ts">
+import { ProductVO } from '@/api/erp/product/product'
+import { erpPriceInputFormatter, getSumValue } from '@/utils'
+import PurchaseInPaymentEnableList from '@/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue'
+import PurchaseReturnRefundEnableList from '@/views/erp/purchase/return/components/PurchaseReturnRefundEnableList.vue'
+import { PurchaseInVO } from '@/api/erp/purchase/in'
+import { ErpBizType } from '@/utils/constants'
+import { PurchaseReturnVO } from '@/api/erp/purchase/return'
+
+const props = defineProps<{
+ items: undefined
+ supplierId: undefined
+ disabled: false
+}>()
+const message = useMessage()
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ paymentPrice: [{ required: true, message: '鏈浠樻涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 鍒濆鍖栬缃叆搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['totalPrice', 'paidPrice', 'paymentPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] = erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+ return sums
+}
+
+/** 鏂板銆愰噰璐叆搴撱�戞寜閽搷浣� */
+const purchaseInPaymentEnableListRef = ref()
+const handleOpenPurchaseIn = () => {
+ if (!props.supplierId) {
+ message.error('璇烽�夋嫨渚涘簲鍟�')
+ return
+ }
+ purchaseInPaymentEnableListRef.value.open(props.supplierId)
+}
+const handleAddPurchaseIn = (rows: PurchaseInVO[]) => {
+ rows.forEach((row) => {
+ formData.value.push({
+ bizId: row.id,
+ bizType: ErpBizType.PURCHASE_IN,
+ bizNo: row.no,
+ totalPrice: row.totalPrice,
+ paidPrice: row.paymentPrice,
+ paymentPrice: row.totalPrice - row.paymentPrice
+ })
+ })
+}
+
+/** 鏂板銆愰噰璐��璐с�戞寜閽搷浣� */
+const purchaseReturnRefundEnableListRef = ref()
+const handleOpenPurchaseReturn = () => {
+ if (!props.supplierId) {
+ message.error('璇烽�夋嫨渚涘簲鍟�')
+ return
+ }
+ purchaseReturnRefundEnableListRef.value.open(props.supplierId)
+}
+const handleAddPurchaseReturn = (rows: PurchaseReturnVO[]) => {
+ rows.forEach((row) => {
+ formData.value.push({
+ bizId: row.id,
+ bizType: ErpBizType.PURCHASE_RETURN,
+ bizNo: row.no,
+ totalPrice: -row.totalPrice,
+ paidPrice: -row.refundPrice,
+ paymentPrice: -row.totalPrice + row.refundPrice
+ })
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+</script>
diff --git a/src/views/erp/finance/payment/index.vue b/src/views/erp/finance/payment/index.vue
new file mode 100644
index 0000000..a449ca5
--- /dev/null
+++ b/src/views/erp/finance/payment/index.vue
@@ -0,0 +1,394 @@
+<template>
+ <doc-alert
+ title="銆愯储鍔°�戦噰璐粯娆俱�侀攢鍞敹娆�"
+ url="https://doc.iocoder.cn/sale/finance-payment-receipt/"
+ />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="浠樻鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヤ粯娆惧崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浠樻鏃堕棿" prop="paymentTime">
+ <el-date-picker
+ v-model="queryParams.paymentTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="queryParams.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涗緵搴斿晢"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璐㈠姟浜哄憳" prop="financeUserId">
+ <el-select
+ v-model="queryParams.financeUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨璐㈠姟浜哄憳"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻璐︽埛" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠樻璐︽埛"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閲囪喘鍗曞彿" prop="bizNo">
+ <el-input
+ v-model="queryParams.bizNo"
+ placeholder="璇疯緭鍏ラ噰璐崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:finance-payment:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:finance-payment:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:finance-payment:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="浠樻鍗曞彿" align="center" prop="no" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column
+ label="浠樻鏃堕棿"
+ align="center"
+ prop="paymentTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column label="璐㈠姟浜哄憳" align="center" prop="financeUserName" />
+ <el-table-column label="浠樻璐︽埛" align="center" prop="accountName" />
+ <el-table-column
+ label="鍚堣浠樻"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="浼樻儬閲戦"
+ align="center"
+ prop="discountPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="瀹為檯浠樻"
+ align="center"
+ prop="paymentPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:finance-payment:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:finance-payment:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:finance-payment:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:finance-payment:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:finance-payment:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <FinancePaymentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { FinancePaymentApi, FinancePaymentVO } from '@/api/erp/finance/payment'
+import FinancePaymentForm from './FinancePaymentForm.vue'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 浠樻鍗曞垪琛� */
+defineOptions({ name: 'ErpPurchaseOrder' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<FinancePaymentVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ paymentTime: [],
+ supplierId: undefined,
+ creator: undefined,
+ financeUserId: undefined,
+ accountId: undefined,
+ status: undefined,
+ remark: undefined,
+ bizNo: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FinancePaymentApi.getFinancePaymentPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await FinancePaymentApi.deleteFinancePayment(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヤ粯娆惧崟鍚楋紵`)
+ // 鍙戣捣瀹℃壒
+ await FinancePaymentApi.updateFinancePaymentStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await FinancePaymentApi.exportFinancePayment(queryParams)
+ download.excel(data, '浠樻鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<FinancePaymentVO[]>([])
+const handleSelectionChange = (rows: FinancePaymentVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇渚涘簲鍟嗐�佺敤鎴枫�佽处鎴�
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+ accountList.value = await AccountApi.getAccountSimpleList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/finance/receipt/FinanceReceiptForm.vue b/src/views/erp/finance/receipt/FinanceReceiptForm.vue
new file mode 100644
index 0000000..96826eb
--- /dev/null
+++ b/src/views/erp/finance/receipt/FinanceReceiptForm.vue
@@ -0,0 +1,278 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鏀舵鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏀舵鏃堕棿" prop="receiptTime">
+ <el-date-picker
+ v-model="formData.receiptTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鏀舵鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璐㈠姟浜哄憳" prop="financeUserId">
+ <el-select
+ v-model="formData.financeUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨璐㈠姟浜哄憳"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="閲囪喘鍏ュ簱銆侀��璐у崟" name="item">
+ <FinanceReceiptItemForm
+ ref="itemFormRef"
+ :customer-id="formData.customerId"
+ :items="formData.items"
+ :disabled="disabled"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鏀舵璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍚堣鏀舵" prop="totalPrice">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬閲戦" prop="discountPrice">
+ <el-input-number
+ v-model="formData.discountPrice"
+ controls-position="right"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犻噾棰�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹為檯鏀舵">
+ <el-input
+ disabled
+ v-model="formData.receiptPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { FinanceReceiptApi, FinanceReceiptVO } from '@/api/erp/finance/receipt'
+import FinanceReceiptItemForm from './components/FinanceReceiptItemForm.vue'
+import { erpPriceInputFormatter } from '@/utils'
+import * as UserApi from '@/api/system/user'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+
+/** ERP 鏀舵鍗曡〃鍗� */
+defineOptions({ name: 'FinanceReceiptForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ financeUserId: undefined,
+ receiptTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ totalPrice: 0,
+ discountPrice: 0,
+ receiptPrice: 0,
+ items: [],
+ no: undefined // 璁㈠崟鍗曞彿锛屽悗绔繑鍥�
+})
+const formRules = reactive({
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ receiptTime: [{ required: true, message: '璁㈠崟鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.receiptPrice, 0)
+ formData.value.totalPrice = totalPrice
+ formData.value.receiptPrice = totalPrice - val.discountPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await FinanceReceiptApi.getFinanceReceipt(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as FinanceReceiptVO
+ if (formType.value === 'create') {
+ await FinanceReceiptApi.createFinanceReceipt(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await FinanceReceiptApi.updateFinanceReceipt(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ financeUserId: undefined,
+ receiptTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ totalPrice: 0,
+ discountPrice: 0,
+ receiptPrice: 0,
+ items: [],
+ no: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/finance/receipt/components/FinanceReceiptItemForm.vue b/src/views/erp/finance/receipt/components/FinanceReceiptItemForm.vue
new file mode 100644
index 0000000..1a48b41
--- /dev/null
+++ b/src/views/erp/finance/receipt/components/FinanceReceiptItemForm.vue
@@ -0,0 +1,176 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="閿�鍞崟鎹紪鍙�" min-width="200">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.bizNo" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴斾粯閲戦" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="宸蹭粯閲戦" prop="receiptedPrice" fixed="right" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.receiptedPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈鏀舵" prop="receiptPrice" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.receiptPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.receiptPrice"
+ controls-position="right"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleOpenSaleOut" round>+ 娣诲姞閿�鍞嚭搴撳崟</el-button>
+ <el-button @click="handleOpenSaleReturn" round>+ 娣诲姞閿�鍞��璐у崟</el-button>
+ </el-row>
+
+ <!-- 鍙敹娆剧殑銆愰攢鍞嚭搴撳崟銆戝垪琛� -->
+ <SaleOutReceiptEnableList ref="saleOutReceiptEnableListRef" @success="handleAddSaleOut" />
+ <!-- 鍙敹娆剧殑銆愰攢鍞嚭搴撳崟銆戝垪琛� -->
+ <SaleReturnRefundEnableList ref="saleReturnRefundEnableListRef" @success="handleAddSaleReturn" />
+</template>
+<script setup lang="ts">
+import { ProductVO } from '@/api/erp/product/product'
+import { erpPriceInputFormatter, getSumValue } from '@/utils'
+import SaleOutReceiptEnableList from '@/views/erp/sale/out/components/SaleOutReceiptEnableList.vue'
+import SaleReturnRefundEnableList from '@/views/erp/sale/return/components/SaleReturnRefundEnableList.vue'
+import { SaleOutVO } from '@/api/erp/sale/out'
+import { ErpBizType } from '@/utils/constants'
+import { SaleReturnVO } from '@/api/erp/sale/return'
+
+const props = defineProps<{
+ items: undefined
+ customerId: undefined
+ disabled: false
+}>()
+const message = useMessage()
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ receiptPrice: [{ required: true, message: '鏈鏀舵涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 鍒濆鍖栬缃嚭搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['totalPrice', 'receiptedPrice', 'receiptPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] = erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+ return sums
+}
+
+/** 鏂板銆愰攢鍞嚭搴撱�戞寜閽搷浣� */
+const saleOutReceiptEnableListRef = ref()
+const handleOpenSaleOut = () => {
+ if (!props.customerId) {
+ message.error('璇烽�夋嫨瀹㈡埛')
+ return
+ }
+ saleOutReceiptEnableListRef.value.open(props.customerId)
+}
+const handleAddSaleOut = (rows: SaleOutVO[]) => {
+ rows.forEach((row) => {
+ formData.value.push({
+ bizId: row.id,
+ bizType: ErpBizType.SALE_OUT,
+ bizNo: row.no,
+ totalPrice: row.totalPrice,
+ receiptedPrice: row.receiptPrice,
+ receiptPrice: row.totalPrice - row.receiptPrice
+ })
+ })
+}
+
+/** 鏂板銆愰攢鍞��璐с�戞寜閽搷浣� */
+const saleReturnRefundEnableListRef = ref()
+const handleOpenSaleReturn = () => {
+ if (!props.customerId) {
+ message.error('璇烽�夋嫨瀹㈡埛')
+ return
+ }
+ saleReturnRefundEnableListRef.value.open(props.customerId)
+}
+const handleAddSaleReturn = (rows: SaleReturnVO[]) => {
+ rows.forEach((row) => {
+ formData.value.push({
+ bizId: row.id,
+ bizType: ErpBizType.SALE_RETURN,
+ bizNo: row.no,
+ totalPrice: -row.totalPrice,
+ receiptedPrice: -row.refundPrice,
+ receiptPrice: -row.totalPrice + row.refundPrice
+ })
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+</script>
diff --git a/src/views/erp/finance/receipt/index.vue b/src/views/erp/finance/receipt/index.vue
new file mode 100644
index 0000000..d754c46
--- /dev/null
+++ b/src/views/erp/finance/receipt/index.vue
@@ -0,0 +1,394 @@
+<template>
+ <doc-alert
+ title="銆愯储鍔°�戦噰璐粯娆俱�侀攢鍞敹娆�"
+ url="https://doc.iocoder.cn/sale/finance-payment-receipt/"
+ />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏀舵鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ユ敹娆惧崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏀舵鏃堕棿" prop="receiptTime">
+ <el-date-picker
+ v-model="queryParams.receiptTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘鎴�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璐㈠姟浜哄憳" prop="financeUserId">
+ <el-select
+ v-model="queryParams.financeUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨璐㈠姟浜哄憳"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀舵璐︽埛" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鏀舵璐︽埛"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閲囪喘鍗曞彿" prop="bizNo">
+ <el-input
+ v-model="queryParams.bizNo"
+ placeholder="璇疯緭鍏ラ噰璐崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:finance-receipt:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:finance-receipt:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:finance-receipt:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鏀舵鍗曞彿" align="center" prop="no" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column
+ label="鏀舵鏃堕棿"
+ align="center"
+ prop="receiptTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column label="璐㈠姟浜哄憳" align="center" prop="financeUserName" />
+ <el-table-column label="鏀舵璐︽埛" align="center" prop="accountName" />
+ <el-table-column
+ label="鍚堣鏀舵"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="浼樻儬閲戦"
+ align="center"
+ prop="discountPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="瀹為檯鏀舵"
+ align="center"
+ prop="receiptPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:finance-receipt:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:finance-receipt:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:finance-receipt:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:finance-receipt:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:finance-receipt:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <FinanceReceiptForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { FinanceReceiptApi, FinanceReceiptVO } from '@/api/erp/finance/receipt'
+import FinanceReceiptForm from './FinanceReceiptForm.vue'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpPriceTableColumnFormatter } from '@/utils'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+
+/** ERP 鏀舵鍗曞垪琛� */
+defineOptions({ name: 'ErpPurchaseOrder' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<FinanceReceiptVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ receiptTime: [],
+ customerId: undefined,
+ creator: undefined,
+ financeUserId: undefined,
+ accountId: undefined,
+ status: undefined,
+ remark: undefined,
+ bizNo: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FinanceReceiptApi.getFinanceReceiptPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await FinanceReceiptApi.deleteFinanceReceipt(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ユ敹娆惧崟鍚楋紵`)
+ // 鍙戣捣瀹℃壒
+ await FinanceReceiptApi.updateFinanceReceiptStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await FinanceReceiptApi.exportFinanceReceipt(queryParams)
+ download.excel(data, '鏀舵鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<FinanceReceiptVO[]>([])
+const handleSelectionChange = (rows: FinanceReceiptVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇瀹㈡埛銆佺敤鎴枫�佽处鎴�
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+ accountList.value = await AccountApi.getAccountSimpleList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/home/components/SummaryCard.vue b/src/views/erp/home/components/SummaryCard.vue
new file mode 100644
index 0000000..21a02e2
--- /dev/null
+++ b/src/views/erp/home/components/SummaryCard.vue
@@ -0,0 +1,21 @@
+<template>
+ <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+ <div class="flex items-center justify-between text-gray-500">
+ <span>{{ title }}</span>
+ </div>
+ <div class="flex flex-row items-baseline justify-between">
+ <CountTo prefix="锟�" :end-val="value" :decimals="2" :duration="500" class="text-3xl" />
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+/** 浠锋牸灞曠ず Card */
+defineOptions({ name: 'ErpSummaryCard' })
+
+defineProps({
+ title: propTypes.string.def('').isRequired,
+ value: propTypes.number.def(0).isRequired
+})
+</script>
diff --git a/src/views/erp/home/components/TimeSummaryChart.vue b/src/views/erp/home/components/TimeSummaryChart.vue
new file mode 100644
index 0000000..127fa87
--- /dev/null
+++ b/src/views/erp/home/components/TimeSummaryChart.vue
@@ -0,0 +1,86 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <CardTitle :title="props.title" />
+ </template>
+ <!-- 鎶樼嚎鍥� -->
+ <Echart :height="300" :options="lineChartOptions" />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { EChartsOption } from 'echarts'
+import { formatDate } from '@/utils/formatTime'
+import { CardTitle } from '@/components/Card'
+import { propTypes } from '@/utils/propTypes'
+
+/** 浼氬憳鐢ㄦ埛缁熻鍗$墖 */
+defineOptions({ name: 'MemberStatisticsCard' })
+
+const props = defineProps({
+ title: propTypes.string.def('').isRequired,
+ value: propTypes.object.isRequired
+})
+
+/** 鎶樼嚎鍥鹃厤缃� */
+const lineChartOptions = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['time', 'price'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [{ name: '閲戦', type: 'line', smooth: true, areaStyle: {} }],
+ toolbox: {
+ feature: {
+ // 鏁版嵁鍖哄煙缂╂斁
+ dataZoom: {
+ yAxisIndex: false // Y杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: props.title } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ },
+ padding: [5, 10]
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ axisTick: {
+ show: false
+ }
+ },
+ yAxis: {
+ axisTick: {
+ show: false
+ }
+ }
+}) as EChartsOption
+
+watch(
+ () => props.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ // 鏇存柊 Echarts 鏁版嵁
+ if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+ lineChartOptions.dataset['source'] = val
+ }
+ }
+)
+</script>
diff --git a/src/views/erp/home/index.vue b/src/views/erp/home/index.vue
new file mode 100644
index 0000000..e399f9a
--- /dev/null
+++ b/src/views/erp/home/index.vue
@@ -0,0 +1,93 @@
+<template>
+ <doc-alert title="ERP 鎵嬪唽锛堝姛鑳藉紑鍚級" url="https://doc.iocoder.cn/erp/build/" />
+
+ <div class="flex flex-col">
+ <!-- 閿�鍞�/閲囪喘鐨勫叏灞�缁熻 -->
+ <el-row :gutter="16" class="row">
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="浠婃棩閿�鍞�" :value="saleSummary?.todayPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="鏄ㄦ棩閿�鍞�" :value="saleSummary?.yesterdayPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="浠婃棩閲囪喘" :value="purchaseSummary?.todayPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="鏄ㄦ棩閲囪喘" :value="purchaseSummary?.yesterdayPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="鏈湀閿�鍞�" :value="saleSummary?.monthPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="浠婂勾閿�鍞�" :value="saleSummary?.yearPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="鏈湀閲囪喘" :value="purchaseSummary?.monthPrice" />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <SummaryCard title="浠婂勾閲囪喘" :value="purchaseSummary?.yearPrice" />
+ </el-col>
+ </el-row>
+ <!-- 閿�鍞�/閲囪喘鐨勬椂娈电粺璁� -->
+ <el-row :gutter="16" class="row">
+ <!-- 閿�鍞粺璁� -->
+ <el-col :md="12" :sm="12" :xs="24" :loading="loading">
+ <TimeSummaryChart title="閿�鍞粺璁�" :value="saleTimeSummaryList" />
+ </el-col>
+ <!-- 閲囪喘缁熻 -->
+ <el-col :md="12" :sm="12" :xs="24" :loading="loading">
+ <TimeSummaryChart title="閲囪喘缁熻" :value="purchaseTimeSummaryList" />
+ </el-col>
+ </el-row>
+ </div>
+</template>
+<script lang="ts" setup>
+import SummaryCard from './components/SummaryCard.vue'
+import TimeSummaryChart from './components/TimeSummaryChart.vue'
+import {
+ ErpSaleSummaryRespVO,
+ ErpSaleTimeSummaryRespVO,
+ SaleStatisticsApi
+} from '@/api/erp/statistics/sale'
+import {
+ ErpPurchaseSummaryRespVO,
+ ErpPurchaseTimeSummaryRespVO,
+ PurchaseStatisticsApi
+} from '@/api/erp/statistics/purchase'
+
+/** 鍟嗗煄棣栭〉 */
+defineOptions({ name: 'ErpHome' })
+
+const loading = ref(true) // 鍔犺浇涓�
+
+/** 鑾峰緱閿�鍞粺璁� */
+const saleSummary = ref<ErpSaleSummaryRespVO>() // 閿�鍞鍐电粺璁�
+const saleTimeSummaryList = ref<ErpSaleTimeSummaryRespVO[]>() // 閿�鍞椂娈电粺璁�
+const getSaleSummary = async () => {
+ saleSummary.value = await SaleStatisticsApi.getSaleSummary()
+ saleTimeSummaryList.value = await SaleStatisticsApi.getSaleTimeSummary()
+}
+
+/** 鑾峰緱閲囪喘缁熻 */
+const purchaseSummary = ref<ErpPurchaseSummaryRespVO>() // 閲囪喘姒傚喌缁熻
+const purchaseTimeSummaryList = ref<ErpPurchaseTimeSummaryRespVO[]>() // 閲囪喘鏃舵缁熻
+const getPurchaseSummary = async () => {
+ purchaseSummary.value = await PurchaseStatisticsApi.getPurchaseSummary()
+ purchaseTimeSummaryList.value = await PurchaseStatisticsApi.getPurchaseTimeSummary()
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ loading.value = true
+ await Promise.all([getSaleSummary(), getPurchaseSummary()])
+ loading.value = false
+})
+</script>
+<style lang="scss" scoped>
+.row {
+ .el-col {
+ margin-bottom: 1rem;
+ }
+}
+</style>
diff --git a/src/views/erp/product/category/ProductCategoryForm.vue b/src/views/erp/product/category/ProductCategoryForm.vue
new file mode 100644
index 0000000..488b31e
--- /dev/null
+++ b/src/views/erp/product/category/ProductCategoryForm.vue
@@ -0,0 +1,145 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="涓婄骇缂栧彿" prop="parentId">
+ <el-tree-select
+ v-model="formData.parentId"
+ :data="productCategoryTree"
+ :props="defaultProps"
+ check-strictly
+ default-expand-all
+ placeholder="璇烽�夋嫨涓婄骇缂栧彿"
+ />
+ </el-form-item>
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="缂栫爜" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ョ紪鐮�" />
+ </el-form-item>
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input v-model="formData.sort" placeholder="璇疯緭鍏ユ帓搴�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** ERP 浜у搧鍒嗙被 琛ㄥ崟 */
+defineOptions({ name: 'ProductCategoryForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ parentId: undefined,
+ name: undefined,
+ code: undefined,
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ parentId: [{ required: true, message: '涓婄骇缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ code: [{ required: true, message: '缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const productCategoryTree = ref() // 鏍戝舰缁撴瀯
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductCategoryApi.getProductCategory(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ await getProductCategoryTree()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductCategoryVO
+ if (formType.value === 'create') {
+ await ProductCategoryApi.createProductCategory(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductCategoryApi.updateProductCategory(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ parentId: undefined,
+ name: undefined,
+ code: undefined,
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鑾峰緱浜у搧鍒嗙被鏍� */
+const getProductCategoryTree = async () => {
+ productCategoryTree.value = []
+ const data = await ProductCategoryApi.getProductCategoryList()
+ const root: Tree = { id: 0, name: '椤剁骇浜у搧鍒嗙被', children: [] }
+ root.children = handleTree(data, 'id', 'parentId')
+ productCategoryTree.value.push(root)
+}
+</script>
diff --git a/src/views/erp/product/category/index.vue b/src/views/erp/product/category/index.vue
new file mode 100644
index 0000000..281835d
--- /dev/null
+++ b/src/views/erp/product/category/index.vue
@@ -0,0 +1,218 @@
+<template>
+ <doc-alert title="銆愪骇鍝併�戜骇鍝佷俊鎭�佸垎绫汇�佸崟浣�" url="https://doc.iocoder.cn/erp/product/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙被鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ垎绫诲悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨寮�鍚姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:product-category:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:product-category:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button type="danger" plain @click="toggleExpandAll">
+ <Icon icon="ep:sort" class="mr-5px" /> 灞曞紑/鎶樺彔
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ row-key="id"
+ :default-expand-all="isExpandAll"
+ v-if="refreshTable"
+ >
+ <el-table-column label="缂栫爜" align="center" prop="code" />
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:product-category:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:product-category:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductCategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import download from '@/utils/download'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
+import ProductCategoryForm from './ProductCategoryForm.vue'
+
+/** ERP 浜у搧鍒嗙被 鍒楄〃 */
+defineOptions({ name: 'ErpProductCategory' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProductCategoryVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductCategoryApi.getProductCategoryList(queryParams)
+ list.value = handleTree(data, 'id', 'parentId')
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductCategoryApi.deleteProductCategory(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ProductCategoryApi.exportProductCategory(queryParams)
+ download.excel(data, '浜у搧鍒嗙被.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+const isExpandAll = ref(true) // 鏄惁灞曞紑锛岄粯璁ゅ叏閮ㄥ睍寮�
+const refreshTable = ref(true) // 閲嶆柊娓叉煋琛ㄦ牸鐘舵��
+const toggleExpandAll = async () => {
+ refreshTable.value = false
+ isExpandAll.value = !isExpandAll.value
+ await nextTick()
+ refreshTable.value = true
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/erp/product/product/ProductForm.vue b/src/views/erp/product/product/ProductForm.vue
new file mode 100644
index 0000000..75be903
--- /dev/null
+++ b/src/views/erp/product/product/ProductForm.vue
@@ -0,0 +1,242 @@
+<!-- ERP 浜у搧鐨勬柊澧�/淇敼 -->
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏉$爜" prop="barCode">
+ <el-input v-model="formData.barCode" placeholder="璇疯緭鍏ユ潯鐮�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍒嗙被" prop="categoryId">
+ <el-tree-select
+ v-model="formData.categoryId"
+ :data="categoryList"
+ :props="defaultProps"
+ check-strictly
+ default-expand-all
+ placeholder="璇烽�夋嫨鍒嗙被"
+ class="w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅" prop="unitId">
+ <el-select v-model="formData.unitId" clearable placeholder="璇烽�夋嫨鍗曚綅" class="w-1/1">
+ <el-option
+ v-for="unit in unitList"
+ :key="unit.id"
+ :label="unit.name"
+ :value="unit.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸" prop="standard">
+ <el-input v-model="formData.standard" placeholder="璇疯緭鍏ヨ鏍�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="淇濊川鏈熷ぉ鏁�" prop="expiryDay">
+ <el-input-number
+ v-model="formData.expiryDay"
+ placeholder="璇疯緭鍏ヤ繚璐ㄦ湡澶╂暟"
+ :min="0"
+ :precision="0"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閲嶉噺锛坘g锛�" prop="weight">
+ <el-input-number
+ v-model="formData.weight"
+ placeholder="璇疯緭鍏ラ噸閲忥紙kg锛�"
+ :min="0"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閲囪喘浠锋牸" prop="purchasePrice">
+ <el-input-number
+ v-model="formData.purchasePrice"
+ placeholder="璇疯緭鍏ラ噰璐环鏍硷紝鍗曚綅锛氬厓"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閿�鍞环鏍�" prop="salePrice">
+ <el-input-number
+ v-model="formData.salePrice"
+ placeholder="璇疯緭鍏ラ攢鍞环鏍硷紝鍗曚綅锛氬厓"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈�浣庝环鏍�" prop="minPrice">
+ <el-input-number
+ v-model="formData.minPrice"
+ placeholder="璇疯緭鍏ユ渶浣庝环鏍硷紝鍗曚綅锛氬厓"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
+import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
+import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+/** ERP 浜у搧 琛ㄥ崟 */
+defineOptions({ name: 'ProductForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ barCode: undefined,
+ categoryId: undefined,
+ unitId: undefined,
+ status: undefined,
+ standard: undefined,
+ remark: undefined,
+ expiryDay: undefined,
+ weight: undefined,
+ purchasePrice: undefined,
+ salePrice: undefined,
+ minPrice: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '浜у搧鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ barCode: [{ required: true, message: '浜у搧鏉$爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ categoryId: [{ required: true, message: '浜у搧鍒嗙被缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ unitId: [{ required: true, message: '鍗曚綅缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '浜у搧鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const categoryList = ref<ProductCategoryVO[]>([]) // 浜у搧鍒嗙被鍒楄〃
+const unitList = ref<ProductUnitVO[]>([]) // 浜у搧鍗曚綅鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductApi.getProduct(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 浜у搧鍒嗙被
+ const categoryData = await ProductCategoryApi.getProductCategorySimpleList()
+ categoryList.value = handleTree(categoryData, 'id', 'parentId')
+ // 浜у搧鍗曚綅
+ unitList.value = await ProductUnitApi.getProductUnitSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductVO
+ if (formType.value === 'create') {
+ await ProductApi.createProduct(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductApi.updateProduct(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ barCode: undefined,
+ categoryId: undefined,
+ unitId: undefined,
+ status: CommonStatusEnum.ENABLE,
+ standard: undefined,
+ remark: undefined,
+ expiryDay: undefined,
+ weight: undefined,
+ purchasePrice: undefined,
+ salePrice: undefined,
+ minPrice: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/product/product/index.vue b/src/views/erp/product/product/index.vue
new file mode 100644
index 0000000..4eeba1e
--- /dev/null
+++ b/src/views/erp/product/product/index.vue
@@ -0,0 +1,224 @@
+<!-- ERP 浜у搧鍒楄〃 -->
+<template>
+ <doc-alert title="銆愪骇鍝併�戜骇鍝佷俊鎭�佸垎绫汇�佸崟浣�" url="https://doc.iocoder.cn/erp/product/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒嗙被" prop="categoryId">
+ <el-tree-select
+ v-model="queryParams.categoryId"
+ :data="categoryList"
+ :props="defaultProps"
+ check-strictly
+ default-expand-all
+ placeholder="璇疯緭鍏ュ垎绫�"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:product:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:product:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鏉$爜" align="center" prop="barCode" />
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="瑙勬牸" align="center" prop="standard" />
+ <el-table-column label="鍒嗙被" align="center" prop="categoryName" />
+ <el-table-column label="鍗曚綅" align="center" prop="unitName" />
+ <el-table-column
+ label="閲囪喘浠锋牸"
+ align="center"
+ prop="purchasePrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="閿�鍞环鏍�"
+ align="center"
+ prop="salePrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鏈�浣庝环鏍�"
+ align="center"
+ prop="minPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="110">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:product:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:product:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/erp/product/category'
+import ProductForm from './ProductForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+/** ERP 浜у搧鍒楄〃 */
+defineOptions({ name: 'ErpProduct' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProductVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ categoryId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const categoryList = ref<ProductCategoryVO[]>([]) // 浜у搧鍒嗙被鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductApi.getProductPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductApi.deleteProduct(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ProductApi.exportProduct(queryParams)
+ download.excel(data, '浜у搧.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 浜у搧鍒嗙被
+ const categoryData = await ProductCategoryApi.getProductCategorySimpleList()
+ categoryList.value = handleTree(categoryData, 'id', 'parentId')
+})
+</script>
diff --git a/src/views/erp/product/unit/ProductUnitForm.vue b/src/views/erp/product/unit/ProductUnitForm.vue
new file mode 100644
index 0000000..bced5db
--- /dev/null
+++ b/src/views/erp/product/unit/ProductUnitForm.vue
@@ -0,0 +1,108 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍗曚綅鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ崟浣嶅悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鍗曚綅鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { ProductUnitApi } from '@/api/erp/product/unit'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** ERP 浜у搧鍗曚綅琛ㄥ崟 */
+defineOptions({ name: 'ProductUnitForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ status: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍗曚綅鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鍗曚綅鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductUnitApi.getProductUnit(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductUnitApi.ProductUnitVO
+ if (formType.value === 'create') {
+ await ProductUnitApi.createProductUnit(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductUnitApi.updateProductUnit(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/product/unit/index.vue b/src/views/erp/product/unit/index.vue
new file mode 100644
index 0000000..04259ac
--- /dev/null
+++ b/src/views/erp/product/unit/index.vue
@@ -0,0 +1,198 @@
+<template>
+ <doc-alert title="銆愪骇鍝併�戜骇鍝佷俊鎭�佸垎绫汇�佸崟浣�" url="https://doc.iocoder.cn/erp/product/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍗曚綅鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ崟浣嶅悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍗曚綅鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鍗曚綅鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:product-unit:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:product-unit:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:product-unit:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:product-unit:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductUnitForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { ProductUnitApi, ProductUnitVO } from '@/api/erp/product/unit'
+import ProductUnitForm from './ProductUnitForm.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+/** ERP 浜у搧鍗曚綅鍒楄〃 */
+defineOptions({ name: 'ErpProductUnit' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProductUnitVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductUnitApi.getProductUnitPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductUnitApi.deleteProductUnit(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ProductUnitApi.exportProductUnit(queryParams)
+ download.excel(data, '浜у搧鍗曚綅.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/erp/purchase/in/PurchaseInForm.vue b/src/views/erp/purchase/in/PurchaseInForm.vue
new file mode 100644
index 0000000..c59d7df
--- /dev/null
+++ b/src/views/erp/purchase/in/PurchaseInForm.vue
@@ -0,0 +1,325 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1440">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鍏ュ簱鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏ュ簱鏃堕棿" prop="inTime">
+ <el-date-picker
+ v-model="formData.inTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍏ュ簱鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input v-model="formData.orderNo" readonly>
+ <template #append>
+ <el-button @click="openPurchaseOrderInEnableList">
+ <Icon icon="ep:search" /> 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="formData.supplierId"
+ clearable
+ filterable
+ disabled
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="鍏ュ簱浜у搧娓呭崟" name="item">
+ <PurchaseInItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鐜囷紙%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犵巼"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠樻浼樻儬" prop="discountPrice">
+ <el-input
+ disabled
+ v-model="formData.discountPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鍚庨噾棰�">
+ <el-input
+ disabled
+ :model-value="formData.totalPrice - formData.otherPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏跺畠璐圭敤" prop="otherPrice">
+ <el-input-number
+ v-model="formData.otherPrice"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ叾瀹冭垂鐢�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="搴斾粯閲戦">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+
+ <!-- 鍙叆搴撶殑璁㈠崟鍒楄〃 -->
+ <PurchaseOrderInEnableList
+ ref="purchaseOrderInEnableListRef"
+ @success="handlePurchaseOrderChange"
+ />
+</template>
+<script setup lang="ts">
+import { PurchaseInApi, PurchaseInVO } from '@/api/erp/purchase/in'
+import PurchaseInItemForm from './components/PurchaseInItemForm.vue'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import PurchaseOrderInEnableList from '@/views/erp/purchase/order/components/PurchaseOrderInEnableList.vue'
+import { PurchaseOrderVO } from '@/api/erp/purchase/order'
+import * as UserApi from '@/api/system/user'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+
+/** ERP 閿�鍞叆搴撹〃鍗� */
+defineOptions({ name: 'PurchaseInForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ inTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ orderNo: undefined,
+ items: [],
+ no: undefined // 鍏ュ簱鍗曞彿锛屽悗绔繑鍥�
+})
+const formRules = reactive({
+ supplierId: [{ required: true, message: '渚涘簲鍟嗕笉鑳戒负绌�', trigger: 'blur' }],
+ inTime: [{ required: true, message: '鍏ュ簱鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ // 璁$畻
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
+ formData.value.discountPrice = discountPrice
+ formData.value.totalPrice = totalPrice - discountPrice + val.otherPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await PurchaseInApi.getPurchaseIn(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇渚涘簲鍟嗗垪琛�
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎵撳紑銆愬彲鍏ュ簱鐨勮鍗曞垪琛ㄣ�戝脊绐� */
+const purchaseOrderInEnableListRef = ref() // 鍙叆搴撶殑璁㈠崟鍒楄〃 Ref
+const openPurchaseOrderInEnableList = () => {
+ purchaseOrderInEnableListRef.value.open()
+}
+
+const handlePurchaseOrderChange = (order: PurchaseOrderVO) => {
+ // 灏嗚鍗曡缃埌鍏ュ簱鍗�
+ formData.value.orderId = order.id
+ formData.value.orderNo = order.no
+ formData.value.supplierId = order.supplierId
+ formData.value.accountId = order.accountId
+ formData.value.discountPercent = order.discountPercent
+ formData.value.remark = order.remark
+ formData.value.fileUrl = order.fileUrl
+ // 灏嗚鍗曢」璁剧疆鍒板叆搴撳崟椤�
+ order.items.forEach((item) => {
+ item.totalCount = item.count
+ item.count = item.totalCount - item.inCount
+ item.orderItemId = item.id
+ item.id = undefined
+ })
+ formData.value.items = order.items.filter((item) => item.count > 0)
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as PurchaseInVO
+ if (formType.value === 'create') {
+ await PurchaseInApi.createPurchaseIn(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PurchaseInApi.updatePurchaseIn(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ inTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/purchase/in/components/PurchaseInItemForm.vue b/src/views/erp/purchase/in/components/PurchaseInItemForm.vue
new file mode 100644
index 0000000..da54e05
--- /dev/null
+++ b/src/views/erp/purchase/in/components/PurchaseInItemForm.vue
@@ -0,0 +1,294 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶇О" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select v-model="row.warehouseId" clearable filterable placeholder="璇烽�夋嫨浠撳簱">
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍘熸暟閲�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.totalCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.totalCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="宸插叆搴�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.inCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.inCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" prop="totalProductPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼锛�%锛�" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
+ <el-input-number
+ v-model="row.taxPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨" prop="taxPrice" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨鍚堣" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button :disabled="formData.length === 1" @click="handleDelete($index)" link>
+ 鈥�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+</template>
+<script setup lang="ts">
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ warehouseId: [{ required: true, message: '浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃叆搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ val.forEach((item) => {
+ if (item.warehouseId == null) {
+ item.warehouseId = defaultWarehouse.value?.id
+ }
+ if (item.stockCount === null && item.warehouseId != null) {
+ setStockCount(item)
+ }
+ })
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
+ item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0)
+ if (item.totalProductPrice != null) {
+ item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalProductPrice: undefined,
+ taxPercent: undefined,
+ taxPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row: any) => {
+ if (!row.productId) {
+ return
+ }
+ const count = await StockApi.getStockCount(row.productId)
+ row.stockCount = count || 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+})
+</script>
diff --git a/src/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue b/src/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue
new file mode 100644
index 0000000..afaa644
--- /dev/null
+++ b/src/views/erp/purchase/in/components/PurchaseInPaymentEnableList.vue
@@ -0,0 +1,199 @@
+<!-- 鍙粯娆剧殑閲囪喘鍏ュ簱鍗曞垪琛� -->
+<template>
+ <Dialog
+ title="閫夋嫨閲囪喘鍏ュ簱锛堜粎灞曠ず鍙粯娆撅級"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏ュ簱鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ュ叆搴撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏ュ簱鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.inTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鍏ュ簱鍗曞彿" align="center" prop="no" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="鍏ュ簱鏃堕棿"
+ align="center"
+ prop="inTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="搴斾粯閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸蹭粯閲戦"
+ align="center"
+ prop="paymentPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈粯閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.paymentPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.paymentPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!selectionList.length" type="primary" @click="submitForm">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { PurchaseInApi, PurchaseInVO } from '@/api/erp/purchase/in'
+
+defineOptions({ name: 'PurchaseInPaymentEnableList' })
+
+const list = ref<PurchaseInVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ inTime: [],
+ paymentEnable: true,
+ supplierId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<PurchaseInVO[]>([])
+const handleSelectionChange = (rows: PurchaseInVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (supplierId: number) => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍙叆搴撶殑璁㈠崟鍒楄〃
+ queryParams.supplierId = supplierId
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: PurchaseInVO[]): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', selectionList.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseInApi.getPurchaseInPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ selectionList.value = []
+ getList()
+}
+</script>
diff --git a/src/views/erp/purchase/in/index.vue b/src/views/erp/purchase/in/index.vue
new file mode 100644
index 0000000..b1277d4
--- /dev/null
+++ b/src/views/erp/purchase/in/index.vue
@@ -0,0 +1,443 @@
+<template>
+ <doc-alert title="銆愰噰璐�戦噰璐鍗曘�佸叆搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/purchase/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏ュ簱鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ュ叆搴撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏ュ簱鏃堕棿" prop="inTime">
+ <el-date-picker
+ v-model="queryParams.inTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="queryParams.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涗緵搴斿晢"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input
+ v-model="queryParams.orderNo"
+ placeholder="璇疯緭鍏ュ叧鑱旇鍗�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻鐘舵��" prop="paymentStatus">
+ <el-select
+ v-model="queryParams.paymentStatus"
+ placeholder="璇烽�夋嫨鏈夋鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈粯娆�" value="0" />
+ <el-option label="閮ㄥ垎浠樻" value="1" />
+ <el-option label="鍏ㄩ儴浠樻" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨瀹℃牳鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:purchase-in:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:purchase-in:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:purchase-in:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鍏ュ簱鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column
+ label="鍏ュ簱鏃堕棿"
+ align="center"
+ prop="inTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="搴斾粯閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸蹭粯閲戦"
+ align="center"
+ prop="paymentPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈粯閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.paymentPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.paymentPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹℃牳鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:purchase-in:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:purchase-in:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:purchase-in:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:purchase-in:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:purchase-in:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <PurchaseInForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { PurchaseInApi, PurchaseInVO } from '@/api/erp/purchase/in'
+import PurchaseInForm from './PurchaseInForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import {
+ erpCountTableColumnFormatter,
+ erpPriceInputFormatter,
+ erpPriceTableColumnFormatter
+} from '@/utils'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+
+/** ERP 閿�鍞叆搴撳垪琛� */
+defineOptions({ name: 'ErpPurchaseIn' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<PurchaseInVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ supplierId: undefined,
+ productId: undefined,
+ warehouseId: undefined,
+ inTime: [],
+ orderNo: undefined,
+ paymentStatus: undefined,
+ accountId: undefined,
+ status: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseInApi.getPurchaseInPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PurchaseInApi.deletePurchaseIn(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ュ叆搴撳悧锛焋)
+ // 鍙戣捣瀹℃壒
+ await PurchaseInApi.updatePurchaseInStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await PurchaseInApi.exportPurchaseIn(queryParams)
+ download.excel(data, '閿�鍞叆搴�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<PurchaseInVO[]>([])
+const handleSelectionChange = (rows: PurchaseInVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佷緵搴斿晢
+ productList.value = await ProductApi.getProductSimpleList()
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ accountList.value = await AccountApi.getAccountSimpleList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/purchase/order/PurchaseOrderForm.vue b/src/views/erp/purchase/order/PurchaseOrderForm.vue
new file mode 100644
index 0000000..a7a6eec
--- /dev/null
+++ b/src/views/erp/purchase/order/PurchaseOrderForm.vue
@@ -0,0 +1,269 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="formData.orderTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨璁㈠崟鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="formData.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="璁㈠崟浜у搧娓呭崟" name="item">
+ <PurchaseOrderItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鐜囷紙%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犵巼"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠樻浼樻儬" prop="discountPrice">
+ <el-input
+ disabled
+ v-model="formData.discountPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鍚庨噾棰�">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏀粯璁㈤噾" prop="depositPrice">
+ <el-input-number
+ v-model="formData.depositPrice"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ユ敮浠樿閲�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
+import PurchaseOrderItemForm from './components/PurchaseOrderItemForm.vue'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import * as UserApi from '@/api/system/user'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 閿�鍞鍗曡〃鍗� */
+defineOptions({ name: 'PurchaseOrderForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ orderTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ depositPrice: 0,
+ items: [],
+ no: undefined // 璁㈠崟鍗曞彿锛屽悗绔繑鍥�
+})
+const formRules = reactive({
+ supplierId: [{ required: true, message: '渚涘簲鍟嗕笉鑳戒负绌�', trigger: 'blur' }],
+ orderTime: [{ required: true, message: '璁㈠崟鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
+ formData.value.discountPrice = discountPrice
+ formData.value.totalPrice = totalPrice - discountPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await PurchaseOrderApi.getPurchaseOrder(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇渚涘簲鍟嗗垪琛�
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as PurchaseOrderVO
+ if (formType.value === 'create') {
+ await PurchaseOrderApi.createPurchaseOrder(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PurchaseOrderApi.updatePurchaseOrder(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ orderTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ depositPrice: 0,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/purchase/order/components/PurchaseOrderInEnableList.vue b/src/views/erp/purchase/order/components/PurchaseOrderInEnableList.vue
new file mode 100644
index 0000000..f3df953
--- /dev/null
+++ b/src/views/erp/purchase/order/components/PurchaseOrderInEnableList.vue
@@ -0,0 +1,205 @@
+<!-- 鍙叆搴撶殑璁㈠崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閲囪喘璁㈠崟锛堜粎灞曠ず鍙叆搴擄級"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ鍗曞崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.orderTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" width="65">
+ <template #default="scope">
+ <el-radio
+ :value="scope.row.id"
+ v-model="currentRowValue"
+ @change="handleCurrentChange(scope.row)"
+ >
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column min-width="180" label="璁㈠崟鍗曞彿" align="center" prop="no" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="璁㈠崟鏃堕棿"
+ align="center"
+ prop="orderTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍏ュ簱鏁伴噺"
+ align="center"
+ prop="inCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦鍚堣"
+ align="center"
+ prop="totalProductPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚◣閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!currentRow" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+
+defineOptions({ name: 'ErpPurchaseOrderOutEnableList' })
+
+const list = ref<PurchaseOrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ orderTime: [],
+ inEnable: true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑琛� */
+const currentRowValue = ref(undefined) // 閫変腑琛岀殑 value
+const currentRow = ref(undefined) // 閫変腑琛�
+const handleCurrentChange = (row) => {
+ currentRow.value = row
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍙叆搴撶殑璁㈠崟鍒楄〃
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: PurchaseOrderVO): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', currentRow.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseOrderApi.getPurchaseOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ currentRowValue.value = undefined
+ currentRow.value = undefined
+ getList()
+}
+</script>
diff --git a/src/views/erp/purchase/order/components/PurchaseOrderItemForm.vue b/src/views/erp/purchase/order/components/PurchaseOrderItemForm.vue
new file mode 100644
index 0000000..70a2433
--- /dev/null
+++ b/src/views/erp/purchase/order/components/PurchaseOrderItemForm.vue
@@ -0,0 +1,276 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.productPrice`"
+ :rules="formRules.productPrice"
+ class="mb-0px!"
+ >
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" prop="totalProductPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼锛�%锛�" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
+ <el-input-number
+ v-model="row.taxPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨" prop="taxPrice" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨鍚堣" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞閲囪喘浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ productPrice: [{ required: true, message: '浜у搧鍗曚环涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 鍒濆鍖栬缃叆搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
+ item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0)
+ if (item.totalProductPrice != null) {
+ item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalProductPrice: undefined,
+ taxPercent: undefined,
+ taxPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnitName = product.unitName
+ row.productBarCode = product.barCode
+ row.productPrice = product.purchasePrice
+ }
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row: any) => {
+ if (!row.productId) {
+ return
+ }
+ const count = await StockApi.getStockCount(row.productId)
+ row.stockCount = count || 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+ // 榛樿娣诲姞涓�涓�
+ if (formData.value.length === 0) {
+ handleAdd()
+ }
+})
+</script>
diff --git a/src/views/erp/purchase/order/components/PurchaseOrderReturnEnableList.vue b/src/views/erp/purchase/order/components/PurchaseOrderReturnEnableList.vue
new file mode 100644
index 0000000..17e59db
--- /dev/null
+++ b/src/views/erp/purchase/order/components/PurchaseOrderReturnEnableList.vue
@@ -0,0 +1,212 @@
+<!-- 鍙��璐х殑璁㈠崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閲囪喘璁㈠崟锛堜粎灞曠ず鍙��璐э級"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ鍗曞崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.orderTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" width="65">
+ <template #default="scope">
+ <el-radio
+ :value="scope.row.id"
+ v-model="currentRowValue"
+ @change="handleCurrentChange(scope.row)"
+ >
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column min-width="180" label="璁㈠崟鍗曞彿" align="center" prop="no" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="璁㈠崟鏃堕棿"
+ align="center"
+ prop="orderTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍏ュ簱鏁伴噺"
+ align="center"
+ prop="inCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閫�璐ф暟閲�"
+ align="center"
+ prop="returnCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦鍚堣"
+ align="center"
+ prop="totalProductPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚◣閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!currentRow" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+
+defineOptions({ name: 'PurchaseOrderReturnEnableList' })
+
+const list = ref<PurchaseOrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ orderTime: [],
+ returnEnable: true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑琛� */
+const currentRowValue = ref(undefined) // 閫変腑琛岀殑 value
+const currentRow = ref(undefined) // 閫変腑琛�
+const handleCurrentChange = (row) => {
+ currentRow.value = row
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍙��璐х殑璁㈠崟鍒楄〃
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: PurchaseOrderVO): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', currentRow.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseOrderApi.getPurchaseOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ currentRowValue.value = undefined
+ currentRow.value = undefined
+ getList()
+}
+</script>
diff --git a/src/views/erp/purchase/order/index.vue b/src/views/erp/purchase/order/index.vue
new file mode 100644
index 0000000..1417761
--- /dev/null
+++ b/src/views/erp/purchase/order/index.vue
@@ -0,0 +1,407 @@
+<template>
+ <doc-alert title="銆愰噰璐�戦噰璐鍗曘�佸叆搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/purchase/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ鍗曞崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.orderTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="queryParams.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涗緵搴斿晢"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍏ュ簱鏁伴噺" prop="inStatus">
+ <el-select
+ v-model="queryParams.inStatus"
+ placeholder="璇烽�夋嫨鍏ュ簱鏁伴噺"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈叆搴�" value="0" />
+ <el-option label="閮ㄥ垎鍏ュ簱" value="1" />
+ <el-option label="鍏ㄩ儴鍏ュ簱" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф暟閲�" prop="returnStatus">
+ <el-select
+ v-model="queryParams.returnStatus"
+ placeholder="璇烽�夋嫨閫�璐ф暟閲�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈��璐�" value="0" />
+ <el-option label="閮ㄥ垎閫�璐�" value="1" />
+ <el-option label="鍏ㄩ儴閫�璐�" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:purchase-order:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:purchase-order:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:purchase-order:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="璁㈠崟鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column
+ label="璁㈠崟鏃堕棿"
+ align="center"
+ prop="orderTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍏ュ簱鏁伴噺"
+ align="center"
+ prop="inCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閫�璐ф暟閲�"
+ align="center"
+ prop="returnCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦鍚堣"
+ align="center"
+ prop="totalProductPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚◣閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鏀粯璁㈤噾"
+ align="center"
+ prop="depositPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:purchase-order:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:purchase-order:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:purchase-order:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:purchase-order:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:purchase-order:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <PurchaseOrderForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { PurchaseOrderApi, PurchaseOrderVO } from '@/api/erp/purchase/order'
+import PurchaseOrderForm from './PurchaseOrderForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+
+/** ERP 閿�鍞鍗曞垪琛� */
+defineOptions({ name: 'ErpPurchaseOrder' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<PurchaseOrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ supplierId: undefined,
+ productId: undefined,
+ orderTime: [],
+ status: undefined,
+ remark: undefined,
+ creator: undefined,
+ inStatus: undefined,
+ returnStatus: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseOrderApi.getPurchaseOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PurchaseOrderApi.deletePurchaseOrder(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ鍗曞悧锛焋)
+ // 鍙戣捣瀹℃壒
+ await PurchaseOrderApi.updatePurchaseOrderStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await PurchaseOrderApi.exportPurchaseOrder(queryParams)
+ download.excel(data, '閿�鍞鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<PurchaseOrderVO[]>([])
+const handleSelectionChange = (rows: PurchaseOrderVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佷緵搴斿晢
+ productList.value = await ProductApi.getProductSimpleList()
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/purchase/return/PurchaseReturnForm.vue b/src/views/erp/purchase/return/PurchaseReturnForm.vue
new file mode 100644
index 0000000..e37fa09
--- /dev/null
+++ b/src/views/erp/purchase/return/PurchaseReturnForm.vue
@@ -0,0 +1,328 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1440">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閫�璐у崟鍙�" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閫�璐ф椂闂�" prop="returnTime">
+ <el-date-picker
+ v-model="formData.returnTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨閫�璐ф椂闂�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input v-model="formData.orderNo" readonly>
+ <template #append>
+ <el-button @click="openPurchaseOrderReturnEnableList">
+ <Icon icon="ep:search" /> 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="formData.supplierId"
+ clearable
+ filterable
+ disabled
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="閫�璐т骇鍝佹竻鍗�" name="item">
+ <PurchaseReturnItemForm
+ ref="itemFormRef"
+ :items="formData.items"
+ :disabled="disabled"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鐜囷紙%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犵巼"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閫�娆句紭鎯�" prop="discountPrice">
+ <el-input
+ disabled
+ v-model="formData.discountPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鍚庨噾棰�">
+ <el-input
+ disabled
+ :model-value="formData.totalPrice - formData.otherPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏跺畠璐圭敤" prop="otherPrice">
+ <el-input-number
+ v-model="formData.otherPrice"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ叾瀹冭垂鐢�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="搴旈��閲戦" prop="totalPrice">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+
+ <!-- 鍙��璐х殑璁㈠崟鍒楄〃 -->
+ <PurchaseOrderReturnEnableList
+ ref="purchaseOrderReturnEnableListRef"
+ @success="handlePurchaseOrderChange"
+ />
+</template>
+<script setup lang="ts">
+import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return'
+import PurchaseReturnItemForm from './components/PurchaseReturnItemForm.vue'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import PurchaseOrderReturnEnableList from '@/views/erp/purchase/order/components/PurchaseOrderReturnEnableList.vue'
+import { PurchaseOrderVO } from '@/api/erp/purchase/order'
+import * as UserApi from '@/api/system/user'
+
+/** ERP 閲囪喘閫�璐ц〃鍗� */
+defineOptions({ name: 'PurchaseReturnForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ returnTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ orderNo: undefined,
+ items: [],
+ no: undefined // 閫�璐у崟鍙凤紝鍚庣杩斿洖
+})
+const formRules = reactive({
+ supplierId: [{ required: true, message: '渚涘簲鍟嗕笉鑳戒负绌�', trigger: 'blur' }],
+ returnTime: [{ required: true, message: '閫�璐ф椂闂翠笉鑳戒负绌�', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ // 璁$畻
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
+ formData.value.discountPrice = discountPrice
+ formData.value.totalPrice = totalPrice - discountPrice + val.otherPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await PurchaseReturnApi.getPurchaseReturn(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇渚涘簲鍟嗗垪琛�
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎵撳紑銆愬彲閫�璐х殑璁㈠崟鍒楄〃銆戝脊绐� */
+const purchaseOrderReturnEnableListRef = ref() // 鍙��璐х殑璁㈠崟鍒楄〃 Ref
+const openPurchaseOrderReturnEnableList = () => {
+ purchaseOrderReturnEnableListRef.value.open()
+}
+
+const handlePurchaseOrderChange = (order: PurchaseOrderVO) => {
+ // 灏嗚鍗曡缃埌閫�璐у崟
+ formData.value.orderId = order.id
+ formData.value.orderNo = order.no
+ formData.value.supplierId = order.supplierId
+ formData.value.accountId = order.accountId
+ formData.value.discountPercent = order.discountPercent
+ formData.value.remark = order.remark
+ formData.value.fileUrl = order.fileUrl
+ // 灏嗚鍗曢」璁剧疆鍒伴��璐у崟椤�
+ order.items.forEach((item) => {
+ item.count = item.inCount - item.returnCount
+ item.orderItemId = item.id
+ item.id = undefined
+ })
+ formData.value.items = order.items.filter((item) => item.count > 0)
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as PurchaseReturnVO
+ if (formType.value === 'create') {
+ await PurchaseReturnApi.createPurchaseReturn(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PurchaseReturnApi.updatePurchaseReturn(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ supplierId: undefined,
+ accountId: undefined,
+ returnTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/purchase/return/components/PurchaseReturnItemForm.vue b/src/views/erp/purchase/return/components/PurchaseReturnItemForm.vue
new file mode 100644
index 0000000..2d3e8c5
--- /dev/null
+++ b/src/views/erp/purchase/return/components/PurchaseReturnItemForm.vue
@@ -0,0 +1,300 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶇О" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="宸插嚭搴�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.inCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.inCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="宸查��璐�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.returnCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.returnCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" prop="totalProductPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼锛�%锛�" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
+ <el-input-number
+ v-model="row.taxPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨" prop="taxPrice" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨鍚堣" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button :disabled="formData.length === 1" @click="handleDelete($index)" link>
+ 鈥�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+</template>
+<script setup lang="ts">
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ warehouseId: [{ required: true, message: '浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃嚭搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ val.forEach((item) => {
+ if (item.warehouseId == null) {
+ item.warehouseId = defaultWarehouse.value?.id
+ }
+ if (item.stockCount === null && item.warehouseId != null) {
+ setStockCount(item)
+ }
+ })
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
+ item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0)
+ if (item.totalProductPrice != null) {
+ item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalProductPrice: undefined,
+ taxPercent: undefined,
+ taxPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row: any) => {
+ if (!row.productId) {
+ return
+ }
+ const count = await StockApi.getStockCount(row.productId)
+ row.stockCount = count || 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+})
+</script>
diff --git a/src/views/erp/purchase/return/components/PurchaseReturnRefundEnableList.vue b/src/views/erp/purchase/return/components/PurchaseReturnRefundEnableList.vue
new file mode 100644
index 0000000..a95749e
--- /dev/null
+++ b/src/views/erp/purchase/return/components/PurchaseReturnRefundEnableList.vue
@@ -0,0 +1,200 @@
+<!-- 鍙��娆剧殑閲囪喘閫�璐у崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閲囪喘閫�璐э紙浠呭睍绀哄彲閫�娆撅級"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閫�璐у崟鍙�" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ラ��璐у崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф椂闂�" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.returnTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="閫�璐у崟鍙�" align="center" prop="no" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="閫�璐ф椂闂�"
+ align="center"
+ prop="returnTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="搴旈��閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸查��閲戦"
+ align="center"
+ prop="refundPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈��閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!selectionList.length" type="primary" @click="submitForm">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return'
+import { SaleReturnVO } from '@/api/erp/sale/return'
+
+defineOptions({ name: 'PurchaseInPaymentEnableList' })
+
+const list = ref<PurchaseReturnVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ returnTime: [],
+ refundEnable: true,
+ supplierId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<SaleReturnVO[]>([])
+const handleSelectionChange = (rows: SaleReturnVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (supplierId: number) => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍒楄〃
+ queryParams.supplierId = supplierId
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: SaleReturnVO[]): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', selectionList.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseReturnApi.getPurchaseReturnPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ selectionList.value = []
+ getList()
+}
+</script>
diff --git a/src/views/erp/purchase/return/index.vue b/src/views/erp/purchase/return/index.vue
new file mode 100644
index 0000000..cdf8411
--- /dev/null
+++ b/src/views/erp/purchase/return/index.vue
@@ -0,0 +1,443 @@
+<template>
+ <doc-alert title="銆愰噰璐�戦噰璐鍗曘�佸叆搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/purchase/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閫�璐у崟鍙�" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ラ��璐у崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф椂闂�" prop="inTime">
+ <el-date-picker
+ v-model="queryParams.inTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="queryParams.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涗緵搴斿晢"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input
+ v-model="queryParams.orderNo"
+ placeholder="璇疯緭鍏ュ叧鑱旇鍗�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�娆剧姸鎬�" prop="refundStatus">
+ <el-select
+ v-model="queryParams.refundStatus"
+ placeholder="璇烽�夋嫨閫�娆剧姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈��娆�" value="0" />
+ <el-option label="閮ㄥ垎閫�娆�" value="1" />
+ <el-option label="鍏ㄩ儴閫�娆�" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨瀹℃牳鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:purchase-return:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:purchase-return:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:purchase-return:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="閫�璐у崟鍙�" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column
+ label="閫�璐ф椂闂�"
+ align="center"
+ prop="returnTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="搴旈��閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸查��閲戦"
+ align="center"
+ prop="refundPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈��閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹℃牳鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:purchase-return:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:purchase-return:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:purchase-return:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:purchase-return:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:purchase-return:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <PurchaseReturnForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { PurchaseReturnApi, PurchaseReturnVO } from '@/api/erp/purchase/return'
+import PurchaseReturnForm from './PurchaseReturnForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import {
+ erpCountTableColumnFormatter,
+ erpPriceInputFormatter,
+ erpPriceTableColumnFormatter
+} from '@/utils'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 閲囪喘閫�璐у垪琛� */
+defineOptions({ name: 'ErpPurchaseReturn' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<PurchaseReturnVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ supplierId: undefined,
+ productId: undefined,
+ warehouseId: undefined,
+ returnTime: [],
+ orderNo: undefined,
+ accountId: undefined,
+ status: undefined,
+ refundStatus: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PurchaseReturnApi.getPurchaseReturnPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PurchaseReturnApi.deletePurchaseReturn(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ラ��璐у悧锛焋)
+ // 鍙戣捣瀹℃壒
+ await PurchaseReturnApi.updatePurchaseReturnStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await PurchaseReturnApi.exportPurchaseReturn(queryParams)
+ download.excel(data, '閲囪喘閫�璐�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<PurchaseReturnVO[]>([])
+const handleSelectionChange = (rows: PurchaseReturnVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佷緵搴斿晢
+ productList.value = await ProductApi.getProductSimpleList()
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ accountList.value = await AccountApi.getAccountSimpleList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/purchase/supplier/SupplierForm.vue b/src/views/erp/purchase/supplier/SupplierForm.vue
new file mode 100644
index 0000000..3225df7
--- /dev/null
+++ b/src/views/erp/purchase/supplier/SupplierForm.vue
@@ -0,0 +1,210 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴浜�" prop="contact">
+ <el-input v-model="formData.contact" placeholder="璇疯緭鍏ヨ仈绯讳汉" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="telephone">
+ <el-input v-model="formData.telephone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢靛瓙閭" prop="email">
+ <el-input v-model="formData.email" placeholder="璇疯緭鍏ョ數瀛愰偖绠�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浼犵湡" prop="fax">
+ <el-input v-model="formData.fax" placeholder="璇疯緭鍏ヤ紶鐪�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number
+ v-model="formData.sort"
+ placeholder="璇疯緭鍏ユ帓搴�"
+ class="!w-1/1"
+ :precision="0"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绾崇◣浜鸿瘑鍒彿" prop="taxNo">
+ <el-input v-model="formData.taxNo" placeholder="璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)" prop="taxPercent">
+ <el-input-number
+ v-model="formData.taxPercent"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ョ◣鐜�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯" prop="bankName">
+ <el-input v-model="formData.bankName" placeholder="璇疯緭鍏ュ紑鎴疯" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯处鍙�" prop="bankAccount">
+ <el-input v-model="formData.bankAccount" placeholder="璇疯緭鍏ュ紑鎴疯处鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴峰湴鍧�" prop="bankAddress">
+ <el-input v-model="formData.bankAddress" placeholder="璇疯緭鍏ュ紑鎴峰湴鍧�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** ERP 琛ㄥ崟 */
+defineOptions({ name: 'SupplierForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ contact: undefined,
+ mobile: undefined,
+ telephone: undefined,
+ email: undefined,
+ fax: undefined,
+ remark: undefined,
+ status: undefined,
+ sort: undefined,
+ taxNo: undefined,
+ taxPercent: undefined,
+ bankName: undefined,
+ bankAccount: undefined,
+ bankAddress: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SupplierApi.getSupplier(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SupplierVO
+ if (formType.value === 'create') {
+ await SupplierApi.createSupplier(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SupplierApi.updateSupplier(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ contact: undefined,
+ mobile: undefined,
+ telephone: undefined,
+ email: undefined,
+ fax: undefined,
+ remark: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sort: undefined,
+ taxNo: undefined,
+ taxPercent: undefined,
+ bankName: undefined,
+ bankAccount: undefined,
+ bankAddress: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/purchase/supplier/index.vue b/src/views/erp/purchase/supplier/index.vue
new file mode 100644
index 0000000..4d3a405
--- /dev/null
+++ b/src/views/erp/purchase/supplier/index.vue
@@ -0,0 +1,201 @@
+<template>
+ <doc-alert title="銆愰噰璐�戦噰璐鍗曘�佸叆搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/purchase/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="telephone">
+ <el-input
+ v-model="queryParams.telephone"
+ placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:supplier:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:supplier:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="鑱旂郴浜�" align="center" prop="contact" />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" prop="mobile" />
+ <el-table-column label="鑱旂郴鐢佃瘽" align="center" prop="telephone" />
+ <el-table-column label="鐢靛瓙閭" align="center" prop="email" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:supplier:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:supplier:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SupplierForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import SupplierForm from './SupplierForm.vue'
+
+/** ERP 渚涘簲鍟� 鍒楄〃 */
+defineOptions({ name: 'ErpSupplier' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<SupplierVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ mobile: undefined,
+ telephone: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SupplierApi.getSupplierPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SupplierApi.deleteSupplier(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await SupplierApi.exportSupplier(queryParams)
+ download.excel(data, 'ERP 渚涘簲鍟�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/erp/sale/customer/CustomerForm.vue b/src/views/erp/sale/customer/CustomerForm.vue
new file mode 100644
index 0000000..ce63cbb
--- /dev/null
+++ b/src/views/erp/sale/customer/CustomerForm.vue
@@ -0,0 +1,210 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴浜�" prop="contact">
+ <el-input v-model="formData.contact" placeholder="璇疯緭鍏ヨ仈绯讳汉" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="telephone">
+ <el-input v-model="formData.telephone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢靛瓙閭" prop="email">
+ <el-input v-model="formData.email" placeholder="璇疯緭鍏ョ數瀛愰偖绠�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浼犵湡" prop="fax">
+ <el-input v-model="formData.fax" placeholder="璇疯緭鍏ヤ紶鐪�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number
+ v-model="formData.sort"
+ placeholder="璇疯緭鍏ユ帓搴�"
+ class="!w-1/1"
+ :precision="0"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绾崇◣浜鸿瘑鍒彿" prop="taxNo">
+ <el-input v-model="formData.taxNo" placeholder="璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)" prop="taxPercent">
+ <el-input-number
+ v-model="formData.taxPercent"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ョ◣鐜�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯" prop="bankName">
+ <el-input v-model="formData.bankName" placeholder="璇疯緭鍏ュ紑鎴疯" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯处鍙�" prop="bankAccount">
+ <el-input v-model="formData.bankAccount" placeholder="璇疯緭鍏ュ紑鎴疯处鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴峰湴鍧�" prop="bankAddress">
+ <el-input v-model="formData.bankAddress" placeholder="璇疯緭鍏ュ紑鎴峰湴鍧�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** ERP 瀹㈡埛 琛ㄥ崟 */
+defineOptions({ name: 'CustomerForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ contact: undefined,
+ mobile: undefined,
+ telephone: undefined,
+ email: undefined,
+ fax: undefined,
+ remark: undefined,
+ status: undefined,
+ sort: undefined,
+ taxNo: undefined,
+ taxPercent: undefined,
+ bankName: undefined,
+ bankAccount: undefined,
+ bankAddress: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '瀹㈡埛鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await CustomerApi.getCustomer(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as CustomerVO
+ if (formType.value === 'create') {
+ await CustomerApi.createCustomer(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await CustomerApi.updateCustomer(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ contact: undefined,
+ mobile: undefined,
+ telephone: undefined,
+ email: undefined,
+ fax: undefined,
+ remark: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sort: undefined,
+ taxNo: undefined,
+ taxPercent: undefined,
+ bankName: undefined,
+ bankAccount: undefined,
+ bankAddress: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/sale/customer/index.vue b/src/views/erp/sale/customer/index.vue
new file mode 100644
index 0000000..c79bbe8
--- /dev/null
+++ b/src/views/erp/sale/customer/index.vue
@@ -0,0 +1,201 @@
+<template>
+ <doc-alert title="銆愰攢鍞�戦攢鍞鍗曘�佸嚭搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/sale/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="telephone">
+ <el-input
+ v-model="queryParams.telephone"
+ placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:customer:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:customer:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="鑱旂郴浜�" align="center" prop="contact" />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" prop="mobile" />
+ <el-table-column label="鑱旂郴鐢佃瘽" align="center" prop="telephone" />
+ <el-table-column label="鐢靛瓙閭" align="center" prop="email" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:customer:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:customer:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CustomerForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import CustomerForm from './CustomerForm.vue'
+
+/** ERP 瀹㈡埛 鍒楄〃 */
+defineOptions({ name: 'ErpCustomer' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<CustomerVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ mobile: undefined,
+ telephone: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CustomerApi.getCustomerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await CustomerApi.deleteCustomer(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await CustomerApi.exportCustomer(queryParams)
+ download.excel(data, '瀹㈡埛.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/erp/sale/order/SaleOrderForm.vue b/src/views/erp/sale/order/SaleOrderForm.vue
new file mode 100644
index 0000000..30b2b30
--- /dev/null
+++ b/src/views/erp/sale/order/SaleOrderForm.vue
@@ -0,0 +1,289 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="formData.orderTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨璁㈠崟鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閿�鍞汉鍛�" prop="saleUserId">
+ <el-select
+ v-model="formData.saleUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨閿�鍞汉鍛�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="璁㈠崟浜у搧娓呭崟" name="item">
+ <SaleOrderItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鐜囷紙%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犵巼"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏀舵浼樻儬" prop="discountPrice">
+ <el-input
+ disabled
+ v-model="formData.discountPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鍚庨噾棰�">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏀跺彇璁㈤噾" prop="depositPrice">
+ <el-input-number
+ v-model="formData.depositPrice"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ユ敹鍙栬閲�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
+import SaleOrderItemForm from './components/SaleOrderItemForm.vue'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import * as UserApi from '@/api/system/user'
+
+/** ERP 閿�鍞鍗曡〃鍗� */
+defineOptions({ name: 'SaleOrderForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ saleUserId: undefined,
+ orderTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ depositPrice: 0,
+ items: [],
+ no: undefined // 璁㈠崟鍗曞彿锛屽悗绔繑鍥�
+})
+const formRules = reactive({
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ orderTime: [{ required: true, message: '璁㈠崟鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
+ formData.value.discountPrice = discountPrice
+ formData.value.totalPrice = totalPrice - discountPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SaleOrderApi.getSaleOrder(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SaleOrderVO
+ if (formType.value === 'create') {
+ await SaleOrderApi.createSaleOrder(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SaleOrderApi.updateSaleOrder(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ saleUserId: undefined,
+ orderTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ depositPrice: 0,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/sale/order/components/SaleOrderItemForm.vue b/src/views/erp/sale/order/components/SaleOrderItemForm.vue
new file mode 100644
index 0000000..817775f
--- /dev/null
+++ b/src/views/erp/sale/order/components/SaleOrderItemForm.vue
@@ -0,0 +1,271 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" prop="totalProductPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼锛�%锛�" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
+ <el-input-number
+ v-model="row.taxPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨" prop="taxPrice" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨鍚堣" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞閿�鍞骇鍝�</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 鍒濆鍖栬缃嚭搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
+ item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0)
+ if (item.totalProductPrice != null) {
+ item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalProductPrice: undefined,
+ taxPercent: undefined,
+ taxPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnitName = product.unitName
+ row.productBarCode = product.barCode
+ row.productPrice = product.salePrice
+ }
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row: any) => {
+ if (!row.productId) {
+ return
+ }
+ const count = await StockApi.getStockCount(row.productId)
+ row.stockCount = count || 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+ // 榛樿娣诲姞涓�涓�
+ if (formData.value.length === 0) {
+ handleAdd()
+ }
+})
+</script>
diff --git a/src/views/erp/sale/order/components/SaleOrderOutEnableList.vue b/src/views/erp/sale/order/components/SaleOrderOutEnableList.vue
new file mode 100644
index 0000000..38dcac4
--- /dev/null
+++ b/src/views/erp/sale/order/components/SaleOrderOutEnableList.vue
@@ -0,0 +1,206 @@
+<!-- 鍙嚭搴撶殑璁㈠崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閿�鍞鍗曪紙浠呭睍绀哄彲鍑哄簱锛�"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ鍗曞崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.orderTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" width="65">
+ <template #default="scope">
+ <el-radio
+ :value="scope.row.id"
+ v-model="currentRowValue"
+ @change="handleCurrentChange(scope.row)"
+ >
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column min-width="180" label="璁㈠崟鍗曞彿" align="center" prop="no" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="璁㈠崟鏃堕棿"
+ align="center"
+ prop="orderTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍑哄簱鏁伴噺"
+ align="center"
+ prop="outCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦鍚堣"
+ align="center"
+ prop="totalProductPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚◣閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!currentRow" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+
+defineOptions({ name: 'ErpSaleOrderOutEnableList' })
+
+const list = ref<SaleOrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ orderTime: [],
+ outEnable: true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑琛� */
+const currentRowValue = ref(undefined) // 閫変腑琛岀殑 value
+const currentRow = ref(undefined) // 閫変腑琛�
+const handleCurrentChange = (row) => {
+ currentRow.value = row
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍙嚭搴撶殑璁㈠崟鍒楄〃
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: SaleOrderVO): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', currentRow.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleOrderApi.getSaleOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ currentRowValue.value = undefined
+ currentRow.value = undefined
+ getList()
+}
+</script>
diff --git a/src/views/erp/sale/order/components/SaleOrderReturnEnableList.vue b/src/views/erp/sale/order/components/SaleOrderReturnEnableList.vue
new file mode 100644
index 0000000..75d925d
--- /dev/null
+++ b/src/views/erp/sale/order/components/SaleOrderReturnEnableList.vue
@@ -0,0 +1,212 @@
+<!-- 鍙��璐х殑璁㈠崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閿�鍞鍗曪紙浠呭睍绀哄彲閫�璐э級"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ鍗曞崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.orderTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" width="65">
+ <template #default="scope">
+ <el-radio
+ :value="scope.row.id"
+ v-model="currentRowValue"
+ @change="handleCurrentChange(scope.row)"
+ >
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column min-width="180" label="璁㈠崟鍗曞彿" align="center" prop="no" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="璁㈠崟鏃堕棿"
+ align="center"
+ prop="orderTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍑哄簱鏁伴噺"
+ align="center"
+ prop="outCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閫�璐ф暟閲�"
+ align="center"
+ prop="returnCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦鍚堣"
+ align="center"
+ prop="totalProductPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚◣閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!currentRow" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+
+defineOptions({ name: 'SaleOrderReturnEnableList' })
+
+const list = ref<SaleOrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ orderTime: [],
+ returnEnable: true
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑琛� */
+const currentRowValue = ref(undefined) // 閫変腑琛岀殑 value
+const currentRow = ref(undefined) // 閫変腑琛�
+const handleCurrentChange = (row) => {
+ currentRow.value = row
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍙��璐х殑璁㈠崟鍒楄〃
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: SaleOrderVO): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', currentRow.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleOrderApi.getSaleOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ currentRowValue.value = undefined
+ currentRow.value = undefined
+ getList()
+}
+</script>
diff --git a/src/views/erp/sale/order/index.vue b/src/views/erp/sale/order/index.vue
new file mode 100644
index 0000000..4d151b7
--- /dev/null
+++ b/src/views/erp/sale/order/index.vue
@@ -0,0 +1,407 @@
+<template>
+ <doc-alert title="銆愰攢鍞�戦攢鍞鍗曘�佸嚭搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/sale/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ鍗曞崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.orderTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘鎴�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏁伴噺" prop="outStatus">
+ <el-select
+ v-model="queryParams.outStatus"
+ placeholder="璇烽�夋嫨鍑哄簱鏁伴噺"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈嚭搴�" value="0" />
+ <el-option label="閮ㄥ垎鍑哄簱" value="1" />
+ <el-option label="鍏ㄩ儴鍑哄簱" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф暟閲�" prop="returnStatus">
+ <el-select
+ v-model="queryParams.returnStatus"
+ placeholder="璇烽�夋嫨閫�璐ф暟閲�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈��璐�" value="0" />
+ <el-option label="閮ㄥ垎閫�璐�" value="1" />
+ <el-option label="鍏ㄩ儴閫�璐�" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:sale-order:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:sale-order:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:sale-order:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="璁㈠崟鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column
+ label="璁㈠崟鏃堕棿"
+ align="center"
+ prop="orderTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍑哄簱鏁伴噺"
+ align="center"
+ prop="outCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閫�璐ф暟閲�"
+ align="center"
+ prop="returnCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦鍚堣"
+ align="center"
+ prop="totalProductPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鍚◣閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鏀跺彇璁㈤噾"
+ align="center"
+ prop="depositPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:sale-order:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:sale-order:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:sale-order:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:sale-order:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:sale-order:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SaleOrderForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { SaleOrderApi, SaleOrderVO } from '@/api/erp/sale/order'
+import SaleOrderForm from './SaleOrderForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+
+/** ERP 閿�鍞鍗曞垪琛� */
+defineOptions({ name: 'ErpSaleOrder' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<SaleOrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ customerId: undefined,
+ productId: undefined,
+ orderTime: [],
+ status: undefined,
+ remark: undefined,
+ creator: undefined,
+ outStatus: undefined,
+ returnStatus: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleOrderApi.getSaleOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SaleOrderApi.deleteSaleOrder(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ鍗曞悧锛焋)
+ // 鍙戣捣瀹℃壒
+ await SaleOrderApi.updateSaleOrderStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await SaleOrderApi.exportSaleOrder(queryParams)
+ download.excel(data, '閿�鍞鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<SaleOrderVO[]>([])
+const handleSelectionChange = (rows: SaleOrderVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佸鎴�
+ productList.value = await ProductApi.getProductSimpleList()
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/sale/out/SaleOutForm.vue b/src/views/erp/sale/out/SaleOutForm.vue
new file mode 100644
index 0000000..7d47713
--- /dev/null
+++ b/src/views/erp/sale/out/SaleOutForm.vue
@@ -0,0 +1,343 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1440">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鍑哄簱鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍑哄簱鏃堕棿" prop="outTime">
+ <el-date-picker
+ v-model="formData.outTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑哄簱鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input v-model="formData.orderNo" readonly>
+ <template #append>
+ <el-button @click="openSaleOrderOutEnableList">
+ <Icon icon="ep:search" /> 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ clearable
+ filterable
+ disabled
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閿�鍞汉鍛�" prop="saleUserId">
+ <el-select
+ v-model="formData.saleUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨閿�鍞汉鍛�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="鍑哄簱浜у搧娓呭崟" name="item">
+ <SaleOutItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鐜囷紙%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犵巼"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏀舵浼樻儬" prop="discountPrice">
+ <el-input
+ disabled
+ v-model="formData.discountPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鍚庨噾棰�">
+ <el-input
+ disabled
+ :model-value="formData.totalPrice - formData.otherPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏跺畠璐圭敤" prop="otherPrice">
+ <el-input-number
+ v-model="formData.otherPrice"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ叾瀹冭垂鐢�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="搴旀敹閲戦">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+
+ <!-- 鍙嚭搴撶殑璁㈠崟鍒楄〃 -->
+ <SaleOrderOutEnableList ref="saleOrderOutEnableListRef" @success="handleSaleOrderChange" />
+</template>
+<script setup lang="ts">
+import { SaleOutApi, SaleOutVO } from '@/api/erp/sale/out'
+import SaleOutItemForm from './components/SaleOutItemForm.vue'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import SaleOrderOutEnableList from '@/views/erp/sale/order/components/SaleOrderOutEnableList.vue'
+import { SaleOrderVO } from '@/api/erp/sale/order'
+import * as UserApi from '@/api/system/user'
+
+/** ERP 閿�鍞嚭搴撹〃鍗� */
+defineOptions({ name: 'SaleOutForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ saleUserId: undefined,
+ outTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ orderNo: undefined,
+ items: [],
+ no: undefined // 鍑哄簱鍗曞彿锛屽悗绔繑鍥�
+})
+const formRules = reactive({
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ outTime: [{ required: true, message: '鍑哄簱鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ // 璁$畻
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
+ formData.value.discountPrice = discountPrice
+ formData.value.totalPrice = totalPrice - discountPrice + val.otherPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SaleOutApi.getSaleOut(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎵撳紑銆愬彲鍑哄簱鐨勮鍗曞垪琛ㄣ�戝脊绐� */
+const saleOrderOutEnableListRef = ref() // 鍙嚭搴撶殑璁㈠崟鍒楄〃 Ref
+const openSaleOrderOutEnableList = () => {
+ saleOrderOutEnableListRef.value.open()
+}
+
+const handleSaleOrderChange = (order: SaleOrderVO) => {
+ // 灏嗚鍗曡缃埌鍑哄簱鍗�
+ formData.value.orderId = order.id
+ formData.value.orderNo = order.no
+ formData.value.customerId = order.customerId
+ formData.value.accountId = order.accountId
+ formData.value.saleUserId = order.saleUserId
+ formData.value.discountPercent = order.discountPercent
+ formData.value.remark = order.remark
+ formData.value.fileUrl = order.fileUrl
+ // 灏嗚鍗曢」璁剧疆鍒板嚭搴撳崟椤�
+ order.items.forEach((item) => {
+ item.totalCount = item.count
+ item.count = item.totalCount - item.outCount
+ item.orderItemId = item.id
+ item.id = undefined
+ })
+ formData.value.items = order.items.filter((item) => item.count > 0)
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SaleOutVO
+ if (formType.value === 'create') {
+ await SaleOutApi.createSaleOut(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SaleOutApi.updateSaleOut(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ saleUserId: undefined,
+ outTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/sale/out/components/SaleOutItemForm.vue b/src/views/erp/sale/out/components/SaleOutItemForm.vue
new file mode 100644
index 0000000..15cbef0
--- /dev/null
+++ b/src/views/erp/sale/out/components/SaleOutItemForm.vue
@@ -0,0 +1,300 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶇О" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍘熸暟閲�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.totalCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.totalCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="宸插嚭搴�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.outCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.outCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" prop="totalProductPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼锛�%锛�" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
+ <el-input-number
+ v-model="row.taxPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨" prop="taxPrice" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨鍚堣" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button :disabled="formData.length === 1" @click="handleDelete($index)" link>
+ 鈥�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+</template>
+<script setup lang="ts">
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ warehouseId: [{ required: true, message: '浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃嚭搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ val.forEach((item) => {
+ if (item.warehouseId == null) {
+ item.warehouseId = defaultWarehouse.value?.id
+ }
+ if (item.stockCount === null && item.warehouseId != null) {
+ setStockCount(item)
+ }
+ })
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
+ item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0)
+ if (item.totalProductPrice != null) {
+ item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalProductPrice: undefined,
+ taxPercent: undefined,
+ taxPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row: any) => {
+ if (!row.productId) {
+ return
+ }
+ const count = await StockApi.getStockCount(row.productId)
+ row.stockCount = count || 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+})
+</script>
diff --git a/src/views/erp/sale/out/components/SaleOutReceiptEnableList.vue b/src/views/erp/sale/out/components/SaleOutReceiptEnableList.vue
new file mode 100644
index 0000000..0c4a21d
--- /dev/null
+++ b/src/views/erp/sale/out/components/SaleOutReceiptEnableList.vue
@@ -0,0 +1,199 @@
+<!-- 鍙敹娆剧殑閿�鍞嚭搴撳崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閿�鍞嚭搴擄紙浠呭睍绀哄彲鏀舵锛�"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍑哄簱鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ュ嚭搴撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏃堕棿" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.outTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鍑哄簱鍗曞彿" align="center" prop="no" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="鍑哄簱鏃堕棿"
+ align="center"
+ prop="outTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="搴旀敹閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸叉敹閲戦"
+ align="center"
+ prop="receiptPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈敹閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.receiptPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.receiptPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!selectionList.length" type="primary" @click="submitForm">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { SaleOutApi, SaleOutVO } from '@/api/erp/sale/out'
+
+defineOptions({ name: 'SaleOutReceiptEnableList' })
+
+const list = ref<SaleOutVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ outTime: [],
+ receiptEnable: true,
+ customerId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<SaleOutVO[]>([])
+const handleSelectionChange = (rows: SaleOutVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (customerId: number) => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍙嚭搴撶殑璁㈠崟鍒楄〃
+ queryParams.customerId = customerId
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: SaleOutVO[]): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', selectionList.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleOutApi.getSaleOutPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ selectionList.value = []
+ getList()
+}
+</script>
diff --git a/src/views/erp/sale/out/index.vue b/src/views/erp/sale/out/index.vue
new file mode 100644
index 0000000..fdc7f71
--- /dev/null
+++ b/src/views/erp/sale/out/index.vue
@@ -0,0 +1,438 @@
+<template>
+ <doc-alert title="銆愰攢鍞�戦攢鍞鍗曘�佸嚭搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/sale/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍑哄簱鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ュ嚭搴撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏃堕棿" prop="outTime">
+ <el-date-picker
+ v-model="queryParams.outTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘鎴�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input
+ v-model="queryParams.orderNo"
+ placeholder="璇疯緭鍏ュ叧鑱旇鍗�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀舵鐘舵��" prop="receiptStatus">
+ <el-select
+ v-model="queryParams.receiptStatus"
+ placeholder="璇烽�夋嫨鏈夋鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈敹娆�" value="0" />
+ <el-option label="閮ㄥ垎鏀舵" value="1" />
+ <el-option label="鍏ㄩ儴鏀舵" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:sale-out:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:sale-out:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:sale-out:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鍑哄簱鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column
+ label="鍑哄簱鏃堕棿"
+ align="center"
+ prop="outTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="搴旀敹閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸叉敹閲戦"
+ align="center"
+ prop="receiptPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈敹閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.receiptPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.receiptPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹℃牳鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:sale-out:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:sale-out:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:sale-out:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:sale-out:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:sale-out:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SaleOutForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { SaleOutApi, SaleOutVO } from '@/api/erp/sale/out'
+import SaleOutForm from './SaleOutForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import {
+ erpCountTableColumnFormatter,
+ erpPriceInputFormatter,
+ erpPriceTableColumnFormatter
+} from '@/utils'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 閿�鍞嚭搴撳垪琛� */
+defineOptions({ name: 'ErpSaleOut' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<SaleOutVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ customerId: undefined,
+ productId: undefined,
+ warehouseId: undefined,
+ outTime: [],
+ orderNo: undefined,
+ receiptStatus: undefined,
+ accountId: undefined,
+ status: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleOutApi.getSaleOutPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SaleOutApi.deleteSaleOut(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ュ嚭搴撳悧锛焋)
+ // 鍙戣捣瀹℃壒
+ await SaleOutApi.updateSaleOutStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await SaleOutApi.exportSaleOut(queryParams)
+ download.excel(data, '閿�鍞嚭搴�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<SaleOutVO[]>([])
+const handleSelectionChange = (rows: SaleOutVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佸鎴�
+ productList.value = await ProductApi.getProductSimpleList()
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ accountList.value = await AccountApi.getAccountSimpleList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/sale/return/SaleReturnForm.vue b/src/views/erp/sale/return/SaleReturnForm.vue
new file mode 100644
index 0000000..b10403b
--- /dev/null
+++ b/src/views/erp/sale/return/SaleReturnForm.vue
@@ -0,0 +1,341 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1440">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閫�璐у崟鍙�" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閫�璐ф椂闂�" prop="returnTime">
+ <el-date-picker
+ v-model="formData.returnTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨閫�璐ф椂闂�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input v-model="formData.orderNo" readonly>
+ <template #append>
+ <el-button @click="openSaleOrderReturnEnableList">
+ <Icon icon="ep:search" /> 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ clearable
+ filterable
+ disabled
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閿�鍞汉鍛�" prop="saleUserId">
+ <el-select
+ v-model="formData.saleUserId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨閿�鍞汉鍛�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="閫�璐т骇鍝佹竻鍗�" name="item">
+ <SaleReturnItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鐜囷紙%锛�" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ紭鎯犵巼"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閫�娆句紭鎯�" prop="discountPrice">
+ <el-input
+ disabled
+ v-model="formData.discountPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浼樻儬鍚庨噾棰�">
+ <el-input
+ disabled
+ :model-value="formData.totalPrice - formData.otherPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏跺畠璐圭敤" prop="otherPrice">
+ <el-input-number
+ v-model="formData.otherPrice"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ叾瀹冭垂鐢�"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="formData.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="搴旈��閲戦" prop="totalPrice">
+ <el-input disabled v-model="formData.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+
+ <!-- 鍙��璐х殑璁㈠崟鍒楄〃 -->
+ <SaleOrderReturnEnableList ref="saleOrderReturnEnableListRef" @success="handleSaleOrderChange" />
+</template>
+<script setup lang="ts">
+import { SaleReturnApi, SaleReturnVO } from '@/api/erp/sale/return'
+import SaleReturnItemForm from './components/SaleReturnItemForm.vue'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+import { erpPriceInputFormatter, erpPriceMultiply } from '@/utils'
+import SaleOrderReturnEnableList from '@/views/erp/sale/order/components/SaleOrderReturnEnableList.vue'
+import { SaleOrderVO } from '@/api/erp/sale/order'
+import * as UserApi from '@/api/system/user'
+
+/** ERP 閿�鍞��璐ц〃鍗� */
+defineOptions({ name: 'SaleReturnForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ saleUserId: undefined,
+ returnTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ orderNo: undefined,
+ items: [],
+ no: undefined // 閫�璐у崟鍙凤紝鍚庣杩斿洖
+})
+const formRules = reactive({
+ customerId: [{ required: true, message: '瀹㈡埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ returnTime: [{ required: true, message: '閫�璐ф椂闂翠笉鑳戒负绌�', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 璁$畻 discountPrice銆乼otalPrice 浠锋牸 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val) {
+ return
+ }
+ // 璁$畻
+ const totalPrice = val.items.reduce((prev, curr) => prev + curr.totalPrice, 0)
+ const discountPrice =
+ val.discountPercent != null ? erpPriceMultiply(totalPrice, val.discountPercent / 100.0) : 0
+ formData.value.totalPrice = totalPrice - discountPrice + val.otherPrice
+ },
+ { deep: true }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SaleReturnApi.getSaleReturn(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鍔犺浇璐︽埛鍒楄〃
+ accountList.value = await AccountApi.getAccountSimpleList()
+ const defaultAccount = accountList.value.find((item) => item.defaultStatus)
+ if (defaultAccount) {
+ formData.value.accountId = defaultAccount.id
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎵撳紑銆愬彲閫�璐х殑璁㈠崟鍒楄〃銆戝脊绐� */
+const saleOrderReturnEnableListRef = ref() // 鍙��璐х殑璁㈠崟鍒楄〃 Ref
+const openSaleOrderReturnEnableList = () => {
+ saleOrderReturnEnableListRef.value.open()
+}
+
+const handleSaleOrderChange = (order: SaleOrderVO) => {
+ // 灏嗚鍗曡缃埌閫�璐у崟
+ formData.value.orderId = order.id
+ formData.value.orderNo = order.no
+ formData.value.customerId = order.customerId
+ formData.value.accountId = order.accountId
+ formData.value.saleUserId = order.saleUserId
+ formData.value.discountPercent = order.discountPercent
+ formData.value.remark = order.remark
+ formData.value.fileUrl = order.fileUrl
+ // 灏嗚鍗曢」璁剧疆鍒伴��璐у崟椤�
+ order.items.forEach((item) => {
+ item.count = item.outCount - item.returnCount
+ item.orderItemId = item.id
+ item.id = undefined
+ })
+ formData.value.items = order.items.filter((item) => item.count > 0)
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SaleReturnVO
+ if (formType.value === 'create') {
+ await SaleReturnApi.createSaleReturn(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SaleReturnApi.updateSaleReturn(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ accountId: undefined,
+ saleUserId: undefined,
+ returnTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ discountPercent: 0,
+ discountPrice: 0,
+ totalPrice: 0,
+ otherPrice: 0,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/sale/return/components/SaleReturnItemForm.vue b/src/views/erp/sale/return/components/SaleReturnItemForm.vue
new file mode 100644
index 0000000..adb9fd4
--- /dev/null
+++ b/src/views/erp/sale/return/components/SaleReturnItemForm.vue
@@ -0,0 +1,300 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶇О" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="宸插嚭搴�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.outCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.outCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="宸查��璐�"
+ fixed="right"
+ min-width="80"
+ v-if="formData[0]?.returnCount != null"
+ >
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.returnCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" prop="totalProductPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalProductPrice`" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.totalProductPrice"
+ :formatter="erpPriceInputFormatter"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼锛�%锛�" fixed="right" min-width="115">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPercent`" class="mb-0px!">
+ <el-input-number
+ v-model="row.taxPercent"
+ controls-position="right"
+ :min="0"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨" prop="taxPrice" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-form-item :prop="`${$index}.taxPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.taxPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庨鍚堣" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button :disabled="formData.length === 1" @click="handleDelete($index)" link>
+ 鈥�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+</template>
+<script setup lang="ts">
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ warehouseId: [{ required: true, message: '浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃嚭搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ val.forEach((item) => {
+ if (item.warehouseId == null) {
+ item.warehouseId = defaultWarehouse.value?.id
+ }
+ if (item.stockCount === null && item.warehouseId != null) {
+ setStockCount(item)
+ }
+ })
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalProductPrice = erpPriceMultiply(item.productPrice, item.count)
+ item.taxPrice = erpPriceMultiply(item.totalProductPrice, item.taxPercent / 100.0)
+ if (item.totalProductPrice != null) {
+ item.totalPrice = item.totalProductPrice + (item.taxPrice || 0)
+ } else {
+ item.totalPrice = undefined
+ }
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index: number) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalProductPrice', 'taxPrice', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalProductPrice: undefined,
+ taxPercent: undefined,
+ taxPrice: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row: any) => {
+ if (!row.productId) {
+ return
+ }
+ const count = await StockApi.getStockCount(row.productId)
+ row.stockCount = count || 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+})
+</script>
diff --git a/src/views/erp/sale/return/components/SaleReturnRefundEnableList.vue b/src/views/erp/sale/return/components/SaleReturnRefundEnableList.vue
new file mode 100644
index 0000000..dc875e6
--- /dev/null
+++ b/src/views/erp/sale/return/components/SaleReturnRefundEnableList.vue
@@ -0,0 +1,199 @@
+<!-- 鍙��娆剧殑閿�鍞��璐у崟鍒楄〃 -->
+<template>
+ <Dialog
+ title="閫夋嫨閿�鍞��璐э紙浠呭睍绀哄彲閫�娆撅級"
+ v-model="dialogVisible"
+ :appendToBody="true"
+ :scroll="true"
+ width="1080"
+ >
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閫�璐у崟鍙�" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ラ��璐у崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-160px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф椂闂�" prop="orderTime">
+ <el-date-picker
+ v-model="queryParams.returnTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-160px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="閫�璐у崟鍙�" align="center" prop="no" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="閫�璐ф椂闂�"
+ align="center"
+ prop="returnTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="搴旈��閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸查��閲戦"
+ align="center"
+ prop="refundPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈��閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="!selectionList.length" type="primary" @click="submitForm">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { erpPriceInputFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { SaleReturnApi, SaleReturnVO } from '@/api/erp/sale/return'
+
+defineOptions({ name: 'SaleReturnPaymentEnableList' })
+
+const list = ref<SaleReturnVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ returnTime: [],
+ refundEnable: true,
+ customerId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<SaleReturnVO[]>([])
+const handleSelectionChange = (rows: SaleReturnVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (customerId: number) => {
+ dialogVisible.value = true
+ await nextTick() // 绛夊緟锛岄伩鍏� queryFormRef 涓虹┖
+ // 鍔犺浇鍒楄〃
+ queryParams.customerId = customerId
+ await resetQuery()
+ // 鍔犺浇浜у搧鍒楄〃
+ productList.value = await ProductApi.getProductSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦閫夋嫨 */
+const emits = defineEmits<{
+ (e: 'success', value: SaleReturnVO[]): void
+}>()
+const submitForm = () => {
+ try {
+ emits('success', selectionList.value)
+ } finally {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ }
+}
+
+/** 鍔犺浇鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleReturnApi.getSaleReturnPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ selectionList.value = []
+ getList()
+}
+</script>
diff --git a/src/views/erp/sale/return/index.vue b/src/views/erp/sale/return/index.vue
new file mode 100644
index 0000000..be0b174
--- /dev/null
+++ b/src/views/erp/sale/return/index.vue
@@ -0,0 +1,443 @@
+<template>
+ <doc-alert title="銆愰攢鍞�戦攢鍞鍗曘�佸嚭搴撱�侀��璐�" url="https://doc.iocoder.cn/erp/sale/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閫�璐у崟鍙�" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ラ��璐у崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф椂闂�" prop="outTime">
+ <el-date-picker
+ v-model="queryParams.outTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘鎴�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈璁㈠崟" prop="orderNo">
+ <el-input
+ v-model="queryParams.orderNo"
+ placeholder="璇疯緭鍏ュ叧鑱旇鍗�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撶畻璐︽埛" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨缁撶畻璐︽埛"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in accountList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�娆剧姸鎬�" prop="refundStatus">
+ <el-select
+ v-model="queryParams.refundStatus"
+ placeholder="璇烽�夋嫨閫�娆剧姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option label="鏈��娆�" value="0" />
+ <el-option label="閮ㄥ垎閫�娆�" value="1" />
+ <el-option label="鍏ㄩ儴閫�娆�" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨瀹℃牳鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:sale-return:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:sale-return:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:sale-return:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="閫�璐у崟鍙�" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column
+ label="閫�璐ф椂闂�"
+ align="center"
+ prop="returnTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鎬绘暟閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="搴旈��閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="宸查��閲戦"
+ align="center"
+ prop="refundPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鏈��閲戦" align="center">
+ <template #default="scope">
+ <span v-if="scope.row.refundPrice === scope.row.totalPrice">0</span>
+ <el-tag type="danger" v-else>
+ {{ erpPriceInputFormatter(scope.row.totalPrice - scope.row.refundPrice) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹℃牳鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:sale-return:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:sale-return:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:sale-return:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:sale-return:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:sale-return:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SaleReturnForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { SaleReturnApi, SaleReturnVO } from '@/api/erp/sale/return'
+import SaleReturnForm from './SaleReturnForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import {
+ erpCountTableColumnFormatter,
+ erpPriceInputFormatter,
+ erpPriceTableColumnFormatter
+} from '@/utils'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { AccountApi, AccountVO } from '@/api/erp/finance/account'
+
+/** ERP 閿�鍞��璐у垪琛� */
+defineOptions({ name: 'ErpSaleReturn' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<SaleReturnVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ customerId: undefined,
+ productId: undefined,
+ warehouseId: undefined,
+ returnTime: [],
+ orderNo: undefined,
+ accountId: undefined,
+ status: undefined,
+ remark: undefined,
+ creator: undefined,
+ refundStatus: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const accountList = ref<AccountVO[]>([]) // 璐︽埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SaleReturnApi.getSaleReturnPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SaleReturnApi.deleteSaleReturn(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ラ��璐у悧锛焋)
+ // 鍙戣捣瀹℃壒
+ await SaleReturnApi.updateSaleReturnStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await SaleReturnApi.exportSaleReturn(queryParams)
+ download.excel(data, '閿�鍞��璐�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<SaleReturnVO[]>([])
+const handleSelectionChange = (rows: SaleReturnVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佸鎴�
+ productList.value = await ProductApi.getProductSimpleList()
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ accountList.value = await AccountApi.getAccountSimpleList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/stock/check/StockCheckForm.vue b/src/views/erp/stock/check/StockCheckForm.vue
new file mode 100644
index 0000000..9e7f673
--- /dev/null
+++ b/src/views/erp/stock/check/StockCheckForm.vue
@@ -0,0 +1,148 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鐩樼偣鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐩樼偣鏃堕棿" prop="checkTime">
+ <el-date-picker
+ v-model="formData.checkTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鐩樼偣鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="鐩樼偣浜у搧娓呭崟" name="item">
+ <StockCheckItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { StockCheckApi, StockCheckVO } from '@/api/erp/stock/check'
+import StockCheckItemForm from './components/StockCheckItemForm.vue'
+
+/** ERP 鍏跺畠鐩樼偣鍗曡〃鍗� */
+defineOptions({ name: 'StockCheckForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ checkTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ items: []
+})
+const formRules = reactive({
+ checkTime: [{ required: true, message: '鐩樼偣鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await StockCheckApi.getStockCheck(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as StockCheckVO
+ if (formType.value === 'create') {
+ await StockCheckApi.createStockCheck(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await StockCheckApi.updateStockCheck(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ checkTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/stock/check/components/StockCheckItemForm.vue b/src/views/erp/stock/check/components/StockCheckItemForm.vue
new file mode 100644
index 0000000..6036311
--- /dev/null
+++ b/src/views/erp/stock/check/components/StockCheckItemForm.vue
@@ -0,0 +1,289 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶅瓧" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱鍚嶅瓧"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="璐﹂潰搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹為檯搴撳瓨" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.actualCount`"
+ :rules="formRules.actualCount"
+ class="mb-0px!"
+ >
+ <el-input-number
+ v-model="row.actualCount"
+ controls-position="right"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐩堜簭鏁伴噺" prop="count" fixed="right" min-width="110">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input
+ disabled
+ v-model="row.count"
+ :formatter="erpCountInputFormatter"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堣閲戦" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞鐩樼偣浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ inId: [{ required: true, message: '鐩樼偣缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ warehouseId: [{ required: true, message: '浠撳簱鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃洏鐐归」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ if (item.stockCount != null && item.actualCount != null) {
+ item.count = item.actualCount - item.stockCount
+ } else {
+ item.count = undefined
+ }
+ item.totalPrice = erpPriceMultiply(item.productPrice, item.count)
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ warehouseId: defaultWarehouse.value?.id,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ actualCount: undefined,
+ count: undefined,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浠撳簱鍙樻洿 */
+const onChangeWarehouse = (warehouseId, row) => {
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnitName = product.unitName
+ row.productBarCode = product.barCode
+ row.productPrice = product.minPrice
+ }
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row) => {
+ if (!row.productId || !row.warehouseId) {
+ return
+ }
+ const stock = await StockApi.getStock2(row.productId, row.warehouseId)
+ row.stockCount = stock ? stock.count : 0
+ row.actualCount = row.stockCount
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+ // 榛樿娣诲姞涓�涓�
+ if (formData.value.length === 0) {
+ handleAdd()
+ }
+})
+</script>
diff --git a/src/views/erp/stock/check/index.vue b/src/views/erp/stock/check/index.vue
new file mode 100644
index 0000000..f661ab7
--- /dev/null
+++ b/src/views/erp/stock/check/index.vue
@@ -0,0 +1,359 @@
+<template>
+ <doc-alert
+ title="銆愬簱瀛樸�戝簱瀛樿皟鎷ㄣ�佸簱瀛樼洏鐐�"
+ url="https://doc.iocoder.cn/erp/stock-move-check/"
+ />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐩樼偣鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ョ洏鐐瑰崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐩樼偣鏃堕棿" prop="checkTime">
+ <el-date-picker
+ v-model="queryParams.checkTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:stock-check:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:stock-check:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:stock-check:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鐩樼偣鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="鐩樼偣鏃堕棿"
+ align="center"
+ prop="checkTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鏁伴噺"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:stock-check:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:stock-check:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:stock-check:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:stock-check:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:stock-check:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <StockCheckForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { StockCheckApi, StockCheckVO } from '@/api/erp/stock/check'
+import StockCheckForm from './StockCheckForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+
+/** ERP 鍏跺畠鐩樼偣鍗曞垪琛� */
+defineOptions({ name: 'ErpStockCheck' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<StockCheckVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ warehouseId: undefined,
+ checkTime: [],
+ status: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await StockCheckApi.getStockCheckPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await StockCheckApi.deleteStockCheck(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ョ洏鐐瑰崟鍚楋紵`)
+ // 鍙戣捣瀹℃壒
+ await StockCheckApi.updateStockCheckStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await StockCheckApi.exportStockCheck(queryParams)
+ download.excel(data, '鍏跺畠鐩樼偣鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<StockCheckVO[]>([])
+const handleSelectionChange = (rows: StockCheckVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佸鎴�
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/stock/in/StockInForm.vue b/src/views/erp/stock/in/StockInForm.vue
new file mode 100644
index 0000000..f36bbb6
--- /dev/null
+++ b/src/views/erp/stock/in/StockInForm.vue
@@ -0,0 +1,170 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鍏ュ簱鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍏ュ簱鏃堕棿" prop="inTime">
+ <el-date-picker
+ v-model="formData.inTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍏ュ簱鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="formData.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="鍏ュ簱浜у搧娓呭崟" name="item">
+ <StockInItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { StockInApi, StockInVO } from '@/api/erp/stock/in'
+import StockInItemForm from './components/StockInItemForm.vue'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+
+/** ERP 鍏跺畠鍏ュ簱鍗� 琛ㄥ崟 */
+defineOptions({ name: 'StockInForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ supplierId: undefined,
+ inTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ items: []
+})
+const formRules = reactive({
+ inTime: [{ required: true, message: '鍏ュ簱鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await StockInApi.getStockIn(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇渚涘簲鍟嗗垪琛�
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as StockInVO
+ if (formType.value === 'create') {
+ await StockInApi.createStockIn(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await StockInApi.updateStockIn(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ supplierId: undefined,
+ inTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/stock/in/components/StockInItemForm.vue b/src/views/erp/stock/in/components/StockInItemForm.vue
new file mode 100644
index 0000000..53a2fd2
--- /dev/null
+++ b/src/views/erp/stock/in/components/StockInItemForm.vue
@@ -0,0 +1,267 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶇О" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堣閲戦" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞鍏ュ簱浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ inId: [{ required: true, message: '鍏ュ簱缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ warehouseId: [{ required: true, message: '浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃叆搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalPrice = erpPriceMultiply(item.productPrice, item.count)
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ warehouseId: defaultWarehouse.value?.id,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浠撳簱鍙樻洿 */
+const onChangeWarehouse = (warehouseId, row) => {
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnitName = product.unitName
+ row.productBarCode = product.barCode
+ row.productPrice = product.minPrice
+ }
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row) => {
+ if (!row.productId || !row.warehouseId) {
+ return
+ }
+ const stock = await StockApi.getStock2(row.productId, row.warehouseId)
+ row.stockCount = stock ? stock.count : 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+ // 榛樿娣诲姞涓�涓�
+ if (formData.value.length === 0) {
+ handleAdd()
+ }
+})
+</script>
diff --git a/src/views/erp/stock/in/index.vue b/src/views/erp/stock/in/index.vue
new file mode 100644
index 0000000..ca697e9
--- /dev/null
+++ b/src/views/erp/stock/in/index.vue
@@ -0,0 +1,376 @@
+<template>
+ <doc-alert title="銆愬簱瀛樸�戝叾瀹冨叆搴撱�佸叾瀹冨嚭搴�" url="https://doc.iocoder.cn/erp/stock-in-out/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏ュ簱鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ュ叆搴撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏ュ簱鏃堕棿" prop="inTime">
+ <el-date-picker
+ v-model="queryParams.inTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="queryParams.supplierId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:stock-in:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:stock-in:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:stock-in:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鍏ュ簱鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="渚涘簲鍟�" align="center" prop="supplierName" />
+ <el-table-column
+ label="鍏ュ簱鏃堕棿"
+ align="center"
+ prop="inTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鏁伴噺"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:stock-in:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:stock-in:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:stock-in:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:stock-in:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:stock-in:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <StockInForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { StockInApi, StockInVO } from '@/api/erp/stock/in'
+import StockInForm from './StockInForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+
+/** ERP 鍏跺畠鍏ュ簱鍗曞垪琛� */
+defineOptions({ name: 'ErpStockIn' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<StockInVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ supplierId: undefined,
+ inTime: [],
+ status: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const supplierList = ref<SupplierVO[]>([]) // 渚涘簲鍟嗗垪琛�
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await StockInApi.getStockInPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await StockInApi.deleteStockIn(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ュ叆搴撳崟鍚楋紵`)
+ // 鍙戣捣瀹℃壒
+ await StockInApi.updateStockInStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await StockInApi.exportStockIn(queryParams)
+ download.excel(data, '鍏跺畠鍏ュ簱鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<StockInVO[]>([])
+const handleSelectionChange = (rows: StockInVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佷緵搴斿晢
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ supplierList.value = await SupplierApi.getSupplierSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/stock/move/StockMoveForm.vue b/src/views/erp/stock/move/StockMoveForm.vue
new file mode 100644
index 0000000..df942c6
--- /dev/null
+++ b/src/views/erp/stock/move/StockMoveForm.vue
@@ -0,0 +1,148 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="璋冨害鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璋冨害鏃堕棿" prop="moveTime">
+ <el-date-picker
+ v-model="formData.moveTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨璋冨害鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="璋冨害浜у搧娓呭崟" name="item">
+ <StockMoveItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { StockMoveApi, StockMoveVO } from '@/api/erp/stock/move'
+import StockMoveItemForm from './components/StockMoveItemForm.vue'
+
+/** ERP 搴撳瓨璋冨害鍗曡〃鍗� */
+defineOptions({ name: 'StockMoveForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ moveTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ items: []
+})
+const formRules = reactive({
+ moveTime: [{ required: true, message: '璋冨害鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await StockMoveApi.getStockMove(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as StockMoveVO
+ if (formType.value === 'create') {
+ await StockMoveApi.createStockMove(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await StockMoveApi.updateStockMove(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ moveTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/stock/move/components/StockMoveItemForm.vue b/src/views/erp/stock/move/components/StockMoveItemForm.vue
new file mode 100644
index 0000000..8971956
--- /dev/null
+++ b/src/views/erp/stock/move/components/StockMoveItemForm.vue
@@ -0,0 +1,292 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="璋冨嚭浠撳簱" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.fromWarehouseId`"
+ :rules="formRules.fromWarehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.fromWarehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨璋冨嚭浠撳簱"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="璋冨叆浠撳簱" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.toWarehouseId`"
+ :rules="formRules.toWarehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.toWarehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨璋冨叆浠撳簱"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堣閲戦" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞璋冨害浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ inId: [{ required: true, message: '璋冨害缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ fromWarehouseId: [{ required: true, message: '璋冨嚭浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ toWarehouseId: [{ required: true, message: '璋冨叆浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃皟搴﹂」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalPrice = erpPriceMultiply(item.productPrice, item.count)
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ fromWarehouseId: defaultWarehouse.value?.id,
+ toWarehouseId: undefined,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浠撳簱鍙樻洿 */
+const onChangeWarehouse = (warehouseId, row) => {
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnitName = product.unitName
+ row.productBarCode = product.barCode
+ row.productPrice = product.minPrice
+ }
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row) => {
+ if (!row.productId || !row.fromWarehouseId) {
+ return
+ }
+ const stock = await StockApi.getStock2(row.productId, row.fromWarehouseId)
+ row.stockCount = stock ? stock.count : 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+ // 榛樿娣诲姞涓�涓�
+ if (formData.value.length === 0) {
+ handleAdd()
+ }
+})
+</script>
diff --git a/src/views/erp/stock/move/index.vue b/src/views/erp/stock/move/index.vue
new file mode 100644
index 0000000..76ea653
--- /dev/null
+++ b/src/views/erp/stock/move/index.vue
@@ -0,0 +1,359 @@
+<template>
+ <doc-alert
+ title="銆愬簱瀛樸�戝簱瀛樿皟鎷ㄣ�佸簱瀛樼洏鐐�"
+ url="https://doc.iocoder.cn/erp/stock-move-check/"
+ />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="璋冨害鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ皟搴﹀崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璋冨害鏃堕棿" prop="moveTime">
+ <el-date-picker
+ v-model="queryParams.moveTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="fromWarehouseId">
+ <el-select
+ v-model="queryParams.fromWarehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:stock-move:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:stock-move:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:stock-move:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="璋冨害鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column
+ label="璋冨害鏃堕棿"
+ align="center"
+ prop="moveTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鏁伴噺"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:stock-move:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:stock-move:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:stock-move:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:stock-move:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:stock-move:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <StockMoveForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { StockMoveApi, StockMoveVO } from '@/api/erp/stock/move'
+import StockMoveForm from './StockMoveForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+
+/** ERP 搴撳瓨璋冨害鍗曞垪琛� */
+defineOptions({ name: 'ErpStockMove' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<StockMoveVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ fromWarehouseId: undefined,
+ moveTime: [],
+ status: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await StockMoveApi.getStockMovePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await StockMoveApi.deleteStockMove(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ヨ皟搴﹀崟鍚楋紵`)
+ // 鍙戣捣瀹℃壒
+ await StockMoveApi.updateStockMoveStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await StockMoveApi.exportStockMove(queryParams)
+ download.excel(data, '搴撳瓨璋冨害鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<StockMoveVO[]>([])
+const handleSelectionChange = (rows: StockMoveVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佸鎴�
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/stock/out/StockOutForm.vue b/src/views/erp/stock/out/StockOutForm.vue
new file mode 100644
index 0000000..8ae8d63
--- /dev/null
+++ b/src/views/erp/stock/out/StockOutForm.vue
@@ -0,0 +1,170 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1080">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ :disabled="disabled"
+ >
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鍑哄簱鍗曞彿" prop="no">
+ <el-input disabled v-model="formData.no" placeholder="淇濆瓨鏃惰嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍑哄簱鏃堕棿" prop="outTime">
+ <el-date-picker
+ v-model="formData.outTime"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑哄簱鏃堕棿"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="formData.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ class="!w-1/1"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="formData.remark"
+ :rows="1"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="闄勪欢" prop="fileUrl">
+ <UploadFile :is-show-tip="false" v-model="formData.fileUrl" :limit="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <ContentWrap>
+ <el-tabs v-model="subTabsName" class="-mt-15px -mb-10px">
+ <el-tab-pane label="鍑哄簱浜у搧娓呭崟" name="item">
+ <StockOutItemForm ref="itemFormRef" :items="formData.items" :disabled="disabled" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading" v-if="!disabled">
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { StockOutApi, StockOutVO } from '@/api/erp/stock/out'
+import StockOutItemForm from './components/StockOutItemForm.vue'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+
+/** ERP 鍏跺畠鍑哄簱鍗曡〃鍗� */
+defineOptions({ name: 'StockOutForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼锛沝etail - 璇︽儏
+const formData = ref({
+ id: undefined,
+ customerId: undefined,
+ outTime: undefined,
+ remark: undefined,
+ fileUrl: '',
+ items: []
+})
+const formRules = reactive({
+ outTime: [{ required: true, message: '鍑哄簱鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const disabled = computed(() => formType.value === 'detail')
+const formRef = ref() // 琛ㄥ崟 Ref
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('item')
+const itemFormRef = ref()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await StockOutApi.getStockOut(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇瀹㈡埛鍒楄〃
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ await itemFormRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as StockOutVO
+ if (formType.value === 'create') {
+ await StockOutApi.createStockOut(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await StockOutApi.updateStockOut(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ customerId: undefined,
+ outTime: undefined,
+ remark: undefined,
+ fileUrl: undefined,
+ items: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/stock/out/components/StockOutItemForm.vue b/src/views/erp/stock/out/components/StockOutItemForm.vue
new file mode 100644
index 0000000..b09a569
--- /dev/null
+++ b/src/views/erp/stock/out/components/StockOutItemForm.vue
@@ -0,0 +1,267 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ :disabled="disabled"
+ >
+ <el-table :data="formData" show-summary :summary-method="getSummaries" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" align="center" width="60" />
+ <el-table-column label="浠撳簱鍚嶇О" min-width="125">
+ <template #default="{ row, $index }">
+ <el-form-item
+ :prop="`${$index}.warehouseId`"
+ :rules="formRules.warehouseId"
+ class="mb-0px!"
+ >
+ <el-select
+ v-model="row.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ @change="onChangeWarehouse($event, row)"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" min-width="180">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ clearable
+ filterable
+ @change="onChangeProduct($event, row)"
+ placeholder="璇烽�夋嫨浜у搧"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" min-width="100">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.stockCount" :formatter="erpCountInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏉$爜" min-width="150">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productBarCode" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" min-width="80">
+ <template #default="{ row }">
+ <el-form-item class="mb-0px!">
+ <el-input disabled v-model="row.productUnitName" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" fixed="right" min-width="140">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.count`" :rules="formRules.count" class="mb-0px!">
+ <el-input-number
+ v-model="row.count"
+ controls-position="right"
+ :min="0.001"
+ :precision="3"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍗曚环" fixed="right" min-width="120">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productPrice`" class="mb-0px!">
+ <el-input-number
+ v-model="row.productPrice"
+ controls-position="right"
+ :min="0.01"
+ :precision="2"
+ class="!w-100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堣閲戦" prop="totalPrice" fixed="right" min-width="100">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.totalPrice`" class="mb-0px!">
+ <el-input disabled v-model="row.totalPrice" :formatter="erpPriceInputFormatter" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.remark`" class="mb-0px!">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3" v-if="!disabled">
+ <el-button @click="handleAdd" round>+ 娣诲姞鍑哄簱浜у搧</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { StockApi } from '@/api/erp/stock/stock'
+import {
+ erpCountInputFormatter,
+ erpPriceInputFormatter,
+ erpPriceMultiply,
+ getSumValue
+} from '@/utils'
+
+const props = defineProps<{
+ items: undefined
+ disabled: false
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref([])
+const formRules = reactive({
+ inId: [{ required: true, message: '鍑哄簱缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ warehouseId: [{ required: true, message: '浠撳簱涓嶈兘涓虹┖', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ count: [{ required: true, message: '浜у搧鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref([]) // 琛ㄥ崟 Ref
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const defaultWarehouse = ref<WarehouseVO>(undefined) // 榛樿浠撳簱
+
+/** 鍒濆鍖栬缃嚭搴撻」 */
+watch(
+ () => props.items,
+ async (val) => {
+ formData.value = val
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉鍚堝悓浜у搧鍙樺寲锛岃绠楀悎鍚屼骇鍝佹�讳环 */
+watch(
+ () => formData.value,
+ (val) => {
+ if (!val || val.length === 0) {
+ return
+ }
+ // 寰幆澶勭悊
+ val.forEach((item) => {
+ item.totalPrice = erpPriceMultiply(item.productPrice, item.count)
+ })
+ },
+ { deep: true }
+)
+
+/** 鍚堣 */
+const getSummaries = (param: SummaryMethodProps) => {
+ const { columns, data } = param
+ const sums: string[] = []
+ columns.forEach((column, index) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['count', 'totalPrice'].includes(column.property)) {
+ const sum = getSumValue(data.map((item) => Number(item[column.property])))
+ sums[index] =
+ column.property === 'count' ? erpCountInputFormatter(sum) : erpPriceInputFormatter(sum)
+ } else {
+ sums[index] = ''
+ }
+ })
+
+ return sums
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ warehouseId: defaultWarehouse.value?.id,
+ productId: undefined,
+ productUnitName: undefined, // 浜у搧鍗曚綅
+ productBarCode: undefined, // 浜у搧鏉$爜
+ productPrice: undefined,
+ stockCount: undefined,
+ count: 1,
+ totalPrice: undefined,
+ remark: undefined
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index) => {
+ formData.value.splice(index, 1)
+}
+
+/** 澶勭悊浠撳簱鍙樻洿 */
+const onChangeWarehouse = (warehouseId, row) => {
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 澶勭悊浜у搧鍙樻洿 */
+const onChangeProduct = (productId, row) => {
+ const product = productList.value.find((item) => item.id === productId)
+ if (product) {
+ row.productUnitName = product.unitName
+ row.productBarCode = product.barCode
+ row.productPrice = product.minPrice
+ }
+ // 鍔犺浇搴撳瓨
+ setStockCount(row)
+}
+
+/** 鍔犺浇搴撳瓨 */
+const setStockCount = async (row) => {
+ if (!row.productId || !row.warehouseId) {
+ return
+ }
+ const stock = await StockApi.getStock2(row.productId, row.warehouseId)
+ row.stockCount = stock ? stock.count : 0
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ defaultWarehouse.value = warehouseList.value.find((item) => item.defaultStatus)
+ // 榛樿娣诲姞涓�涓�
+ if (formData.value.length === 0) {
+ handleAdd()
+ }
+})
+</script>
diff --git a/src/views/erp/stock/out/index.vue b/src/views/erp/stock/out/index.vue
new file mode 100644
index 0000000..555b985
--- /dev/null
+++ b/src/views/erp/stock/out/index.vue
@@ -0,0 +1,378 @@
+<template>
+ <doc-alert title="銆愬簱瀛樸�戝叾瀹冨叆搴撱�佸叾瀹冨嚭搴�" url="https://doc.iocoder.cn/erp/stock-in-out/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍑哄簱鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ュ嚭搴撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏃堕棿" prop="outTime">
+ <el-date-picker
+ v-model="queryParams.outTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="queryParams.customerId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨渚涘鎴�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in customerList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select
+ v-model="queryParams.creator"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍒涘缓浜�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_AUDIT_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="queryParams.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:stock-out:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:stock-out:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete(selectionList.map((item) => item.id))"
+ v-hasPermi="['erp:stock-out:delete']"
+ :disabled="selectionList.length === 0"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column width="30" label="閫夋嫨" type="selection" />
+ <el-table-column min-width="180" label="鍑哄簱鍗曞彿" align="center" prop="no" />
+ <el-table-column label="浜у搧淇℃伅" align="center" prop="productNames" min-width="200" />
+ <el-table-column label="瀹㈡埛" align="center" prop="customerName" />
+ <el-table-column
+ label="鍑哄簱鏃堕棿"
+ align="center"
+ prop="outTime"
+ :formatter="dateFormatter2"
+ width="120px"
+ />
+ <el-table-column label="鍒涘缓浜�" align="center" prop="creatorName" />
+ <el-table-column
+ label="鏁伴噺"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="閲戦"
+ align="center"
+ prop="totalPrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="鐘舵��" align="center" fixed="right" width="90" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_AUDIT_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="220">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openForm('detail', scope.row.id)"
+ v-hasPermi="['erp:stock-out:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:stock-out:update']"
+ :disabled="scope.row.status === 20"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="handleUpdateStatus(scope.row.id, 20)"
+ v-hasPermi="['erp:stock-out:update-status']"
+ v-if="scope.row.status === 10"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleUpdateStatus(scope.row.id, 10)"
+ v-hasPermi="['erp:stock-out:update-status']"
+ v-else
+ >
+ 鍙嶅鎵�
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete([scope.row.id])"
+ v-hasPermi="['erp:stock-out:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <StockOutForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter2 } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { StockOutApi, StockOutVO } from '@/api/erp/stock/out'
+import StockOutForm from './StockOutForm.vue'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { SupplierApi, SupplierVO } from '@/api/erp/purchase/supplier'
+import { UserVO } from '@/api/system/user'
+import * as UserApi from '@/api/system/user'
+import { erpCountTableColumnFormatter, erpPriceTableColumnFormatter } from '@/utils'
+import { CustomerApi, CustomerVO } from '@/api/erp/sale/customer'
+
+/** ERP 鍏跺畠鍑哄簱鍗曞垪琛� */
+defineOptions({ name: 'ErpStockOut' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<StockOutVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: undefined,
+ productId: undefined,
+ customerId: undefined,
+ warehouseId: undefined,
+ outTime: [],
+ status: undefined,
+ remark: undefined,
+ creator: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+const customerList = ref<CustomerVO[]>([]) // 瀹㈡埛鍒楄〃
+const userList = ref<UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await StockOutApi.getStockOutPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (ids: number[]) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await StockOutApi.deleteStockOut(ids)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ selectionList.value = selectionList.value.filter((item) => !ids.includes(item.id))
+ } catch {}
+}
+
+/** 瀹℃壒/鍙嶅鎵规搷浣� */
+const handleUpdateStatus = async (id: number, status: number) => {
+ try {
+ // 瀹℃壒鐨勪簩娆$‘璁�
+ await message.confirm(`纭畾${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}璇ュ嚭搴撳崟鍚楋紵`)
+ // 鍙戣捣瀹℃壒
+ await StockOutApi.updateStockOutStatus(id, status)
+ message.success(`${status === 20 ? '瀹℃壒' : '鍙嶅鎵�'}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await StockOutApi.exportStockOut(queryParams)
+ download.excel(data, '鍏跺畠鍑哄簱鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑鎿嶄綔 */
+const selectionList = ref<StockOutVO[]>([])
+const handleSelectionChange = (rows: StockOutVO[]) => {
+ selectionList.value = rows
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛ㄣ�佸鎴�
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+ customerList.value = await CustomerApi.getCustomerSimpleList()
+ userList.value = await UserApi.getSimpleUserList()
+})
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氬垪琛ㄧ晫闈紝鏀寔瀵煎叆
+// TODO 鑺嬭壙锛氬彲浼樺寲鍔熻兘锛氳鎯呯晫闈紝鏀寔鎵撳嵃
+</script>
diff --git a/src/views/erp/stock/record/index.vue b/src/views/erp/stock/record/index.vue
new file mode 100644
index 0000000..35b58ca
--- /dev/null
+++ b/src/views/erp/stock/record/index.vue
@@ -0,0 +1,250 @@
+<!-- ERP 浜у搧搴撳瓨鏄庣粏鍒楄〃 -->
+<template>
+ <doc-alert title="銆愬簱瀛樸�戜骇鍝佸簱瀛樸�佸簱瀛樻槑缁�" url="https://doc.iocoder.cn/erp/stock/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="bizType">
+ <el-select
+ v-model="queryParams.bizType"
+ placeholder="璇烽�夋嫨绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.ERP_STOCK_RECORD_BIZ_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="涓氬姟鍗曞彿" prop="bizNo">
+ <el-input
+ v-model="queryParams.bizNo"
+ placeholder="璇疯緭鍏ヤ笟鍔″崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:stock-record:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:stock-record:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="浜у搧鍚嶇О" align="center" prop="productName" />
+ <el-table-column label="浜у搧鍒嗙被" align="center" prop="categoryName" />
+ <el-table-column label="浜у搧鍗曚綅" align="center" prop="unitName" />
+ <el-table-column label="浠撳簱缂栧彿" align="center" prop="warehouseName" />
+ <el-table-column label="绫诲瀷" align="center" prop="bizType" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.ERP_STOCK_RECORD_BIZ_TYPE" :value="scope.row.bizType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍑哄叆搴撳崟鍙�" align="center" prop="bizNo" width="200" />
+ <el-table-column
+ label="鍑哄叆搴撴棩鏈�"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column
+ label="鍑哄叆搴撴暟閲�"
+ align="center"
+ prop="count"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column
+ label="搴撳瓨閲�"
+ align="center"
+ prop="totalCount"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column label="鎿嶄綔浜�" align="center" prop="creatorName" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { StockRecordApi, StockRecordVO } from '@/api/erp/stock/record'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { erpCountTableColumnFormatter } from '@/utils'
+
+/** ERP 浜у搧搴撳瓨鏄庣粏鍒楄〃 */
+defineOptions({ name: 'ErpStockRecord' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<StockRecordVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ productId: undefined,
+ warehouseId: undefined,
+ bizType: undefined,
+ bizNo: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await StockRecordApi.getStockRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await StockRecordApi.deleteStockRecord(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await StockRecordApi.exportStockRecord(queryParams)
+ download.excel(data, '浜у搧搴撳瓨鏄庣粏.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onActivated(() => {
+ getList()
+})
+
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛�
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+})
+</script>
diff --git a/src/views/erp/stock/stock/index.vue b/src/views/erp/stock/stock/index.vue
new file mode 100644
index 0000000..4d80117
--- /dev/null
+++ b/src/views/erp/stock/stock/index.vue
@@ -0,0 +1,186 @@
+<!-- ERP 浜у搧搴撳瓨鍒楄〃 -->
+<template>
+ <doc-alert title="銆愬簱瀛樸�戜骇鍝佸簱瀛樸�佸簱瀛樻槑缁�" url="https://doc.iocoder.cn/erp/stock/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浜у搧"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in productList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠撳簱" prop="warehouseId">
+ <el-select
+ v-model="queryParams.warehouseId"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨浠撳簱"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="item in warehouseList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:stock:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:stock:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="浜у搧鍚嶇О" align="center" prop="productName" />
+ <el-table-column label="浜у搧鍗曚綅" align="center" prop="unitName" />
+ <el-table-column label="浜у搧鍒嗙被" align="center" prop="categoryName" />
+ <el-table-column
+ label="搴撳瓨閲�"
+ align="center"
+ prop="count"
+ :formatter="erpCountTableColumnFormatter"
+ />
+ <el-table-column label="浠撳簱" align="center" prop="warehouseName" />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import download from '@/utils/download'
+import { StockApi, StockVO } from '@/api/erp/stock/stock'
+import { ProductApi, ProductVO } from '@/api/erp/product/product'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { erpCountTableColumnFormatter } from '@/utils'
+
+/** ERP 浜у搧搴撳瓨鍒楄〃 */
+defineOptions({ name: 'ErpStock' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<StockVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ productId: undefined,
+ warehouseId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const warehouseList = ref<WarehouseVO[]>([]) // 浠撳簱鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await StockApi.getStockPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await StockApi.deleteStock(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await StockApi.exportStock(queryParams)
+ download.excel(data, '浜у搧搴撳瓨.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇浜у搧銆佷粨搴撳垪琛�
+ productList.value = await ProductApi.getProductSimpleList()
+ warehouseList.value = await WarehouseApi.getWarehouseSimpleList()
+})
+</script>
diff --git a/src/views/erp/stock/warehouse/WarehouseForm.vue b/src/views/erp/stock/warehouse/WarehouseForm.vue
new file mode 100644
index 0000000..f65cea6
--- /dev/null
+++ b/src/views/erp/stock/warehouse/WarehouseForm.vue
@@ -0,0 +1,157 @@
+<!-- ERP 浠撳簱琛ㄥ崟 -->
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="浠撳簱鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヤ粨搴撳悕绉�" />
+ </el-form-item>
+ <el-form-item label="浠撳簱鍦板潃" prop="address">
+ <el-input v-model="formData.address" placeholder="璇疯緭鍏ヤ粨搴撳湴鍧�" />
+ </el-form-item>
+ <el-form-item label="浠撳簱鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="浠撳偍璐�" prop="warehousePrice">
+ <el-input-number
+ v-model="formData.warehousePrice"
+ placeholder="璇疯緭鍏ヤ粨鍌ㄨ垂锛屽崟浣嶏細鍏�/澶�/KG"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="鎼繍璐�" prop="truckagePrice">
+ <el-input-number
+ v-model="formData.truckagePrice"
+ placeholder="璇疯緭鍏ユ惉杩愯垂锛屽崟浣嶏細鍏�"
+ :min="0"
+ :precision="2"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="璐熻矗浜�" prop="principal">
+ <el-input v-model="formData.principal" placeholder="璇疯緭鍏ヨ礋璐d汉" />
+ </el-form-item>
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number
+ v-model="formData.sort"
+ placeholder="璇疯緭鍏ユ帓搴�"
+ :precision="0"
+ class="!w-1/1"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** ERP 浠撳簱琛ㄥ崟 */
+defineOptions({ name: 'WarehouseForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ address: undefined,
+ sort: undefined,
+ remark: undefined,
+ principal: undefined,
+ warehousePrice: undefined,
+ truckagePrice: undefined,
+ status: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '浠撳簱鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await WarehouseApi.getWarehouse(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as WarehouseVO
+ if (formType.value === 'create') {
+ await WarehouseApi.createWarehouse(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await WarehouseApi.updateWarehouse(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ address: undefined,
+ sort: undefined,
+ remark: undefined,
+ principal: undefined,
+ warehousePrice: undefined,
+ truckagePrice: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/erp/stock/warehouse/index.vue b/src/views/erp/stock/warehouse/index.vue
new file mode 100644
index 0000000..40bdebe
--- /dev/null
+++ b/src/views/erp/stock/warehouse/index.vue
@@ -0,0 +1,242 @@
+<!-- ERP 浠撳簱鍒楄〃 -->
+<template>
+ <doc-alert title="銆愬簱瀛樸�戜骇鍝佸簱瀛樸�佸簱瀛樻槑缁�" url="https://doc.iocoder.cn/erp/stock/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="浠撳簱鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヤ粨搴撳悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浠撳簱鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨浠撳簱鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['erp:warehouse:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['erp:warehouse:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="浠撳簱鍚嶇О" align="center" prop="name" />
+ <el-table-column label="浠撳簱鍦板潃" align="center" prop="address" />
+ <el-table-column
+ label="浠撳偍璐�"
+ align="center"
+ prop="warehousePrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column
+ label="鎼繍璐�"
+ align="center"
+ prop="truckagePrice"
+ :formatter="erpPriceTableColumnFormatter"
+ />
+ <el-table-column label="璐熻矗浜�" align="center" prop="principal" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁榛樿" align="center" prop="defaultStatus">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.defaultStatus"
+ :active-value="true"
+ :inactive-value="false"
+ @change="handleDefaultStatusChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['erp:warehouse:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['erp:warehouse:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <WarehouseForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { WarehouseApi, WarehouseVO } from '@/api/erp/stock/warehouse'
+import WarehouseForm from './WarehouseForm.vue'
+import { erpPriceTableColumnFormatter } from '@/utils'
+
+/** ERP 浠撳簱鍒楄〃 */
+defineOptions({ name: 'ErpWarehouse' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<WarehouseVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await WarehouseApi.getWarehousePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await WarehouseApi.deleteWarehouse(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼榛樿鐘舵�� */
+const handleDefaultStatusChange = async (row: WarehouseVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.defaultStatus ? '璁剧疆' : '鍙栨秷'
+ await message.confirm('纭瑕�' + text + '"' + row.name + '"榛樿鍚�?')
+ // 鍙戣捣淇敼鐘舵��
+ await WarehouseApi.updateWarehouseDefaultStatus(row.id, row.defaultStatus)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch (e) {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.defaultStatus = !row.defaultStatus
+ }
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await WarehouseApi.exportWarehouse(queryParams)
+ download.excel(data, '浠撳簱.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue b/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue
new file mode 100644
index 0000000..314fd26
--- /dev/null
+++ b/src/views/infra/apiAccessLog/ApiAccessLogDetail.vue
@@ -0,0 +1,79 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織涓婚敭" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閾捐矾杩借釜">
+ {{ detailData.traceId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="搴旂敤鍚�">
+ {{ detailData.applicationName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛淇℃伅">
+ {{ detailData.userId }}
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛 IP">
+ {{ detailData.userIp }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛 UA">
+ {{ detailData.userAgent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰淇℃伅">
+ {{ detailData.requestMethod }} {{ detailData.requestUrl }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰鍙傛暟">
+ {{ detailData.requestParams }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰缁撴灉">
+ {{ detailData.responseBody }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰鏃堕棿">
+ {{ formatDate(detailData.beginTime) }} ~ {{ formatDate(detailData.endTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰鑰楁椂">{{ detailData.duration }} ms</el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔缁撴灉">
+ <div v-if="detailData.resultCode === 0">姝e父</div>
+ <div v-else-if="detailData.resultCode > 0">
+ 澶辫触 | {{ detailData.resultCode }} | {{ detailData.resultMsg }}
+ </div>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔妯″潡">
+ {{ detailData.operateModule }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔鍚�">
+ {{ detailData.operateName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔鍚�">
+ <dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="detailData.operateType" />
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as ApiAccessLog from '@/api/infra/apiAccessLog'
+
+defineOptions({ name: 'ApiAccessLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鍦板姞杞戒腑
+const detailData = ref({} as ApiAccessLog.ApiAccessLogVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: ApiAccessLog.ApiAccessLogVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+}
+
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/infra/apiAccessLog/index.vue b/src/views/infra/apiAccessLog/index.vue
new file mode 100644
index 0000000..570f579
--- /dev/null
+++ b/src/views/infra/apiAccessLog/index.vue
@@ -0,0 +1,226 @@
+<template>
+ <doc-alert title="绯荤粺鏃ュ織" url="https://doc.iocoder.cn/system-log/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="搴旂敤鍚�" prop="applicationName">
+ <el-input
+ v-model="queryParams.applicationName"
+ placeholder="璇疯緭鍏ュ簲鐢ㄥ悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="璇锋眰鏃堕棿" prop="beginTime">
+ <el-date-picker
+ v-model="queryParams.beginTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎵ц鏃堕暱" prop="duration">
+ <el-input
+ v-model="queryParams.duration"
+ placeholder="璇疯緭鍏ユ墽琛屾椂闀�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴灉鐮�" prop="resultCode">
+ <el-input
+ v-model="queryParams.resultCode"
+ placeholder="璇疯緭鍏ョ粨鏋滅爜"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:api-access-log:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="id" width="100" fix="right" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" />
+ <el-table-column label="鐢ㄦ埛绫诲瀷" align="center" prop="userType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴旂敤鍚�" align="center" prop="applicationName" width="150" />
+ <el-table-column label="璇锋眰鏂规硶" align="center" prop="requestMethod" width="80" />
+ <el-table-column label="璇锋眰鍦板潃" align="center" prop="requestUrl" width="500" />
+ <el-table-column label="璇锋眰鏃堕棿" align="center" prop="beginTime" width="180">
+ <template #default="scope">
+ <span>{{ formatDate(scope.row.beginTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵ц鏃堕暱" align="center" prop="duration" width="180">
+ <template #default="scope"> {{ scope.row.duration }} ms </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔缁撴灉" align="center" prop="status">
+ <template #default="scope">
+ {{ scope.row.resultCode === 0 ? '鎴愬姛' : '澶辫触(' + scope.row.resultMsg + ')' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔妯″潡" align="center" prop="operateModule" width="180" />
+ <el-table-column label="鎿嶄綔鍚�" align="center" prop="operateName" width="180" />
+ <el-table-column label="鎿嶄綔绫诲瀷" align="center" prop="operateType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_OPERATE_TYPE" :value="scope.row.operateType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="60">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['infra:api-access-log:query']"
+ >
+ 璇︾粏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <ApiAccessLogDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import download from '@/utils/download'
+import { formatDate } from '@/utils/formatTime'
+import * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
+import ApiAccessLogDetail from './ApiAccessLogDetail.vue'
+
+defineOptions({ name: 'InfraApiAccessLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ userType: null,
+ applicationName: null,
+ requestUrl: null,
+ duration: null,
+ resultCode: null,
+ beginTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ApiAccessLogApi.getApiAccessLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: ApiAccessLogApi.ApiAccessLogVO) => {
+ detailRef.value.open(data)
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ApiAccessLogApi.exportApiAccessLog(queryParams)
+ download.excel(data, 'API 璁块棶鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue b/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue
new file mode 100644
index 0000000..41153a2
--- /dev/null
+++ b/src/views/infra/apiErrorLog/ApiErrorLogDetail.vue
@@ -0,0 +1,81 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織涓婚敭" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閾捐矾杩借釜">
+ {{ detailData.traceId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="搴旂敤鍚�">
+ {{ detailData.applicationName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛缂栧彿">
+ {{ detailData.userId }}
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛 IP">
+ {{ detailData.userIp }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛 UA">
+ {{ detailData.userAgent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰淇℃伅">
+ {{ detailData.requestMethod }} {{ detailData.requestUrl }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰鍙傛暟">
+ {{ detailData.requestParams }}
+ </el-descriptions-item>
+ <el-descriptions-item label="寮傚父鏃堕棿">
+ {{ formatDate(detailData.exceptionTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="寮傚父鍚�">
+ {{ detailData.exceptionName }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detailData.exceptionStackTrace" label="寮傚父鍫嗘爤">
+ <el-input
+ v-model="detailData.exceptionStackTrace"
+ :autosize="{ maxRows: 20 }"
+ :readonly="true"
+ type="textarea"
+ />
+ </el-descriptions-item>
+ <el-descriptions-item label="澶勭悊鐘舵��">
+ <dict-tag
+ :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
+ :value="detailData.processStatus"
+ />
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detailData.processUserId" label="澶勭悊浜�">
+ {{ detailData.processUserId }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detailData.processTime" label="澶勭悊鏃堕棿">
+ {{ formatDate(detailData.processTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as ApiErrorLog from '@/api/infra/apiErrorLog'
+
+defineOptions({ name: 'ApiErrorLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as ApiErrorLog.ApiErrorLogVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: ApiErrorLog.ApiErrorLogVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/infra/apiErrorLog/index.vue b/src/views/infra/apiErrorLog/index.vue
new file mode 100644
index 0000000..ca145a7
--- /dev/null
+++ b/src/views/infra/apiErrorLog/index.vue
@@ -0,0 +1,252 @@
+<template>
+ <doc-alert title="绯荤粺鏃ュ織" url="https://doc.iocoder.cn/system-log/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="搴旂敤鍚�" prop="applicationName">
+ <el-input
+ v-model="queryParams.applicationName"
+ placeholder="璇疯緭鍏ュ簲鐢ㄥ悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="寮傚父鏃堕棿" prop="exceptionTime">
+ <el-date-picker
+ v-model="queryParams.exceptionTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="澶勭悊鐘舵��" prop="processStatus">
+ <el-select
+ v-model="queryParams.processStatus"
+ placeholder="璇烽�夋嫨澶勭悊鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:api-error-log:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" />
+ <el-table-column label="鐢ㄦ埛绫诲瀷" align="center" prop="userType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴旂敤鍚�" align="center" prop="applicationName" width="200" />
+ <el-table-column label="璇锋眰鏂规硶" align="center" prop="requestMethod" width="80" />
+ <el-table-column label="璇锋眰鍦板潃" align="center" prop="requestUrl" width="180" />
+ <el-table-column
+ label="寮傚父鍙戠敓鏃堕棿"
+ align="center"
+ prop="exceptionTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="寮傚父鍚�" align="center" prop="exceptionName" width="180" />
+ <el-table-column label="澶勭悊鐘舵��" align="center" prop="processStatus">
+ <template #default="scope">
+ <dict-tag
+ :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS"
+ :value="scope.row.processStatus"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="200">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['infra:api-error-log:query']"
+ >
+ 璇︾粏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ v-if="scope.row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
+ @click="handleProcess(scope.row.id, InfraApiErrorLogProcessStatusEnum.DONE)"
+ v-hasPermi="['infra:api-error-log:update-status']"
+ >
+ 宸插鐞�
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ v-if="scope.row.processStatus === InfraApiErrorLogProcessStatusEnum.INIT"
+ @click="handleProcess(scope.row.id, InfraApiErrorLogProcessStatusEnum.IGNORE)"
+ v-hasPermi="['infra:api-error-log:update-status']"
+ >
+ 宸插拷鐣�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <ApiErrorLogDetail ref="detailRef" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
+import ApiErrorLogDetail from './ApiErrorLogDetail.vue'
+import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'InfraApiErrorLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ userType: null,
+ applicationName: null,
+ requestUrl: null,
+ processStatus: null,
+ exceptionTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ApiErrorLogApi.getApiErrorLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: ApiErrorLogApi.ApiErrorLogVO) => {
+ detailRef.value.open(data)
+}
+
+/** 澶勭悊宸插鐞� / 宸插拷鐣ョ殑鎿嶄綔 **/
+const handleProcess = async (id: number, processStatus: number) => {
+ try {
+ // 鎿嶄綔鐨勪簩娆$‘璁�
+ const type = processStatus === InfraApiErrorLogProcessStatusEnum.DONE ? '宸插鐞�' : '宸插拷鐣�'
+ await message.confirm('纭鏍囪涓�' + type + '?')
+ // 鎵ц鎿嶄綔
+ await ApiErrorLogApi.updateApiErrorLogPage(id, processStatus)
+ await message.success(type)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ApiErrorLogApi.exportApiErrorLog(queryParams)
+ download.excel(data, '寮傚父鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/build/index.vue b/src/views/infra/build/index.vue
new file mode 100644
index 0000000..191fc90
--- /dev/null
+++ b/src/views/infra/build/index.vue
@@ -0,0 +1,184 @@
+<template>
+ <ContentWrap :body-style="{ padding: '0px' }" class="!mb-0">
+ <!-- 琛ㄥ崟璁捐鍣� -->
+ <div
+ class="h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
+ >
+ <fc-designer class="my-designer" ref="designer" :config="designerConfig">
+ <template #handle>
+ <el-button size="small" type="primary" plain @click="showJson">鐢熸垚JSON</el-button>
+ <el-button size="small" type="success" plain @click="showOption">鐢熸垚Options</el-button>
+ <el-button size="small" type="danger" plain @click="showTemplate">鐢熸垚缁勪欢</el-button>
+ </template>
+ </fc-designer>
+ </div>
+ </ContentWrap>
+
+ <!-- 寮圭獥锛氳〃鍗曢瑙� -->
+ <Dialog v-model="dialogVisible" :title="dialogTitle" max-height="600">
+ <div v-if="dialogVisible" ref="editor">
+ <el-button style="float: right" @click="copy(formData)">
+ {{ t('common.copy') }}
+ </el-button>
+ <el-scrollbar height="580">
+ <div>
+ <pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre>
+ </div>
+ </el-scrollbar>
+ </div>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { useFormCreateDesigner } from '@/components/FormCreate'
+import { useClipboard } from '@vueuse/core'
+import { isString } from '@/utils/is'
+
+import hljs from 'highlight.js' // 瀵煎叆浠g爜楂樹寒鏂囦欢
+import 'highlight.js/styles/github.css' // 瀵煎叆浠g爜楂樹寒鏍峰紡
+import xml from 'highlight.js/lib/languages/java'
+import json from 'highlight.js/lib/languages/json'
+import formCreate from '@form-create/element-ui'
+
+defineOptions({ name: 'InfraBuild' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅
+
+// 琛ㄥ崟璁捐鍣ㄩ厤缃�
+const designerConfig = ref({
+ switchType: [], // 鏄惁鍙互鍒囨崲缁勪欢绫诲瀷,鎴栬�呭彲浠ョ浉浜掑垏鎹㈢殑瀛楁
+ autoActive: true, // 鏄惁鑷姩閫変腑鎷栧叆鐨勭粍浠�
+ useTemplate: false, // 鏄惁鐢熸垚vue2璇硶鐨勬ā鏉跨粍浠�
+ formOptions: {
+ form: {
+ labelWidth: '100px' // 璁剧疆榛樿鐨� label 瀹藉害涓� 100px
+ }
+ }, // 瀹氫箟琛ㄥ崟閰嶇疆榛樿鍊�
+ fieldReadonly: false, // 閰嶇疆field鏄惁鍙互缂栬緫
+ hiddenDragMenu: false, // 闅愯棌鎷栨嫿鎿嶄綔鎸夐挳
+ hiddenDragBtn: false, // 闅愯棌鎷栨嫿鎸夐挳
+ hiddenMenu: [], // 闅愯棌閮ㄥ垎鑿滃崟
+ hiddenItem: [], // 闅愯棌閮ㄥ垎缁勪欢
+ hiddenItemConfig: {}, // 闅愯棌缁勪欢鐨勯儴鍒嗛厤缃」
+ disabledItemConfig: {}, // 绂佺敤缁勪欢鐨勯儴鍒嗛厤缃」
+ showSaveBtn: false, // 鏄惁鏄剧ず淇濆瓨鎸夐挳
+ showConfig: true, // 鏄惁鏄剧ず鍙充晶鐨勯厤缃晫闈�
+ showBaseForm: true, // 鏄惁鏄剧ず缁勪欢鐨勫熀纭�閰嶇疆琛ㄥ崟
+ showControl: true, // 鏄惁鏄剧ず缁勪欢鑱斿姩
+ showPropsForm: true, // 鏄惁鏄剧ず缁勪欢鐨勫睘鎬ч厤缃〃鍗�
+ showEventForm: true, // 鏄惁鏄剧ず缁勪欢鐨勪簨浠堕厤缃〃鍗�
+ showValidateForm: true, // 鏄惁鏄剧ず缁勪欢鐨勯獙璇侀厤缃〃鍗�
+ showFormConfig: true, // 鏄惁鏄剧ず琛ㄥ崟閰嶇疆
+ showInputData: true, // 鏄惁鏄剧ず褰曞叆鎸夐挳
+ showDevice: true, // 鏄惁鏄剧ず澶氱閫傞厤閫夐」
+ appendConfigData: [] // 瀹氫箟娓叉煋瑙勫垯鎵�闇�鐨刦ormData
+})
+const designer = ref() // 琛ㄥ崟璁捐鍣�
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formType = ref(-1) // 琛ㄥ崟鐨勭被鍨嬶細0 - 鐢熸垚 JSON锛�1 - 鐢熸垚 Options锛�2 - 鐢熸垚缁勪欢
+const formData = ref('') // 琛ㄥ崟鏁版嵁
+useFormCreateDesigner(designer) // 琛ㄥ崟璁捐鍣ㄥ寮�
+
+/** 鎵撳紑寮圭獥 */
+const openModel = (title: string) => {
+ dialogVisible.value = true
+ dialogTitle.value = title
+}
+
+/** 鐢熸垚 JSON */
+const showJson = () => {
+ openModel('鐢熸垚 JSON')
+ formType.value = 0
+ formData.value = designer.value.getRule()
+}
+
+/** 鐢熸垚 Options */
+const showOption = () => {
+ openModel('鐢熸垚 Options')
+ formType.value = 1
+ formData.value = designer.value.getOption()
+}
+
+/** 鐢熸垚缁勪欢 */
+const showTemplate = () => {
+ openModel('鐢熸垚缁勪欢')
+ formType.value = 2
+ formData.value = makeTemplate()
+}
+
+const makeTemplate = () => {
+ const rule = designer.value.getRule()
+ const opt = designer.value.getOption()
+ return `<template>
+ <form-create
+ v-model:api="fApi"
+ :rule="rule"
+ :option="option"
+ @submit="onSubmit"
+ ></form-create>
+ </template>
+ <script setup lang=ts>
+ const faps = ref(null)
+ const rule = ref('')
+ const option = ref('')
+ const init = () => {
+ rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}')
+ option.value = formCreate.parseJson('${JSON.stringify(opt)}')
+ }
+ const onSubmit = (formData) => {
+ //todo 鎻愪氦琛ㄥ崟
+ }
+ init()
+ <\/script>`
+}
+
+/** 澶嶅埗 **/
+const copy = async (text: string) => {
+ const textToCopy = JSON.stringify(text, null, 2)
+ const { copy, copied, isSupported } = useClipboard({ legacy: true, source: textToCopy })
+ if (!isSupported) {
+ message.error(t('common.copyError'))
+ } else {
+ await copy()
+ if (unref(copied)) {
+ message.success(t('common.copySuccess'))
+ }
+ }
+}
+
+/**
+ * 浠g爜楂樹寒
+ */
+const highlightedCode = (code: string) => {
+ // 澶勭悊璇█鍜屼唬鐮�
+ let language = 'json'
+ if (formType.value === 2) {
+ language = 'xml'
+ }
+ // debugger
+ if (!isString(code)) {
+ code = JSON.stringify(code, null, 2)
+ }
+ // 楂樹寒
+ const result = hljs.highlight(code, { language: language, ignoreIllegals: true })
+ return result.value || ' '
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 娉ㄥ唽浠g爜楂樹寒鐨勫悇绉嶈瑷�
+ hljs.registerLanguage('xml', xml)
+ hljs.registerLanguage('json', json)
+})
+</script>
+
+<style>
+.my-designer {
+ ._fc-l,
+ ._fc-m,
+ ._fc-r {
+ border-top: none;
+ }
+}
+</style>
diff --git a/src/views/infra/codegen/EditTable.vue b/src/views/infra/codegen/EditTable.vue
new file mode 100644
index 0000000..f8473e3
--- /dev/null
+++ b/src/views/infra/codegen/EditTable.vue
@@ -0,0 +1,87 @@
+<template>
+ <ContentWrap v-loading="formLoading">
+ <el-tabs v-model="activeName">
+ <el-tab-pane label="鍩烘湰淇℃伅" name="basicInfo">
+ <basic-info-form ref="basicInfoRef" :table="formData.table" />
+ </el-tab-pane>
+ <el-tab-pane label="瀛楁淇℃伅" name="colum">
+ <colum-info-form ref="columInfoRef" :columns="formData.columns" />
+ </el-tab-pane>
+ <el-tab-pane label="鐢熸垚淇℃伅" name="generateInfo">
+ <generate-info-form
+ ref="generateInfoRef"
+ :table="formData.table"
+ :columns="formData.columns"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ <el-form>
+ <el-form-item style="float: right">
+ <el-button :loading="formLoading" type="primary" @click="submitForm">淇濆瓨</el-button>
+ <el-button @click="close">杩斿洖</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { BasicInfoForm, ColumInfoForm, GenerateInfoForm } from './components'
+import * as CodegenApi from '@/api/infra/codegen'
+
+defineOptions({ name: 'InfraCodegenEditTable' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const { push, currentRoute } = useRouter() // 璺敱
+const { query } = useRoute() // 鏌ヨ鍙傛暟
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const activeName = ref('colum') // Tag 婵�娲荤殑绐楀彛
+const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>()
+const columInfoRef = ref<ComponentRef<typeof ColumInfoForm>>()
+const generateInfoRef = ref<ComponentRef<typeof GenerateInfoForm>>()
+const formData = ref<CodegenApi.CodegenUpdateReqVO>({
+ table: {},
+ columns: []
+})
+
+/** 鑾峰緱璇︽儏 */
+const getDetail = async () => {
+ const id = query.id as unknown as number
+ if (!id) {
+ return
+ }
+ formLoading.value = true
+ try {
+ formData.value = await CodegenApi.getCodegenTable(id)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鎻愪氦鎸夐挳 */
+const submitForm = async () => {
+ // 鍙傛暟鏍¢獙
+ if (!unref(formData)) return
+ await unref(basicInfoRef)?.validate()
+ await unref(generateInfoRef)?.validate()
+ try {
+ // 鎻愪氦璇锋眰
+ await CodegenApi.updateCodegenTable(formData.value)
+ message.success(t('common.updateSuccess'))
+ close()
+ } catch {}
+}
+
+/** 鍏抽棴鎸夐挳 */
+const close = () => {
+ delView(unref(currentRoute))
+ push('/infra/codegen')
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getDetail()
+})
+</script>
diff --git a/src/views/infra/codegen/ImportTable.vue b/src/views/infra/codegen/ImportTable.vue
new file mode 100644
index 0000000..132a602
--- /dev/null
+++ b/src/views/infra/codegen/ImportTable.vue
@@ -0,0 +1,160 @@
+<template>
+ <Dialog v-model="dialogVisible" title="瀵煎叆琛�" width="800px">
+ <!-- 鎼滅储鏍� -->
+ <el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
+ <el-form-item label="鏁版嵁婧�" prop="dataSourceConfigId">
+ <el-select
+ v-model="queryParams.dataSourceConfigId"
+ class="!w-240px"
+ placeholder="璇烽�夋嫨鏁版嵁婧�"
+ >
+ <el-option
+ v-for="config in dataSourceConfigList"
+ :key="config.id"
+ :label="config.name"
+ :value="config.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="琛ㄥ悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ〃鍚嶇О"
+ @keyup.enter="getList"
+ />
+ </el-form-item>
+ <el-form-item label="琛ㄦ弿杩�" prop="comment">
+ <el-input
+ v-model="queryParams.comment"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ〃鎻忚堪"
+ @keyup.enter="getList"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="getList">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <!-- 鍒楄〃 -->
+ <el-row>
+ <el-table
+ ref="tableRef"
+ v-loading="dbTableLoading"
+ :data="dbTableList"
+ height="260px"
+ @row-click="handleRowClick"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column :show-overflow-tooltip="true" label="琛ㄥ悕绉�" prop="name" />
+ <el-table-column :show-overflow-tooltip="true" label="琛ㄦ弿杩�" prop="comment" />
+ </el-table>
+ </el-row>
+ <!-- 鎿嶄綔 -->
+ <template #footer>
+ <el-button
+ :disabled="tableList.length === 0 || dbTableLoading"
+ type="primary"
+ @click="handleImportTable"
+ >
+ 瀵煎叆
+ </el-button>
+ <el-button @click="close">鍏抽棴</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as CodegenApi from '@/api/infra/codegen'
+import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
+import { ElTable } from 'element-plus'
+
+defineOptions({ name: 'InfraCodegenImportTable' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dbTableLoading = ref(true) // 鏁版嵁婧愮殑鍔犺浇涓�
+const dbTableList = ref<CodegenApi.DatabaseTableVO[]>([]) // 琛ㄧ殑鍒楄〃
+const queryParams = reactive({
+ name: undefined,
+ comment: undefined,
+ dataSourceConfigId: 0
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const dataSourceConfigList = ref<DataSourceConfigApi.DataSourceConfigVO[]>([]) // 鏁版嵁婧愬垪琛�
+
+/** 鏌ヨ琛ㄦ暟鎹� */
+const getList = async () => {
+ dbTableLoading.value = true
+ try {
+ dbTableList.value = await CodegenApi.getSchemaTableList(queryParams)
+ } finally {
+ dbTableLoading.value = false
+ }
+}
+
+/** 閲嶇疆鎿嶄綔 */
+const resetQuery = async () => {
+ queryParams.name = undefined
+ queryParams.comment = undefined
+ queryParams.dataSourceConfigId = dataSourceConfigList.value[0].id as number
+ await getList()
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ // 鍔犺浇鏁版嵁婧愮殑鍒楄〃
+ dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
+ queryParams.dataSourceConfigId = dataSourceConfigList.value[0].id as number
+ dialogVisible.value = true
+ // 鍔犺浇琛ㄧ殑鍒楄〃
+ await getList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鍏抽棴寮圭獥 */
+const close = () => {
+ dialogVisible.value = false
+ tableList.value = []
+}
+
+const tableRef = ref<typeof ElTable>() // 琛ㄦ牸鐨� Ref
+const tableList = ref<string[]>([]) // 閫変腑鐨勮〃鍚�
+
+/** 澶勭悊鏌愪竴琛岀殑鐐瑰嚮 */
+const handleRowClick = (row) => {
+ unref(tableRef)?.toggleRowSelection(row)
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+const handleSelectionChange = (selection) => {
+ tableList.value = selection.map((item) => item.name)
+}
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+const handleImportTable = async () => {
+ dbTableLoading.value = true
+ try {
+ await CodegenApi.createCodegenList({
+ dataSourceConfigId: queryParams.dataSourceConfigId,
+ tableNames: tableList.value
+ })
+ message.success('瀵煎叆鎴愬姛')
+ emit('success')
+ close()
+ } finally {
+ dbTableLoading.value = false
+ }
+}
+const emit = defineEmits(['success'])
+</script>
diff --git a/src/views/infra/codegen/PreviewCode.vue b/src/views/infra/codegen/PreviewCode.vue
new file mode 100644
index 0000000..819fca6
--- /dev/null
+++ b/src/views/infra/codegen/PreviewCode.vue
@@ -0,0 +1,222 @@
+<template>
+ <Dialog
+ v-model="dialogVisible"
+ align-center
+ class="app-infra-codegen-preview-container"
+ title="浠g爜棰勮"
+ width="80%"
+ >
+ <div class="flex">
+ <!-- 浠g爜鐩綍鏍� -->
+ <el-card
+ v-loading="loading"
+ :gutter="12"
+ class="w-1/3"
+ element-loading-text="鐢熸垚鏂囦欢鐩綍涓�..."
+ shadow="hover"
+ >
+ <el-scrollbar height="calc(100vh - 88px - 40px)">
+ <el-tree
+ ref="treeRef"
+ :data="preview.fileTree"
+ :expand-on-click-node="false"
+ default-expand-all
+ highlight-current
+ node-key="id"
+ @node-click="handleNodeClick"
+ />
+ </el-scrollbar>
+ </el-card>
+ <!-- 浠g爜 -->
+ <el-card
+ v-loading="loading"
+ :gutter="12"
+ class="ml-3 w-2/3"
+ element-loading-text="鍔犺浇浠g爜涓�..."
+ shadow="hover"
+ >
+ <el-tabs v-model="preview.activeName">
+ <el-tab-pane
+ v-for="item in previewCodegen"
+ :key="item.filePath"
+ :label="item.filePath.substring(item.filePath.lastIndexOf('/') + 1)"
+ :name="item.filePath"
+ >
+ <el-button class="float-right" text type="primary" @click="copy(item.code)">
+ {{ t('common.copy') }}
+ </el-button>
+ <el-scrollbar height="600px">
+ <pre><code v-dompurify-html="highlightedCode(item)" class="hljs"></code></pre>
+ </el-scrollbar>
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </div>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { useClipboard } from '@vueuse/core'
+import { handleTree2 } from '@/utils/tree'
+import * as CodegenApi from '@/api/infra/codegen'
+
+import hljs from 'highlight.js' // 瀵煎叆浠g爜楂樹寒鏂囦欢
+import 'highlight.js/styles/github.css' // 瀵煎叆浠g爜楂樹寒鏍峰紡
+import java from 'highlight.js/lib/languages/java'
+import xml from 'highlight.js/lib/languages/java'
+import javascript from 'highlight.js/lib/languages/javascript'
+import sql from 'highlight.js/lib/languages/sql'
+import typescript from 'highlight.js/lib/languages/typescript'
+
+defineOptions({ name: 'InfraCodegenPreviewCode' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const loading = ref(false) // 鍔犺浇涓殑鐘舵��
+const preview = reactive({
+ fileTree: [], // 鏂囦欢鏍�
+ activeName: '' // 婵�娲荤殑鏂囦欢鍚�
+})
+const previewCodegen = ref<CodegenApi.CodegenPreviewVO[]>()
+
+/** 鐐瑰嚮鏂囦欢 */
+const handleNodeClick = async (data, node) => {
+ if (node && !node.isLeaf) {
+ return false
+ }
+ preview.activeName = data.id
+}
+
+/** 鐢熸垚 files 鐩綍 **/
+interface filesType {
+ id: string
+ label: string
+ parentId: string
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ try {
+ loading.value = true
+ // 鐢熸垚浠g爜
+ const data = await CodegenApi.previewCodegen(id)
+ previewCodegen.value = data
+ // 澶勭悊鏂囦欢
+ let file = handleFiles(data)
+ preview.fileTree = handleTree2(file, 'id', 'parentId', 'children', '/')
+ // 鐐瑰嚮棣栦釜鏂囦欢
+ preview.activeName = data[0].filePath
+ } finally {
+ loading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 澶勭悊鏂囦欢 */
+const handleFiles = (datas: CodegenApi.CodegenPreviewVO[]) => {
+ let exists = {} // key锛歠ile 鐨� id锛泇alue锛歵rue
+ let files: filesType[] = []
+ // 閬嶅巻姣忎釜鍏冪礌
+ for (const data of datas) {
+ let paths = data.filePath.split('/')
+ let fullPath = '' // 浠庡ご寮�濮嬬殑璺緞锛岀敤浜庣敓鎴� id
+ // 鐗规畩澶勭悊 java 鏂囦欢
+ if (paths[paths.length - 1].indexOf('.java') >= 0) {
+ let newPaths: string[] = []
+ for (let i = 0; i < paths.length; i++) {
+ let path = paths[i]
+ if (path !== 'java') {
+ newPaths.push(path)
+ continue
+ }
+ newPaths.push(path)
+ // 鐗规畩澶勭悊涓棿鐨� package锛岃繘琛屽悎骞�
+ let tmp = ''
+ while (i < paths.length) {
+ path = paths[i + 1]
+ if (
+ path === 'controller' ||
+ path === 'convert' ||
+ path === 'dal' ||
+ path === 'enums' ||
+ path === 'service' ||
+ path === 'vo' || // 涓嬮潰涓変釜锛屼富瑕佹槸鍏滃簳銆傚彲鑳借�冭檻鍒版湁浜烘敼浜嗗寘缁撴瀯
+ path === 'mysql' ||
+ path === 'dataobject'
+ ) {
+ break
+ }
+ tmp = tmp ? tmp + '.' + path : path
+ i++
+ }
+ if (tmp) {
+ newPaths.push(tmp)
+ }
+ }
+ paths = newPaths
+ }
+ // 閬嶅巻姣忎釜 path锛� 鎷兼帴鎴愭爲
+ for (let i = 0; i < paths.length; i++) {
+ // 宸茬粡娣诲姞鍒� files 涓紝鍒欒烦杩�
+ let oldFullPath = fullPath
+ // 涓嬮潰鐨� replaceAll 鐨勫師鍥狅紝鏄洜涓轰笂闈㈠寘澶勭悊浜嗭紝瀵艰嚧鍜� tabs 涓嶅尮閰嶏紝鎵�浠� replaceAll 涓�
+ fullPath = fullPath.length === 0 ? paths[i] : fullPath.replaceAll('.', '/') + '/' + paths[i]
+ if (exists[fullPath]) {
+ continue
+ }
+ // 娣诲姞鍒� files 涓�
+ exists[fullPath] = true
+ files.push({
+ id: fullPath,
+ label: paths[i],
+ parentId: oldFullPath || '/' // "/" 涓烘牴鑺傜偣
+ })
+ }
+ }
+ return files
+}
+
+/** 澶嶅埗 **/
+const copy = async (text: string) => {
+ const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
+ if (!isSupported) {
+ message.error(t('common.copyError'))
+ return
+ }
+ await copy()
+ if (unref(copied)) {
+ message.success(t('common.copySuccess'))
+ }
+}
+
+/**
+ * 浠g爜楂樹寒
+ */
+const highlightedCode = (item) => {
+ const language = item.filePath.substring(item.filePath.lastIndexOf('.') + 1)
+ const result = hljs.highlight(language, item.code || '', true)
+ return result.value || ' '
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 娉ㄥ唽浠g爜楂樹寒鐨勫悇绉嶈瑷�
+ hljs.registerLanguage('java', java)
+ hljs.registerLanguage('xml', xml)
+ hljs.registerLanguage('html', xml)
+ hljs.registerLanguage('vue', xml)
+ hljs.registerLanguage('javascript', javascript)
+ hljs.registerLanguage('sql', sql)
+ hljs.registerLanguage('typescript', typescript)
+})
+</script>
+<style lang="scss">
+.app-infra-codegen-preview-container {
+ .el-scrollbar .el-scrollbar__wrap .el-scrollbar__view {
+ display: inline-block;
+ white-space: nowrap;
+ }
+}
+</style>
diff --git a/src/views/infra/codegen/components/BasicInfoForm.vue b/src/views/infra/codegen/components/BasicInfoForm.vue
new file mode 100644
index 0000000..1859300
--- /dev/null
+++ b/src/views/infra/codegen/components/BasicInfoForm.vue
@@ -0,0 +1,87 @@
+<template>
+ <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="琛ㄥ悕绉�" prop="tableName">
+ <el-input v-model="formData.tableName" placeholder="璇疯緭鍏ヤ粨搴撳悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="琛ㄦ弿杩�" prop="tableComment">
+ <el-input v-model="formData.tableComment" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item prop="className">
+ <template #label>
+ <span>
+ 瀹炰綋绫诲悕绉�
+ <el-tooltip
+ content="榛樿鍘婚櫎琛ㄥ悕鐨勫墠缂�銆傚鏋滃瓨鍦ㄩ噸澶嶏紝鍒欓渶瑕佹墜鍔ㄦ坊鍔犲墠缂�锛岄伩鍏� MyBatis 鎶� Alias 閲嶅鐨勯棶棰樸��"
+ placement="top"
+ >
+ <Icon class="" icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="formData.className" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浣滆��" prop="author">
+ <el-input v-model="formData.author" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" :rows="3" type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import * as CodegenApi from '@/api/infra/codegen'
+import { PropType } from 'vue'
+
+defineOptions({ name: 'InfraCodegenBasicInfoForm' })
+
+const props = defineProps({
+ table: {
+ type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
+ default: () => null
+ }
+})
+
+const formRef = ref()
+const formData = ref({
+ tableName: '',
+ tableComment: '',
+ className: '',
+ author: '',
+ remark: ''
+})
+const rules = reactive({
+ tableName: [required],
+ tableComment: [required],
+ className: [required],
+ author: [required]
+})
+
+/** 鐩戝惉 table 灞炴�э紝澶嶅埗缁� formData 灞炴�� */
+watch(
+ () => props.table,
+ (table) => {
+ if (!table) return
+ formData.value = table
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+defineExpose({
+ validate: async () => unref(formRef)?.validate()
+})
+</script>
diff --git a/src/views/infra/codegen/components/ColumInfoForm.vue b/src/views/infra/codegen/components/ColumInfoForm.vue
new file mode 100644
index 0000000..2be931f
--- /dev/null
+++ b/src/views/infra/codegen/components/ColumInfoForm.vue
@@ -0,0 +1,167 @@
+<template>
+ <el-table ref="dragTable" :data="formData" :max-height="tableHeight" row-key="columnId">
+ <el-table-column
+ :show-overflow-tooltip="true"
+ label="瀛楁鍒楀悕"
+ min-width="10%"
+ prop="columnName"
+ />
+ <el-table-column label="瀛楁鎻忚堪" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.columnComment" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :show-overflow-tooltip="true"
+ label="鐗╃悊绫诲瀷"
+ min-width="10%"
+ prop="dataType"
+ />
+ <el-table-column label="Java绫诲瀷" min-width="11%">
+ <template #default="scope">
+ <el-select v-model="scope.row.javaType">
+ <el-option label="Long" value="Long" />
+ <el-option label="String" value="String" />
+ <el-option label="Integer" value="Integer" />
+ <el-option label="Double" value="Double" />
+ <el-option label="BigDecimal" value="BigDecimal" />
+ <el-option label="LocalDateTime" value="LocalDateTime" />
+ <el-option label="Boolean" value="Boolean" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="java灞炴��" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.javaField" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻掑叆" min-width="4%">
+ <template #default="scope">
+ <el-checkbox v-model="scope.row.createOperation" false-value="false" true-value="true" />
+ </template>
+ </el-table-column>
+ <el-table-column label="缂栬緫" min-width="4%">
+ <template #default="scope">
+ <el-checkbox v-model="scope.row.updateOperation" false-value="false" true-value="true" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒楄〃" min-width="4%">
+ <template #default="scope">
+ <el-checkbox
+ v-model="scope.row.listOperationResult"
+ false-value="false"
+ true-value="true"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏌ヨ" min-width="4%">
+ <template #default="scope">
+ <el-checkbox v-model="scope.row.listOperation" false-value="false" true-value="true" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏌ヨ鏂瑰紡" min-width="10%">
+ <template #default="scope">
+ <el-select v-model="scope.row.listOperationCondition">
+ <el-option label="=" value="=" />
+ <el-option label="!=" value="!=" />
+ <el-option label=">" value=">" />
+ <el-option label=">=" value=">=" />
+ <el-option label="<" value="<>" />
+ <el-option label="<=" value="<=" />
+ <el-option label="LIKE" value="LIKE" />
+ <el-option label="BETWEEN" value="BETWEEN" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏佽绌�" min-width="5%">
+ <template #default="scope">
+ <el-checkbox v-model="scope.row.nullable" false-value="false" true-value="true" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄剧ず绫诲瀷" min-width="12%">
+ <template #default="scope">
+ <el-select v-model="scope.row.htmlType">
+ <el-option label="鏂囨湰妗�" value="input" />
+ <el-option label="鏂囨湰鍩�" value="textarea" />
+ <el-option label="涓嬫媺妗�" value="select" />
+ <el-option label="鍗曢�夋" value="radio" />
+ <el-option label="澶嶉�夋" value="checkbox" />
+ <el-option label="鏃ユ湡鎺т欢" value="datetime" />
+ <el-option label="鍥剧墖涓婁紶" value="imageUpload" />
+ <el-option label="鏂囦欢涓婁紶" value="fileUpload" />
+ <el-option label="瀵屾枃鏈帶浠�" value="editor" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀛楀吀绫诲瀷" min-width="12%">
+ <template #default="scope">
+ <el-select v-model="scope.row.dictType" :value-on-clear="''" clearable filterable placeholder="璇烽�夋嫨">
+ <template #header>
+ <div class="flex justify-end">
+ <el-popover
+ class="box-item"
+ content="鍔犺浇鏈�鏂板瓧鍏�"
+ placement="top-start"
+ >
+ <template #reference>
+ <el-button :icon="Refresh" size="small" circle @click="getDictOptions" class=""/>
+ </template>
+ </el-popover>
+ </div>
+ </template>
+ <el-option
+ v-for="dict in dictOptions"
+ :key="dict.id"
+ :label="dict.name"
+ :value="dict.type"
+ />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="绀轰緥" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.example" />
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { Refresh } from '@element-plus/icons-vue'
+import * as CodegenApi from '@/api/infra/codegen'
+import * as DictDataApi from '@/api/system/dict/dict.type'
+
+defineOptions({ name: 'InfraCodegenColumInfoForm' })
+
+const props = defineProps({
+ columns: {
+ type: Array as unknown as PropType<CodegenApi.CodegenColumnVO[]>,
+ default: () => null
+ }
+})
+
+const formData = ref<CodegenApi.CodegenColumnVO[]>([])
+const tableHeight = document.documentElement.scrollHeight - 350 + 'px'
+
+/** 鏌ヨ瀛楀吀涓嬫媺鍒楄〃 */
+const dictOptions = ref<DictDataApi.DictTypeVO[]>()
+const getDictOptions = async () => {
+ dictOptions.value = await DictDataApi.getSimpleDictTypeList()
+}
+
+watch(
+ () => props.columns,
+ (columns) => {
+ if (!columns) return
+ formData.value = columns
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+onMounted(async () => {
+ await getDictOptions()
+})
+</script>
diff --git a/src/views/infra/codegen/components/GenerateInfoForm.vue b/src/views/infra/codegen/components/GenerateInfoForm.vue
new file mode 100644
index 0000000..aaf176f
--- /dev/null
+++ b/src/views/infra/codegen/components/GenerateInfoForm.vue
@@ -0,0 +1,385 @@
+<template>
+ <el-form ref="formRef" :model="formData" :rules="rules" label-width="150px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢熸垚妯℃澘" prop="templateType">
+ <el-select v-model="formData.templateType">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_TEMPLATE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍓嶇绫诲瀷" prop="frontType">
+ <el-select v-model="formData.frontType">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_FRONT_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item label="鐢熸垚鍦烘櫙" prop="scene">
+ <el-select v-model="formData.scene">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CODEGEN_SCENE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ <span>
+ 涓婄骇鑿滃崟
+ <el-tooltip content="鍒嗛厤鍒版寚瀹氳彍鍗曚笅锛屼緥濡� 绯荤粺绠$悊" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-tree-select
+ v-model="formData.parentMenuId"
+ :data="menus"
+ :props="menuTreeProps"
+ check-strictly
+ node-key="id"
+ placeholder="璇烽�夋嫨绯荤粺鑿滃崟"
+ />
+ </el-form-item>
+ </el-col>
+
+ <!-- <el-col :span="12">-->
+ <!-- <el-form-item prop="packageName">-->
+ <!-- <span slot="label">-->
+ <!-- 鐢熸垚鍖呰矾寰�-->
+ <!-- <el-tooltip content="鐢熸垚鍦ㄥ摢涓猨ava鍖呬笅锛屼緥濡� com.ruoyi.system" placement="top">-->
+ <!-- <i class="el-icon-question"></i>-->
+ <!-- </el-tooltip>-->
+ <!-- </span>-->
+ <!-- <el-input v-model="formData.packageName" />-->
+ <!-- </el-form-item>-->
+ <!-- </el-col>-->
+
+ <el-col :span="12">
+ <el-form-item prop="moduleName">
+ <template #label>
+ <span>
+ 妯″潡鍚�
+ <el-tooltip
+ content="妯″潡鍚嶏紝鍗充竴绾х洰褰曪紝渚嬪 system銆乮nfra銆乼ool 绛夌瓑"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="formData.moduleName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="businessName">
+ <template #label>
+ <span>
+ 涓氬姟鍚�
+ <el-tooltip
+ content="涓氬姟鍚嶏紝鍗充簩绾х洰褰曪紝渚嬪 user銆乸ermission銆乨ict 绛夌瓑"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="formData.businessName" />
+ </el-form-item>
+ </el-col>
+
+ <!-- <el-col :span="12">-->
+ <!-- <el-form-item prop="businessPackage">-->
+ <!-- <span slot="label">-->
+ <!-- 涓氬姟鍖�-->
+ <!-- <el-tooltip content="涓氬姟鍖咃紝鑷畾涔変簩绾х洰褰曘�備緥濡傝锛屾垜浠笇鏈涘皢 dictType 鍜� dictData 褰掔被鎴� dict 涓氬姟" placement="top">-->
+ <!-- <i class="el-icon-question"></i>-->
+ <!-- </el-tooltip>-->
+ <!-- </span>-->
+ <!-- <el-input v-model="formData.businessPackage" />-->
+ <!-- </el-form-item>-->
+ <!-- </el-col>-->
+
+ <el-col :span="12">
+ <el-form-item prop="className">
+ <template #label>
+ <span>
+ 绫诲悕绉�
+ <el-tooltip
+ content="绫诲悕绉帮紙棣栧瓧姣嶅ぇ鍐欙級锛屼緥濡係ysUser銆丼ysMenu銆丼ysDictData 绛夌瓑"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="formData.className" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="classComment">
+ <template #label>
+ <span>
+ 绫绘弿杩�
+ <el-tooltip content="鐢ㄤ綔绫绘弿杩帮紝渚嬪 鐢ㄦ埛" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="formData.classComment" />
+ </el-form-item>
+ </el-col>
+
+ <el-col v-if="formData.genType === '1'" :span="24">
+ <el-form-item prop="genPath">
+ <template #label>
+ <span>
+ 鑷畾涔夎矾寰�
+ <el-tooltip
+ content="濉啓纾佺洏缁濆璺緞锛岃嫢涓嶅~鍐欙紝鍒欑敓鎴愬埌褰撳墠Web椤圭洰涓�"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="formData.genPath">
+ <template #append>
+ <el-dropdown>
+ <el-button type="primary">
+ 鏈�杩戣矾寰勫揩閫熼�夋嫨
+ <i class="el-icon-arrow-down el-icon--right"></i>
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item @click="formData.genPath = '/'">
+ 鎭㈠榛樿鐨勭敓鎴愬熀纭�璺緞
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 鏍戣〃淇℃伅 -->
+ <el-row v-if="formData.templateType == 2">
+ <el-col :span="24">
+ <h4 class="form-header">鏍戣〃淇℃伅</h4>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item prop="treeParentColumnId">
+ <template #label>
+ <span>
+ 鐖剁紪鍙峰瓧娈�
+ <el-tooltip content="鏍戞樉绀虹殑鐖剁紪鐮佸瓧娈靛悕锛� 濡傦細parent_Id" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-select v-model="formData.treeParentColumnId" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in props.columns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item prop="treeNameColumnId">
+ <template #label>
+ <span>
+ 鏍戝悕绉板瓧娈�
+ <el-tooltip content="鏍戣妭鐐圭殑鏄剧ず鍚嶇О瀛楁鍚嶏紝 濡傦細dept_name" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-select v-model="formData.treeNameColumnId" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in props.columns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 涓昏〃淇℃伅 -->
+ <el-row v-if="formData.templateType == 15">
+ <el-col :span="24">
+ <h4 class="form-header">涓昏〃淇℃伅</h4>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item prop="masterTableId">
+ <template #label>
+ <span>
+ 鍏宠仈鐨勪富琛�
+ <el-tooltip content="鍏宠仈涓昏〃锛堢埗琛級鐨勮〃鍚嶏紝 濡傦細system_user" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-select v-model="formData.masterTableId" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(table0, index) in tables"
+ :key="index"
+ :label="table0.tableName + '锛�' + table0.tableComment"
+ :value="table0.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item prop="subJoinColumnId">
+ <template #label>
+ <span>
+ 瀛愯〃鍏宠仈鐨勫瓧娈�
+ <el-tooltip content="瀛愯〃鍏宠仈鐨勫瓧娈碉紝 濡傦細user_id" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-select v-model="formData.subJoinColumnId" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in props.columns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item prop="subJoinMany">
+ <template #label>
+ <span>
+ 鍏宠仈鍏崇郴
+ <el-tooltip content="涓昏〃涓庡瓙琛ㄧ殑鍏宠仈鍏崇郴" placement="top">
+ <Icon icon="ep:question-filled" />
+ </el-tooltip>
+ </span>
+ </template>
+ <el-radio-group v-model="formData.subJoinMany" placeholder="璇烽�夋嫨">
+ <el-radio :value="true">涓�瀵瑰</el-radio>
+ <el-radio :value="false">涓�瀵逛竴</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { handleTree } from '@/utils/tree'
+import * as CodegenApi from '@/api/infra/codegen'
+import * as MenuApi from '@/api/system/menu'
+import { PropType } from 'vue'
+
+defineOptions({ name: 'InfraCodegenGenerateInfoForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const props = defineProps({
+ table: {
+ type: Object as PropType<Nullable<CodegenApi.CodegenTableVO>>,
+ default: () => null
+ },
+ columns: {
+ type: Array as unknown as PropType<CodegenApi.CodegenColumnVO[]>,
+ default: () => null
+ }
+})
+
+const formRef = ref()
+const formData = ref({
+ templateType: null,
+ frontType: null,
+ scene: null,
+ moduleName: '',
+ businessName: '',
+ className: '',
+ classComment: '',
+ parentMenuId: null,
+ genPath: '',
+ genType: '',
+ masterTableId: undefined,
+ subJoinColumnId: undefined,
+ subJoinMany: undefined,
+ treeParentColumnId: undefined,
+ treeNameColumnId: undefined
+})
+
+const rules = reactive({
+ templateType: [required],
+ frontType: [required],
+ scene: [required],
+ moduleName: [required],
+ businessName: [required],
+ businessPackage: [required],
+ className: [required],
+ classComment: [required],
+ masterTableId: [required],
+ subJoinColumnId: [required],
+ subJoinMany: [required],
+ treeParentColumnId: [required],
+ treeNameColumnId: [required]
+})
+
+const tables = ref([]) // 琛ㄥ畾涔夊垪琛�
+const menus = ref<any[]>([])
+const menuTreeProps = {
+ label: 'name'
+}
+
+watch(
+ () => props.table,
+ async (table) => {
+ if (!table) return
+ formData.value = table as any
+ // 鍔犺浇琛ㄥ垪琛�
+ if (table.dataSourceConfigId >= 0) {
+ tables.value = await CodegenApi.getCodegenTableList(formData.value.dataSourceConfigId)
+ }
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+onMounted(async () => {
+ try {
+ // 鍔犺浇鑿滃崟
+ const resp = await MenuApi.getSimpleMenusList()
+ menus.value = handleTree(resp)
+ } catch {}
+})
+
+defineExpose({
+ validate: async () => unref(formRef)?.validate()
+})
+</script>
diff --git a/src/views/infra/codegen/components/index.ts b/src/views/infra/codegen/components/index.ts
new file mode 100644
index 0000000..1634a76
--- /dev/null
+++ b/src/views/infra/codegen/components/index.ts
@@ -0,0 +1,4 @@
+import BasicInfoForm from './BasicInfoForm.vue'
+import ColumInfoForm from './ColumInfoForm.vue'
+import GenerateInfoForm from './GenerateInfoForm.vue'
+export { BasicInfoForm, ColumInfoForm, GenerateInfoForm }
diff --git a/src/views/infra/codegen/index.vue b/src/views/infra/codegen/index.vue
new file mode 100644
index 0000000..893eba9
--- /dev/null
+++ b/src/views/infra/codegen/index.vue
@@ -0,0 +1,287 @@
+<template>
+ <doc-alert title="浠g爜鐢熸垚锛堝崟琛級" url="https://doc.iocoder.cn/new-feature/" />
+ <doc-alert title="浠g爜鐢熸垚锛堟爲琛級" url="https://doc.iocoder.cn/new-feature/tree/" />
+ <doc-alert title="浠g爜鐢熸垚锛堜富瀛愯〃锛�" url="https://doc.iocoder.cn/new-feature/master-sub/" />
+ <doc-alert title="鍗曞厓娴嬭瘯" url="https://doc.iocoder.cn/unit-test/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="琛ㄥ悕绉�" prop="tableName">
+ <el-input
+ v-model="queryParams.tableName"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ〃鍚嶇О"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="琛ㄦ弿杩�" prop="tableComment">
+ <el-input
+ v-model="queryParams.tableComment"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ〃鎻忚堪"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['infra:codegen:create']" type="primary" @click="openImportTable()">
+ <Icon class="mr-5px" icon="ep:zoom-in" />
+ 瀵煎叆
+ </el-button>
+ <el-button
+ v-hasPermi="['infra:codegen:delete']"
+ type="danger"
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ >
+ <Icon class="mr-5px" icon="ep:delete" />
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column align="center" label="鏁版嵁婧�">
+ <template #default="scope">
+ {{
+ dataSourceConfigList.find((config) => config.id === scope.row.dataSourceConfigId)?.name
+ }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="琛ㄥ悕绉�" prop="tableName" width="200" />
+ <el-table-column
+ :show-overflow-tooltip="true"
+ align="center"
+ label="琛ㄦ弿杩�"
+ prop="tableComment"
+ width="200"
+ />
+ <el-table-column align="center" label="瀹炰綋" prop="className" width="200" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="300px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['infra:codegen:preview']"
+ link
+ type="primary"
+ @click="handlePreview(scope.row)"
+ >
+ 棰勮
+ </el-button>
+ <el-button
+ v-hasPermi="['infra:codegen:update']"
+ link
+ type="primary"
+ @click="handleUpdate(scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['infra:codegen:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ <el-button
+ v-hasPermi="['infra:codegen:update']"
+ link
+ type="primary"
+ @click="handleSyncDB(scope.row)"
+ >
+ 鍚屾
+ </el-button>
+ <el-button
+ v-hasPermi="['infra:codegen:download']"
+ link
+ type="primary"
+ @click="handleGenTable(scope.row)"
+ >
+ 鐢熸垚浠g爜
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 寮圭獥锛氬鍏ヨ〃 -->
+ <ImportTable ref="importRef" @success="getList" />
+ <!-- 寮圭獥锛氶瑙堜唬鐮� -->
+ <PreviewCode ref="previewRef" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as CodegenApi from '@/api/infra/codegen'
+import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
+import ImportTable from './ImportTable.vue'
+import PreviewCode from './PreviewCode.vue'
+
+defineOptions({ name: 'InfraCodegen' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter() // 璺敱璺宠浆
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ tableName: undefined,
+ tableComment: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const dataSourceConfigList = ref<DataSourceConfigApi.DataSourceConfigVO[]>([]) // 鏁版嵁婧愬垪琛�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CodegenApi.getCodegenTablePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎叆鎿嶄綔 */
+const importRef = ref()
+const openImportTable = () => {
+ importRef.value.open()
+}
+
+/** 缂栬緫鎿嶄綔 */
+const handleUpdate = (id: number) => {
+ push('/codegen/edit?id=' + id)
+}
+
+/** 棰勮鎿嶄綔 */
+const previewRef = ref()
+const handlePreview = (row: CodegenApi.CodegenTableVO) => {
+ previewRef.value.open(row.id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await CodegenApi.deleteCodegenTable(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: CodegenApi.CodegenTableVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await CodegenApi.deleteCodegenTableList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍚屾鎿嶄綔 */
+const handleSyncDB = async (row: CodegenApi.CodegenTableVO) => {
+ // 鍩轰簬 DB 鍚屾
+ const tableName = row.tableName
+ try {
+ await message.confirm('纭瑕佸己鍒跺悓姝�' + tableName + '琛ㄧ粨鏋勫悧?', t('common.reminder'))
+ await CodegenApi.syncCodegenFromDB(row.id)
+ message.success('鍚屾鎴愬姛')
+ } catch {}
+}
+
+/** 鐢熸垚浠g爜鎿嶄綔 */
+const handleGenTable = async (row: CodegenApi.CodegenTableVO) => {
+ const res = await CodegenApi.downloadCodegen(row.id)
+ download.zip(res, 'codegen-' + row.className + '.zip')
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇鏁版嵁婧愬垪琛�
+ dataSourceConfigList.value = await DataSourceConfigApi.getDataSourceConfigList()
+})
+</script>
diff --git a/src/views/infra/config/ConfigForm.vue b/src/views/infra/config/ConfigForm.vue
new file mode 100644
index 0000000..4f7333a
--- /dev/null
+++ b/src/views/infra/config/ConfigForm.vue
@@ -0,0 +1,131 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鍙傛暟鍒嗙被" prop="category">
+ <el-input v-model="formData.category" placeholder="璇疯緭鍏ュ弬鏁板垎绫�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ弬鏁板悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟閿悕" prop="key">
+ <el-input v-model="formData.key" placeholder="璇疯緭鍏ュ弬鏁伴敭鍚�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟閿��" prop="value">
+ <el-input v-model="formData.value" placeholder="璇疯緭鍏ュ弬鏁伴敭鍊�" />
+ </el-form-item>
+ <el-form-item label="鏄惁鍙" prop="visible">
+ <el-radio-group v-model="formData.visible">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value as string"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
+import * as ConfigApi from '@/api/infra/config'
+
+defineOptions({ name: 'InfraConfigForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ category: '',
+ name: '',
+ key: '',
+ value: '',
+ visible: true,
+ remark: ''
+})
+const formRules = reactive({
+ category: [{ required: true, message: '鍙傛暟鍒嗙被涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍙傛暟鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ key: [{ required: true, message: '鍙傛暟閿悕涓嶈兘涓虹┖', trigger: 'blur' }],
+ value: [{ required: true, message: '鍙傛暟閿�间笉鑳戒负绌�', trigger: 'blur' }],
+ visible: [{ required: true, message: '鏄惁鍙涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ConfigApi.getConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as ConfigApi.ConfigVO
+ if (formType.value === 'create') {
+ await ConfigApi.createConfig(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ConfigApi.updateConfig(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ category: '',
+ name: '',
+ key: '',
+ value: '',
+ visible: true,
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/config/index.vue b/src/views/infra/config/index.vue
new file mode 100644
index 0000000..eecc947
--- /dev/null
+++ b/src/views/infra/config/index.vue
@@ -0,0 +1,257 @@
+<template>
+ <doc-alert title="閰嶇疆涓績" url="https://doc.iocoder.cn/config-center/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍙傛暟鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍙傛暟閿悕" prop="key">
+ <el-input
+ v-model="queryParams.key"
+ placeholder="璇疯緭鍏ュ弬鏁伴敭鍚�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绯荤粺鍐呯疆" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ placeholder="璇烽�夋嫨绯荤粺鍐呯疆"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:config:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:config:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="鍙傛暟涓婚敭" align="center" prop="id" />
+ <el-table-column label="鍙傛暟鍒嗙被" align="center" prop="category" />
+ <el-table-column label="鍙傛暟鍚嶇О" align="center" prop="name" :show-overflow-tooltip="true" />
+ <el-table-column label="鍙傛暟閿悕" align="center" prop="key" :show-overflow-tooltip="true" />
+ <el-table-column label="鍙傛暟閿��" align="center" prop="value" />
+ <el-table-column label="鏄惁鍙" align="center" prop="visible">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.visible" />
+ </template>
+ </el-table-column>
+ <el-table-column label="绯荤粺鍐呯疆" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:config:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:config:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ConfigForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as ConfigApi from '@/api/infra/config'
+import ConfigForm from './ConfigForm.vue'
+
+defineOptions({ name: 'InfraConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ key: undefined,
+ type: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ConfigApi.getConfigPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ConfigApi.deleteConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: ConfigApi.ConfigVO[]) => {
+ checkedIds.value = rows.map((row) => row.id!).filter(Boolean)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await ConfigApi.deleteConfigList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ConfigApi.exportConfig(queryParams)
+ download.excel(data, '鍙傛暟閰嶇疆.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue b/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue
new file mode 100644
index 0000000..e2a4eaa
--- /dev/null
+++ b/src/views/infra/dataSourceConfig/DataSourceConfigForm.vue
@@ -0,0 +1,111 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="鏁版嵁婧愬悕绉�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ弬鏁板悕绉�" />
+ </el-form-item>
+ <el-form-item label="鏁版嵁婧愯繛鎺�" prop="url">
+ <el-input v-model="formData.url" placeholder="璇疯緭鍏ユ暟鎹簮杩炴帴" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚�" prop="username">
+ <el-input v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
+ </el-form-item>
+ <el-form-item label="瀵嗙爜" prop="password">
+ <el-input v-model="formData.password" placeholder="璇疯緭鍏ュ瘑鐮�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
+
+defineOptions({ name: 'InfraDataSourceConfigForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<DataSourceConfigApi.DataSourceConfigVO>({
+ id: undefined,
+ name: '',
+ url: '',
+ username: '',
+ password: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鏁版嵁婧愬悕绉颁笉鑳戒负绌�', trigger: 'blur' }],
+ url: [{ required: true, message: '鏁版嵁婧愯繛鎺ヤ笉鑳戒负绌�', trigger: 'blur' }],
+ username: [{ required: true, message: '鐢ㄦ埛鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ password: [{ required: true, message: '瀵嗙爜涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DataSourceConfigApi.getDataSourceConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as DataSourceConfigApi.DataSourceConfigVO
+ if (formType.value === 'create') {
+ await DataSourceConfigApi.createDataSourceConfig(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DataSourceConfigApi.updateDataSourceConfig(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ url: '',
+ username: '',
+ password: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/dataSourceConfig/index.vue b/src/views/infra/dataSourceConfig/index.vue
new file mode 100644
index 0000000..2076d63
--- /dev/null
+++ b/src/views/infra/dataSourceConfig/index.vue
@@ -0,0 +1,136 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form class="-mb-15px" :inline="true">
+ <el-form-item>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:data-source-config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:data-source-config:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="涓婚敭缂栧彿" align="center" prop="id" />
+ <el-table-column label="鏁版嵁婧愬悕绉�" align="center" prop="name" />
+ <el-table-column label="鏁版嵁婧愯繛鎺�" align="center" prop="url" :show-overflow-tooltip="true" />
+ <el-table-column label="鐢ㄦ埛鍚�" align="center" prop="username" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:data-source-config:update']"
+ :disabled="scope.row.id === 0"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:data-source-config:delete']"
+ :disabled="scope.row.id === 0"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DataSourceConfigForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as DataSourceConfigApi from '@/api/infra/dataSourceConfig'
+import DataSourceConfigForm from './DataSourceConfigForm.vue'
+
+defineOptions({ name: 'InfraDataSourceConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ list.value = await DataSourceConfigApi.getDataSourceConfigList()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DataSourceConfigApi.deleteDataSourceConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: DataSourceConfigApi.DataSourceConfigVO[]) => {
+ // 杩囨护鎺塱d涓� 0 鐨勪富鏁版嵁婧�
+ checkedIds.value = rows.map((row) => row.id!).filter((id) => id !== 0 && Boolean(id))
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await DataSourceConfigApi.deleteDataSourceConfigList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo01/Demo01ContactForm.vue b/src/views/infra/demo/demo01/Demo01ContactForm.vue
new file mode 100644
index 0000000..d32a702
--- /dev/null
+++ b/src/views/infra/demo/demo01/Demo01ContactForm.vue
@@ -0,0 +1,129 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-radio-group v-model="formData.sex">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍑虹敓骞�" prop="birthday">
+ <el-date-picker
+ v-model="formData.birthday"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑虹敓骞�"
+ />
+ </el-form-item>
+ <el-form-item label="绠�浠�" prop="description">
+ <Editor v-model="formData.description" height="150px" />
+ </el-form-item>
+ <el-form-item label="澶村儚" prop="avatar">
+ <UploadImg v-model="formData.avatar" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { Demo01ContactApi, Demo01Contact } from '@/api/infra/demo/demo01'
+
+/** 绀轰緥鑱旂郴浜� 琛ㄥ崟 */
+defineOptions({ name: 'Demo01ContactForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined,
+ avatar: undefined,
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ sex: [{ required: true, message: '鎬у埆涓嶈兘涓虹┖', trigger: 'blur' }],
+ birthday: [{ required: true, message: '鍑虹敓骞翠笉鑳戒负绌�', trigger: 'blur' }],
+ description: [{ required: true, message: '绠�浠嬩笉鑳戒负绌�', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo01ContactApi.getDemo01Contact(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo01Contact
+ if (formType.value === 'create') {
+ await Demo01ContactApi.createDemo01Contact(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo01ContactApi.updateDemo01Contact(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined,
+ avatar: undefined,
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo/demo01/index.vue b/src/views/infra/demo/demo01/index.vue
new file mode 100644
index 0000000..82adc28
--- /dev/null
+++ b/src/views/infra/demo/demo01/index.vue
@@ -0,0 +1,253 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select
+ v-model="queryParams.sex"
+ placeholder="璇烽�夋嫨鎬у埆"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo01-contact:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:demo01-contact:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="isEmpty(checkedIds)"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:demo01-contact:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鎬у埆" align="center" prop="sex">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍑虹敓骞�"
+ align="center"
+ prop="birthday"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="绠�浠�" align="center" prop="description" />
+ <el-table-column label="澶村儚" align="center" prop="avatar" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo01-contact:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo01-contact:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo01ContactForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { Demo01ContactApi, Demo01Contact } from '@/api/infra/demo/demo01'
+import Demo01ContactForm from './Demo01ContactForm.vue'
+
+/** 绀轰緥鑱旂郴浜� 鍒楄〃 */
+defineOptions({ name: 'Demo01Contact' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<Demo01Contact[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ sex: undefined,
+ createTime: [],
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo01ContactApi.getDemo01ContactPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo01ContactApi.deleteDemo01Contact(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎绀轰緥鑱旂郴浜� */
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ await Demo01ContactApi.deleteDemo01ContactList(checkedIds.value)
+ checkedIds.value = [];
+ message.success(t('common.delSuccess'))
+ await getList();
+ } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo01Contact[]) => {
+ checkedIds.value = records.map((item) => item.id);
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await Demo01ContactApi.exportDemo01Contact(queryParams)
+ download.excel(data, '绀轰緥鑱旂郴浜�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo02/Demo02CategoryForm.vue b/src/views/infra/demo/demo02/Demo02CategoryForm.vue
new file mode 100644
index 0000000..f4c5f8e
--- /dev/null
+++ b/src/views/infra/demo/demo02/Demo02CategoryForm.vue
@@ -0,0 +1,114 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐖剁骇缂栧彿" prop="parentId">
+ <el-tree-select
+ v-model="formData.parentId"
+ :data="demo02CategoryTree"
+ :props="defaultProps"
+ check-strictly
+ default-expand-all
+ placeholder="璇烽�夋嫨鐖剁骇缂栧彿"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as Demo02CategoryApi from '@/api/infra/demo/demo02'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ parentId: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ parentId: [{ required: true, message: '鐖剁骇缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const demo02CategoryTree = ref() // 鏍戝舰缁撴瀯
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo02CategoryApi.getDemo02Category(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ await getDemo02CategoryTree()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo02CategoryApi.Demo02CategoryVO
+ if (formType.value === 'create') {
+ await Demo02CategoryApi.createDemo02Category(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo02CategoryApi.updateDemo02Category(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ parentId: undefined
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鑾峰緱绀轰緥鍒嗙被鏍� */
+const getDemo02CategoryTree = async () => {
+ demo02CategoryTree.value = []
+ const data = await Demo02CategoryApi.getDemo02CategoryList()
+ const root: Tree = { id: 0, name: '椤剁骇绀轰緥鍒嗙被', children: [] }
+ root.children = handleTree(data, 'id', 'parentId')
+ demo02CategoryTree.value.push(root)
+}
+</script>
diff --git a/src/views/infra/demo/demo02/index.vue b/src/views/infra/demo/demo02/index.vue
new file mode 100644
index 0000000..9faa8c9
--- /dev/null
+++ b/src/views/infra/demo/demo02/index.vue
@@ -0,0 +1,207 @@
+<template>
+ <doc-alert title="浠g爜鐢熸垚锛堟爲琛級" url="https://doc.iocoder.cn/new-feature/tree/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo02-category:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:demo02-category:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button type="danger" plain @click="toggleExpandAll">
+ <Icon icon="ep:sort" class="mr-5px" /> 灞曞紑/鎶樺彔
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ row-key="id"
+ :default-expand-all="isExpandAll"
+ v-if="refreshTable"
+ >
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo02-category:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo02-category:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo02CategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import download from '@/utils/download'
+import * as Demo02CategoryApi from '@/api/infra/demo/demo02'
+import Demo02CategoryForm from './Demo02CategoryForm.vue'
+
+defineOptions({ name: 'Demo02Category' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ name: null,
+ parentId: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo02CategoryApi.getDemo02CategoryList(queryParams)
+ list.value = handleTree(data, 'id', 'parentId')
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo02CategoryApi.deleteDemo02Category(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await Demo02CategoryApi.exportDemo02Category(queryParams)
+ download.excel(data, '绀轰緥鍒嗙被.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+const isExpandAll = ref(true) // 鏄惁灞曞紑锛岄粯璁ゅ叏閮ㄥ睍寮�
+const refreshTable = ref(true) // 閲嶆柊娓叉煋琛ㄦ牸鐘舵��
+const toggleExpandAll = async () => {
+ refreshTable.value = false
+ isExpandAll.value = !isExpandAll.value
+ await nextTick()
+ refreshTable.value = true
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue b/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
new file mode 100644
index 0000000..c34bf9f
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/Demo03StudentForm.vue
@@ -0,0 +1,124 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-radio-group v-model="formData.sex">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthday">
+ <el-date-picker
+ v-model="formData.birthday"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑虹敓鏃ユ湡"
+ />
+ </el-form-item>
+ <el-form-item label="绠�浠�" prop="description">
+ <Editor v-model="formData.description" height="150px" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { Demo03StudentApi, Demo03Student } from '@/api/infra/demo/demo03/erp'
+
+/** 瀛︾敓 琛ㄥ崟 */
+defineOptions({ name: 'Demo03StudentForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined,
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ sex: [{ required: true, message: '鎬у埆涓嶈兘涓虹┖', trigger: 'blur' }],
+ birthday: [{ required: true, message: '鍑虹敓鏃ユ湡涓嶈兘涓虹┖', trigger: 'blur' }],
+ description: [{ required: true, message: '绠�浠嬩笉鑳戒负绌�', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo03StudentApi.getDemo03Student(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo03Student
+ if (formType.value === 'create') {
+ await Demo03StudentApi.createDemo03Student(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo03StudentApi.updateDemo03Student(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined,
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue b/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
new file mode 100644
index 0000000..29b5cf1
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03CourseForm.vue
@@ -0,0 +1,99 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鍒嗘暟" prop="score">
+ <el-input v-model="formData.score" placeholder="璇疯緭鍏ュ垎鏁�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { Demo03StudentApi, Demo03Course } from '@/api/infra/demo/demo03/erp'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ score: undefined,
+})
+const formRules = reactive({
+ studentId: [{ required: true, message: '瀛︾敓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ score: [{ required: true, message: '鍒嗘暟涓嶈兘涓虹┖', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, studentId?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ formData.value.studentId = studentId as any
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo03StudentApi.getDemo03Course(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo03Course
+ if (formType.value === 'create') {
+ await Demo03StudentApi.createDemo03Course(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo03StudentApi.updateDemo03Course(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ score: undefined,
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue b/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
new file mode 100644
index 0000000..4d45bac
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03CourseList.vue
@@ -0,0 +1,163 @@
+<template>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo03-student:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="isEmpty(checkedIds)"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鍒嗘暟" align="center" prop="score" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo03-student:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo03CourseForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { isEmpty } from '@/utils/is'
+import {Demo03Course, Demo03StudentApi} from '@/api/infra/demo/demo03/erp'
+import Demo03CourseForm from './Demo03CourseForm.vue'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps<{
+ studentId?: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ studentId: undefined as unknown
+})
+
+/** 鐩戝惉涓昏〃鐨勫叧鑱斿瓧娈电殑鍙樺寲锛屽姞杞藉搴旂殑瀛愯〃鏁版嵁 */
+watch(
+ () => props.studentId,
+ (val: number) => {
+ if (!val) {
+ return
+ }
+ queryParams.studentId = val
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo03StudentApi.getDemo03CoursePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ if (!props.studentId) {
+ message.error('璇烽�夋嫨涓�涓鐢�')
+ return
+ }
+ formRef.value.open(type, id, props.studentId)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo03StudentApi.deleteDemo03Course(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎瀛︾敓璇剧▼ */
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ await Demo03StudentApi.deleteDemo03CourseList(checkedIds.value)
+ checkedIds.value = [];
+ message.success(t('common.delSuccess'))
+ await getList();
+ } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Course[]) => {
+ checkedIds.value = records.map((item) => item.id);
+}
+</script>
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue b/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
new file mode 100644
index 0000000..49bb490
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03GradeForm.vue
@@ -0,0 +1,99 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐝富浠�" prop="teacher">
+ <el-input v-model="formData.teacher" placeholder="璇疯緭鍏ョ彮涓讳换" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { Demo03StudentApi, Demo03Grade } from '@/api/infra/demo/demo03/erp'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ teacher: undefined,
+})
+const formRules = reactive({
+ studentId: [{ required: true, message: '瀛︾敓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ teacher: [{ required: true, message: '鐝富浠讳笉鑳戒负绌�', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, studentId?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ formData.value.studentId = studentId as any
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo03StudentApi.getDemo03Grade(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo03Grade
+ if (formType.value === 'create') {
+ await Demo03StudentApi.createDemo03Grade(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo03StudentApi.updateDemo03Grade(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ teacher: undefined,
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue b/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
new file mode 100644
index 0000000..cb516f4
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/components/Demo03GradeList.vue
@@ -0,0 +1,163 @@
+<template>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo03-student:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="isEmpty(checkedIds)"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鐝富浠�" align="center" prop="teacher" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo03-student:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo03GradeForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { isEmpty } from '@/utils/is'
+import {Demo03Grade, Demo03StudentApi} from '@/api/infra/demo/demo03/erp'
+import Demo03GradeForm from './Demo03GradeForm.vue'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps<{
+ studentId?: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ studentId: undefined as unknown
+})
+
+/** 鐩戝惉涓昏〃鐨勫叧鑱斿瓧娈电殑鍙樺寲锛屽姞杞藉搴旂殑瀛愯〃鏁版嵁 */
+watch(
+ () => props.studentId,
+ (val: number) => {
+ if (!val) {
+ return
+ }
+ queryParams.studentId = val
+ handleQuery()
+ },
+ { immediate: true, deep: true }
+)
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo03StudentApi.getDemo03GradePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ if (!props.studentId) {
+ message.error('璇烽�夋嫨涓�涓鐢�')
+ return
+ }
+ formRef.value.open(type, id, props.studentId)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo03StudentApi.deleteDemo03Grade(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎瀛︾敓鐝骇 */
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ await Demo03StudentApi.deleteDemo03GradeList(checkedIds.value)
+ checkedIds.value = [];
+ message.success(t('common.delSuccess'))
+ await getList();
+ } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Grade[]) => {
+ checkedIds.value = records.map((item) => item.id);
+}
+</script>
diff --git a/src/views/infra/demo/demo03/erp/index.vue b/src/views/infra/demo/demo03/erp/index.vue
new file mode 100644
index 0000000..e5649d0
--- /dev/null
+++ b/src/views/infra/demo/demo03/erp/index.vue
@@ -0,0 +1,274 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select
+ v-model="queryParams.sex"
+ placeholder="璇烽�夋嫨鎬у埆"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo03-student:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:demo03-student:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="isEmpty(checkedIds)"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ highlight-current-row
+ @current-change="handleCurrentChange"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鎬у埆" align="center" prop="sex">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍑虹敓鏃ユ湡"
+ align="center"
+ prop="birthday"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="绠�浠�" align="center" prop="description" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo03-student:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo03StudentForm ref="formRef" @success="getList" />
+ <!-- 瀛愯〃鐨勫垪琛� -->
+ <ContentWrap>
+ <el-tabs model-value="demo03Course">
+ <el-tab-pane label="瀛︾敓璇剧▼" name="demo03Course">
+ <Demo03CourseList :student-id="currentRow.id" />
+ </el-tab-pane>
+ <el-tab-pane label="瀛︾敓鐝骇" name="demo03Grade">
+ <Demo03GradeList :student-id="currentRow.id" />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { Demo03StudentApi, Demo03Student } from '@/api/infra/demo/demo03/erp'
+import Demo03StudentForm from './Demo03StudentForm.vue'
+import Demo03CourseList from './components/Demo03CourseList.vue'
+import Demo03GradeList from './components/Demo03GradeList.vue'
+
+/** 瀛︾敓 鍒楄〃 */
+defineOptions({ name: 'Demo03Student' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<Demo03Student[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ sex: undefined,
+ description: undefined,
+ createTime: [],
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo03StudentApi.getDemo03StudentPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo03StudentApi.deleteDemo03Student(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎瀛︾敓 */
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ await Demo03StudentApi.deleteDemo03StudentList(checkedIds.value)
+ checkedIds.value = [];
+ message.success(t('common.delSuccess'))
+ await getList();
+ } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Student[]) => {
+ checkedIds.value = records.map((item) => item.id);
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await Demo03StudentApi.exportDemo03Student(queryParams)
+ download.excel(data, '瀛︾敓.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 閫変腑琛屾搷浣� */
+const currentRow = ref({}) // 閫変腑琛�
+const handleCurrentChange = (row) => {
+ currentRow.value = row
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo03/inner/Demo03StudentForm.vue b/src/views/infra/demo/demo03/inner/Demo03StudentForm.vue
new file mode 100644
index 0000000..ecdae97
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/Demo03StudentForm.vue
@@ -0,0 +1,156 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-radio-group v-model="formData.sex">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthday">
+ <el-date-picker
+ v-model="formData.birthday"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑虹敓鏃ユ湡"
+ />
+ </el-form-item>
+ <el-form-item label="绠�浠�" prop="description">
+ <Editor v-model="formData.description" height="150px" />
+ </el-form-item>
+ </el-form>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <el-tabs v-model="subTabsName">
+ <el-tab-pane label="瀛︾敓璇剧▼" name="demo03Course">
+ <Demo03CourseForm ref="demo03CourseFormRef" :student-id="formData.id" />
+ </el-tab-pane>
+ <el-tab-pane label="瀛︾敓鐝骇" name="demo03Grade">
+ <Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData.id" />
+ </el-tab-pane>
+ </el-tabs>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { Demo03Student, Demo03StudentApi } from '@/api/infra/demo/demo03/inner'
+import Demo03CourseForm from './components/Demo03CourseForm.vue'
+import Demo03GradeForm from './components/Demo03GradeForm.vue'
+
+/** 瀛︾敓 琛ㄥ崟 */
+defineOptions({ name: 'Demo03StudentForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ sex: [{ required: true, message: '鎬у埆涓嶈兘涓虹┖', trigger: 'blur' }],
+ birthday: [{ required: true, message: '鍑虹敓鏃ユ湡涓嶈兘涓虹┖', trigger: 'blur' }],
+ description: [{ required: true, message: '绠�浠嬩笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('demo03Course')
+const demo03CourseFormRef = ref()
+const demo03GradeFormRef = ref()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo03StudentApi.getDemo03Student(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鏍¢獙瀛愯〃鍗�
+ try {
+ await demo03CourseFormRef.value.validate()
+ } catch (e) {
+ subTabsName.value = 'demo03Course'
+ return
+ }
+ try {
+ await demo03GradeFormRef.value.validate()
+ } catch (e) {
+ subTabsName.value = 'demo03Grade'
+ return
+ }
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo03Student
+ // 鎷兼帴瀛愯〃鐨勬暟鎹�
+ data.demo03Courses = demo03CourseFormRef.value.getData()
+ data.demo03Grade = demo03GradeFormRef.value.getData()
+ if (formType.value === 'create') {
+ await Demo03StudentApi.createDemo03Student(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo03StudentApi.updateDemo03Student(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue b/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue
new file mode 100644
index 0000000..bde79b5
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03CourseForm.vue
@@ -0,0 +1,100 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ >
+ <el-table :data="formData" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" width="100" />
+ <el-table-column label="鍚嶅瓧" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+ <el-input v-model="row.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒嗘暟" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.score`" :rules="formRules.score" class="mb-0px!">
+ <el-input v-model="row.score" placeholder="璇疯緭鍏ュ垎鏁�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3">
+ <el-button @click="handleAdd" round>+ 娣诲姞瀛︾敓璇剧▼</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { Demo03StudentApi } from '@/api/infra/demo/demo03/inner'
+
+const props = defineProps<{
+ studentId: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref<any[]>([])
+const formRules = reactive({
+ studentId: [{ required: true, message: '瀛︾敓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ score: [{ required: true, message: '鍒嗘暟涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鐩戝惉涓昏〃鐨勫叧鑱斿瓧娈电殑鍙樺寲锛屽姞杞藉搴旂殑瀛愯〃鏁版嵁 */
+watch(
+ () => props.studentId,
+ async (val) => {
+ // 1. 閲嶇疆琛ㄥ崟
+ formData.value = []
+ // 2. val 闈炵┖锛屽垯鍔犺浇鏁版嵁
+ if (!val) {
+ return
+ }
+ try {
+ formLoading.value = true
+ formData.value = await Demo03StudentApi.getDemo03CourseListByStudentId(val)
+ } finally {
+ formLoading.value = false
+ }
+ },
+ { immediate: true }
+)
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ score: undefined
+ }
+ row.studentId = props.studentId as any
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index) => {
+ formData.value.splice(index, 1)
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+
+/** 琛ㄥ崟鍊� */
+const getData = () => {
+ return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue b/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue
new file mode 100644
index 0000000..cfad83f
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03CourseList.vue
@@ -0,0 +1,48 @@
+<template>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鍒嗘暟" align="center" prop="score" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { Demo03StudentApi } from '@/api/infra/demo/demo03/inner'
+
+const props = defineProps<{
+ studentId?: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ list.value = await Demo03StudentApi.getDemo03CourseListByStudentId(props.studentId)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue b/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue
new file mode 100644
index 0000000..a15bf51
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03GradeForm.vue
@@ -0,0 +1,72 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐝富浠�" prop="teacher">
+ <el-input v-model="formData.teacher" placeholder="璇疯緭鍏ョ彮涓讳换" />
+ </el-form-item>
+ </el-form>
+</template>
+<script setup lang="ts">
+import { Demo03StudentApi } from '@/api/infra/demo/demo03/inner'
+
+const props = defineProps<{
+ studentId: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref<any>({})
+const formRules = reactive({
+ studentId: [{ required: true, message: '瀛︾敓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ teacher: [{ required: true, message: '鐝富浠讳笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鐩戝惉涓昏〃鐨勫叧鑱斿瓧娈电殑鍙樺寲锛屽姞杞藉搴旂殑瀛愯〃鏁版嵁 */
+watch(
+ () => props.studentId,
+ async (val) => {
+ // 1. 閲嶇疆琛ㄥ崟
+ formData.value = {
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ teacher: undefined
+ }
+ // 2. val 闈炵┖锛屽垯鍔犺浇鏁版嵁
+ if (!val) {
+ return
+ }
+ try {
+ formLoading.value = true
+ const data = await Demo03StudentApi.getDemo03GradeByStudentId(val)
+ if (!data) {
+ return
+ }
+ formData.value = data
+ } finally {
+ formLoading.value = false
+ }
+ },
+ { immediate: true }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+
+/** 琛ㄥ崟鍊� */
+const getData = () => {
+ return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue b/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue
new file mode 100644
index 0000000..ffd66a6
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/components/Demo03GradeList.vue
@@ -0,0 +1,52 @@
+<template>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鐝富浠�" align="center" prop="teacher" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { Demo03StudentApi } from '@/api/infra/demo/demo03/inner'
+
+const props = defineProps<{
+ studentId?: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo03StudentApi.getDemo03GradeByStudentId(props.studentId)
+ if (!data) {
+ return
+ }
+ list.value.push(data)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo03/inner/index.vue b/src/views/infra/demo/demo03/inner/index.vue
new file mode 100644
index 0000000..30ddc76
--- /dev/null
+++ b/src/views/infra/demo/demo03/inner/index.vue
@@ -0,0 +1,263 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select v-model="queryParams.sex" placeholder="璇烽�夋嫨鎬у埆" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo03-student:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:demo03-student:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="isEmpty(checkedIds)"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <!-- 瀛愯〃鐨勫垪琛� -->
+ <el-table-column type="expand">
+ <template #default="scope">
+ <el-tabs model-value="demo03Course">
+ <el-tab-pane label="瀛︾敓璇剧▼" name="demo03Course">
+ <Demo03CourseList :student-id="scope.row.id" />
+ </el-tab-pane>
+ <el-tab-pane label="瀛︾敓鐝骇" name="demo03Grade">
+ <Demo03GradeList :student-id="scope.row.id" />
+ </el-tab-pane>
+ </el-tabs>
+ </template>
+ </el-table-column>
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鎬у埆" align="center" prop="sex">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍑虹敓鏃ユ湡"
+ align="center"
+ prop="birthday"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="绠�浠�" align="center" prop="description" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo03-student:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo03StudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { Demo03Student, Demo03StudentApi } from '@/api/infra/demo/demo03/inner'
+import Demo03StudentForm from './Demo03StudentForm.vue'
+import Demo03CourseList from './components/Demo03CourseList.vue'
+import Demo03GradeList from './components/Demo03GradeList.vue'
+
+/** 瀛︾敓 鍒楄〃 */
+defineOptions({ name: 'Demo03Student' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<Demo03Student[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ sex: undefined,
+ description: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo03StudentApi.getDemo03StudentPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo03StudentApi.deleteDemo03Student(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎瀛︾敓 */
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ await Demo03StudentApi.deleteDemo03StudentList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ await getList()
+ } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Student[]) => {
+ checkedIds.value = records.map((item) => item.id)
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await Demo03StudentApi.exportDemo03Student(queryParams)
+ download.excel(data, '瀛︾敓.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue b/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue
new file mode 100644
index 0000000..46cf6e9
--- /dev/null
+++ b/src/views/infra/demo/demo03/normal/Demo03StudentForm.vue
@@ -0,0 +1,156 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-radio-group v-model="formData.sex">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthday">
+ <el-date-picker
+ v-model="formData.birthday"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑虹敓鏃ユ湡"
+ />
+ </el-form-item>
+ <el-form-item label="绠�浠�" prop="description">
+ <Editor v-model="formData.description" height="150px" />
+ </el-form-item>
+ </el-form>
+ <!-- 瀛愯〃鐨勮〃鍗� -->
+ <el-tabs v-model="subTabsName">
+ <el-tab-pane label="瀛︾敓璇剧▼" name="demo03Course">
+ <Demo03CourseForm ref="demo03CourseFormRef" :student-id="formData.id" />
+ </el-tab-pane>
+ <el-tab-pane label="瀛︾敓鐝骇" name="demo03Grade">
+ <Demo03GradeForm ref="demo03GradeFormRef" :student-id="formData.id" />
+ </el-tab-pane>
+ </el-tabs>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { Demo03StudentApi, Demo03Student } from '@/api/infra/demo/demo03/normal'
+import Demo03CourseForm from './components/Demo03CourseForm.vue'
+import Demo03GradeForm from './components/Demo03GradeForm.vue'
+
+/** 瀛︾敓 琛ㄥ崟 */
+defineOptions({ name: 'Demo03StudentForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined,
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ sex: [{ required: true, message: '鎬у埆涓嶈兘涓虹┖', trigger: 'blur' }],
+ birthday: [{ required: true, message: '鍑虹敓鏃ユ湡涓嶈兘涓虹┖', trigger: 'blur' }],
+ description: [{ required: true, message: '绠�浠嬩笉鑳戒负绌�', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 瀛愯〃鐨勮〃鍗� */
+const subTabsName = ref('demo03Course')
+const demo03CourseFormRef = ref()
+const demo03GradeFormRef = ref()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await Demo03StudentApi.getDemo03Student(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鏍¢獙瀛愯〃鍗�
+ try {
+ await demo03CourseFormRef.value.validate()
+ } catch (e) {
+ subTabsName.value = 'demo03Course'
+ return
+ }
+ try {
+ await demo03GradeFormRef.value.validate()
+ } catch (e) {
+ subTabsName.value = 'demo03Grade'
+ return
+ }
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as Demo03Student
+ // 鎷兼帴瀛愯〃鐨勬暟鎹�
+ data.demo03Courses = demo03CourseFormRef.value.getData()
+ data.demo03Grade = demo03GradeFormRef.value.getData()
+ if (formType.value === 'create') {
+ await Demo03StudentApi.createDemo03Student(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await Demo03StudentApi.updateDemo03Student(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ sex: undefined,
+ birthday: undefined,
+ description: undefined,
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue b/src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue
new file mode 100644
index 0000000..23766ba
--- /dev/null
+++ b/src/views/infra/demo/demo03/normal/components/Demo03CourseForm.vue
@@ -0,0 +1,100 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ v-loading="formLoading"
+ label-width="0px"
+ :inline-message="true"
+ >
+ <el-table :data="formData" class="-mt-10px">
+ <el-table-column label="搴忓彿" type="index" width="100" />
+ <el-table-column label="鍚嶅瓧" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+ <el-input v-model="row.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒嗘暟" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.score`" :rules="formRules.score" class="mb-0px!">
+ <el-input v-model="row.score" placeholder="璇疯緭鍏ュ垎鏁�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link>鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ <el-row justify="center" class="mt-3">
+ <el-button @click="handleAdd" round>+ 娣诲姞瀛︾敓璇剧▼</el-button>
+ </el-row>
+</template>
+<script setup lang="ts">
+import { Demo03StudentApi } from '@/api/infra/demo/demo03/normal'
+
+const props = defineProps<{
+ studentId: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref<any[]>([])
+const formRules = reactive({
+ studentId: [{ required: true, message: '瀛︾敓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ score: [{ required: true, message: '鍒嗘暟涓嶈兘涓虹┖', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鐩戝惉涓昏〃鐨勫叧鑱斿瓧娈电殑鍙樺寲锛屽姞杞藉搴旂殑瀛愯〃鏁版嵁 */
+watch(
+ () => props.studentId,
+ async (val) => {
+ // 1. 閲嶇疆琛ㄥ崟
+ formData.value = []
+ // 2. val 闈炵┖锛屽垯鍔犺浇鏁版嵁
+ if (!val) {
+ return
+ }
+ try {
+ formLoading.value = true
+ formData.value = await Demo03StudentApi.getDemo03CourseListByStudentId(val)
+ } finally {
+ formLoading.value = false
+ }
+ },
+ { immediate: true }
+)
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ score: undefined,
+ }
+ row.studentId = props.studentId as any
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index) => {
+ formData.value.splice(index, 1)
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+
+/** 琛ㄥ崟鍊� */
+const getData = () => {
+ return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue b/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue
new file mode 100644
index 0000000..df64a7f
--- /dev/null
+++ b/src/views/infra/demo/demo03/normal/components/Demo03GradeForm.vue
@@ -0,0 +1,72 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐝富浠�" prop="teacher">
+ <el-input v-model="formData.teacher" placeholder="璇疯緭鍏ョ彮涓讳换" />
+ </el-form-item>
+ </el-form>
+</template>
+<script setup lang="ts">
+import { Demo03StudentApi } from '@/api/infra/demo/demo03/normal'
+
+const props = defineProps<{
+ studentId: number // 瀛︾敓缂栧彿锛堜富琛ㄧ殑鍏宠仈瀛楁锛�
+}>()
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref({})
+const formRules = reactive({
+ studentId: [{ required: true, message: '瀛︾敓缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ teacher: [{ required: true, message: '鐝富浠讳笉鑳戒负绌�', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鐩戝惉涓昏〃鐨勫叧鑱斿瓧娈电殑鍙樺寲锛屽姞杞藉搴旂殑瀛愯〃鏁版嵁 */
+watch(
+ () => props.studentId,
+ async (val) => {
+ // 1. 閲嶇疆琛ㄥ崟
+ formData.value = {
+ id: undefined,
+ studentId: undefined,
+ name: undefined,
+ teacher: undefined
+ }
+ // 2. val 闈炵┖锛屽垯鍔犺浇鏁版嵁
+ if (!val) {
+ return
+ }
+ try {
+ formLoading.value = true
+ const data = await Demo03StudentApi.getDemo03GradeByStudentId(val)
+ if (!data) {
+ return
+ }
+ formData.value = data
+ } finally {
+ formLoading.value = false
+ }
+ },
+ { immediate: true }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+
+/** 琛ㄥ崟鍊� */
+const getData = () => {
+ return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>
diff --git a/src/views/infra/demo/demo03/normal/index.vue b/src/views/infra/demo/demo03/normal/index.vue
new file mode 100644
index 0000000..f64e21c
--- /dev/null
+++ b/src/views/infra/demo/demo03/normal/index.vue
@@ -0,0 +1,253 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select
+ v-model="queryParams.sex"
+ placeholder="璇烽�夋嫨鎬у埆"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:demo03-student:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:demo03-student:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="isEmpty(checkedIds)"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鎬у埆" align="center" prop="sex">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍑虹敓鏃ユ湡"
+ align="center"
+ prop="birthday"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="绠�浠�" align="center" prop="description" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:demo03-student:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:demo03-student:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <Demo03StudentForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { Demo03StudentApi, Demo03Student } from '@/api/infra/demo/demo03/normal'
+import Demo03StudentForm from './Demo03StudentForm.vue'
+
+/** 瀛︾敓 鍒楄〃 */
+defineOptions({ name: 'Demo03Student' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<Demo03Student[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ sex: undefined,
+ description: undefined,
+ createTime: [],
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await Demo03StudentApi.getDemo03StudentPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await Demo03StudentApi.deleteDemo03Student(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎瀛︾敓 */
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ await Demo03StudentApi.deleteDemo03StudentList(checkedIds.value)
+ checkedIds.value = [];
+ message.success(t('common.delSuccess'))
+ await getList();
+ } catch {}
+}
+
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (records: Demo03Student[]) => {
+ checkedIds.value = records.map((item) => item.id);
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await Demo03StudentApi.exportDemo03Student(queryParams)
+ download.excel(data, '瀛︾敓.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/druid/index.vue b/src/views/infra/druid/index.vue
new file mode 100644
index 0000000..2ac99d2
--- /dev/null
+++ b/src/views/infra/druid/index.vue
@@ -0,0 +1,28 @@
+<template>
+ <doc-alert title="鏁版嵁搴� MyBatis" url="https://doc.iocoder.cn/mybatis/" />
+ <doc-alert title="澶氭暟鎹簮锛堣鍐欏垎绂伙級" url="https://doc.iocoder.cn/dynamic-datasource/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame v-if="!loading" v-loading="loading" :src="url" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/infra/config'
+
+defineOptions({ name: 'InfraDruid' })
+
+const loading = ref(true) // 鏄惁鍔犺浇涓�
+const url = ref(import.meta.env.VITE_BASE_URL + '/druid/index.html')
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ try {
+ const data = await ConfigApi.getConfigKey('url.druid')
+ if (data && data.length > 0) {
+ url.value = data
+ }
+ } finally {
+ loading.value = false
+ }
+})
+</script>
diff --git a/src/views/infra/file/FileForm.vue b/src/views/infra/file/FileForm.vue
new file mode 100644
index 0000000..84b2504
--- /dev/null
+++ b/src/views/infra/file/FileForm.vue
@@ -0,0 +1,107 @@
+<template>
+ <Dialog v-model="dialogVisible" title="涓婁紶鏂囦欢">
+ <el-upload
+ ref="uploadRef"
+ v-model:file-list="fileList"
+ :action="uploadUrl"
+ :auto-upload="false"
+ :data="data"
+ :disabled="formLoading"
+ :limit="1"
+ :on-change="handleFileChange"
+ :on-error="submitFormError"
+ :on-exceed="handleExceed"
+ :on-progress="handleProgress"
+ :on-success="submitFormSuccess"
+ :http-request="httpRequest"
+ accept=".jpg, .png, .gif"
+ drag
+ >
+ <i class="el-icon-upload"></i>
+ <div class="el-upload__text"> 灏嗘枃浠舵嫋鍒版澶勶紝鎴� <em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip" style="color: red">
+ 鎻愮ず锛氫粎鍏佽瀵煎叆 jpg銆乸ng銆乬if 鏍煎紡鏂囦欢锛�
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { useUpload } from '@/components/UploadFile/src/useUpload'
+import { UploadFile, UploadProgressEvent } from 'element-plus/es/components/upload/src/upload'
+
+defineOptions({ name: 'InfraFileForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const fileList = ref([]) // 鏂囦欢鍒楄〃
+const data = ref({ path: '' })
+const uploadRef = ref()
+
+const { uploadUrl, httpRequest } = useUpload()
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ resetForm()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 澶勭悊涓婁紶鐨勬枃浠跺彂鐢熷彉鍖� */
+const handleFileChange = (file: UploadFile) => {
+ data.value.path = file.name
+}
+
+/** 澶勭悊鏂囦欢涓婁紶杩涘害鏄剧ず */
+const handleProgress = (upEvt: UploadProgressEvent, file: UploadFile) => {
+ file.percentage = upEvt.percent
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitFileForm = () => {
+ if (fileList.value.length == 0) {
+ message.error('璇蜂笂浼犳枃浠�')
+ return
+ }
+ formLoading.value = true
+ unref(uploadRef)?.submit()
+}
+
+/** 鏂囦欢涓婁紶鎴愬姛澶勭悊 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitFormSuccess = () => {
+ // 娓呯悊
+ dialogVisible.value = false
+ formLoading.value = false
+ unref(uploadRef)?.clearFiles()
+ // 鎻愮ず鎴愬姛锛屽苟鍒锋柊
+ message.success(t('common.createSuccess'))
+ emit('success')
+}
+
+/** 涓婁紶閿欒鎻愮ず */
+const submitFormError = (): void => {
+ message.error('涓婁紶澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒')
+ formLoading.value = false
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ // 閲嶇疆涓婁紶鐘舵�佸拰鏂囦欢
+ formLoading.value = false
+ uploadRef.value?.clearFiles()
+}
+
+/** 鏂囦欢鏁拌秴鍑烘彁绀� */
+const handleExceed = (): void => {
+ message.error('鏈�澶氬彧鑳戒笂浼犱竴涓枃浠讹紒')
+}
+</script>
diff --git a/src/views/infra/file/index.vue b/src/views/infra/file/index.vue
new file mode 100644
index 0000000..e431218
--- /dev/null
+++ b/src/views/infra/file/index.vue
@@ -0,0 +1,238 @@
+<template>
+ <doc-alert title="涓婁紶涓嬭浇" url="https://doc.iocoder.cn/file/" />
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏂囦欢璺緞" prop="path">
+ <el-input
+ v-model="queryParams.path"
+ placeholder="璇疯緭鍏ユ枃浠惰矾寰�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏂囦欢绫诲瀷" prop="type" width="80">
+ <el-input
+ v-model="queryParams.type"
+ placeholder="璇疯緭鍏ユ枃浠剁被鍨�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" plain @click="openForm">
+ <Icon icon="ep:upload" class="mr-5px" /> 涓婁紶鏂囦欢
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:file:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="鏂囦欢鍚�" align="center" prop="name" :show-overflow-tooltip="true" />
+ <el-table-column label="鏂囦欢璺緞" align="center" prop="path" :show-overflow-tooltip="true" />
+ <el-table-column label="URL" align="center" prop="url" :show-overflow-tooltip="true" />
+ <el-table-column
+ label="鏂囦欢澶у皬"
+ align="center"
+ prop="size"
+ width="120"
+ :formatter="fileSizeFormatter"
+ />
+ <el-table-column label="鏂囦欢绫诲瀷" align="center" prop="type" width="180px" />
+ <el-table-column label="鏂囦欢鍐呭" align="center" prop="url" width="110px">
+ <template #default="{ row }">
+ <el-image
+ v-if="row.type.includes('image')"
+ class="h-80px w-80px"
+ lazy
+ :src="row.url"
+ :preview-src-list="[row.url]"
+ preview-teleported
+ fit="cover"
+ />
+ <el-link
+ v-else-if="row.type.includes('pdf')"
+ type="primary"
+ :href="row.url"
+ :underline="false"
+ target="_blank"
+ >棰勮</el-link
+ >
+ <el-link v-else type="primary" download :href="row.url" :underline="false" target="_blank"
+ >涓嬭浇</el-link
+ >
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓婁紶鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="copyToClipboard(scope.row.url)">
+ 澶嶅埗閾炬帴
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:file:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <FileForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { fileSizeFormatter } from '@/utils'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FileApi from '@/api/infra/file'
+import FileForm from './FileForm.vue'
+import { useClipboard } from '@vueuse/core'
+
+defineOptions({ name: 'InfraFile' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ type: undefined,
+ path: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FileApi.getFilePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open()
+}
+
+/** 澶嶅埗鍒板壀璐存澘鏂规硶 */
+const copyToClipboard = async (text: string) => {
+ const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
+ if (!isSupported) {
+ message.error(t('common.copyError'))
+ return
+ }
+ await copy()
+ if (unref(copied)) {
+ message.success(t('common.copySuccess'))
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await FileApi.deleteFile(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await FileApi.deleteFileList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/fileConfig/FileConfigForm.vue b/src/views/infra/fileConfig/FileConfigForm.vue
new file mode 100644
index 0000000..34691d9
--- /dev/null
+++ b/src/views/infra/fileConfig/FileConfigForm.vue
@@ -0,0 +1,224 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="130px"
+ >
+ <el-form-item label="閰嶇疆鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ厤缃悕" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ <el-form-item label="瀛樺偍鍣�" prop="storage">
+ <el-select
+ v-model="formData.storage"
+ :disabled="formData.id !== undefined"
+ placeholder="璇烽�夋嫨瀛樺偍鍣�"
+ >
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="parseInt(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ <!-- DB -->
+ <!-- Local / FTP / SFTP -->
+ <el-form-item
+ v-if="formData.storage >= 10 && formData.storage <= 12"
+ label="鍩虹璺緞"
+ prop="config.basePath"
+ >
+ <el-input v-model="formData.config.basePath" placeholder="璇疯緭鍏ュ熀纭�璺緞" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.storage >= 11 && formData.storage <= 12"
+ label="涓绘満鍦板潃"
+ prop="config.host"
+ >
+ <el-input v-model="formData.config.host" placeholder="璇疯緭鍏ヤ富鏈哄湴鍧�" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.storage >= 11 && formData.storage <= 12"
+ label="涓绘満绔彛"
+ prop="config.port"
+ >
+ <el-input-number v-model="formData.config.port" :min="0" placeholder="璇疯緭鍏ヤ富鏈虹鍙�" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.storage >= 11 && formData.storage <= 12"
+ label="鐢ㄦ埛鍚�"
+ prop="config.username"
+ >
+ <el-input v-model="formData.config.username" placeholder="璇疯緭鍏ュ瘑鐮�" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.storage >= 11 && formData.storage <= 12"
+ label="瀵嗙爜"
+ prop="config.password"
+ >
+ <el-input v-model="formData.config.password" placeholder="璇疯緭鍏ュ瘑鐮�" />
+ </el-form-item>
+ <el-form-item v-if="formData.storage === 11" label="杩炴帴妯″紡" prop="config.mode">
+ <el-radio-group v-model="formData.config.mode">
+ <el-radio key="Active" value="Active">涓诲姩妯″紡</el-radio>
+ <el-radio key="Passive" value="Passive">琚姩妯″紡</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <!-- S3 -->
+ <el-form-item v-if="formData.storage === 20" label="鑺傜偣鍦板潃" prop="config.endpoint">
+ <el-input v-model="formData.config.endpoint" placeholder="璇疯緭鍏ヨ妭鐐瑰湴鍧�" />
+ </el-form-item>
+ <el-form-item v-if="formData.storage === 20" label="瀛樺偍 bucket" prop="config.bucket">
+ <el-input v-model="formData.config.bucket" placeholder="璇疯緭鍏� bucket" />
+ </el-form-item>
+ <el-form-item v-if="formData.storage === 20" label="accessKey" prop="config.accessKey">
+ <el-input v-model="formData.config.accessKey" placeholder="璇疯緭鍏� accessKey" />
+ </el-form-item>
+ <el-form-item v-if="formData.storage === 20" label="accessSecret" prop="config.accessSecret">
+ <el-input v-model="formData.config.accessSecret" placeholder="璇疯緭鍏� accessSecret" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.storage === 20"
+ label="鏄惁 Path Style"
+ prop="config.enablePathStyleAccess"
+ >
+ <el-radio-group v-model="formData.config.enablePathStyleAccess">
+ <el-radio key="true" :value="true">鍚敤</el-radio>
+ <el-radio key="false" :value="false">绂佺敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="formData.storage === 20"
+ label="鍏紑璁块棶"
+ prop="config.enablePublicAccess"
+ >
+ <el-radio-group v-model="formData.config.enablePublicAccess">
+ <el-radio key="true" :value="true">鍏紑</el-radio>
+ <el-radio key="false" :value="false">绉佹湁</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="formData.storage === 20" label="鍖哄煙">
+ <!-- 閫夊~锛屾棤闇�鍙傛暟鏍¢獙 -->
+ <el-input v-model="formData.config.region" placeholder="璇峰~鍐欏尯鍩燂紝涓�鑸粎 AWS 闇�瑕佸~鍐�" />
+ </el-form-item>
+ <!-- 閫氱敤 -->
+ <el-form-item v-if="formData.storage === 20" label="鑷畾涔夊煙鍚�">
+ <!-- 鏃犻渶鍙傛暟鏍¢獙锛屾墍浠ュ幓鎺� prop -->
+ <el-input v-model="formData.config.domain" placeholder="璇疯緭鍏ヨ嚜瀹氫箟鍩熷悕" />
+ </el-form-item>
+ <el-form-item v-else-if="formData.storage" label="鑷畾涔夊煙鍚�" prop="config.domain">
+ <el-input v-model="formData.config.domain" placeholder="璇疯緭鍏ヨ嚜瀹氫箟鍩熷悕" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as FileConfigApi from '@/api/infra/fileConfig'
+import { FormRules } from 'element-plus'
+
+defineOptions({ name: 'InfraFileConfigForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ storage: 0,
+ remark: '',
+ config: {} as FileConfigApi.FileClientConfig
+})
+const formRules = reactive<FormRules>({
+ name: [{ required: true, message: '閰嶇疆鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ storage: [{ required: true, message: '瀛樺偍鍣ㄤ笉鑳戒负绌�', trigger: 'change' }],
+ config: {
+ basePath: [{ required: true, message: '鍩虹璺緞涓嶈兘涓虹┖', trigger: 'blur' }],
+ host: [{ required: true, message: '涓绘満鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ port: [{ required: true, message: '涓绘満绔彛涓嶈兘涓虹┖', trigger: 'blur' }],
+ username: [{ required: true, message: '鐢ㄦ埛鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ password: [{ required: true, message: '瀵嗙爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ mode: [{ required: true, message: '杩炴帴妯″紡涓嶈兘涓虹┖', trigger: 'change' }],
+ endpoint: [{ required: true, message: '鑺傜偣鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ bucket: [{ required: true, message: '瀛樺偍 bucket 涓嶈兘涓虹┖', trigger: 'blur' }],
+ accessKey: [{ required: true, message: 'accessKey 涓嶈兘涓虹┖', trigger: 'blur' }],
+ accessSecret: [{ required: true, message: 'accessSecret 涓嶈兘涓虹┖', trigger: 'blur' }],
+ enablePathStyleAccess: [
+ { required: true, message: '鏄惁 PathStyle 璁块棶涓嶈兘涓虹┖', trigger: 'change' }
+ ],
+ enablePublicAccess: [{ required: true, message: '鍏紑璁块棶璁剧疆涓嶈兘涓虹┖', trigger: 'change' }],
+ domain: [{ required: true, message: '鑷畾涔夊煙鍚嶄笉鑳戒负绌�', trigger: 'blur' }]
+ } as FormRules
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await FileConfigApi.getFileConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as FileConfigApi.FileConfigVO
+ if (formType.value === 'create') {
+ await FileConfigApi.createFileConfig(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await FileConfigApi.updateFileConfig(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ storage: undefined!,
+ remark: '',
+ config: {} as FileConfigApi.FileClientConfig
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/fileConfig/index.vue b/src/views/infra/fileConfig/index.vue
new file mode 100644
index 0000000..ab99790
--- /dev/null
+++ b/src/views/infra/fileConfig/index.vue
@@ -0,0 +1,247 @@
+<template>
+ <doc-alert title="涓婁紶涓嬭浇" url="https://doc.iocoder.cn/file/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閰嶇疆鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ラ厤缃悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瀛樺偍鍣�" prop="storage">
+ <el-select
+ v-model="queryParams.storage"
+ placeholder="璇烽�夋嫨瀛樺偍鍣�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:file-config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:file-config:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="閰嶇疆鍚�" align="center" prop="name" />
+ <el-table-column label="瀛樺偍鍣�" align="center" prop="storage">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="scope.row.storage" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="涓婚厤缃�" align="center" prop="primary">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.master" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="240px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:file-config:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ :disabled="scope.row.master"
+ @click="handleMaster(scope.row.id)"
+ v-hasPermi="['infra:file-config:update']"
+ >
+ 涓婚厤缃�
+ </el-button>
+ <el-button link type="primary" @click="handleTest(scope.row.id)"> 娴嬭瘯 </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:file-config:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <FileConfigForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import * as FileConfigApi from '@/api/infra/fileConfig'
+import FileConfigForm from './FileConfigForm.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'InfraFileConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ storage: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FileConfigApi.getFileConfigPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await FileConfigApi.deleteFileConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await FileConfigApi.deleteFileConfigList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 涓婚厤缃寜閽搷浣� */
+const handleMaster = async (id) => {
+ try {
+ await message.confirm('鏄惁纭淇敼閰嶇疆缂栧彿涓�"' + id + '"鐨勬暟鎹」涓轰富閰嶇疆?')
+ await FileConfigApi.updateFileConfigMaster(id)
+ message.success(t('common.updateSuccess'))
+ await getList()
+ } catch {}
+}
+
+/** 娴嬭瘯鎸夐挳鎿嶄綔 */
+const handleTest = async (id) => {
+ try {
+ const response = await FileConfigApi.testFileConfig(id)
+ await message.confirm('鏄惁瑕佽闂鏂囦欢锛�', '娴嬭瘯涓婁紶鎴愬姛')
+ window.open(response, '_blank')
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/job/JobDetail.vue b/src/views/infra/job/JobDetail.vue
new file mode 100644
index 0000000..584c89b
--- /dev/null
+++ b/src/views/infra/job/JobDetail.vue
@@ -0,0 +1,73 @@
+<template>
+ <Dialog v-model="dialogVisible" title="浠诲姟璇︾粏" width="700px">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="浠诲姟缂栧彿" min-width="60">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鍚嶇О">
+ {{ detailData.name }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鐘舵��">
+ <dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="detailData.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label="澶勭悊鍣ㄧ殑鍚嶅瓧">
+ {{ detailData.handlerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶勭悊鍣ㄧ殑鍙傛暟">
+ {{ detailData.handlerParam }}
+ </el-descriptions-item>
+ <el-descriptions-item label="Cron 琛ㄨ揪寮�">
+ {{ detailData.cronExpression }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閲嶈瘯娆℃暟">
+ {{ detailData.retryCount }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閲嶈瘯闂撮殧">
+ {{ detailData.retryInterval + ' 姣' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐩戞帶瓒呮椂鏃堕棿">
+ {{ detailData.monitorTimeout > 0 ? detailData.monitorTimeout + ' 姣' : '鏈紑鍚�' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍚庣画鎵ц鏃堕棿">
+ <el-timeline>
+ <el-timeline-item
+ v-for="(nextTime, index) in nextTimes"
+ :key="index"
+ :timestamp="formatDate(nextTime)"
+ >
+ 绗� {{ index + 1 }} 娆�
+ </el-timeline-item>
+ </el-timeline>
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as JobApi from '@/api/infra/job'
+
+defineOptions({ name: 'InfraJobDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as JobApi.JobVO) // 璇︽儏鏁版嵁
+const nextTimes = ref([]) // 涓嬩竴杞墽琛屾椂闂寸殑鏁扮粍
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 鏌ョ湅锛岃缃暟鎹�
+ if (id) {
+ detailLoading.value = true
+ try {
+ detailData.value = await JobApi.getJob(id)
+ // 鑾峰彇涓嬩竴娆℃墽琛屾椂闂�
+ nextTimes.value = await JobApi.getJobNextTimes(id)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/infra/job/JobForm.vue b/src/views/infra/job/JobForm.vue
new file mode 100644
index 0000000..79f85e7
--- /dev/null
+++ b/src/views/infra/job/JobForm.vue
@@ -0,0 +1,137 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="浠诲姟鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヤ换鍔″悕绉�" />
+ </el-form-item>
+ <el-form-item label="澶勭悊鍣ㄧ殑鍚嶅瓧" prop="handlerName">
+ <el-input
+ :readonly="formData.id !== undefined"
+ v-model="formData.handlerName"
+ placeholder="璇疯緭鍏ュ鐞嗗櫒鐨勫悕瀛�"
+ />
+ </el-form-item>
+ <el-form-item label="澶勭悊鍣ㄧ殑鍙傛暟" prop="handlerParam">
+ <el-input v-model="formData.handlerParam" placeholder="璇疯緭鍏ュ鐞嗗櫒鐨勫弬鏁�" />
+ </el-form-item>
+ <el-form-item label="CRON 琛ㄨ揪寮�" prop="cronExpression">
+ <crontab v-model="formData.cronExpression" />
+ </el-form-item>
+ <el-form-item label="閲嶈瘯娆℃暟" prop="retryCount">
+ <el-input
+ v-model="formData.retryCount"
+ placeholder="璇疯緭鍏ラ噸璇曟鏁般�傝缃负 0 鏃讹紝涓嶈繘琛岄噸璇�"
+ />
+ </el-form-item>
+ <el-form-item label="閲嶈瘯闂撮殧" prop="retryInterval">
+ <el-input
+ v-model="formData.retryInterval"
+ placeholder="璇疯緭鍏ラ噸璇曢棿闅旓紝鍗曚綅锛氭绉掋�傝缃负 0 鏃讹紝鏃犻渶闂撮殧"
+ />
+ </el-form-item>
+ <el-form-item label="鐩戞帶瓒呮椂鏃堕棿" prop="monitorTimeout">
+ <el-input v-model="formData.monitorTimeout" placeholder="璇疯緭鍏ョ洃鎺ц秴鏃舵椂闂达紝鍗曚綅锛氭绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" @click="submitForm" :loading="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as JobApi from '@/api/infra/job'
+
+defineOptions({ name: 'JobForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ handlerName: '',
+ handlerParam: '',
+ cronExpression: '',
+ retryCount: undefined,
+ retryInterval: undefined,
+ monitorTimeout: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '浠诲姟鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ handlerName: [{ required: true, message: '澶勭悊鍣ㄧ殑鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ cronExpression: [{ required: true, message: 'CRON 琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }],
+ retryCount: [{ required: true, message: '閲嶈瘯娆℃暟涓嶈兘涓虹┖', trigger: 'blur' }],
+ retryInterval: [{ required: true, message: '閲嶈瘯闂撮殧涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await JobApi.getJob(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦鎸夐挳 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as JobApi.JobVO
+ if (formType.value === 'create') {
+ await JobApi.createJob(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await JobApi.updateJob(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ handlerName: '',
+ handlerParam: '',
+ cronExpression: '',
+ retryCount: undefined,
+ retryInterval: undefined,
+ monitorTimeout: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/infra/job/index.vue b/src/views/infra/job/index.vue
new file mode 100644
index 0000000..2090b9d
--- /dev/null
+++ b/src/views/infra/job/index.vue
@@ -0,0 +1,332 @@
+<template>
+ <doc-alert title="瀹氭椂浠诲姟" url="https://doc.iocoder.cn/job/" />
+ <doc-alert title="寮傛浠诲姟" url="https://doc.iocoder.cn/async-task/" />
+ <doc-alert title="娑堟伅闃熷垪" url="https://doc.iocoder.cn/message-queue/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="浠诲姟鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浠诲姟鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨浠诲姟鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_JOB_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶勭悊鍣ㄧ殑鍚嶅瓧" prop="handlerName">
+ <el-input
+ v-model="queryParams.handlerName"
+ placeholder="璇疯緭鍏ュ鐞嗗櫒鐨勫悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['infra:job:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['infra:job:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:job:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button type="info" plain @click="handleJobLog()" v-hasPermi="['infra:job:query']">
+ <Icon icon="ep:zoom-in" class="mr-5px" /> 鎵ц鏃ュ織
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="浠诲姟缂栧彿" align="center" prop="id" />
+ <el-table-column label="浠诲姟鍚嶇О" align="center" prop="name" />
+ <el-table-column label="浠诲姟鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶勭悊鍣ㄧ殑鍚嶅瓧" align="center" prop="handlerName" />
+ <el-table-column label="澶勭悊鍣ㄧ殑鍙傛暟" align="center" prop="handlerParam" />
+ <el-table-column label="CRON 琛ㄨ揪寮�" align="center" prop="cronExpression" />
+ <el-table-column label="鎿嶄綔" align="center" width="200">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['infra:job:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ type="primary"
+ link
+ @click="handleChangeStatus(scope.row)"
+ v-hasPermi="['infra:job:update']"
+ >
+ {{ scope.row.status === InfraJobStatusEnum.STOP ? '寮�鍚�' : '鏆傚仠' }}
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['infra:job:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ <el-dropdown
+ @command="(command) => handleCommand(command, scope.row)"
+ v-hasPermi="['infra:job:trigger', 'infra:job:query']"
+ >
+ <el-button type="primary" link><Icon icon="ep:d-arrow-right" /> 鏇村</el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="handleRun" v-if="checkPermi(['infra:job:trigger'])">
+ 鎵ц涓�娆�
+ </el-dropdown-item>
+ <el-dropdown-item command="openDetail" v-if="checkPermi(['infra:job:query'])">
+ 浠诲姟璇︾粏
+ </el-dropdown-item>
+ <el-dropdown-item command="handleJobLog" v-if="checkPermi(['infra:job:query'])">
+ 璋冨害鏃ュ織
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <JobForm ref="formRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氭煡鐪� -->
+ <JobDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { checkPermi } from '@/utils/permission'
+import JobForm from './JobForm.vue'
+import JobDetail from './JobDetail.vue'
+import download from '@/utils/download'
+import * as JobApi from '@/api/infra/job'
+import { InfraJobStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'InfraJob' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const { push } = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ handlerName: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await JobApi.getJobPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await JobApi.exportJob(queryParams)
+ download.excel(data, '瀹氭椂浠诲姟.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 淇敼鐘舵�佹搷浣� */
+const handleChangeStatus = async (row: JobApi.JobVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.status === InfraJobStatusEnum.STOP ? '寮�鍚�' : '鍏抽棴'
+ await message.confirm(
+ '纭瑕�' + text + '瀹氭椂浠诲姟缂栧彿涓�"' + row.id + '"鐨勬暟鎹」?',
+ t('common.reminder')
+ )
+ const status =
+ row.status === InfraJobStatusEnum.STOP ? InfraJobStatusEnum.NORMAL : InfraJobStatusEnum.STOP
+ await JobApi.updateJobStatus(row.id, status)
+ message.success(text + '鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await JobApi.deleteJob(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: JobApi.JobVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await JobApi.deleteJobList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** '鏇村'鎿嶄綔鎸夐挳 */
+const handleCommand = (command, row) => {
+ switch (command) {
+ case 'handleRun':
+ handleRun(row)
+ break
+ case 'openDetail':
+ openDetail(row.id)
+ break
+ case 'handleJobLog':
+ handleJobLog(row?.id)
+ break
+ default:
+ break
+ }
+}
+
+/** 鎵ц涓�娆� */
+const handleRun = async (row: JobApi.JobVO) => {
+ try {
+ // 浜屾纭
+ await message.confirm('纭瑕佺珛鍗虫墽琛屼竴娆�' + row.name + '?', t('common.reminder'))
+ // 鎻愪氦鎵ц
+ await JobApi.runJob(row.id)
+ message.success('鎵ц鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鏌ョ湅鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+ detailRef.value.open(id)
+}
+
+/** 璺宠浆鎵ц鏃ュ織 */
+const handleJobLog = (id?: number) => {
+ if (id && id > 0) {
+ push('/job/job-log?id=' + id)
+ } else {
+ push('/job/job-log')
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/job/logger/JobLogDetail.vue b/src/views/infra/job/logger/JobLogDetail.vue
new file mode 100644
index 0000000..7216f52
--- /dev/null
+++ b/src/views/infra/job/logger/JobLogDetail.vue
@@ -0,0 +1,59 @@
+<template>
+ <Dialog v-model="dialogVisible" title="浠诲姟璇︾粏" width="700px">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織缂栧彿" min-width="60">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠诲姟缂栧彿">
+ {{ detailData.jobId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶勭悊鍣ㄧ殑鍚嶅瓧">
+ {{ detailData.handlerName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶勭悊鍣ㄧ殑鍙傛暟">
+ {{ detailData.handlerParam }}
+ </el-descriptions-item>
+ <el-descriptions-item label="绗嚑娆℃墽琛�">
+ {{ detailData.executeIndex }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵ц鏃堕棿">
+ {{ formatDate(detailData.beginTime) + ' ~ ' + formatDate(detailData.endTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵ц鏃堕暱">
+ {{ detailData.duration + ' 姣' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鐘舵��">
+ <dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="detailData.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵ц缁撴灉">
+ {{ detailData.result }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as JobLogApi from '@/api/infra/jobLog'
+
+defineOptions({ name: 'JobLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as JobLogApi.JobLogVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 鏌ョ湅锛岃缃暟鎹�
+ if (id) {
+ detailLoading.value = true
+ try {
+ detailData.value = await JobLogApi.getJobLog(id)
+ } finally {
+ detailLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/infra/job/logger/index.vue b/src/views/infra/job/logger/index.vue
new file mode 100644
index 0000000..3d2be80
--- /dev/null
+++ b/src/views/infra/job/logger/index.vue
@@ -0,0 +1,200 @@
+<template>
+ <doc-alert title="瀹氭椂浠诲姟" url="https://doc.iocoder.cn/job/" />
+ <doc-alert title="寮傛浠诲姟" url="https://doc.iocoder.cn/async-task/" />
+ <doc-alert title="娑堟伅闃熷垪" url="https://doc.iocoder.cn/message-queue/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="120px"
+ >
+ <el-form-item label="澶勭悊鍣ㄧ殑鍚嶅瓧" prop="handlerName">
+ <el-input
+ v-model="queryParams.handlerName"
+ placeholder="璇疯緭鍏ュ鐞嗗櫒鐨勫悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="寮�濮嬫墽琛屾椂闂�" prop="beginTime">
+ <el-date-picker
+ v-model="queryParams.beginTime"
+ type="date"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ placeholder="閫夋嫨寮�濮嬫墽琛屾椂闂�"
+ clearable
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴潫鎵ц鏃堕棿" prop="endTime">
+ <el-date-picker
+ v-model="queryParams.endTime"
+ type="date"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ placeholder="閫夋嫨缁撴潫鎵ц鏃堕棿"
+ clearable
+ :default-time="new Date('1 23:59:59')"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浠诲姟鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨浠诲姟鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_JOB_LOG_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['infra:job:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="id" />
+ <el-table-column label="浠诲姟缂栧彿" align="center" prop="jobId" />
+ <el-table-column label="澶勭悊鍣ㄧ殑鍚嶅瓧" align="center" prop="handlerName" />
+ <el-table-column label="澶勭悊鍣ㄧ殑鍙傛暟" align="center" prop="handlerParam" />
+ <el-table-column label="绗嚑娆℃墽琛�" align="center" prop="executeIndex" />
+ <el-table-column label="鎵ц鏃堕棿" align="center" width="170s">
+ <template #default="scope">
+ <span>{{ formatDate(scope.row.beginTime) + ' ~ ' + formatDate(scope.row.endTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵ц鏃堕暱" align="center" prop="duration">
+ <template #default="scope">
+ <span>{{ scope.row.duration + ' 姣' }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="浠诲姟鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="openDetail(scope.row.id)"
+ v-hasPermi="['infra:job:query']"
+ >
+ 璇︾粏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭煡鐪� -->
+ <JobLogDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import download from '@/utils/download'
+import JobLogDetail from './JobLogDetail.vue'
+import * as JobLogApi from '@/api/infra/jobLog'
+
+defineOptions({ name: 'InfraJobLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { query } = useRoute() // 鏌ヨ鍙傛暟
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ jobId: query.id,
+ handlerName: undefined,
+ beginTime: undefined,
+ endTime: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await JobLogApi.getJobLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鏌ョ湅鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (rowId?: number) => {
+ detailRef.value.open(rowId)
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await JobLogApi.exportJobLog(queryParams)
+ download.excel(data, '瀹氭椂浠诲姟鎵ц鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/infra/redis/index.vue b/src/views/infra/redis/index.vue
new file mode 100644
index 0000000..a897b7c
--- /dev/null
+++ b/src/views/infra/redis/index.vue
@@ -0,0 +1,269 @@
+<template>
+ <doc-alert title="Redis 缂撳瓨" url="https://doc.iocoder.cn/redis-cache/" />
+ <doc-alert title="鏈湴缂撳瓨" url="https://doc.iocoder.cn/local-cache/" />
+
+ <el-scrollbar height="calc(100vh - 88px - 40px - 50px)">
+ <el-row>
+ <!-- 鍩烘湰淇℃伅 -->
+ <el-col :span="24" class="card-box" shadow="hover">
+ <el-card>
+ <el-descriptions title="鍩烘湰淇℃伅" :column="6" border>
+ <el-descriptions-item label="Redis鐗堟湰 :">
+ {{ cache?.info?.redis_version }}
+ </el-descriptions-item>
+ <el-descriptions-item label="杩愯妯″紡 :">
+ {{ cache?.info?.redis_mode == 'standalone' ? '鍗曟満' : '闆嗙兢' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="绔彛 :">
+ {{ cache?.info?.tcp_port }}
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛绔暟 :">
+ {{ cache?.info?.connected_clients }}
+ </el-descriptions-item>
+ <el-descriptions-item label="杩愯鏃堕棿(澶�) :">
+ {{ cache?.info?.uptime_in_days }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浣跨敤鍐呭瓨 :">
+ {{ cache?.info?.used_memory_human }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浣跨敤CPU :">
+ {{ cache?.info ? parseFloat(cache?.info?.used_cpu_user_children).toFixed(2) : '' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍐呭瓨閰嶇疆 :">
+ {{ cache?.info?.maxmemory_human }}
+ </el-descriptions-item>
+ <el-descriptions-item label="AOF鏄惁寮�鍚� :">
+ {{ cache?.info?.aof_enabled == '0' ? '鍚�' : '鏄�' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="RDB鏄惁鎴愬姛 :">
+ {{ cache?.info?.rdb_last_bgsave_status }}
+ </el-descriptions-item>
+ <el-descriptions-item label="Key鏁伴噺 :">
+ {{ cache?.dbSize }}
+ </el-descriptions-item>
+ <el-descriptions-item label="缃戠粶鍏ュ彛/鍑哄彛 :">
+ {{ cache?.info?.instantaneous_input_kbps }}kps/
+ {{ cache?.info?.instantaneous_output_kbps }}kps
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+ </el-col>
+ <!-- 鍛戒护缁熻 -->
+ <el-col :span="12" class="mt-3">
+ <el-card :gutter="12" shadow="hover">
+ <Echart :options="commandStatsRefChika" :height="420" />
+ </el-card>
+ </el-col>
+ <!-- 鍐呭瓨浣跨敤閲忕粺璁� -->
+ <el-col :span="12" class="mt-3">
+ <el-card class="ml-3" :gutter="12" shadow="hover">
+ <Echart :options="usedmemoryEchartChika" :height="420" />
+ </el-card>
+ </el-col>
+ </el-row>
+ </el-scrollbar>
+</template>
+<script lang="ts" setup>
+import * as RedisApi from '@/api/infra/redis'
+import { RedisMonitorInfoVO } from '@/api/infra/redis/types'
+const cache = ref<RedisMonitorInfoVO>()
+
+// 鍩烘湰淇℃伅
+const readRedisInfo = async () => {
+ const data = await RedisApi.getCache()
+ cache.value = data
+}
+
+// 鍐呭瓨浣跨敤鎯呭喌
+const usedmemoryEchartChika = reactive<any>({
+ title: {
+ // 浠〃鐩樻爣棰樸��
+ text: '鍐呭瓨浣跨敤鎯呭喌',
+ left: 'center',
+ show: true, // 鏄惁鏄剧ず鏍囬,榛樿 true銆�
+ offsetCenter: [0, '20%'], //鐩稿浜庝华琛ㄧ洏涓績鐨勫亸绉讳綅缃紝鏁扮粍绗竴椤规槸姘村钩鏂瑰悜鐨勫亸绉伙紝绗簩椤规槸鍨傜洿鏂瑰悜鐨勫亸绉汇�傚彲浠ユ槸缁濆鐨勬暟鍊硷紝涔熷彲浠ユ槸鐩稿浜庝华琛ㄧ洏鍗婂緞鐨勭櫨鍒嗘瘮銆�
+ color: 'yellow', // 鏂囧瓧鐨勯鑹�,榛樿 #333銆�
+ fontSize: 20 // 鏂囧瓧鐨勫瓧浣撳ぇ灏�,榛樿 15銆�
+ },
+ toolbox: {
+ show: false,
+ feature: {
+ restore: { show: true },
+ saveAsImage: { show: true }
+ }
+ },
+ series: [
+ {
+ name: '宄板��',
+ type: 'gauge',
+ min: 0,
+ max: 50,
+ splitNumber: 10,
+ //杩欐槸鎸囬拡鐨勯鑹�
+ color: '#F5C74E',
+ radius: '85%',
+ center: ['50%', '50%'],
+ startAngle: 225,
+ endAngle: -45,
+ axisLine: {
+ // 鍧愭爣杞寸嚎
+ lineStyle: {
+ // 灞炴�ineStyle鎺у埗绾挎潯鏍峰紡
+ color: [
+ [0.2, '#7FFF00'],
+ [0.8, '#00FFFF'],
+ [1, '#FF0000']
+ ],
+ //width: 6 澶栨鐨勫ぇ灏忥紙鐜殑瀹藉害锛�
+ width: 10
+ }
+ },
+ axisTick: {
+ // 鍧愭爣杞村皬鏍囪
+ //閲岄潰鐨勭嚎闀挎槸5锛堢煭绾匡級
+ length: 5, // 灞炴�ength鎺у埗绾块暱
+ lineStyle: {
+ // 灞炴�ineStyle鎺у埗绾挎潯鏍峰紡
+ color: '#76D9D7'
+ }
+ },
+ splitLine: {
+ // 鍒嗛殧绾�
+ length: 20, // 灞炴�ength鎺у埗绾块暱
+ lineStyle: {
+ // 灞炴�ineStyle锛堣瑙乴ineStyle锛夋帶鍒剁嚎鏉℃牱寮�
+ color: '#76D9D7'
+ }
+ },
+ axisLabel: {
+ color: '#76D9D7',
+ distance: 15,
+ fontSize: 15
+ },
+ pointer: {
+ // 鎸囬拡鐨勫ぇ灏�
+ width: 7,
+ show: true
+ },
+ detail: {
+ textStyle: {
+ fontWeight: 'normal',
+ // 閲岄潰鏂囧瓧涓嬬殑鏁板�煎ぇ灏忥紙50锛�
+ fontSize: 15,
+ color: '#FFFFFF'
+ },
+ valueAnimation: true
+ },
+ progress: {
+ show: true
+ }
+ }
+ ]
+})
+
+// 鎸囦护浣跨敤鎯呭喌
+const commandStatsRefChika = reactive({
+ title: {
+ text: '鍛戒护缁熻',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c} ({d}%)'
+ },
+ legend: {
+ type: 'scroll',
+ orient: 'vertical',
+ right: 30,
+ top: 10,
+ bottom: 20,
+ data: [] as any[],
+ textStyle: {
+ color: '#a1a1a1'
+ }
+ },
+ series: [
+ {
+ name: '鍛戒护',
+ type: 'pie',
+ radius: [20, 120],
+ center: ['40%', '60%'],
+ data: [] as any[],
+ roseType: 'radius',
+ label: {
+ show: true
+ },
+ emphasis: {
+ label: {
+ show: true
+ },
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+ ]
+})
+
+/** 鍔犺浇鏁版嵁 */
+const getSummary = () => {
+ // 鍒濆鍖栧懡浠ゅ浘琛�
+ initCommandStatsChart()
+ usedMemoryInstance()
+}
+
+/** 鍛戒护浣跨敤鎯呭喌 */
+const initCommandStatsChart = async () => {
+ usedmemoryEchartChika.series[0].data = []
+ // 鍙戣捣璇锋眰
+ try {
+ const data = await RedisApi.getCache()
+ cache.value = data
+ // 澶勭悊鏁版嵁
+ const commandStats = [] as any[]
+ const nameList = [] as string[]
+ data.commandStats.forEach((row) => {
+ commandStats.push({
+ name: row.command,
+ value: row.calls
+ })
+ nameList.push(row.command)
+ })
+ commandStatsRefChika.legend.data = nameList
+ commandStatsRefChika.series[0].data = commandStats
+ } catch {}
+}
+const usedMemoryInstance = async () => {
+ try {
+ const data = await RedisApi.getCache()
+ cache.value = data
+ // 浠〃鐩樿鎯咃紝鐢ㄤ簬鏄剧ず鏁版嵁銆�
+ usedmemoryEchartChika.series[0].detail = {
+ show: true, // 鏄惁鏄剧ず璇︽儏,榛樿 true銆�
+ offsetCenter: [0, '50%'], // 鐩稿浜庝华琛ㄧ洏涓績鐨勫亸绉讳綅缃紝鏁扮粍绗竴椤规槸姘村钩鏂瑰悜鐨勫亸绉伙紝绗簩椤规槸鍨傜洿鏂瑰悜鐨勫亸绉汇�傚彲浠ユ槸缁濆鐨勬暟鍊硷紝涔熷彲浠ユ槸鐩稿浜庝华琛ㄧ洏鍗婂緞鐨勭櫨鍒嗘瘮銆�
+ color: 'auto', // 鏂囧瓧鐨勯鑹�,榛樿 auto銆�
+ fontSize: 30, // 鏂囧瓧鐨勫瓧浣撳ぇ灏�,榛樿 15銆�
+ formatter: cache.value!.info.used_memory_human // 鏍煎紡鍖栧嚱鏁版垨鑰呭瓧绗︿覆
+ }
+
+ usedmemoryEchartChika.series[0].data[0] = {
+ value: cache.value!.info.used_memory_human,
+ name: '鍐呭瓨娑堣��'
+ }
+ console.log(cache.value!.info)
+ usedmemoryEchartChika.tooltip = {
+ formatter: '{b} <br/>{a} : ' + cache.value!.info.used_memory_human
+ }
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ // 璇诲彇 redis 淇℃伅
+ readRedisInfo()
+ // 鍔犺浇鏁版嵁
+ getSummary()
+})
+</script>
diff --git a/src/views/infra/server/index.vue b/src/views/infra/server/index.vue
new file mode 100644
index 0000000..6dcdec6
--- /dev/null
+++ b/src/views/infra/server/index.vue
@@ -0,0 +1,30 @@
+<template>
+ <doc-alert title="鏈嶅姟鐩戞帶" url="https://doc.iocoder.cn/server-monitor/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame v-if="!loading" v-loading="loading" :src="src" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/infra/config'
+
+defineOptions({ name: 'InfraAdminServer' })
+
+const loading = ref(true) // 鏄惁鍔犺浇涓�
+const src = ref(import.meta.env.VITE_BASE_URL + '/admin/applications')
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ try {
+ // 鍙嬫儏鎻愮ず锛氬鏋滆闂嚭鐜� 404 闂锛�
+ // 1锛塨oot 鍙傝�� https://doc.iocoder.cn/server-monitor/ 瑙e喅锛�
+ // 2锛塩loud 鍙傝�� https://cloud.iocoder.cn/server-monitor/ 瑙e喅
+ const data = await ConfigApi.getConfigKey('url.spring-boot-admin')
+ if (data && data.length > 0) {
+ src.value = data
+ }
+ } finally {
+ loading.value = false
+ }
+})
+</script>
diff --git a/src/views/infra/skywalking/index.vue b/src/views/infra/skywalking/index.vue
new file mode 100644
index 0000000..f5055d7
--- /dev/null
+++ b/src/views/infra/skywalking/index.vue
@@ -0,0 +1,27 @@
+<template>
+ <doc-alert title="鏈嶅姟鐩戞帶" url="https://doc.iocoder.cn/server-monitor/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame v-if="!loading" v-loading="loading" :src="src" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/infra/config'
+
+defineOptions({ name: 'InfraSkyWalking' })
+
+const loading = ref(true) // 鏄惁鍔犺浇涓�
+const src = ref('http://skywalking.shop.iocoder.cn')
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ try {
+ const data = await ConfigApi.getConfigKey('url.skywalking')
+ if (data && data.length > 0) {
+ src.value = data
+ }
+ } finally {
+ loading.value = false
+ }
+})
+</script>
diff --git a/src/views/infra/swagger/index.vue b/src/views/infra/swagger/index.vue
new file mode 100644
index 0000000..7ed35f8
--- /dev/null
+++ b/src/views/infra/swagger/index.vue
@@ -0,0 +1,28 @@
+<template>
+ <doc-alert title="鎺ュ彛鏂囨。" url="https://doc.iocoder.cn/api-doc/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame v-if="!loading" v-loading="loading" :src="src" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/infra/config'
+
+defineOptions({ name: 'InfraSwagger' })
+
+const loading = ref(true) // 鏄惁鍔犺浇涓�
+const src = ref(import.meta.env.VITE_BASE_URL + '/doc.html') // Knife4j UI
+// const src = ref(import.meta.env.VITE_BASE_URL + '/swagger-ui') // Swagger UI
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ try {
+ const data = await ConfigApi.getConfigKey('url.swagger')
+ if (data && data.length > 0) {
+ src.value = data
+ }
+ } finally {
+ loading.value = false
+ }
+})
+</script>
diff --git a/src/views/infra/webSocket/index.vue b/src/views/infra/webSocket/index.vue
new file mode 100644
index 0000000..37f1322
--- /dev/null
+++ b/src/views/infra/webSocket/index.vue
@@ -0,0 +1,185 @@
+<template>
+ <doc-alert title="WebSocket 瀹炴椂閫氫俊" url="https://doc.iocoder.cn/websocket/" />
+
+ <div class="flex">
+ <!-- 宸︿晶锛氬缓绔嬭繛鎺ャ�佸彂閫佹秷鎭� -->
+ <el-card :gutter="12" class="w-1/2" shadow="always">
+ <template #header>
+ <div class="card-header">
+ <span>杩炴帴</span>
+ </div>
+ </template>
+ <div class="flex items-center">
+ <span class="mr-4 text-lg font-medium"> 杩炴帴鐘舵��: </span>
+ <el-tag :color="getTagColor">{{ status }}</el-tag>
+ </div>
+ <hr class="my-4" />
+ <div class="flex">
+ <el-input v-model="server" disabled>
+ <template #prepend>鏈嶅姟鍦板潃</template>
+ </el-input>
+ <el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggleConnectStatus">
+ {{ getIsOpen ? '鍏抽棴杩炴帴' : '寮�鍚繛鎺�' }}
+ </el-button>
+ </div>
+ <p class="mt-4 text-lg font-medium">娑堟伅杈撳叆妗�</p>
+ <hr class="my-4" />
+ <el-input
+ v-model="sendText"
+ :autosize="{ minRows: 2, maxRows: 4 }"
+ :disabled="!getIsOpen"
+ clearable
+ placeholder="璇疯緭鍏ヤ綘瑕佸彂閫佺殑娑堟伅"
+ type="textarea"
+ />
+ <el-select v-model="sendUserId" class="mt-4" placeholder="璇烽�夋嫨鍙戦�佷汉">
+ <el-option key="" label="鎵�鏈変汉" value="" />
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ <el-button :disabled="!getIsOpen" block class="ml-2 mt-4" type="primary" @click="handlerSend">
+ 鍙戦��
+ </el-button>
+ </el-card>
+ <!-- 鍙充晶锛氭秷鎭褰� -->
+ <el-card :gutter="12" class="w-1/2" shadow="always">
+ <template #header>
+ <div class="card-header">
+ <span>娑堟伅璁板綍</span>
+ </div>
+ </template>
+ <div class="max-h-80 overflow-auto">
+ <ul>
+ <li v-for="msg in messageReverseList" :key="msg.time" class="mt-2">
+ <div class="flex items-center">
+ <span class="text-primary mr-2 font-medium">鏀跺埌娑堟伅:</span>
+ <span>{{ formatDate(msg.time) }}</span>
+ </div>
+ <div>
+ {{ msg.text }}
+ </div>
+ </li>
+ </ul>
+ </div>
+ </el-card>
+ </div>
+</template>
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import { useWebSocket } from '@vueuse/core'
+import { getRefreshToken } from '@/utils/auth'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'InfraWebSocket' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const server = ref(
+ (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
+ '?token=' +
+ getRefreshToken() // 浣跨敤 getRefreshToken() 鏂规硶锛岃�屼笉浣跨敤 getAccessToken() 鏂规硶鐨勫師鍥狅細WebSocket 鏃犳硶鏂逛究鐨勫埛鏂拌闂护鐗�
+) // WebSocket 鏈嶅姟鍦板潃
+const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 杩炴帴鏄惁鎵撳紑
+const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 杩炴帴鐨勫睍绀洪鑹�
+
+/** 鍙戣捣 WebSocket 杩炴帴 */
+const { status, data, send, close, open } = useWebSocket(server.value, {
+ autoReconnect: true,
+ heartbeat: true
+})
+
+/** 鐩戝惉鎺ユ敹鍒扮殑鏁版嵁 */
+const messageList = ref([] as { time: number; text: string }[]) // 娑堟伅鍒楄〃
+const messageReverseList = computed(() => messageList.value.slice().reverse())
+watchEffect(() => {
+ if (!data.value) {
+ return
+ }
+ try {
+ // 1. 鏀跺埌蹇冭烦
+ if (data.value === 'pong') {
+ // state.recordList.push({
+ // text: '銆愬績璺炽��',
+ // time: new Date().getTime()
+ // })
+ return
+ }
+
+ // 2.1 瑙f瀽 type 娑堟伅绫诲瀷
+ const jsonMessage = JSON.parse(data.value)
+ const type = jsonMessage.type
+ const content = JSON.parse(jsonMessage.content)
+ if (!type) {
+ message.error('鏈煡鐨勬秷鎭被鍨嬶細' + data.value)
+ return
+ }
+ // 2.2 娑堟伅绫诲瀷锛歞emo-message-receive
+ if (type === 'demo-message-receive') {
+ const single = content.single
+ if (single) {
+ messageList.value.push({
+ text: `銆愬崟鍙戙�戠敤鎴风紪鍙�(${content.fromUserId})锛�${content.text}`,
+ time: new Date().getTime()
+ })
+ } else {
+ messageList.value.push({
+ text: `銆愮兢鍙戙�戠敤鎴风紪鍙�(${content.fromUserId})锛�${content.text}`,
+ time: new Date().getTime()
+ })
+ }
+ return
+ }
+ // 2.3 娑堟伅绫诲瀷锛歯otice-push
+ if (type === 'notice-push') {
+ messageList.value.push({
+ text: `銆愮郴缁熼�氱煡銆戯細${content.title}`,
+ time: new Date().getTime()
+ })
+ return
+ }
+ message.error('鏈鐞嗘秷鎭細' + data.value)
+ } catch (error) {
+ message.error('澶勭悊娑堟伅鍙戠敓寮傚父锛�' + data.value)
+ console.error(error)
+ }
+})
+
+/** 鍙戦�佹秷鎭� */
+const sendText = ref('') // 鍙戦�佸唴瀹�
+const sendUserId = ref('') // 鍙戦�佷汉
+const handlerSend = () => {
+ // 1.1 鍏� JSON 鍖� message 娑堟伅鍐呭
+ const messageContent = JSON.stringify({
+ text: sendText.value,
+ toUserId: sendUserId.value
+ })
+ // 1.2 鍐� JSON 鍖栨暣涓秷鎭�
+ const jsonMessage = JSON.stringify({
+ type: 'demo-message-send',
+ content: messageContent
+ })
+ // 2. 鏈�鍚庡彂閫佹秷鎭�
+ send(jsonMessage)
+ sendText.value = ''
+}
+
+/** 鍒囨崲 websocket 杩炴帴鐘舵�� */
+const toggleConnectStatus = () => {
+ if (getIsOpen.value) {
+ close()
+ } else {
+ open()
+ }
+}
+
+/** 鍒濆鍖� **/
+const userList = ref<any[]>([]) // 鐢ㄦ埛鍒楄〃
+onMounted(async () => {
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/iot/alert/config/AlertConfigForm.vue b/src/views/iot/alert/config/AlertConfigForm.vue
new file mode 100644
index 0000000..bece0ff
--- /dev/null
+++ b/src/views/iot/alert/config/AlertConfigForm.vue
@@ -0,0 +1,201 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="閰嶇疆鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ厤缃悕绉�" />
+ </el-form-item>
+ <el-form-item label="閰嶇疆鎻忚堪" prop="description">
+ <el-input v-model="formData.description" placeholder="璇疯緭鍏ラ厤缃弿杩�" />
+ </el-form-item>
+ <el-form-item label="鍛婅绾у埆" prop="level">
+ <el-select v-model="formData.level" placeholder="璇烽�夋嫨鍛婅绾у埆">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_LEVEL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閰嶇疆鐘舵��" prop="status">
+ <el-select v-model="formData.status">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈鍦烘櫙鑱斿姩瑙勫垯" prop="sceneRuleIds">
+ <el-select
+ v-model="formData.sceneRuleIds"
+ multiple
+ placeholder="璇烽�夋嫨鍏宠仈鐨勫満鏅仈鍔ㄨ鍒�"
+ class="w-full"
+ >
+ <el-option
+ v-for="scene in sceneRuleOptions"
+ :key="scene.id"
+ :label="scene.name"
+ :value="scene.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎺ユ敹鐨勭敤鎴�" prop="receiveUserIds">
+ <el-select
+ v-model="formData.receiveUserIds"
+ multiple
+ placeholder="璇烽�夋嫨鎺ユ敹鐨勭敤鎴�"
+ class="w-full"
+ >
+ <el-option
+ v-for="user in userOptions"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎺ユ敹绫诲瀷" prop="receiveTypes">
+ <el-select
+ v-model="formData.receiveTypes"
+ multiple
+ placeholder="璇烽�夋嫨鎺ユ敹绫诲瀷"
+ class="w-full"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_RECEIVE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { RuleSceneApi } from '@/api/iot/rule/scene'
+import * as UserApi from '@/api/system/user'
+
+/** IoT 鍛婅閰嶇疆 琛ㄥ崟 */
+defineOptions({ name: 'AlertConfigForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ level: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sceneRuleIds: [],
+ receiveUserIds: [],
+ receiveTypes: []
+})
+const formRules = reactive({
+ name: [{ required: true, message: '閰嶇疆鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ level: [{ required: true, message: '鍛婅绾у埆涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '閰嶇疆鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ sceneRuleIds: [{ required: true, message: '鍏宠仈鍦烘櫙鑱斿姩瑙勫垯涓嶈兘涓虹┖', trigger: 'blur' }],
+ receiveUserIds: [{ required: true, message: '鎺ユ敹鐢ㄦ埛涓嶈兘涓虹┖', trigger: 'blur' }],
+ receiveTypes: [{ required: true, message: '鎺ユ敹绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// 閫夐」鏁版嵁
+const sceneRuleOptions = ref<any[]>([])
+const userOptions = ref<UserApi.UserVO[]>([])
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await AlertConfigApi.getAlertConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+
+ // 鍔犺浇閫夐」鏁版嵁
+ await loadOptions()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鍔犺浇閫夐」鏁版嵁 */
+const loadOptions = async () => {
+ try {
+ // 鍔犺浇鍦烘櫙鑱斿姩瑙勫垯閫夐」
+ sceneRuleOptions.value = await RuleSceneApi.getSimpleRuleSceneList()
+ // 鍔犺浇鐢ㄦ埛閫夐」
+ userOptions.value = await UserApi.getSimpleUserList()
+ } catch (error) {
+ console.error('鍔犺浇閫夐」鏁版嵁澶辫触:', error)
+ }
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as AlertConfig
+ if (formType.value === 'create') {
+ await AlertConfigApi.createAlertConfig(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await AlertConfigApi.updateAlertConfig(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ level: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sceneRuleIds: [],
+ receiveUserIds: [],
+ receiveTypes: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/alert/config/index.vue b/src/views/iot/alert/config/index.vue
new file mode 100644
index 0000000..03504dd
--- /dev/null
+++ b/src/views/iot/alert/config/index.vue
@@ -0,0 +1,210 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閰嶇疆鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ラ厤缃悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閰嶇疆鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨閰嶇疆鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:alert-config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="閰嶇疆缂栧彿" align="center" prop="id" />
+ <el-table-column label="閰嶇疆鍚嶇О" align="center" prop="name" />
+ <el-table-column label="閰嶇疆鎻忚堪" align="center" prop="description" />
+ <el-table-column label="鍛婅绾у埆" align="center" prop="level">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_ALERT_LEVEL" :value="scope.row.level" />
+ </template>
+ </el-table-column>
+ <el-table-column label="閰嶇疆鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏宠仈鍦烘櫙鑱斿姩瑙勫垯" align="center" prop="sceneRuleIds" min-width="100">
+ <template #default="scope"> {{ scope.row.sceneRuleIds?.length || 0 }} 鏉� </template>
+ </el-table-column>
+ <el-table-column label="鎺ユ敹浜�" align="center" prop="receiveUserNames" />
+ <el-table-column label="鎺ユ敹绫诲瀷" align="center" prop="receiveTypes">
+ <template #default="scope">
+ <dict-tag
+ v-for="(receiveType, index) in scope.row.receiveTypes"
+ :key="index"
+ :type="DICT_TYPE.IOT_ALERT_RECEIVE_TYPE"
+ :value="receiveType"
+ class="mr-1"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['iot:alert-config:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:alert-config:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <AlertConfigForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
+import AlertConfigForm from './AlertConfigForm.vue'
+
+/** IoT 鍛婅閰嶇疆 鍒楄〃 */
+defineOptions({ name: 'IotAlertConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<AlertConfig[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await AlertConfigApi.getAlertConfigPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await AlertConfigApi.deleteAlertConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/alert/record/index.vue b/src/views/iot/alert/record/index.vue
new file mode 100644
index 0000000..e0bef5c
--- /dev/null
+++ b/src/views/iot/alert/record/index.vue
@@ -0,0 +1,296 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍛婅閰嶇疆" prop="configId">
+ <el-select
+ v-model="queryParams.configId"
+ placeholder="璇烽�夋嫨鍛婅閰嶇疆"
+ clearable
+ filterable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="config in alertConfigList"
+ :key="config.id"
+ :label="config.name"
+ :value="config.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍛婅绾у埆" prop="configLevel">
+ <el-select
+ v-model="queryParams.configLevel"
+ placeholder="璇烽�夋嫨鍛婅绾у埆"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_ALERT_LEVEL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ clearable
+ filterable
+ @change="handleProductChange"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="product in productList"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧" prop="deviceId">
+ <el-select
+ v-model="queryParams.deviceId"
+ placeholder="璇烽�夋嫨璁惧"
+ clearable
+ filterable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="device in filteredDeviceList"
+ :key="device.id"
+ :label="device.deviceName"
+ :value="device.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏄惁澶勭悊" prop="processStatus">
+ <el-select
+ v-model="queryParams.processStatus"
+ placeholder="璇烽�夋嫨鏄惁澶勭悊"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="String(dict.value)"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="璁板綍缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍛婅鍚嶇О" align="center" prop="configName" />
+ <el-table-column label="鍛婅绾у埆" align="center" prop="configLevel">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_ALERT_LEVEL" :value="scope.row.configLevel" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О" align="center" prop="productId">
+ <template #default="scope">
+ {{ getProductName(scope.row.productId) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="璁惧鍚嶇О" align="center" prop="deviceId">
+ <template #default="scope">
+ {{ getDeviceName(scope.row.deviceId) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙﹀彂鐨勮澶囨秷鎭�" align="center" prop="deviceMessage">
+ <template #default="scope">
+ <el-popover
+ placement="top-start"
+ :width="600"
+ trigger="hover"
+ v-if="scope.row.deviceMessage"
+ >
+ <template #reference>
+ <el-button link type="primary">
+ <Icon icon="ep:view" class="mr-5px" />
+ 鏌ョ湅娑堟伅
+ </el-button>
+ </template>
+ <pre>{{ scope.row.deviceMessage }}</pre>
+ </el-popover>
+ <span v-else class="text-gray-400">-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁澶勭悊" align="center" prop="processStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.processStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶勭悊缁撴灉" align="center" prop="processRemark" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ v-if="!scope.row.processStatus"
+ link
+ type="primary"
+ @click="handleProcess(scope.row)"
+ v-hasPermi="['iot:alert-record:process']"
+ >
+ 澶勭悊
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { AlertRecordApi, AlertRecord } from '@/api/iot/alert/record'
+import { AlertConfigApi, AlertConfig } from '@/api/iot/alert/config'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { DICT_TYPE, getIntDictOptions, getBoolDictOptions } from '@/utils/dict'
+
+/** IoT 鍛婅璁板綍鍒楄〃 */
+defineOptions({ name: 'IotAlertRecord' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<AlertRecord[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const alertConfigList = ref<AlertConfig[]>([]) // 鍛婅閰嶇疆鍒楄〃
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const deviceList = ref<DeviceVO[]>([]) // 璁惧鍒楄〃
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ configId: undefined as number | undefined,
+ configLevel: undefined as number | undefined,
+ productId: undefined as number | undefined,
+ deviceId: undefined as number | undefined,
+ processStatus: undefined as boolean | undefined,
+ createTime: [] as string[]
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏍规嵁閫夋嫨鐨勪骇鍝� ID锛岀瓫閫夎澶囧垪琛� */
+const filteredDeviceList = computed(() => {
+ if (!queryParams.productId) {
+ return deviceList.value
+ }
+ return deviceList.value.filter((device) => device.productId === queryParams.productId)
+})
+
+/** 鏍规嵁浜у搧 ID 鑾峰彇浜у搧鍚嶇О */
+const getProductName = (productId: number) => {
+ if (!productId) {
+ return `-`
+ }
+ const product = productList.value.find((p) => p.id === productId)
+ return product ? product.name : `鍔犺浇涓�...`
+}
+
+/** 鏍规嵁璁惧 ID 鑾峰彇璁惧鍚嶇О */
+const getDeviceName = (deviceId: number) => {
+ if (!deviceId) {
+ return `-`
+ }
+ const device = deviceList.value.find((d) => d.id === deviceId)
+ return device ? device.deviceName : `鍔犺浇涓�...`
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await AlertRecordApi.getAlertRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 浜у搧鍙樻洿澶勭悊 */
+const handleProductChange = () => {
+ queryParams.deviceId = undefined // 娓呯┖璁惧閫夋嫨
+}
+
+/** 澶勭悊鍛婅璁板綍 */
+const handleProcess = async (row: AlertRecord) => {
+ try {
+ const { value: processRemark } = await ElMessageBox.prompt('璇疯緭鍏ュ鐞嗗師鍥�', '澶勭悊鍛婅璁板綍', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷'
+ })
+ await AlertRecordApi.processAlertRecord(row.id, processRemark)
+ message.success('澶勭悊鎴愬姛')
+ await getList()
+ } catch (error) {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ alertConfigList.value = await AlertConfigApi.getSimpleAlertConfigList()
+ productList.value = await ProductApi.getSimpleProductList()
+ deviceList.value = await DeviceApi.getSimpleDeviceList()
+})
+</script>
diff --git a/src/views/iot/device/device/DeviceForm.vue b/src/views/iot/device/device/DeviceForm.vue
new file mode 100644
index 0000000..dfed0c6
--- /dev/null
+++ b/src/views/iot/device/device/DeviceForm.vue
@@ -0,0 +1,325 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="formData.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ :disabled="formType === 'update'"
+ clearable
+ @change="handleProductChange"
+ >
+ <el-option
+ v-for="product in products"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="DeviceName" prop="deviceName">
+ <el-input
+ v-model="formData.deviceName"
+ placeholder="璇疯緭鍏� DeviceName"
+ :disabled="formType === 'update'"
+ />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.deviceType === DeviceTypeEnum.GATEWAY_SUB"
+ label="缃戝叧璁惧"
+ prop="gatewayId"
+ >
+ <el-select v-model="formData.gatewayId" placeholder="瀛愯澶囧彲閫夋嫨鐖惰澶�" clearable>
+ <el-option
+ v-for="gateway in gatewayDevices"
+ :key="gateway.id"
+ :label="gateway.nickname || gateway.deviceName"
+ :value="gateway.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-collapse>
+ <el-collapse-item title="鏇村閰嶇疆">
+ <el-form-item label="澶囨敞鍚嶇О" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ュ娉ㄥ悕绉�" />
+ </el-form-item>
+ <el-form-item label="璁惧鍥剧墖" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" />
+ </el-form-item>
+ <el-form-item label="璁惧鍒嗙粍" prop="groupIds">
+ <el-select v-model="formData.groupIds" placeholder="璇烽�夋嫨璁惧鍒嗙粍" multiple clearable>
+ <el-option
+ v-for="group in deviceGroups"
+ :key="group.id"
+ :label="group.name"
+ :value="group.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧搴忓垪鍙�" prop="serialNumber">
+ <el-input v-model="formData.serialNumber" placeholder="璇疯緭鍏ヨ澶囧簭鍒楀彿" />
+ </el-form-item>
+ <el-form-item label="瀹氫綅绫诲瀷" prop="locationType">
+ <el-radio-group v-model="formData.locationType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_LOCATION_TYPE)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <!-- LocationTypeEnum.MANUAL锛氭墜鍔ㄥ畾浣� -->
+ <template v-if="LocationTypeEnum.MANUAL === formData.locationType">
+ <el-form-item label="璁惧缁忓害" prop="longitude" type="number">
+ <el-input
+ v-model="formData.longitude"
+ placeholder="璇疯緭鍏ヨ澶囩粡搴�"
+ @blur="updateLocationFromCoordinates"
+ />
+ </el-form-item>
+ <el-form-item label="璁惧缁村害" prop="latitude" type="number">
+ <el-input
+ v-model="formData.latitude"
+ placeholder="璇疯緭鍏ヨ澶囩淮搴�"
+ @blur="updateLocationFromCoordinates"
+ />
+ </el-form-item>
+ <div class="pl-0 h-[400px] w-full ml-[-18px]" v-if="showMap">
+ <Map
+ :isWrite="true"
+ :clickMap="true"
+ :center="formData.location"
+ @locate-change="handleLocationChange"
+ ref="mapRef"
+ class="h-full w-full"
+ />
+ </div>
+ </template>
+ </el-collapse-item>
+ </el-collapse>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { DeviceGroupApi } from '@/api/iot/device/group'
+import { DeviceTypeEnum, LocationTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
+import { UploadImg } from '@/components/UploadFile'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import Map from '@/components/Map/index.vue'
+import { ref } from 'vue'
+
+/** IoT 璁惧琛ㄥ崟 */
+defineOptions({ name: 'IoTDeviceForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅绐�
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const showMap = ref(false) // 鏄惁鏄剧ず鍦板浘缁勪欢
+const mapRef = ref(null)
+
+const formData = ref({
+ id: undefined,
+ productId: undefined,
+ deviceName: undefined,
+ nickname: undefined,
+ picUrl: undefined,
+ gatewayId: undefined,
+ deviceType: undefined as number | undefined,
+ serialNumber: undefined,
+ locationType: undefined as number | undefined,
+ longitude: undefined,
+ latitude: undefined,
+ location: '', // 鏍煎紡: "缁忓害,绾害"
+ groupIds: [] as number[]
+})
+
+/** 鐩戝惉缁忕含搴﹀彉鍖栵紝鏇存柊location */
+watch([() => formData.value.longitude, () => formData.value.latitude], ([newLong, newLat]) => {
+ if (newLong && newLat) {
+ formData.value.location = `${newLong},${newLat}`
+ // 鏈変簡缁忕含搴︽暟鎹悗鏄剧ず鍦板浘
+ showMap.value = true
+ }
+})
+
+const formRules = reactive({
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ deviceName: [
+ { required: true, message: 'DeviceName 涓嶈兘涓虹┖', trigger: 'blur' },
+ {
+ pattern: /^[a-zA-Z0-9_.\-:@]{4,32}$/,
+ message:
+ '鏀寔鑻辨枃瀛楁瘝銆佹暟瀛椼�佷笅鍒掔嚎锛坃锛夈�佷腑鍒掔嚎锛�-锛夈�佺偣鍙凤紙.锛夈�佸崐瑙掑啋鍙凤紙:锛夊拰鐗规畩瀛楃@锛岄暱搴﹂檺鍒朵负 4~32 涓瓧绗�',
+ trigger: 'blur'
+ }
+ ],
+ nickname: [
+ {
+ validator: (_rule, value: any, callback) => {
+ if (value === undefined || value === null) {
+ callback()
+ return
+ }
+ const length = value.replace(/[\u4e00-\u9fa5\u3040-\u30ff]/g, 'aa').length
+ if (length < 4 || length > 64) {
+ callback(new Error('澶囨敞鍚嶇О闀垮害闄愬埗涓� 4~64 涓瓧绗︼紝涓枃鍙婃棩鏂囩畻 2 涓瓧绗�'))
+ } else if (!/^[\u4e00-\u9fa5\u3040-\u30ff_a-zA-Z0-9]+$/.test(value)) {
+ callback(new Error('澶囨敞鍚嶇О鍙兘鍖呭惈涓枃銆佽嫳鏂囧瓧姣嶃�佹棩鏂囥�佹暟瀛楀拰涓嬪垝绾匡紙_锛�'))
+ } else {
+ callback()
+ }
+ },
+ trigger: 'blur'
+ }
+ ],
+ serialNumber: [
+ {
+ pattern: /^[a-zA-Z0-9-_]+$/,
+ message: '搴忓垪鍙峰彧鑳藉寘鍚瓧姣嶃�佹暟瀛椼�佷腑鍒掔嚎鍜屼笅鍒掔嚎',
+ trigger: 'blur'
+ }
+ ]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const products = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const gatewayDevices = ref<DeviceVO[]>([]) // 缃戝叧璁惧鍒楄〃
+const deviceGroups = ref<any[]>([])
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+
+ // 榛樿涓嶆樉绀哄湴鍥撅紝绛夊緟鏁版嵁鍔犺浇
+ showMap.value = false
+
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DeviceApi.getDevice(id)
+
+ // 濡傛灉鏈夌粡绾害锛岃缃� location 瀛楁鐢ㄤ簬鍦板浘鏄剧ず
+ if (formData.value.longitude && formData.value.latitude) {
+ formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
+ }
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 濡傛灉鏈夌粡绾俊鎭紝鍒欐暟鎹姞杞藉畬鎴愬悗锛屾樉绀哄湴鍥�
+ showMap.value = true
+
+ // 鍔犺浇缃戝叧璁惧鍒楄〃
+ gatewayDevices.value = await DeviceApi.getSimpleDeviceList(DeviceTypeEnum.GATEWAY)
+ // 鍔犺浇浜у搧鍒楄〃
+ products.value = await ProductApi.getSimpleProductList()
+ // 鍔犺浇璁惧鍒嗙粍鍒楄〃
+ deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as DeviceVO
+ // 濡傛灉闈炴墜鍔ㄥ畾浣嶏紝涓嶈繘琛屾彁浜よ瀛楁
+ if (data.locationType !== LocationTypeEnum.MANUAL) {
+ data.longitude = undefined
+ data.latitude = undefined
+ }
+ // TODO @瀹楄秴锛氥�愯澶囧畾浣嶃�慳ddress 鍜� areaId 涔熻澶勭悊锛�
+ // 1. 鎵嬪姩瀹氫綅鏃讹細longitude + latitude + areaId + address锛氳绋嶅井娉ㄦ剰锛宎ddress 鍙兘瑕佸幓鎺夌渷甯傚尯閮ㄥ垎锛燂紒
+ // 2. IP 瀹氫綅鏃讹細IotDeviceMessage 鐨� buildStateUpdateOnline 鏃讹紝澧炲姞 ip 瀛楁銆傝繖鏍凤紝瑙f瀽鍒� areaId锛涘彟澶栫湅鐪嬭兘涓嶈兘閫氳繃 https://lbsyun.baidu.com/faq/api?title=webapi/ip-api-base锛堝彧鑾峰彇 location 灏� ok 鍟︼級
+ // 3. 璁惧瀹氫綅鏃讹細闂棶 haohao锛屼竴鑸�庝箞鍋氥��
+
+ if (formType.value === 'create') {
+ await DeviceApi.createDevice(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DeviceApi.updateDevice(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ productId: undefined,
+ deviceName: undefined,
+ nickname: undefined,
+ picUrl: undefined,
+ gatewayId: undefined,
+ deviceType: undefined,
+ serialNumber: undefined,
+ locationType: undefined,
+ longitude: undefined,
+ latitude: undefined,
+ // TODO @瀹楄秴锛氥�愯澶囧畾浣嶃�憀ocation 鏄笉鏄嬁鍑烘潵锛屼笉鏀惧湪 formData 閲�
+ location: '',
+ groupIds: []
+ }
+ formRef.value?.resetFields()
+ // 閲嶇疆琛ㄥ崟鏃讹紝闅愯棌鍦板浘
+ showMap.value = false
+}
+
+/** 浜у搧閫夋嫨鍙樺寲 */
+const handleProductChange = (productId: number) => {
+ if (!productId) {
+ formData.value.deviceType = undefined
+ return
+ }
+ const product = products.value?.find((item) => item.id === productId)
+ formData.value.deviceType = product?.deviceType
+ formData.value.locationType = product?.locationType
+}
+
+/** 澶勭悊浣嶇疆鍙樺寲 */
+const handleLocationChange = (lnglat) => {
+ formData.value.longitude = lnglat[0]
+ formData.value.latitude = lnglat[1]
+}
+
+/** 鏍规嵁缁忕含搴︽洿鏂板湴鍥句綅缃� */
+const updateLocationFromCoordinates = () => {
+ // 楠岃瘉缁忕含搴︽槸鍚︽湁鏁�
+ if (formData.value.longitude && formData.value.latitude) {
+ // 鏇存柊 location 瀛楁锛屽湴鍥剧粍浠朵細鏍规嵁姝ゅ瓧娈垫洿鏂�
+ formData.value.location = `${formData.value.longitude},${formData.value.latitude}`
+ mapRef.value.regeoCode(formData.value.location)
+ }
+}
+</script>
diff --git a/src/views/iot/device/device/DeviceGroupForm.vue b/src/views/iot/device/device/DeviceGroupForm.vue
new file mode 100644
index 0000000..322e8a7
--- /dev/null
+++ b/src/views/iot/device/device/DeviceGroupForm.vue
@@ -0,0 +1,90 @@
+<template>
+ <Dialog :title="'娣诲姞璁惧鍒板垎缁�'" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="璁惧鍒嗙粍" prop="groupIds">
+ <el-select v-model="formData.groupIds" placeholder="璇烽�夋嫨璁惧鍒嗙粍" multiple clearable>
+ <el-option
+ v-for="group in deviceGroups"
+ :key="group.id"
+ :label="group.name"
+ :value="group.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { DeviceGroupApi } from '@/api/iot/device/group'
+
+defineOptions({ name: 'IoTDeviceGroupForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅绐�
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref({
+ ids: [] as number[],
+ groupIds: [] as number[]
+})
+const formRules = reactive({
+ groupIds: [{ required: true, message: '璁惧鍒嗙粍涓嶈兘涓虹┖', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const deviceGroups = ref<any[]>([]) // 璁惧鍒嗙粍鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (ids: number[]) => {
+ dialogVisible.value = true
+ resetForm()
+ formData.value.ids = ids
+
+ // 鍔犺浇璁惧鍒嗙粍鍒楄〃
+ try {
+ deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
+ } catch (error) {
+ console.error('鍔犺浇璁惧鍒嗙粍鍒楄〃澶辫触:', error)
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await DeviceApi.updateDeviceGroup(formData.value)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ ids: [],
+ groupIds: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/device/device/DeviceImportForm.vue b/src/views/iot/device/device/DeviceImportForm.vue
new file mode 100644
index 0000000..9594965
--- /dev/null
+++ b/src/views/iot/device/device/DeviceImportForm.vue
@@ -0,0 +1,139 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璁惧瀵煎叆" width="400">
+ <el-upload
+ ref="uploadRef"
+ v-model:file-list="fileList"
+ :action="importUrl + '?updateSupport=' + updateSupport"
+ :auto-upload="false"
+ :disabled="formLoading"
+ :headers="uploadHeaders"
+ :limit="1"
+ :on-error="submitFormError"
+ :on-exceed="handleExceed"
+ :on-success="submitFormSuccess"
+ accept=".xlsx, .xls"
+ drag
+ >
+ <Icon icon="ep:upload" />
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <div class="el-upload__tip">
+ <el-checkbox v-model="updateSupport" />
+ 鏄惁鏇存柊宸茬粡瀛樺湪鐨勮澶囨暟鎹�
+ </div>
+ <span>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆�</span>
+ <el-link
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ type="primary"
+ @click="importTemplate"
+ >
+ 涓嬭浇妯℃澘
+ </el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DeviceApi } from '@/api/iot/device/device'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import download from '@/utils/download'
+
+defineOptions({ name: 'IoTDeviceImportForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const uploadRef = ref()
+const importUrl =
+ import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/iot/device/import'
+const uploadHeaders = ref() // 涓婁紶 Header 澶�
+const fileList = ref([]) // 鏂囦欢鍒楄〃
+const updateSupport = ref(0) // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勮澶囨暟鎹�
+
+/** 鎵撳紑寮圭獥 */
+const open = () => {
+ dialogVisible.value = true
+ updateSupport.value = 0
+ fileList.value = []
+ resetForm()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ if (fileList.value.length == 0) {
+ message.error('璇蜂笂浼犳枃浠�')
+ return
+ }
+ // 鎻愪氦璇锋眰
+ uploadHeaders.value = {
+ Authorization: 'Bearer ' + getAccessToken(),
+ 'tenant-id': getTenantId()
+ }
+ formLoading.value = true
+ uploadRef.value!.submit()
+}
+
+/** 鏂囦欢涓婁紶鎴愬姛 */
+const emits = defineEmits(['success'])
+const submitFormSuccess = (response: any) => {
+ if (response.code !== 0) {
+ message.error(response.msg)
+ formLoading.value = false
+ return
+ }
+ // 鎷兼帴鎻愮ず璇�
+ const data = response.data
+ let text = '涓婁紶鎴愬姛鏁伴噺锛�' + data.createDeviceNames.length + ';'
+ for (let deviceName of data.createDeviceNames) {
+ text += '< ' + deviceName + ' >'
+ }
+ text += '鏇存柊鎴愬姛鏁伴噺锛�' + data.updateDeviceNames.length + ';'
+ for (const deviceName of data.updateDeviceNames) {
+ text += '< ' + deviceName + ' >'
+ }
+ text += '鏇存柊澶辫触鏁伴噺锛�' + Object.keys(data.failureDeviceNames).length + ';'
+ for (const deviceName in data.failureDeviceNames) {
+ text += '< ' + deviceName + ': ' + data.failureDeviceNames[deviceName] + ' >'
+ }
+ message.alert(text)
+ formLoading.value = false
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emits('success')
+}
+
+/** 涓婁紶閿欒鎻愮ず */
+const submitFormError = (): void => {
+ message.error('涓婁紶澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒')
+ resetForm()
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async (): Promise<void> => {
+ // 閲嶇疆涓婁紶鐘舵�佸拰鏂囦欢
+ formLoading.value = false
+ await nextTick()
+ uploadRef.value?.clearFiles()
+}
+
+/** 鏂囦欢鏁拌秴鍑烘彁绀� */
+const handleExceed = (): void => {
+ message.error('鏈�澶氬彧鑳戒笂浼犱竴涓枃浠讹紒')
+}
+
+/** 涓嬭浇妯℃澘鎿嶄綔 */
+const importTemplate = async () => {
+ const res = await DeviceApi.importDeviceTemplate()
+ download.excel(res, '璁惧瀵煎叆妯$増.xls')
+}
+</script>
diff --git a/src/views/iot/device/device/components/DeviceTableSelect.vue b/src/views/iot/device/device/components/DeviceTableSelect.vue
new file mode 100644
index 0000000..73c252d
--- /dev/null
+++ b/src/views/iot/device/device/components/DeviceTableSelect.vue
@@ -0,0 +1,303 @@
+<!-- IoT 璁惧閫夋嫨锛屼娇鐢ㄥ脊绐楀睍绀� -->
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="100px"
+ >
+ <el-form-item v-if="!props.productId" label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="product in products"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="DeviceName" prop="deviceName">
+ <el-input
+ v-model="queryParams.deviceName"
+ placeholder="璇疯緭鍏� DeviceName"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞鍚嶇О" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ placeholder="璇疯緭鍏ュ娉ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="璁惧绫诲瀷" prop="deviceType">
+ <el-select
+ v-model="queryParams.deviceType"
+ placeholder="璇烽�夋嫨璁惧绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨璁惧鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧鍒嗙粍" prop="groupId">
+ <el-select
+ v-model="queryParams.groupId"
+ placeholder="璇烽�夋嫨璁惧鍒嗙粍"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="group in deviceGroups"
+ :key="group.id"
+ :label="group.name"
+ :value="group.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ ref="tableRef"
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @row-click="handleRowClick"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column v-if="multiple" type="selection" width="55" />
+ <el-table-column v-else width="55">
+ <template #default="scope">
+ <el-radio
+ v-model="selectedId"
+ :value="scope.row.id"
+ @change="() => handleRadioChange(scope.row)"
+ >
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column label="DeviceName" align="center" prop="deviceName" />
+ <el-table-column label="澶囨敞鍚嶇О" align="center" prop="nickname" />
+ <el-table-column label="鎵�灞炰骇鍝�" align="center" prop="productId">
+ <template #default="scope">
+ {{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="璁惧绫诲瀷" align="center" prop="deviceType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵�灞炲垎缁�" align="center" prop="groupId">
+ <template #default="scope">
+ <template v-if="scope.row.groupIds?.length">
+ <el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
+ {{ deviceGroups.find((g) => g.id === id)?.name }}
+ </el-tag>
+ </template>
+ </template>
+ </el-table-column>
+ <el-table-column label="璁惧鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鏈�鍚庝笂绾挎椂闂�"
+ align="center"
+ prop="onlineTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
+
+defineOptions({ name: 'IoTDeviceTableSelect' })
+
+const props = defineProps({
+ multiple: {
+ type: Boolean,
+ default: false
+ },
+ productId: {
+ type: Number,
+ default: null
+ }
+})
+
+const message = useMessage()
+const dialogVisible = ref(false)
+const dialogTitle = ref('璁惧閫夋嫨鍣�')
+const formLoading = ref(false)
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<DeviceVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const selectedDevices = ref<DeviceVO[]>([]) // 閫変腑鐨勮澶囧垪琛�
+const selectedId = ref<number>() // 鍗曢�夋ā寮忎笅閫変腑鐨処D
+const products = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const deviceGroups = ref<DeviceGroupVO[]>([]) // 璁惧鍒嗙粍鍒楄〃
+
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ deviceName: undefined,
+ productId: undefined,
+ deviceType: undefined,
+ nickname: undefined,
+ status: undefined,
+ groupId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ if (props.productId) {
+ queryParams.productId = props.productId as unknown as any
+ }
+ const data = await DeviceApi.getDevicePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ // 閲嶇疆閫夋嫨鐘舵��
+ selectedDevices.value = []
+ selectedId.value = undefined
+ if (!props.productId) {
+ // 鑾峰彇浜у搧鍒楄〃
+ products.value = await ProductApi.getSimpleProductList()
+ }
+ // 鑾峰彇璁惧鍒楄〃
+ await getList()
+}
+defineExpose({ open })
+
+/** 澶勭悊琛岀偣鍑讳簨浠� */
+const tableRef = ref()
+const handleRowClick = (row: DeviceVO) => {
+ if (props.multiple) {
+ tableRef.value?.toggleRowSelection(row)
+ } else {
+ selectedId.value = row.id
+ selectedDevices.value = [row]
+ }
+}
+
+/** 澶勭悊鍗曢�夊彉鏇翠簨浠� */
+const handleRadioChange = (row: DeviceVO) => {
+ selectedDevices.value = [row]
+}
+
+/** 澶勭悊閫夋嫨鍙樻洿浜嬩欢 */
+const handleSelectionChange = (selection: DeviceVO[]) => {
+ if (props.multiple) {
+ selectedDevices.value = selection
+ }
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+ if (selectedDevices.value.length === 0) {
+ message.warning(props.multiple ? '璇疯嚦灏戦�夋嫨涓�涓澶�' : '璇烽�夋嫨涓�涓澶�')
+ return
+ }
+ emit('success', props.multiple ? selectedDevices.value : selectedDevices.value[0])
+ dialogVisible.value = false
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 鑾峰彇鍒嗙粍鍒楄〃
+ deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
+})
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailConfig.vue b/src/views/iot/device/device/detail/DeviceDetailConfig.vue
new file mode 100644
index 0000000..d382f79
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailConfig.vue
@@ -0,0 +1,134 @@
+<!-- 璁惧閰嶇疆 -->
+<template>
+ <div>
+ <el-alert
+ title="鏀寔杩滅▼鏇存柊璁惧鐨勯厤缃枃浠�(JSON 鏍煎紡)锛屽彲浠ュ湪涓嬫柟缂栬緫閰嶇疆妯℃澘锛屽璁惧鐨勭郴缁熷弬鏁般�佺綉缁滃弬鏁扮瓑杩涜杩滅▼閰嶇疆銆傞厤缃畬鎴愬悗锛岄渶鐐瑰嚮銆屼笅鍙戙�嶆寜閽紝璁惧鍗冲彲杩涜杩滅▼閰嶇疆銆�"
+ type="info"
+ show-icon
+ class="my-4"
+ description="濡傞渶缂栬緫鏂囦欢锛岃鐐瑰嚮涓嬫柟缂栬緫鎸夐挳"
+ />
+ <JsonEditor
+ v-model="config"
+ :mode="isEditing ? 'code' : 'view'"
+ height="600px"
+ @error="onError"
+ />
+ <div class="mt-5 text-center">
+ <el-button v-if="isEditing" @click="cancelEdit">鍙栨秷</el-button>
+ <el-button v-if="isEditing" type="primary" @click="saveConfig" :disabled="hasJsonError">
+ 淇濆瓨
+ </el-button>
+ <el-button v-else @click="enableEdit">缂栬緫</el-button>
+ <el-button v-if="!isEditing" type="success" @click="handleConfigPush" :loading="pushLoading">
+ 閰嶇疆鎺ㄩ��
+ </el-button>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
+import { jsonParse } from '@/utils'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'DeviceDetailConfig' })
+
+const props = defineProps<{
+ device: DeviceVO
+}>()
+
+const emit = defineEmits<{
+ (e: 'success'): void // 瀹氫箟 success 浜嬩欢锛屼笉闇�瑕佸弬鏁�
+}>()
+
+const message = useMessage()
+const loading = ref(false) // 鍔犺浇涓�
+const pushLoading = ref(false) // 鎺ㄩ�佸姞杞戒腑
+const config = ref<any>({}) // 鍙瓨鍌� config 瀛楁
+const hasJsonError = ref(false) // 鏄惁鏈� JSON 鏍煎紡閿欒
+
+/** 鐩戝惉 props.device 鐨勫彉鍖栵紝鍙洿鏂� config 瀛楁 */
+watchEffect(() => {
+ config.value = jsonParse(props.device.config)
+})
+
+const isEditing = ref(false) // 缂栬緫鐘舵��
+/** 鍚敤缂栬緫妯″紡鐨勫嚱鏁� */
+const enableEdit = () => {
+ isEditing.value = true
+ hasJsonError.value = false // 閲嶇疆閿欒鐘舵��
+}
+
+/** 鍙栨秷缂栬緫鐨勫嚱鏁� */
+const cancelEdit = () => {
+ config.value = jsonParse(props.device.config)
+ isEditing.value = false
+ hasJsonError.value = false // 閲嶇疆閿欒鐘舵��
+}
+
+/** 淇濆瓨閰嶇疆鐨勫嚱鏁� */
+const saveConfig = async () => {
+ if (hasJsonError.value) {
+ message.error('JSON鏍煎紡閿欒锛岃淇鍚庡啀鎻愪氦锛�')
+ return
+ }
+ await updateDeviceConfig()
+ isEditing.value = false
+}
+
+/** 閰嶇疆鎺ㄩ�佸鐞嗗嚱鏁� */
+const handleConfigPush = async () => {
+ try {
+ // 浜屾纭
+ await message.confirm('纭畾瑕佹帹閫侀厤缃埌璁惧鍚楋紵姝ゆ搷浣滃皢杩滅▼鏇存柊璁惧閰嶇疆銆�', '閰嶇疆鎺ㄩ�佺‘璁�')
+
+ pushLoading.value = true
+
+ // 璋冪敤閰嶇疆鎺ㄩ�佹帴鍙�
+ await DeviceApi.sendDeviceMessage({
+ deviceId: props.device.id,
+ method: IotDeviceMessageMethodEnum.CONFIG_PUSH.method,
+ params: config.value
+ })
+
+ message.success('閰嶇疆鎺ㄩ�佹垚鍔燂紒')
+ } catch (error) {
+ if (error !== 'cancel') {
+ message.error('閰嶇疆鎺ㄩ�佸け璐ワ紒')
+ console.error('閰嶇疆鎺ㄩ�侀敊璇�:', error)
+ }
+ } finally {
+ pushLoading.value = false
+ }
+}
+
+/** 鏇存柊璁惧閰嶇疆 */
+const updateDeviceConfig = async () => {
+ try {
+ // 鎻愪氦璇锋眰
+ loading.value = true
+ await DeviceApi.updateDevice({
+ id: props.device.id,
+ config: JSON.stringify(config.value)
+ } as DeviceVO)
+ message.success('鏇存柊鎴愬姛锛�')
+ // 瑙﹀彂 success 浜嬩欢
+ emit('success')
+ } catch (error) {
+ console.error(error)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 澶勭悊 JSON 缂栬緫鍣ㄩ敊璇殑鍑芥暟 */
+const onError = (errors: any) => {
+ if (isEmpty(errors)) {
+ hasJsonError.value = false
+ return
+ }
+ hasJsonError.value = true
+}
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsHeader.vue b/src/views/iot/device/device/detail/DeviceDetailsHeader.vue
new file mode 100644
index 0000000..c6d031f
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsHeader.vue
@@ -0,0 +1,74 @@
+<!-- 璁惧淇℃伅锛堝ご閮級 -->
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ device.deviceName }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <el-button
+ @click="openForm('update', device.id)"
+ v-hasPermi="['iot:device:update']"
+ v-if="product.status === 0"
+ >
+ 缂栬緫
+ </el-button>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="5" direction="horizontal">
+ <el-descriptions-item label="浜у搧">
+ <el-link @click="goToProductDetail(product.id)">{{ product.name }}</el-link>
+ </el-descriptions-item>
+ <el-descriptions-item label="ProductKey">
+ {{ product.productKey }}
+ <el-button @click="copyToClipboard(product.productKey)">澶嶅埗</el-button>
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DeviceForm ref="formRef" @success="emit('refresh')" />
+</template>
+<script setup lang="ts">
+import DeviceForm from '@/views/iot/device/device/DeviceForm.vue'
+import { ProductVO } from '@/api/iot/product/product'
+import { DeviceVO } from '@/api/iot/device/device'
+import { useClipboard } from '@vueuse/core'
+
+const message = useMessage()
+const { t } = useI18n() // 鍥介檯鍖�
+const router = useRouter()
+
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>()
+const emit = defineEmits(['refresh'])
+
+/** 鎿嶄綔淇敼 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 澶嶅埗鍒板壀璐存澘鏂规硶 */
+const copyToClipboard = async (text: string) => {
+ const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
+ if (!isSupported) {
+ message.error(t('common.copyError'))
+ return
+ }
+ await copy()
+ if (unref(copied)) {
+ message.success(t('common.copySuccess'))
+ }
+}
+
+/** 璺宠浆鍒颁骇鍝佽鎯呴〉闈� */
+const goToProductDetail = (productId: number) => {
+ router.push({ name: 'IoTProductDetail', params: { id: productId } })
+}
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsInfo.vue b/src/views/iot/device/device/detail/DeviceDetailsInfo.vue
new file mode 100644
index 0000000..e3beda7
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsInfo.vue
@@ -0,0 +1,197 @@
+<!-- 璁惧淇℃伅 -->
+<template>
+ <div>
+ <ContentWrap>
+ <el-row :gutter="16">
+ <!-- 宸︿晶璁惧淇℃伅 -->
+ <el-col :span="12">
+ <el-card class="h-full">
+ <template #header>
+ <div class="flex items-center">
+ <Icon icon="ep:info-filled" class="mr-2 text-primary" />
+ <span>璁惧淇℃伅</span>
+ </div>
+ </template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ product.name }}</el-descriptions-item>
+ <el-descriptions-item label="ProductKey">
+ {{ product.productKey }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁惧绫诲瀷">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹氫綅绫诲瀷">
+ <dict-tag :type="DICT_TYPE.IOT_LOCATION_TYPE" :value="device.locationType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="DeviceName">
+ {{ device.deviceName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞鍚嶇О">{{ device.nickname }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(device.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="褰撳墠鐘舵��">
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
+ </el-descriptions-item>
+ <el-descriptions-item label="婵�娲绘椂闂�">
+ {{ formatDate(device.activeTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庝笂绾挎椂闂�">
+ {{ formatDate(device.onlineTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庣绾挎椂闂�">
+ {{ formatDate(device.offlineTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁よ瘉淇℃伅">
+ <el-button type="primary" @click="handleAuthInfoDialogOpen" plain size="small"
+ >鏌ョ湅</el-button
+ >
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+ </el-col>
+
+ <!-- 鍙充晶鍦板浘 -->
+ <el-col :span="12">
+ <el-card class="h-full">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div class="flex items-center">
+ <Icon icon="ep:location" class="mr-2 text-primary" />
+ <span>璁惧浣嶇疆</span>
+ </div>
+ <div class="text-[14px] text-[var(--el-text-color-secondary)]">
+ 鏈�鍚庝笂绾挎椂闂达細
+ {{ device.onlineTime ? formatDate(device.onlineTime) : '--' }}
+ </div>
+ </div>
+ </template>
+ <div class="h-[400px] w-full">
+ <Map v-if="showMap" :center="getLocationString()" class="h-full w-full" />
+ <div
+ v-else
+ class="flex items-center justify-center h-full w-full bg-[var(--el-fill-color-light)] text-[var(--el-text-color-secondary)]"
+ >
+ <Icon icon="ep:warning" class="mr-2 text-warning" />
+ <span>鏆傛棤浣嶇疆淇℃伅</span>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </ContentWrap>
+
+ <!-- 璁よ瘉淇℃伅寮规 -->
+ <Dialog
+ title="璁惧璁よ瘉淇℃伅"
+ v-model="authDialogVisible"
+ width="640px"
+ :before-close="handleAuthInfoDialogClose"
+ >
+ <el-form :model="authInfo" label-width="120px">
+ <el-form-item label="clientId">
+ <el-input v-model="authInfo.clientId" readonly>
+ <template #append>
+ <el-button @click="copyToClipboard(authInfo.clientId)" type="primary">
+ <Icon icon="ph:copy" />
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="username">
+ <el-input v-model="authInfo.username" readonly>
+ <template #append>
+ <el-button @click="copyToClipboard(authInfo.username)" type="primary">
+ <Icon icon="ph:copy" />
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="password">
+ <el-input
+ v-model="authInfo.password"
+ readonly
+ :type="authPasswordVisible ? 'text' : 'password'"
+ >
+ <template #append>
+ <el-button @click="authPasswordVisible = !authPasswordVisible" type="primary">
+ <Icon :icon="authPasswordVisible ? 'ph:eye-slash' : 'ph:eye'" />
+ </el-button>
+ <el-button @click="copyToClipboard(authInfo.password)" type="primary">
+ <Icon icon="ph:copy" />
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="handleAuthInfoDialogClose">鍏抽棴</el-button>
+ </template>
+ </Dialog>
+ </div>
+
+ <!-- TODO 寰呭紑鍙戯細璁惧鏍囩 -->
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { ProductVO } from '@/api/iot/product/product'
+import { formatDate } from '@/utils/formatTime'
+import { DeviceVO } from '@/api/iot/device/device'
+import { DeviceApi, IotDeviceAuthInfoVO } from '@/api/iot/device/device'
+import Map from '@/components/Map/index.vue'
+import { ref, computed } from 'vue'
+import { useClipboard } from '@vueuse/core'
+
+const message = useMessage() // 娑堟伅鎻愮ず
+const { t } = useI18n() // 鍥介檯鍖�
+
+const { product, device } = defineProps<{ product: ProductVO; device: DeviceVO }>() // 瀹氫箟 Props
+const emit = defineEmits(['refresh']) // 瀹氫箟 Emits
+
+const authDialogVisible = ref(false) // 瀹氫箟璁惧璁よ瘉淇℃伅寮规鐨勫彲瑙佹��
+const authPasswordVisible = ref(false) // 瀹氫箟瀵嗙爜鍙鎬х姸鎬�
+const authInfo = ref<IotDeviceAuthInfoVO>({} as IotDeviceAuthInfoVO) // 瀹氫箟璁惧璁よ瘉淇℃伅瀵硅薄
+
+/** 鎺у埗鍦板浘鏄剧ず鐨勬爣蹇� */
+const showMap = computed(() => {
+ return !!(device.longitude && device.latitude)
+})
+
+/** 鑾峰彇浣嶇疆瀛楃涓诧紝鐢ㄤ簬鍦板浘缁勪欢 */
+const getLocationString = () => {
+ if (device.longitude && device.latitude) {
+ return `${device.longitude},${device.latitude}`
+ }
+ return ''
+}
+
+/** 澶嶅埗鍒板壀璐存澘鏂规硶 */
+const copyToClipboard = async (text: string) => {
+ const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
+ if (!isSupported) {
+ message.error(t('common.copyError'))
+ return
+ }
+ await copy()
+ if (unref(copied)) {
+ message.success(t('common.copySuccess'))
+ }
+}
+
+/** 鎵撳紑璁惧璁よ瘉淇℃伅寮规鐨勬柟娉� */
+const handleAuthInfoDialogOpen = async () => {
+ try {
+ authInfo.value = await DeviceApi.getDeviceAuthInfo(device.id)
+ // 鏄剧ず璁惧璁よ瘉淇℃伅寮规
+ authDialogVisible.value = true
+ } catch (error) {
+ console.error('鑾峰彇璁惧璁よ瘉淇℃伅鍑洪敊锛�', error)
+ message.error('鑾峰彇璁惧璁よ瘉淇℃伅澶辫触锛岃妫�鏌ョ綉缁滆繛鎺ユ垨鑱旂郴绠$悊鍛�')
+ }
+}
+
+/** 鍏抽棴璁惧璁よ瘉淇℃伅寮规鐨勬柟娉� */
+const handleAuthInfoDialogClose = () => {
+ authDialogVisible.value = false
+}
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsMessage.vue b/src/views/iot/device/device/detail/DeviceDetailsMessage.vue
new file mode 100644
index 0000000..aebb03b
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsMessage.vue
@@ -0,0 +1,201 @@
+<!-- 璁惧娑堟伅鍒楄〃 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-form :model="queryParams" inline>
+ <el-form-item>
+ <el-select v-model="queryParams.method" placeholder="鎵�鏈夋柟娉�" class="!w-160px" clearable>
+ <el-option
+ v-for="item in methodOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-select
+ v-model="queryParams.upstream"
+ placeholder="涓婅/涓嬭"
+ class="!w-160px"
+ clearable
+ >
+ <el-option label="涓婅" value="true" />
+ <el-option label="涓嬭" value="false" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" /> 鎼滅储
+ </el-button>
+ <el-switch
+ size="large"
+ width="80"
+ v-model="autoRefresh"
+ class="ml-20px"
+ inline-prompt
+ active-text="瀹氭椂鍒锋柊"
+ inactive-text="瀹氭椂鍒锋柊"
+ style="--el-switch-on-color: #13ce66"
+ />
+ </el-form-item>
+ </el-form>
+
+ <!-- 娑堟伅鍒楄〃 -->
+ <el-table v-loading="loading" :data="list" :stripe="true" class="whitespace-nowrap">
+ <el-table-column label="鏃堕棿" align="center" prop="ts" width="180">
+ <template #default="scope">
+ {{ formatDate(scope.row.ts) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="涓婅/涓嬭" align="center" prop="upstream" width="140">
+ <template #default="scope">
+ <el-tag :type="scope.row.upstream ? 'primary' : 'success'">
+ {{ scope.row.upstream ? '涓婅' : '涓嬭' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁鍥炲" align="center" prop="reply" width="140">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.reply" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璇锋眰缂栧彿" align="center" prop="requestId" width="300" />
+ <el-table-column label="璇锋眰鏂规硶" align="center" prop="method" width="140">
+ <template #default="scope">
+ {{ methodOptions.find((item) => item.value === scope.row.method)?.label }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="璇锋眰/鍝嶅簲鏁版嵁"
+ align="center"
+ prop="params"
+ :show-overflow-tooltip="true"
+ >
+ <template #default="scope">
+ <span v-if="scope.row.reply">
+ {{ `{"code":${scope.row.code},"msg":"${scope.row.msg}","data":${scope.row.data}\}` }}
+ </span>
+ <span v-else>{{ scope.row.params }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="mt-10px flex justify-end">
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getMessageList"
+ />
+ </div>
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { DeviceApi } from '@/api/iot/device/device'
+import { formatDate } from '@/utils/formatTime'
+import { IotDeviceMessageMethodEnum } from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+ deviceId: number
+}>()
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ deviceId: props.deviceId,
+ method: undefined,
+ upstream: undefined,
+ pageNo: 1,
+ pageSize: 10
+})
+
+// 鍒楄〃鏁版嵁
+const loading = ref(false)
+const total = ref(0)
+const list = ref([])
+const autoRefresh = ref(false) // 鑷姩鍒锋柊寮�鍏�
+let autoRefreshTimer: any = null // 鑷姩鍒锋柊瀹氭椂鍣�
+
+// 娑堟伅鏂规硶閫夐」
+const methodOptions = computed(() => {
+ return Object.values(IotDeviceMessageMethodEnum).map((item) => ({
+ label: item.name,
+ value: item.method
+ }))
+})
+
+/** 鏌ヨ娑堟伅鍒楄〃 */
+const getMessageList = async () => {
+ if (!props.deviceId) return
+ loading.value = true
+ try {
+ const data = await DeviceApi.getDeviceMessagePage(queryParams)
+ total.value = data.total
+ list.value = data.list
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getMessageList()
+}
+
+/** 鐩戝惉鑷姩鍒锋柊 */
+watch(autoRefresh, (newValue) => {
+ if (newValue) {
+ autoRefreshTimer = setInterval(() => {
+ getMessageList()
+ }, 5000)
+ } else {
+ clearInterval(autoRefreshTimer)
+ autoRefreshTimer = null
+ }
+})
+
+/** 鐩戝惉璁惧鏍囪瘑鍙樺寲 */
+watch(
+ () => props.deviceId,
+ (newValue) => {
+ if (newValue) {
+ handleQuery()
+ }
+ }
+)
+
+/** 缁勪欢鍗歌浇鏃舵竻闄ゅ畾鏃跺櫒 */
+onBeforeUnmount(() => {
+ if (autoRefreshTimer) {
+ clearInterval(autoRefreshTimer)
+ autoRefreshTimer = null
+ }
+})
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ if (props.deviceId) {
+ getMessageList()
+ }
+})
+
+/** 鍒锋柊娑堟伅鍒楄〃 */
+const refresh = (delay = 0) => {
+ if (delay > 0) {
+ setTimeout(() => {
+ handleQuery()
+ }, delay)
+ } else {
+ handleQuery()
+ }
+}
+
+/** 鏆撮湶鏂规硶缁欑埗缁勪欢 */
+defineExpose({
+ refresh
+})
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue b/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue
new file mode 100644
index 0000000..599de70
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsSimulator.vue
@@ -0,0 +1,420 @@
+<!-- 妯℃嫙璁惧 -->
+<template>
+ <ContentWrap>
+ <el-row :gutter="20">
+ <!-- 宸︿晶鎸囦护璋冭瘯鍖哄煙 -->
+ <el-col :span="12">
+ <el-tabs v-model="activeTab" type="border-card">
+ <!-- 涓婅鎸囦护璋冭瘯 -->
+ <el-tab-pane label="涓婅鎸囦护璋冭瘯" name="upstream">
+ <el-tabs v-if="activeTab === 'upstream'" v-model="upstreamTab">
+ <!-- 灞炴�т笂鎶� -->
+ <el-tab-pane label="灞炴�т笂鎶�" :name="IotDeviceMessageMethodEnum.PROPERTY_POST.method">
+ <ContentWrap>
+ <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鍔熻兘鍚嶇О"
+ prop="name"
+ width="120"
+ />
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鏍囪瘑绗�"
+ prop="identifier"
+ width="120"
+ />
+ <el-table-column align="center" label="鏁版嵁绫诲瀷" width="100">
+ <template #default="{ row }">
+ {{ row.property?.dataType ?? '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column align="left" label="鏁版嵁瀹氫箟" min-width="200">
+ <template #default="{ row }">
+ <DataDefinition :data="row" />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" align="center" label="鍊�" width="150">
+ <template #default="scope">
+ <el-input
+ :model-value="getFormValue(scope.row.identifier)"
+ @update:model-value="setFormValue(scope.row.identifier, $event)"
+ placeholder="杈撳叆鍊�"
+ size="small"
+ />
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="flex justify-between items-center mt-4">
+ <span class="text-sm text-gray-600">
+ 璁剧疆灞炴�у�煎悗锛岀偣鍑汇�屽彂閫佸睘鎬т笂鎶ャ�嶆寜閽�
+ </span>
+ <el-button type="primary" @click="handlePropertyPost">鍙戦�佸睘鎬т笂鎶�</el-button>
+ </div>
+ </ContentWrap>
+ </el-tab-pane>
+
+ <!-- 浜嬩欢涓婃姤 -->
+ <el-tab-pane label="浜嬩欢涓婃姤" :name="IotDeviceMessageMethodEnum.EVENT_POST.method">
+ <ContentWrap>
+ <el-table :data="eventList" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鍔熻兘鍚嶇О"
+ prop="name"
+ width="120"
+ />
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鏍囪瘑绗�"
+ prop="identifier"
+ width="120"
+ />
+ <el-table-column align="center" label="鏁版嵁绫诲瀷" width="100">
+ <template #default="{ row }">
+ {{ row.event?.dataType ?? '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column align="left" label="鏁版嵁瀹氫箟" min-width="200">
+ <template #default="{ row }">
+ <DataDefinition :data="row" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍊�" width="200">
+ <template #default="scope">
+ <el-input
+ :model-value="getFormValue(scope.row.identifier)"
+ @update:model-value="setFormValue(scope.row.identifier, $event)"
+ type="textarea"
+ :rows="3"
+ placeholder="杈撳叆浜嬩欢鍙傛暟锛圝SON鏍煎紡锛�"
+ size="small"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" align="center" label="鎿嶄綔" width="100">
+ <template #default="scope">
+ <el-button type="primary" size="small" @click="handleEventPost(scope.row)">
+ 涓婃姤浜嬩欢
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+ </el-tab-pane>
+
+ <!-- 鐘舵�佸彉鏇� -->
+ <el-tab-pane label="鐘舵�佸彉鏇�" :name="IotDeviceMessageMethodEnum.STATE_UPDATE.method">
+ <ContentWrap>
+ <div class="flex gap-4">
+ <el-button type="primary" @click="handleDeviceState(DeviceStateEnum.ONLINE)">
+ 璁惧涓婄嚎
+ </el-button>
+ <el-button type="danger" @click="handleDeviceState(DeviceStateEnum.OFFLINE)">
+ 璁惧涓嬬嚎
+ </el-button>
+ </div>
+ </ContentWrap>
+ </el-tab-pane>
+ </el-tabs>
+ </el-tab-pane>
+
+ <!-- 涓嬭鎸囦护璋冭瘯 -->
+ <el-tab-pane label="涓嬭鎸囦护璋冭瘯" name="downstream">
+ <el-tabs v-if="activeTab === 'downstream'" v-model="downstreamTab">
+ <!-- 灞炴�ц皟璇� -->
+ <el-tab-pane label="灞炴�ц缃�" :name="IotDeviceMessageMethodEnum.PROPERTY_SET.method">
+ <ContentWrap>
+ <el-table :data="propertyList" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鍔熻兘鍚嶇О"
+ prop="name"
+ width="120"
+ />
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鏍囪瘑绗�"
+ prop="identifier"
+ width="120"
+ />
+ <el-table-column align="center" label="鏁版嵁绫诲瀷" width="100">
+ <template #default="{ row }">
+ {{ row.property?.dataType ?? '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column align="left" label="鏁版嵁瀹氫箟" min-width="200">
+ <template #default="{ row }">
+ <DataDefinition :data="row" />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" align="center" label="鍊�" width="150">
+ <template #default="scope">
+ <el-input
+ :model-value="getFormValue(scope.row.identifier)"
+ @update:model-value="setFormValue(scope.row.identifier, $event)"
+ placeholder="杈撳叆鍊�"
+ size="small"
+ />
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="flex justify-between items-center mt-4">
+ <span class="text-sm text-gray-600">
+ 璁剧疆灞炴�у�煎悗锛岀偣鍑汇�屽彂閫佸睘鎬ц缃�嶆寜閽�
+ </span>
+ <el-button type="primary" @click="handlePropertySet">鍙戦�佸睘鎬ц缃�</el-button>
+ </div>
+ </ContentWrap>
+ </el-tab-pane>
+
+ <!-- 鏈嶅姟璋冪敤 -->
+ <el-tab-pane
+ label="璁惧鏈嶅姟璋冪敤"
+ :name="IotDeviceMessageMethodEnum.SERVICE_INVOKE.method"
+ >
+ <ContentWrap>
+ <el-table :data="serviceList" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鏈嶅姟鍚嶇О"
+ prop="name"
+ width="120"
+ />
+ <el-table-column
+ fixed="left"
+ align="center"
+ label="鏍囪瘑绗�"
+ prop="identifier"
+ width="120"
+ />
+ <el-table-column align="left" label="杈撳叆鍙傛暟" min-width="200">
+ <template #default="{ row }">
+ <DataDefinition :data="row" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍙傛暟鍊�" width="200">
+ <template #default="scope">
+ <el-input
+ :model-value="getFormValue(scope.row.identifier)"
+ @update:model-value="setFormValue(scope.row.identifier, $event)"
+ type="textarea"
+ :rows="3"
+ placeholder="杈撳叆鏈嶅姟鍙傛暟锛圝SON鏍煎紡锛�"
+ size="small"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" align="center" label="鎿嶄綔" width="100">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ size="small"
+ @click="handleServiceInvoke(scope.row)"
+ >
+ 鏈嶅姟璋冪敤
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+ </el-tab-pane>
+ </el-tabs>
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+
+ <!-- 鍙充晶璁惧鏃ュ織鍖哄煙 -->
+ <el-col :span="12">
+ <ContentWrap title="璁惧娑堟伅">
+ <DeviceDetailsMessage ref="deviceMessageRef" :device-id="device.id" />
+ </ContentWrap>
+ </el-col>
+ </el-row>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { ProductVO } from '@/api/iot/product/product'
+import { ThingModelData } from '@/api/iot/thingmodel'
+import { DeviceApi, DeviceStateEnum, DeviceVO } from '@/api/iot/device/device'
+import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
+import { DataDefinition } from '@/views/iot/thingmodel/components'
+import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+ product: ProductVO
+ device: DeviceVO
+ thingModelList: ThingModelData[]
+}>()
+
+const message = useMessage() // 娑堟伅寮圭獥
+const activeTab = ref('upstream') // 涓婅upstream銆佷笅琛宒ownstream
+const upstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_POST.method) // 涓婅瀛愭爣绛�
+const downstreamTab = ref(IotDeviceMessageMethodEnum.PROPERTY_SET.method) // 涓嬭瀛愭爣绛�
+const deviceMessageRef = ref() // 璁惧娑堟伅缁勪欢寮曠敤
+const deviceMessageRefreshDelay = 2000 // 寤惰繜 N 绉掞紝淇濊瘉妯℃嫙涓婅鐨勬秷鎭澶勭悊
+
+// 琛ㄥ崟鏁版嵁锛氬瓨鍌ㄧ敤鎴疯緭鍏ョ殑妯℃嫙鍊�
+const formData = ref<Record<string, string>>({})
+
+// 鏍规嵁绫诲瀷杩囨护鐗╂ā鍨嬫暟鎹�
+const getFilteredThingModelList = (type: number) => {
+ return props.thingModelList.filter((item) => item.type === type)
+}
+const propertyList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.PROPERTY))
+const eventList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.EVENT))
+const serviceList = computed(() => getFilteredThingModelList(IoTThingModelTypeEnum.SERVICE))
+
+/** 鑾峰彇琛ㄥ崟鍊肩殑杈呭姪鍑芥暟 */
+const getFormValue = (identifier: string | number | undefined) => {
+ if (!identifier) return ''
+ return formData.value[String(identifier)] || ''
+}
+/** 璁剧疆琛ㄥ崟鍊肩殑杈呭姪鍑芥暟 */
+const setFormValue = (identifier: string | number | undefined, value: string) => {
+ if (!identifier) return
+ formData.value[String(identifier)] = value
+}
+
+/** 妯℃嫙灞炴�т笂鎶� */
+const handlePropertyPost = async () => {
+ const data: Record<string, any> = {}
+ propertyList.value.forEach((item) => {
+ const value = getFormValue(item.identifier)
+ if (value && item.identifier) {
+ data[String(item.identifier)] = value
+ }
+ })
+ if (Object.keys(data).length === 0) {
+ message.warning('璇疯嚦灏戣缃竴涓睘鎬у��')
+ return
+ }
+
+ try {
+ await DeviceApi.sendDeviceMessage({
+ deviceId: props.device.id,
+ method: IotDeviceMessageMethodEnum.PROPERTY_POST.method,
+ params: data
+ })
+ message.success('灞炴�т笂鎶ユ垚鍔�')
+ deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+ } catch (error) {
+ message.error('灞炴�т笂鎶ュけ璐�')
+ }
+}
+
+/** 妯℃嫙浜嬩欢涓婃姤 */
+const handleEventPost = async (eventItem: ThingModelData) => {
+ const value = getFormValue(eventItem.identifier)
+ if (!value) {
+ message.warning('璇疯緭鍏ヤ簨浠跺弬鏁�')
+ return
+ }
+ let eventParams: any
+ try {
+ eventParams = JSON.parse(value)
+ } catch {
+ message.error('浜嬩欢鍙傛暟鏍煎紡涓嶆纭紝璇疯緭鍏ユ湁鏁堢殑JSON鏍煎紡')
+ return
+ }
+
+ try {
+ await DeviceApi.sendDeviceMessage({
+ deviceId: props.device.id,
+ method: IotDeviceMessageMethodEnum.EVENT_POST.method,
+ params: {
+ identifier: String(eventItem.identifier),
+ value: eventParams,
+ time: Date.now()
+ }
+ })
+ message.success(`浜嬩欢銆�${String(eventItem.name)}銆戜笂鎶ユ垚鍔焋)
+ deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+ } catch (error) {
+ message.error(`浜嬩欢銆�${String(eventItem.name)}銆戜笂鎶ュけ璐)
+ }
+}
+
+/** 妯℃嫙璁惧鐘舵�� */
+const handleDeviceState = async (state: number) => {
+ try {
+ await DeviceApi.sendDeviceMessage({
+ deviceId: props.device.id,
+ method: IotDeviceMessageMethodEnum.STATE_UPDATE.method,
+ params: {
+ state: state
+ }
+ })
+ message.success(`璁惧${state === DeviceStateEnum.ONLINE ? '涓婄嚎' : '涓嬬嚎'}鎴愬姛`)
+ deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+ } catch (error) {
+ message.error(`璁惧${state === DeviceStateEnum.ONLINE ? '涓婄嚎' : '涓嬬嚎'}澶辫触`)
+ }
+}
+
+/** 妯℃嫙灞炴�ц缃� */
+const handlePropertySet = async () => {
+ const data: Record<string, any> = {}
+ propertyList.value.forEach((item) => {
+ const value = getFormValue(item.identifier)
+ if (value && item.identifier) {
+ data[String(item.identifier)] = value
+ }
+ })
+ if (Object.keys(data).length === 0) {
+ message.warning('璇疯嚦灏戣缃竴涓睘鎬у��')
+ return
+ }
+
+ try {
+ await DeviceApi.sendDeviceMessage({
+ deviceId: props.device.id,
+ method: IotDeviceMessageMethodEnum.PROPERTY_SET.method,
+ params: data
+ })
+ message.success('灞炴�ц缃垚鍔�')
+ deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+ } catch (error) {
+ message.error('灞炴�ц缃け璐�')
+ }
+}
+
+/** 妯℃嫙鏈嶅姟璋冪敤 */
+const handleServiceInvoke = async (serviceItem: ThingModelData) => {
+ const value = getFormValue(serviceItem.identifier)
+ if (!value) {
+ message.warning('璇疯緭鍏ユ湇鍔″弬鏁�')
+ return
+ }
+ let serviceParams: any
+ try {
+ serviceParams = JSON.parse(value)
+ } catch {
+ message.error('鏈嶅姟鍙傛暟鏍煎紡涓嶆纭紝璇疯緭鍏ユ湁鏁堢殑JSON鏍煎紡')
+ return
+ }
+
+ try {
+ await DeviceApi.sendDeviceMessage({
+ deviceId: props.device.id,
+ method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method,
+ params: {
+ identifier: String(serviceItem.identifier),
+ inputParams: serviceParams
+ }
+ })
+ message.success(`鏈嶅姟銆�${String(serviceItem.name)}銆戣皟鐢ㄦ垚鍔焋)
+ deviceMessageRef.value.refresh(deviceMessageRefreshDelay)
+ } catch (error) {
+ message.error(`鏈嶅姟銆�${String(serviceItem.name)}銆戣皟鐢ㄥけ璐)
+ }
+}
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModel.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModel.vue
new file mode 100644
index 0000000..9969874
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsThingModel.vue
@@ -0,0 +1,35 @@
+<!-- 璁惧鐗╂ā鍨嬶細璁惧灞炴�с�佷簨浠剁鐞嗐�佹湇鍔¤皟鐢� -->
+<template>
+ <ContentWrap>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="璁惧灞炴�э紙杩愯鐘舵�侊級" name="property">
+ <DeviceDetailsThingModelProperty :device-id="deviceId" />
+ </el-tab-pane>
+ <el-tab-pane label="璁惧浜嬩欢涓婃姤" name="event">
+ <DeviceDetailsThingModelEvent
+ :device-id="props.deviceId"
+ :thing-model-list="props.thingModelList"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="璁惧鏈嶅姟璋冪敤" name="service">
+ <DeviceDetailsThingModelService
+ :device-id="deviceId"
+ :thing-model-list="props.thingModelList"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { ThingModelData } from '@/api/iot/thingmodel'
+import DeviceDetailsThingModelProperty from './DeviceDetailsThingModelProperty.vue'
+import DeviceDetailsThingModelEvent from './DeviceDetailsThingModelEvent.vue'
+import DeviceDetailsThingModelService from './DeviceDetailsThingModelService.vue'
+
+const props = defineProps<{
+ deviceId: number
+ thingModelList: ThingModelData[]
+}>()
+
+const activeTab = ref('property') // 榛樿閫変腑璁惧灞炴��
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue
new file mode 100644
index 0000000..04f9a9f
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelEvent.vue
@@ -0,0 +1,192 @@
+<!-- 璁惧浜嬩欢绠$悊 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="80px"
+ @submit.prevent
+ >
+ <el-form-item label="鏍囪瘑绗�" prop="identifier">
+ <el-select
+ v-model="queryParams.identifier"
+ placeholder="璇烽�夋嫨浜嬩欢鏍囪瘑绗�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="event in eventThingModels"
+ :key="event.identifier"
+ :label="`${event.name}(${event.identifier})`"
+ :value="event.identifier!"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃堕棿鑼冨洿" prop="times">
+ <el-date-picker
+ v-model="queryParams.times"
+ type="datetimerange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫椂闂�"
+ end-placeholder="缁撴潫鏃堕棿"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ class="!w-360px"
+ :shortcuts="defaultShortcuts"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <!-- 浜嬩欢鍒楄〃 -->
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="涓婃姤鏃堕棿" align="center" prop="reportTime" width="180px">
+ <template #default="scope">
+ {{ scope.row.request?.reportTime ? formatDate(scope.row.request.reportTime) : '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏍囪瘑绗�" align="center" prop="identifier" width="160px">
+ <template #default="scope">
+ <el-tag type="primary" size="small">
+ {{ scope.row.request?.identifier }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜嬩欢鍚嶇О" align="center" prop="eventName" width="160px">
+ <template #default="scope">
+ {{ getEventName(scope.row.request?.identifier) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="浜嬩欢绫诲瀷" align="center" prop="eventType" width="100px">
+ <template #default="scope">
+ {{ getEventType(scope.row.request?.identifier) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="杈撳叆鍙傛暟" align="center" prop="params">
+ <template #default="scope"> {{ parseParams(scope.row.request.params) }} </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelData } from '@/api/iot/thingmodel'
+import { formatDate, defaultShortcuts } from '@/utils/formatTime'
+import {
+ getEventTypeLabel,
+ IotDeviceMessageMethodEnum,
+ IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+ deviceId: number
+ thingModelList: ThingModelData[]
+}>()
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([] as any[]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ deviceId: props.deviceId,
+ method: IotDeviceMessageMethodEnum.EVENT_POST.method, // 鍥哄畾绛涢�変簨浠舵秷鎭�
+ identifier: '',
+ times: [] as any[],
+ pageNo: 1,
+ pageSize: 10
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 浜嬩欢绫诲瀷鐨勭墿妯″瀷鏁版嵁 */
+const eventThingModels = computed(() => {
+ return props.thingModelList.filter(
+ (item: ThingModelData) => item.type === IoTThingModelTypeEnum.EVENT
+ )
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ if (!props.deviceId) return
+ loading.value = true
+ try {
+ const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
+ list.value = data.list
+ total.value = data.length
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ queryParams.identifier = ''
+ queryParams.times = []
+ handleQuery()
+}
+
+/** 鑾峰彇浜嬩欢鍚嶇О */
+const getEventName = (identifier: string | undefined) => {
+ if (!identifier) return '-'
+ const event = eventThingModels.value.find(
+ (item: ThingModelData) => item.identifier === identifier
+ )
+ return event?.name || identifier
+}
+
+/** 鑾峰彇浜嬩欢绫诲瀷 */
+const getEventType = (identifier: string | undefined) => {
+ if (!identifier) return '-'
+ const event = eventThingModels.value.find(
+ (item: ThingModelData) => item.identifier === identifier
+ )
+ if (!event?.event?.type) return '-'
+ return getEventTypeLabel(event.event.type) || '-'
+}
+
+/** 瑙f瀽鍙傛暟 */
+const parseParams = (params: string) => {
+ try {
+ const parsed = JSON.parse(params)
+ if (parsed.params) {
+ return parsed.params
+ }
+ return parsed
+ } catch (error) {
+ return {}
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue
new file mode 100644
index 0000000..e282436
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelProperty.vue
@@ -0,0 +1,245 @@
+<!-- 璁惧灞炴�х鐞� -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ @submit.prevent
+ >
+ <el-form-item label="" prop="keyword">
+ <el-input
+ v-model="queryParams.keyword"
+ placeholder="璇疯緭鍏ュ睘鎬у悕绉般�佹爣蹇楃"
+ clearable
+ class="!w-240px"
+ @keyup.enter="handleQuery"
+ @clear="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item class="float-right !mr-0 !mb-0">
+ <el-button-group>
+ <el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
+ <Icon icon="ep:grid" />
+ </el-button>
+ <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
+ <Icon icon="ep:list" />
+ </el-button>
+ </el-button-group>
+ </el-form-item>
+ <!-- TODO @鑺嬭壙锛氬弬鑰冮樋閲屼簯锛屽疄鏃跺埛鏂帮紒 -->
+ <el-form-item>
+ <el-switch
+ size="large"
+ width="80"
+ v-model="autoRefresh"
+ class="-ml-15px"
+ inline-prompt
+ active-text="瀹氭椂鍒锋柊"
+ inactive-text="瀹氭椂鍒锋柊"
+ style="--el-switch-on-color: #13ce66"
+ />
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <ContentWrap>
+ <!-- 鍗$墖瑙嗗浘 -->
+ <template v-if="viewMode === 'card'">
+ <el-row :gutter="16" v-loading="loading">
+ <el-col
+ v-for="item in list"
+ :key="item.identifier"
+ :xs="24"
+ :sm="12"
+ :md="12"
+ :lg="6"
+ class="mb-4"
+ >
+ <el-card
+ class="h-full transition-colors relative overflow-hidden"
+ :body-style="{ padding: '0' }"
+ >
+ <!-- 娣诲姞娓愬彉鑳屾櫙灞� -->
+ <div
+ class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none bg-gradient-to-b from-[#eefaff] to-transparent"
+ >
+ </div>
+ <div class="p-4 relative">
+ <!-- 鏍囬鍖哄煙 -->
+ <div class="flex items-center mb-3">
+ <div class="mr-2.5 flex items-center">
+ <Icon icon="ep:cpu" class="text-[18px] text-[#0070ff]" />
+ </div>
+ <div class="text-[16px] font-600 flex-1">{{ item.name }}</div>
+ <!-- 鏍囪瘑绗� -->
+ <div class="inline-flex items-center mr-2">
+ <el-tag size="small" type="primary">
+ {{ item.identifier }}
+ </el-tag>
+ </div>
+ <!-- 鏁版嵁绫诲瀷鏍囩 -->
+ <div class="inline-flex items-center mr-2">
+ <el-tag size="small" type="info">
+ {{ item.dataType }}
+ </el-tag>
+ </div>
+ <!-- 鏁版嵁鍥炬爣 - 鍙偣鍑� -->
+ <div
+ class="cursor-pointer flex items-center justify-center w-8 h-8 rounded-full hover:bg-blue-50 transition-colors"
+ @click="openHistory(props.deviceId, item.identifier, item.dataType)"
+ >
+ <Icon icon="ep:data-line" class="text-[18px] text-[#0070ff]" />
+ </div>
+ </div>
+
+ <!-- 淇℃伅鍖哄煙 -->
+ <div class="text-[14px]">
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">灞炴�у��</span>
+ <span class="text-[#0b1d30] font-600">
+ {{ formatValueWithUnit(item) }}
+ </span>
+ </div>
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">鏇存柊鏃堕棿</span>
+ <span class="text-[#0b1d30] text-[12px]">
+ {{ item.updateTime ? formatDate(item.updateTime) : '-' }}
+ </span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </template>
+
+ <!-- 鍒楄〃瑙嗗浘 -->
+ <el-table v-else v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="灞炴�ф爣璇嗙" align="center" prop="identifier" />
+ <el-table-column label="灞炴�у悕绉�" align="center" prop="name" />
+ <el-table-column label="鏁版嵁绫诲瀷" align="center" prop="dataType" />
+ <el-table-column label="灞炴�у��" align="center" prop="value">
+ <template #default="scope">
+ {{ formatValueWithUnit(scope.row) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鏇存柊鏃堕棿"
+ align="center"
+ prop="updateTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openHistory(props.deviceId, scope.row.identifier, scope.row.dataType)"
+ >
+ 鏌ョ湅鏁版嵁
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DeviceDetailsThingModelPropertyHistory ref="historyRef" :deviceId="props.deviceId" />
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DeviceApi, IotDevicePropertyDetailRespVO } from '@/api/iot/device/device'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import DeviceDetailsThingModelPropertyHistory from './DeviceDetailsThingModelPropertyHistory.vue'
+
+const props = defineProps<{ deviceId: number }>()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<IotDevicePropertyDetailRespVO[]>([]) // 鏄剧ず鐨勫垪琛ㄦ暟鎹�
+const filterList = ref<IotDevicePropertyDetailRespVO[]>([]) // 瀹屾暣鐨勬暟鎹垪琛�
+const queryParams = reactive({
+ keyword: '' as string
+})
+const autoRefresh = ref(false) // 鑷姩鍒锋柊寮�鍏�
+let autoRefreshTimer: any = null // 瀹氭椂鍣�
+const viewMode = ref<'card' | 'list'>('card') // 瑙嗗浘妯″紡鐘舵��
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const params = {
+ deviceId: props.deviceId,
+ identifier: undefined as string | undefined,
+ name: undefined as string | undefined
+ }
+ filterList.value = await DeviceApi.getLatestDeviceProperties(params)
+ handleFilter()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍓嶇绛涢�夋暟鎹� */
+const handleFilter = () => {
+ if (!queryParams.keyword.trim()) {
+ list.value = filterList.value
+ } else {
+ const keyword = queryParams.keyword.toLowerCase()
+ list.value = filterList.value.filter(
+ (item) =>
+ item.identifier?.toLowerCase().includes(keyword) ||
+ item.name?.toLowerCase().includes(keyword)
+ )
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ handleFilter()
+}
+
+/** 鍘嗗彶鎿嶄綔 */
+const historyRef = ref()
+const openHistory = (deviceId: number, identifier: string, dataType: string) => {
+ historyRef.value.open(deviceId, identifier, dataType)
+}
+
+/** 鏍煎紡鍖栧睘鎬у�煎拰鍗曚綅 */
+const formatValueWithUnit = (item: IotDevicePropertyDetailRespVO) => {
+ if (item.value === null || item.value === undefined || item.value === '') {
+ return '-'
+ }
+ const unitName = item.dataSpecs?.unitName
+ return unitName ? `${item.value} ${unitName}` : item.value
+}
+
+/** 鐩戝惉鑷姩鍒锋柊 */
+watch(autoRefresh, (newValue) => {
+ if (newValue) {
+ autoRefreshTimer = setInterval(() => {
+ getList()
+ }, 5000) // 姣� 5 绉掑埛鏂颁竴娆�
+ } else {
+ clearInterval(autoRefreshTimer)
+ autoRefreshTimer = null
+ }
+})
+
+/** 缁勪欢鍗歌浇鏃舵竻闄ゅ畾鏃跺櫒 */
+onBeforeUnmount(() => {
+ if (autoRefreshTimer) {
+ clearInterval(autoRefreshTimer)
+ }
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue
new file mode 100644
index 0000000..c913f1d
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelPropertyHistory.vue
@@ -0,0 +1,216 @@
+<!-- 璁惧鐗╂ā鍨� -> 杩愯鐘舵�� -> 鏌ョ湅鏁版嵁锛堣澶囩殑灞炴�у�煎巻鍙诧級-->
+<template>
+ <Dialog title="鏌ョ湅鏁版嵁" v-model="dialogVisible" width="1024px" :appendToBody="true">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.times"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="datetimerange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-360px"
+ @change="handleTimeChange"
+ :shortcuts="defaultShortcuts"
+ />
+ </el-form-item>
+ <el-form-item class="float-right !mr-0 !mb-0">
+ <el-button-group>
+ <el-button
+ :type="viewMode === 'chart' ? 'primary' : 'default'"
+ @click="viewMode = 'chart'"
+ :disabled="isComplexDataType"
+ >
+ <Icon icon="ep:histogram" />
+ </el-button>
+ <el-button
+ :type="viewMode === 'list' ? 'primary' : 'default'"
+ @click="viewMode = 'list'"
+ >
+ <Icon icon="ep:list" />
+ </el-button>
+ </el-button-group>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鏁版嵁灞曠ず鍖哄煙 -->
+ <ContentWrap>
+ <!-- 鍥捐〃妯″紡 -->
+ <div v-if="viewMode === 'chart'" class="chart-container">
+ <div v-if="list.length === 0" class="text-center text-gray-500 py-20"> 鏆傛棤鏁版嵁 </div>
+ <Echart v-else :key="'erchart' + Date.now()" :options="echartsOption" height="400px" />
+ </div>
+
+ <!-- 琛ㄦ牸妯″紡 -->
+ <div v-else>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鏃堕棿" align="center" prop="time" width="180px">
+ <template #default="scope">
+ {{ formatDate(new Date(scope.row.updateTime)) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="灞炴�у��" align="center" prop="value">
+ <template #default="scope">
+ {{ scope.row.value }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </ContentWrap>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DeviceApi, IotDevicePropertyRespVO } from '@/api/iot/device/device'
+import { beginOfDay, defaultShortcuts, endOfDay, formatDate } from '@/utils/formatTime'
+import { Echart } from '@/components/Echart'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+
+defineProps<{ deviceId: number }>()
+
+/** IoT 璁惧灞炴�у巻鍙叉暟鎹鎯� */
+defineOptions({ name: 'DeviceDetailsThingModelPropertyHistory' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const loading = ref(false)
+const viewMode = ref<'chart' | 'list'>('chart') // 瑙嗗浘妯″紡鐘舵��
+const list = ref<IotDevicePropertyRespVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const chartKey = ref(0) // 鍥捐〃閲嶆柊娓叉煋鐨刱ey
+const thingModelDataType = ref<string>('') // 鐗╂ā鍨嬫暟鎹被鍨�
+const queryParams = reactive({
+ deviceId: -1,
+ identifier: '',
+ times: [
+ // 榛樿鏄剧ず鏈�杩戜竴鍛ㄧ殑鏁版嵁
+ formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+ formatDate(endOfDay(new Date()))
+ ]
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+// 鍒ゆ柇鏄惁涓哄鏉傛暟鎹被鍨嬶紙struct 鎴� array锛�
+const isComplexDataType = computed(() => {
+ if (!thingModelDataType.value) return false
+ return [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY].includes(
+ thingModelDataType.value as any
+ )
+})
+
+// Echarts 鏁版嵁
+const echartsData = computed(() => {
+ if (!list.value || list.value.length === 0) return []
+ return list.value.map((item) => [item.updateTime, item.value])
+})
+// Echarts 閰嶇疆
+const echartsOption = reactive<any>({
+ title: {
+ text: '璁惧灞炴�у��',
+ left: 'center'
+ },
+ grid: {
+ left: 60,
+ right: 40,
+ bottom: 80,
+ top: 80,
+ containLabel: true
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ }
+ },
+ xAxis: {
+ type: 'time',
+ name: '鏃堕棿',
+ axisLabel: {
+ formatter: (value: number) => formatDate(new Date(value), 'MM-DD HH:mm')
+ }
+ },
+ yAxis: {
+ type: 'value',
+ name: '灞炴�у��'
+ },
+ series: [
+ {
+ name: '灞炴�у��',
+ type: 'line',
+ smooth: true,
+ symbol: 'circle',
+ symbolSize: 6,
+ lineStyle: {
+ width: 2,
+ color: '#1890FF'
+ },
+ itemStyle: {
+ color: '#1890FF'
+ },
+ data: []
+ }
+ ],
+ dataZoom: [
+ {
+ type: 'inside'
+ },
+ {
+ type: 'slider',
+ height: 30
+ }
+ ]
+})
+
+/** 鑾峰緱璁惧鍘嗗彶鏁版嵁 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeviceApi.getHistoryDevicePropertyList(queryParams)
+ list.value = data || []
+ updateChartData()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (deviceId: number, identifier: string, dataType: string) => {
+ dialogVisible.value = true
+ queryParams.deviceId = deviceId
+ queryParams.identifier = identifier
+ thingModelDataType.value = dataType
+
+ // 濡傛灉鐗╂ā鍨嬫槸 struct銆乤rray锛岄渶瑕侀粯璁や娇鐢� list 妯″紡
+ if (isComplexDataType.value) {
+ viewMode.value = 'list'
+ } else {
+ viewMode.value = 'chart'
+ }
+ // 閲嶇疆鍥捐〃 key锛岀‘淇濇瘡娆℃墦寮�閮借兘姝e父娓叉煋
+ chartKey.value = 0
+
+ // 绛夊緟寮圭獥瀹屽叏娓叉煋鍚庡啀鑾峰彇鏁版嵁
+ await nextTick()
+ await getList()
+}
+
+/** 鏃堕棿鍙樺寲澶勭悊 */
+const handleTimeChange = () => {
+ getList()
+}
+
+/** 鏇存柊鍥捐〃鏁版嵁 */
+const updateChartData = () => {
+ if (echartsOption.series && echartsOption.series[0]) {
+ echartsOption.series[0].data = echartsData.value
+ }
+}
+
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue b/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue
new file mode 100644
index 0000000..fd84561
--- /dev/null
+++ b/src/views/iot/device/device/detail/DeviceDetailsThingModelService.vue
@@ -0,0 +1,208 @@
+<!-- 璁惧鏈嶅姟璋冪敤 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="80px"
+ @submit.prevent
+ >
+ <el-form-item label="鏍囪瘑绗�" prop="identifier">
+ <el-select
+ v-model="queryParams.identifier"
+ placeholder="璇烽�夋嫨鏈嶅姟鏍囪瘑绗�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="service in serviceThingModels"
+ :key="service.identifier"
+ :label="`${service.name}(${service.identifier})`"
+ :value="service.identifier!"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃堕棿鑼冨洿" prop="times">
+ <el-date-picker
+ v-model="queryParams.times"
+ type="datetimerange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫椂闂�"
+ end-placeholder="缁撴潫鏃堕棿"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ class="!w-360px"
+ :shortcuts="defaultShortcuts"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <!-- 鏈嶅姟璋冪敤鍒楄〃 -->
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="璋冪敤鏃堕棿" align="center" prop="requestTime" width="180px">
+ <template #default="scope">
+ {{ scope.row.request?.reportTime ? formatDate(scope.row.request.reportTime) : '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍝嶅簲鏃堕棿" align="center" prop="responseTime" width="180px">
+ <template #default="scope">
+ {{ scope.row.reply?.reportTime ? formatDate(scope.row.reply.reportTime) : '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏍囪瘑绗�" align="center" prop="identifier" width="160px">
+ <template #default="scope">
+ <el-tag type="primary" size="small">
+ {{ scope.row.request?.identifier }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈嶅姟鍚嶇О" align="center" prop="serviceName" width="160px">
+ <template #default="scope">
+ {{ getServiceName(scope.row.request?.identifier) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="璋冪敤鏂瑰紡" align="center" prop="callType" width="100px">
+ <template #default="scope">
+ {{ getCallType(scope.row.request?.identifier) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="杈撳叆鍙傛暟" align="center" prop="inputParams">
+ <template #default="scope"> {{ parseParams(scope.row.request?.params) }} </template>
+ </el-table-column>
+ <el-table-column label="杈撳嚭鍙傛暟" align="center" prop="outputParams">
+ <template #default="scope">
+ <span v-if="scope.row.reply">
+ {{
+ `{"code":${scope.row.reply.code},"msg":"${scope.row.reply.msg}","data":${scope.row.reply.data}\}`
+ }}
+ </span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelData } from '@/api/iot/thingmodel'
+import { formatDate, defaultShortcuts } from '@/utils/formatTime'
+import {
+ getThingModelServiceCallTypeLabel,
+ IotDeviceMessageMethodEnum,
+ IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+
+const props = defineProps<{
+ deviceId: number
+ thingModelList: ThingModelData[]
+}>()
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([] as any[]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ deviceId: props.deviceId,
+ method: IotDeviceMessageMethodEnum.SERVICE_INVOKE.method, // 鍥哄畾绛涢�夋湇鍔¤皟鐢ㄦ秷鎭�
+ identifier: '',
+ times: [] as any[],
+ pageNo: 1,
+ pageSize: 10
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏈嶅姟绫诲瀷鐨勭墿妯″瀷鏁版嵁 */
+const serviceThingModels = computed(() => {
+ return props.thingModelList.filter(
+ (item: ThingModelData) => item.type === IoTThingModelTypeEnum.SERVICE
+ )
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ if (!props.deviceId) return
+ loading.value = true
+ try {
+ const data = await DeviceApi.getDeviceMessagePairPage(queryParams)
+ list.value = data.list
+ total.value = data.length
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ queryParams.identifier = ''
+ queryParams.times = []
+ handleQuery()
+}
+
+/** 鑾峰彇鏈嶅姟鍚嶇О */
+const getServiceName = (identifier: string | undefined) => {
+ if (!identifier) return '-'
+ const service = serviceThingModels.value.find(
+ (item: ThingModelData) => item.identifier === identifier
+ )
+ return service?.name || identifier
+}
+
+/** 鑾峰彇璋冪敤鏂瑰紡 */
+const getCallType = (identifier: string | undefined) => {
+ if (!identifier) return '-'
+ const service = serviceThingModels.value.find(
+ (item: ThingModelData) => item.identifier === identifier
+ )
+ if (!service?.service?.callType) return '-'
+ return getThingModelServiceCallTypeLabel(service.service.callType) || '-'
+}
+
+/** 瑙f瀽鍙傛暟 */
+const parseParams = (params: string) => {
+ if (!params) return '-'
+ try {
+ const parsed = JSON.parse(params)
+ if (parsed.params) {
+ return JSON.stringify(parsed.params, null, 2)
+ }
+ return JSON.stringify(parsed, null, 2)
+ } catch (error) {
+ return params
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/device/device/detail/index.vue b/src/views/iot/device/device/detail/index.vue
new file mode 100644
index 0000000..3ec756b
--- /dev/null
+++ b/src/views/iot/device/device/detail/index.vue
@@ -0,0 +1,108 @@
+<template>
+ <DeviceDetailsHeader
+ :loading="loading"
+ :product="product"
+ :device="device"
+ @refresh="getDeviceData"
+ />
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="璁惧淇℃伅" name="info">
+ <DeviceDetailsInfo v-if="activeTab === 'info'" :product="product" :device="device" />
+ </el-tab-pane>
+ <el-tab-pane label="鐗╂ā鍨嬫暟鎹�" name="model">
+ <DeviceDetailsThingModel
+ v-if="activeTab === 'model'"
+ :device-id="device.id"
+ :thing-model-list="thingModelList"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="瀛愯澶囩鐞�" v-if="product.deviceType === DeviceTypeEnum.GATEWAY" />
+ <el-tab-pane label="璁惧娑堟伅" name="log">
+ <DeviceDetailsMessage v-if="activeTab === 'log'" :device-id="device.id" />
+ </el-tab-pane>
+ <el-tab-pane label="妯℃嫙璁惧" name="simulator">
+ <DeviceDetailsSimulator
+ v-if="activeTab === 'simulator'"
+ :product="product"
+ :device="device"
+ :thing-model-list="thingModelList"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="璁惧閰嶇疆" name="config">
+ <DeviceDetailConfig
+ v-if="activeTab === 'config'"
+ :device="device"
+ @success="getDeviceData"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+<script lang="ts" setup>
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+import { DeviceTypeEnum, ProductApi, ProductVO } from '@/api/iot/product/product'
+import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
+import DeviceDetailsHeader from './DeviceDetailsHeader.vue'
+import DeviceDetailsInfo from './DeviceDetailsInfo.vue'
+import DeviceDetailsThingModel from './DeviceDetailsThingModel.vue'
+import DeviceDetailsMessage from './DeviceDetailsMessage.vue'
+import DeviceDetailsSimulator from './DeviceDetailsSimulator.vue'
+import DeviceDetailConfig from './DeviceDetailConfig.vue'
+
+defineOptions({ name: 'IoTDeviceDetail' })
+
+const route = useRoute()
+const message = useMessage()
+const id = Number(route.params.id) // 灏嗗瓧绗︿覆杞崲涓烘暟瀛�
+const loading = ref(true) // 鍔犺浇涓�
+const product = ref<ProductVO>({} as ProductVO) // 浜у搧璇︽儏
+const device = ref<DeviceVO>({} as DeviceVO) // 璁惧璇︽儏
+const activeTab = ref('info') // 榛樿婵�娲荤殑鏍囩椤�
+const thingModelList = ref<ThingModelData[]>([]) // 鐗╂ā鍨嬪垪琛ㄦ暟鎹�
+
+/** 鑾峰彇璁惧璇︽儏 */
+const getDeviceData = async () => {
+ loading.value = true
+ try {
+ device.value = await DeviceApi.getDevice(id)
+ await getProductData(device.value.productId)
+ await getThingModelList(device.value.productId)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鑾峰彇浜у搧璇︽儏 */
+const getProductData = async (id: number) => {
+ product.value = await ProductApi.getProduct(id)
+}
+
+/** 鑾峰彇鐗╂ā鍨嬪垪琛� */
+const getThingModelList = async (productId: number) => {
+ try {
+ const data = await ThingModelApi.getThingModelList({
+ productId: productId
+ })
+ thingModelList.value = data || []
+ } catch (error) {
+ console.error('鑾峰彇鐗╂ā鍨嬪垪琛ㄥけ璐�:', error)
+ thingModelList.value = []
+ }
+}
+
+/** 鍒濆鍖� */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const router = useRouter() // 璺敱
+const { currentRoute } = router
+onMounted(async () => {
+ if (!id) {
+ message.warning('鍙傛暟閿欒锛屼骇鍝佷笉鑳戒负绌猴紒')
+ delView(unref(currentRoute))
+ return
+ }
+ await getDeviceData()
+ activeTab.value = (route.query.tab as string) || 'info'
+})
+</script>
diff --git a/src/views/iot/device/device/index.vue b/src/views/iot/device/device/index.vue
new file mode 100644
index 0000000..67ef01c
--- /dev/null
+++ b/src/views/iot/device/device/index.vue
@@ -0,0 +1,529 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="product in products"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="DeviceName" prop="deviceName">
+ <el-input
+ v-model="queryParams.deviceName"
+ placeholder="璇疯緭鍏� DeviceName"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞鍚嶇О" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ placeholder="璇疯緭鍏ュ娉ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="璁惧绫诲瀷" prop="deviceType">
+ <el-select
+ v-model="queryParams.deviceType"
+ placeholder="璇烽�夋嫨璁惧绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨璁惧鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DEVICE_STATE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧鍒嗙粍" prop="groupId">
+ <el-select
+ v-model="queryParams.groupId"
+ placeholder="璇烽�夋嫨璁惧鍒嗙粍"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="group in deviceGroups"
+ :key="group.id"
+ :label="group.name"
+ :value="group.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item class="float-right !mr-0 !mb-0">
+ <el-button-group>
+ <el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
+ <Icon icon="ep:grid" />
+ </el-button>
+ <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
+ <Icon icon="ep:list" />
+ </el-button>
+ </el-button-group>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:device:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />
+ 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['iot:device:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button type="warning" plain @click="handleImport" v-hasPermi="['iot:device:import']">
+ <Icon icon="ep:upload" /> 瀵煎叆
+ </el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openGroupForm"
+ :disabled="selectedIds.length === 0"
+ v-hasPermi="['iot:device:update']"
+ >
+ <Icon icon="ep:folder-add" class="mr-5px" /> 娣诲姞鍒板垎缁�
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDeleteList"
+ :disabled="selectedIds.length === 0"
+ v-hasPermi="['iot:device:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <template v-if="viewMode === 'card'">
+ <el-row :gutter="16">
+ <el-col v-for="item in list" :key="item.id" :xs="24" :sm="12" :md="12" :lg="6" class="mb-4">
+ <el-card
+ class="h-full transition-colors relative overflow-hidden"
+ :body-style="{ padding: '0' }"
+ >
+ <!-- 娣诲姞娓愬彉鑳屾櫙灞� -->
+ <div
+ class="absolute top-0 left-0 right-0 h-[50px] pointer-events-none"
+ :class="[
+ item.state === DeviceStateEnum.ONLINE
+ ? 'bg-gradient-to-b from-[#eefaff] to-transparent'
+ : 'bg-gradient-to-b from-[#fff1f1] to-transparent'
+ ]"
+ >
+ </div>
+ <div class="p-4 relative">
+ <!-- 鏍囬鍖哄煙 -->
+ <div class="flex items-center mb-3">
+ <div class="mr-2.5 flex items-center">
+ <el-image :src="defaultIconUrl" class="w-[18px] h-[18px]" />
+ </div>
+ <div class="text-[16px] font-600 flex-1">{{ item.deviceName }}</div>
+ <!-- 娣诲姞璁惧鐘舵�佹爣绛� -->
+ <div class="inline-flex items-center">
+ <div
+ class="w-1 h-1 rounded-full mr-1.5"
+ :class="
+ item.state === DeviceStateEnum.ONLINE
+ ? 'bg-[var(--el-color-success)]'
+ : 'bg-[var(--el-color-danger)]'
+ "
+ >
+ </div>
+ <el-text
+ class="!text-xs font-bold"
+ :type="item.state === DeviceStateEnum.ONLINE ? 'success' : 'danger'"
+ >
+ {{ getDictLabel(DICT_TYPE.IOT_DEVICE_STATE, item.state) }}
+ </el-text>
+ </div>
+ </div>
+
+ <!-- 淇℃伅鍖哄煙 -->
+ <div class="flex items-center text-[14px]">
+ <div class="flex-1">
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">鎵�灞炰骇鍝�</span>
+ <el-link class="text-[#0070ff]" @click="openProductDetail(item.productId)">
+ {{ products.find((p) => p.id === item.productId)?.name }}
+ </el-link>
+ </div>
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">璁惧绫诲瀷</span>
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="item.deviceType" />
+ </div>
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">澶囨敞鍚嶇О</span>
+ <span
+ class="text-[#0b1d30] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[130px]"
+ >
+ {{ item.nickname || item.deviceName }}
+ </span>
+ </div>
+ </div>
+ <div class="w-[100px] h-[100px]">
+ <el-image :src="defaultPicUrl" class="w-full h-full" />
+ </div>
+ </div>
+
+ <!-- 鍒嗛殧绾� -->
+ <el-divider class="!my-3" />
+
+ <!-- 鎸夐挳 -->
+ <div class="flex items-center px-0">
+ <el-button
+ class="flex-1 !px-2 !h-[32px] text-[13px]"
+ type="primary"
+ plain
+ @click="openForm('update', item.id)"
+ v-hasPermi="['iot:device:update']"
+ >
+ <Icon icon="ep:edit-pen" class="mr-1" />
+ 缂栬緫
+ </el-button>
+ <el-button
+ class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
+ type="warning"
+ plain
+ @click="openDetail(item.id)"
+ >
+ <Icon icon="ep:view" class="mr-1" />
+ 璇︽儏
+ </el-button>
+ <el-button
+ class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
+ type="info"
+ plain
+ @click="openModel(item.id)"
+ >
+ <Icon icon="ep:tickets" class="mr-1" />
+ 鏁版嵁
+ </el-button>
+ <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
+ <el-button
+ class="!px-2 !h-[32px] text-[13px]"
+ type="danger"
+ plain
+ @click="handleDelete(item.id)"
+ v-hasPermi="['iot:device:delete']"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </template>
+
+ <!-- 鍒楄〃瑙嗗浘 -->
+ <el-table
+ v-else
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="DeviceName" align="center" prop="deviceName">
+ <template #default="scope">
+ <el-link @click="openDetail(scope.row.id)">{{ scope.row.deviceName }}</el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞鍚嶇О" align="center" prop="nickname" />
+ <el-table-column label="鎵�灞炰骇鍝�" align="center" prop="productId">
+ <template #default="scope">
+ <el-link @click="openProductDetail(scope.row.productId)">
+ {{ products.find((p) => p.id === scope.row.productId)?.name || '-' }}
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="璁惧绫诲瀷" align="center" prop="deviceType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵�灞炲垎缁�" align="center" prop="groupId">
+ <template #default="scope">
+ <template v-if="scope.row.groupIds?.length">
+ <el-tag v-for="id in scope.row.groupIds" :key="id" class="ml-5px" size="small">
+ {{ deviceGroups.find((g) => g.id === id)?.name }}
+ </el-tag>
+ </template>
+ </template>
+ </el-table-column>
+ <el-table-column label="璁惧鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鏈�鍚庝笂绾挎椂闂�"
+ align="center"
+ prop="onlineTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row.id)"
+ v-hasPermi="['iot:product:query']"
+ >
+ 鏌ョ湅
+ </el-button>
+ <el-button link type="primary" @click="openModel(scope.row.id)"> 鏃ュ織 </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['iot:device:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:device:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DeviceForm ref="formRef" @success="getList" />
+ <!-- 鍒嗙粍琛ㄥ崟缁勪欢 -->
+ <DeviceGroupForm ref="groupFormRef" @success="getList" />
+ <!-- 瀵煎叆琛ㄥ崟缁勪欢 -->
+ <DeviceImportForm ref="importFormRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DeviceApi, DeviceVO, DeviceStateEnum } from '@/api/iot/device/device'
+import DeviceForm from './DeviceForm.vue'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
+import download from '@/utils/download'
+import DeviceGroupForm from './DeviceGroupForm.vue'
+import DeviceImportForm from './DeviceImportForm.vue'
+
+/** IoT 璁惧鍒楄〃 */
+defineOptions({ name: 'IoTDevice' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const route = useRoute()
+
+const loading = ref(true) // 鍒楄〃鍔犺浇涓�
+const list = ref<DeviceVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ deviceName: undefined,
+ productId: undefined as number | undefined,
+ deviceType: undefined,
+ nickname: undefined,
+ status: undefined,
+ groupId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鍔犺浇鐘舵��
+const products = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const deviceGroups = ref<DeviceGroupVO[]>([]) // 璁惧鍒嗙粍鍒楄〃
+const selectedIds = ref<number[]>([]) // 閫変腑鐨勮澶囩紪鍙锋暟缁�
+const viewMode = ref<'card' | 'list'>('card') // 瑙嗗浘妯″紡鐘舵��
+const defaultPicUrl = ref('/src/assets/imgs/iot/device.png') // 榛樿璁惧鍥剧墖
+const defaultIconUrl = ref('/src/assets/svgs/iot/card-fill.svg') // 榛樿璁惧鍥炬爣
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeviceApi.getDevicePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ selectedIds.value = [] // 娓呯┖閫夋嫨
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鎵撳紑璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'IoTDeviceDetail', params: { id } })
+}
+
+/** 璺宠浆鍒颁骇鍝佽鎯呴〉闈� */
+const openProductDetail = (productId: number) => {
+ push({ name: 'IoTProductDetail', params: { id: productId } })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 璧峰垹闄�
+ await DeviceApi.deleteDevice(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鏂规硶 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await DeviceApi.exportDeviceExcel(queryParams)
+ download.excel(data, '鐗╄仈缃戣澶�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+const handleSelectionChange = (selection: DeviceVO[]) => {
+ selectedIds.value = selection.map((item) => item.id)
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDeleteList = async () => {
+ try {
+ await message.delConfirm()
+ // 鎵ц鎵归噺鍒犻櫎
+ await DeviceApi.deleteDeviceList(selectedIds.value)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 娣诲姞鍒板垎缁勬搷浣� */
+const groupFormRef = ref()
+const openGroupForm = () => {
+ groupFormRef.value.open(selectedIds.value)
+}
+
+/** 鎵撳紑鐗╂ā鍨嬫暟鎹� */
+const openModel = (id: number) => {
+ push({ name: 'IoTDeviceDetail', params: { id }, query: { tab: 'model' } })
+}
+
+/** 璁惧瀵煎叆 */
+const importFormRef = ref()
+const handleImport = () => {
+ importFormRef.value.open()
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 澶勭悊 productId 鍙傛暟
+ const { productId } = route.query
+ if (productId) {
+ queryParams.productId = Number(productId)
+ }
+ await getList()
+
+ // 鑾峰彇浜у搧鍒楄〃
+ products.value = await ProductApi.getSimpleProductList()
+ // 鑾峰彇鍒嗙粍鍒楄〃
+ deviceGroups.value = await DeviceGroupApi.getSimpleDeviceGroupList()
+})
+</script>
diff --git a/src/views/iot/device/group/DeviceGroupForm.vue b/src/views/iot/device/group/DeviceGroupForm.vue
new file mode 100644
index 0000000..6487257
--- /dev/null
+++ b/src/views/iot/device/group/DeviceGroupForm.vue
@@ -0,0 +1,112 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍒嗙粍鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ垎缁勫悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鍒嗙粍鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍒嗙粍鎻忚堪" prop="description">
+ <el-input type="textarea" v-model="formData.description" placeholder="璇疯緭鍏ュ垎缁勬弿杩�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
+
+/** IoT 璁惧鍒嗙粍 琛ㄥ崟 */
+defineOptions({ name: 'IoTDeviceGroupForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ status: undefined,
+ description: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍒嗙粍鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鍒嗙粍鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DeviceGroupApi.getDeviceGroup(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as DeviceGroupVO
+ if (formType.value === 'create') {
+ await DeviceGroupApi.createDeviceGroup(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DeviceGroupApi.updateDeviceGroup(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ status: undefined,
+ description: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/device/group/index.vue b/src/views/iot/device/group/index.vue
new file mode 100644
index 0000000..ea2e4be
--- /dev/null
+++ b/src/views/iot/device/group/index.vue
@@ -0,0 +1,169 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙粍鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ垎缁勫悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:device-group:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍒嗙粍 ID" align="center" prop="id" />
+ <el-table-column label="鍒嗙粍鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鍒嗙粍鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒嗙粍鎻忚堪" align="center" prop="description" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="璁惧鏁伴噺" align="center" prop="deviceCount" />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['iot:device-group:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:device-group:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DeviceGroupForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DeviceGroupApi, DeviceGroupVO } from '@/api/iot/device/group'
+import DeviceGroupForm from './DeviceGroupForm.vue'
+
+/** IoT 璁惧鍒嗙粍鍒楄〃 */
+defineOptions({ name: 'IoTDeviceGroup' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<DeviceGroupVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeviceGroupApi.getDeviceGroupPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DeviceGroupApi.deleteDeviceGroup(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/home/components/ComparisonCard.vue b/src/views/iot/home/components/ComparisonCard.vue
new file mode 100644
index 0000000..2da729e
--- /dev/null
+++ b/src/views/iot/home/components/ComparisonCard.vue
@@ -0,0 +1,50 @@
+<template>
+ <el-card class="stat-card" shadow="never" :loading="loading">
+ <div class="flex flex-col">
+ <div class="flex justify-between items-center mb-1">
+ <span class="text-gray-500 text-base font-medium">{{ title }}</span>
+ <Icon :icon="icon" :class="`text-[32px] ${iconColor}`" />
+ </div>
+ <span class="text-3xl font-bold text-gray-700">
+ <span v-if="value === -1">--</span>
+ <span v-else>{{ value }}</span>
+ </span>
+ <el-divider class="my-2" />
+ <div class="flex justify-between items-center text-gray-400 text-sm">
+ <span>浠婃棩鏂板</span>
+ <span class="text-green-500" v-if="todayCount !== -1">+{{ todayCount }}</span>
+ <span v-else>--</span>
+ </div>
+ </div>
+ </el-card>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+
+/** 銆愭�绘暟 + 鏂板鏁般�戠粺璁″崱鐗囩粍浠� */
+defineOptions({ name: 'IoTComparisonCard' })
+
+const props = defineProps({
+ title: propTypes.string.def('').isRequired,
+ value: propTypes.number.def(0).isRequired,
+ todayCount: propTypes.number.def(0).isRequired,
+ icon: propTypes.string.def('').isRequired,
+ iconColor: propTypes.string.def(''),
+ loading: {
+ type: Boolean,
+ default: false
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+.stat-card {
+ transition: all 0.3s;
+
+ &:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgb(0 0 0 / 8%);
+ }
+}
+</style>
diff --git a/src/views/iot/home/components/DeviceCountCard.vue b/src/views/iot/home/components/DeviceCountCard.vue
new file mode 100644
index 0000000..5514eff
--- /dev/null
+++ b/src/views/iot/home/components/DeviceCountCard.vue
@@ -0,0 +1,131 @@
+<template>
+ <el-card class="chart-card" shadow="never" :loading="loading">
+ <template #header>
+ <div class="flex items-center">
+ <span class="text-base font-medium text-gray-600">璁惧鏁伴噺缁熻</span>
+ </div>
+ </template>
+ <div v-if="loading && !hasData" class="h-[240px] flex justify-center items-center">
+ <el-empty description="鍔犺浇涓�..." />
+ </div>
+ <div v-else-if="!hasData" class="h-[240px] flex justify-center items-center">
+ <el-empty description="鏆傛棤鏁版嵁" />
+ </div>
+ <div v-else ref="deviceCountChartRef" class="h-[240px]"></div>
+ </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { PieChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { TooltipComponent, LegendComponent } from 'echarts/components'
+import { LabelLayout } from 'echarts/features'
+import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
+import type { PropType } from 'vue'
+
+/** 銆愯澶囨暟閲忋�戠粺璁″崱鐗� */
+defineOptions({ name: 'DeviceCountCard' })
+
+const props = defineProps({
+ statsData: {
+ type: Object as PropType<IotStatisticsSummaryRespVO>,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const deviceCountChartRef = ref()
+
+/** 鏄惁鏈夋暟鎹� */
+const hasData = computed(() => {
+ if (!props.statsData) return false
+
+ const categories = Object.entries(props.statsData.productCategoryDeviceCounts || {})
+ return categories.length > 0 && props.statsData.deviceCount !== -1
+})
+
+/** 鍒濆鍖栧浘琛� */
+const initChart = () => {
+ // 濡傛灉娌℃湁鏁版嵁锛屽垯涓嶅垵濮嬪寲鍥捐〃
+ if (!hasData.value) return
+ // 纭繚 DOM 鍏冪礌瀛樺湪涓斿凡娓叉煋
+ if (!deviceCountChartRef.value) {
+ console.warn('鍥捐〃DOM鍏冪礌涓嶅瓨鍦�')
+ return
+ }
+
+ echarts.use([TooltipComponent, LegendComponent, PieChart, CanvasRenderer, LabelLayout])
+ try {
+ const chart = echarts.init(deviceCountChartRef.value)
+ chart.setOption({
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ top: '5%',
+ right: '10%',
+ align: 'left',
+ orient: 'vertical',
+ icon: 'circle'
+ },
+ series: [
+ {
+ name: 'Access From',
+ type: 'pie',
+ radius: ['50%', '80%'],
+ avoidLabelOverlap: false,
+ center: ['30%', '50%'],
+ label: {
+ show: false,
+ position: 'outside'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 20,
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: Object.entries(props.statsData.productCategoryDeviceCounts).map(
+ ([name, value]) => ({
+ name,
+ value
+ })
+ )
+ }
+ ]
+ })
+ return chart
+ } catch (error) {
+ console.error('鍒濆鍖栧浘琛ㄥけ璐�:', error)
+ return null
+ }
+}
+
+/** 鐩戝惉鏁版嵁鍙樺寲 */
+watch(
+ () => props.statsData,
+ () => {
+ // 浣跨敤 nextTick 纭繚 DOM 宸叉洿鏂�
+ nextTick(() => {
+ initChart()
+ })
+ },
+ { deep: true }
+)
+
+/** 缁勪欢鎸傝浇鏃跺垵濮嬪寲鍥捐〃 */
+onMounted(async () => {
+ // 浣跨敤 nextTick 纭繚 DOM 宸叉洿鏂�
+ await nextTick(() => {
+ initChart()
+ })
+})
+</script>
diff --git a/src/views/iot/home/components/DeviceStateCountCard.vue b/src/views/iot/home/components/DeviceStateCountCard.vue
new file mode 100644
index 0000000..fbda0a9
--- /dev/null
+++ b/src/views/iot/home/components/DeviceStateCountCard.vue
@@ -0,0 +1,163 @@
+<template>
+ <el-card class="chart-card" shadow="never" :loading="loading">
+ <template #header>
+ <div class="flex items-center">
+ <span class="text-base font-medium text-gray-600">璁惧鐘舵�佺粺璁�</span>
+ </div>
+ </template>
+ <div v-if="loading && !hasData" class="h-[240px] flex justify-center items-center">
+ <el-empty description="鍔犺浇涓�..." />
+ </div>
+ <div v-else-if="!hasData" class="h-[240px] flex justify-center items-center">
+ <el-empty description="鏆傛棤鏁版嵁" />
+ </div>
+ <el-row v-else class="h-[240px]">
+ <el-col :span="8" class="flex flex-col items-center">
+ <div ref="deviceOnlineCountChartRef" class="h-[160px] w-full"></div>
+ <div class="text-center mt-2">
+ <span class="text-sm text-gray-600">鍦ㄧ嚎璁惧</span>
+ </div>
+ </el-col>
+ <el-col :span="8" class="flex flex-col items-center">
+ <div ref="deviceOfflineChartRef" class="h-[160px] w-full"></div>
+ <div class="text-center mt-2">
+ <span class="text-sm text-gray-600">绂荤嚎璁惧</span>
+ </div>
+ </el-col>
+ <el-col :span="8" class="flex flex-col items-center">
+ <div ref="deviceActiveChartRef" class="h-[160px] w-full"></div>
+ <div class="text-center mt-2">
+ <span class="text-sm text-gray-600">寰呮縺娲昏澶�</span>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { GaugeChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { IotStatisticsSummaryRespVO } from '@/api/iot/statistics'
+import type { PropType } from 'vue'
+
+/** 銆愯澶囩姸鎬併�戠粺璁″崱鐗� */
+defineOptions({ name: 'DeviceStateCountCard' })
+
+const props = defineProps({
+ statsData: {
+ type: Object as PropType<IotStatisticsSummaryRespVO>,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const deviceOnlineCountChartRef = ref()
+const deviceOfflineChartRef = ref()
+const deviceActiveChartRef = ref()
+
+/** 鏄惁鏈夋暟鎹� */
+const hasData = computed(() => {
+ if (!props.statsData) return false
+ return props.statsData.deviceCount !== -1
+})
+
+/** 鍒濆鍖栦华琛ㄧ洏鍥捐〃 */
+const initGaugeChart = (el: any, value: number, color: string) => {
+ // 纭繚 DOM 鍏冪礌瀛樺湪涓斿凡娓叉煋
+ if (!el) {
+ console.warn('鍥捐〃DOM鍏冪礌涓嶅瓨鍦�')
+ return
+ }
+
+ echarts.use([GaugeChart, CanvasRenderer])
+ try {
+ const chart = echarts.init(el)
+ chart.setOption({
+ series: [
+ {
+ type: 'gauge',
+ startAngle: 360,
+ endAngle: 0,
+ min: 0,
+ max: props.statsData.deviceCount || 100, // 浣跨敤璁惧鎬绘暟浣滀负鏈�澶у��
+ progress: {
+ show: true,
+ width: 12,
+ itemStyle: {
+ color: color
+ }
+ },
+ axisLine: {
+ lineStyle: {
+ width: 12,
+ color: [[1, '#E5E7EB']]
+ }
+ },
+ axisTick: { show: false },
+ splitLine: { show: false },
+ axisLabel: { show: false },
+ pointer: { show: false },
+ anchor: { show: false },
+ title: { show: false },
+ detail: {
+ valueAnimation: true,
+ fontSize: 24,
+ fontWeight: 'bold',
+ fontFamily: 'Inter, sans-serif',
+ color: color,
+ offsetCenter: [0, '0'],
+ formatter: (value: number) => {
+ return `${value} 涓猔
+ }
+ },
+ data: [{ value: value }]
+ }
+ ]
+ })
+ return chart
+ } catch (error) {
+ console.error('鍒濆鍖栧浘琛ㄥけ璐�:', error)
+ return null
+ }
+}
+
+/** 鍒濆鍖栨墍鏈夊浘琛� */
+const initCharts = () => {
+ // 濡傛灉娌℃湁鏁版嵁锛屽垯涓嶅垵濮嬪寲鍥捐〃
+ if (!hasData.value) return
+
+ // 浣跨敤 nextTick 纭繚 DOM 宸叉洿鏂�
+ nextTick(() => {
+ // 鍦ㄧ嚎璁惧缁熻
+ if (deviceOnlineCountChartRef.value) {
+ initGaugeChart(deviceOnlineCountChartRef.value, props.statsData.deviceOnlineCount, '#0d9')
+ }
+ // 绂荤嚎璁惧缁熻
+ if (deviceOfflineChartRef.value) {
+ initGaugeChart(deviceOfflineChartRef.value, props.statsData.deviceOfflineCount, '#f50')
+ }
+ // 寰呮縺娲昏澶囩粺璁�
+ if (deviceActiveChartRef.value) {
+ initGaugeChart(deviceActiveChartRef.value, props.statsData.deviceInactiveCount, '#05b')
+ }
+ })
+}
+
+/** 鐩戝惉鏁版嵁鍙樺寲 */
+watch(
+ () => props.statsData,
+ () => {
+ initCharts()
+ },
+ { deep: true }
+)
+
+/** 缁勪欢鎸傝浇鏃跺垵濮嬪寲鍥捐〃 */
+onMounted(() => {
+ initCharts()
+})
+</script>
diff --git a/src/views/iot/home/components/MessageTrendCard.vue b/src/views/iot/home/components/MessageTrendCard.vue
new file mode 100644
index 0000000..1d2d4c5
--- /dev/null
+++ b/src/views/iot/home/components/MessageTrendCard.vue
@@ -0,0 +1,227 @@
+<template>
+ <el-card class="chart-card" shadow="never" :loading="loading">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <span class="text-base font-medium text-gray-600">娑堟伅閲忕粺璁�</span>
+ <div class="flex flex-wrap items-center gap-4">
+ <el-form-item label="鏃堕棿鑼冨洿" class="!mb-0">
+ <el-date-picker
+ v-model="queryParams.times"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ :shortcuts="defaultShortcuts"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鏃堕棿闂撮殧" class="!mb-0">
+ <el-select
+ v-model="queryParams.interval"
+ class="!w-120px"
+ placeholder="闂撮殧绫诲瀷"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.DATE_INTERVAL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+ </div>
+ </template>
+ <div v-if="loading && !hasData" class="h-[300px] flex justify-center items-center">
+ <el-empty description="鍔犺浇涓�..." />
+ </div>
+ <div v-else-if="!hasData" class="h-[300px] flex justify-center items-center">
+ <el-empty description="鏆傛棤鏁版嵁" />
+ </div>
+ <div v-else ref="messageChartRef" class="h-[300px]"></div>
+ </el-card>
+</template>
+
+<script lang="ts" setup>
+import * as echarts from 'echarts/core'
+import { LineChart } from 'echarts/charts'
+import { CanvasRenderer } from 'echarts/renderers'
+import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
+import { UniversalTransition } from 'echarts/features'
+import {
+ StatisticsApi,
+ IotStatisticsDeviceMessageSummaryByDateRespVO,
+ IotStatisticsDeviceMessageReqVO
+} from '@/api/iot/statistics'
+import { formatDate, beginOfDay, endOfDay, defaultShortcuts } from '@/utils/formatTime'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+/** 娑堟伅瓒嬪娍缁熻鍗$墖 */
+defineOptions({ name: 'MessageTrendCard' })
+
+const messageChartRef = ref()
+const loading = ref(false)
+const messageData = ref<IotStatisticsDeviceMessageSummaryByDateRespVO[]>([])
+
+const queryParams = reactive<IotStatisticsDeviceMessageReqVO>({
+ interval: 1, // DAY, 鏃�
+ times: [
+ // 榛樿鏄剧ず鏈�杩戜竴鍛ㄧ殑鏁版嵁
+ formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7))),
+ formatDate(endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24)))
+ ]
+}) // 鏌ヨ鍙傛暟
+
+// 鏄惁鏈夋暟鎹�
+const hasData = computed(() => {
+ return messageData.value && messageData.value.length > 0
+})
+
+// 澶勭悊鏌ヨ鎿嶄綔
+const handleQuery = () => {
+ fetchMessageData()
+}
+
+// 鑾峰彇娑堟伅缁熻鏁版嵁
+const fetchMessageData = async () => {
+ loading.value = true
+ try {
+ messageData.value = await StatisticsApi.getDeviceMessageSummaryByDate(queryParams)
+
+ // 浣跨敤 nextTick 纭繚鏁版嵁鏇存柊鍚庨噸鏂版覆鏌撳浘琛�
+ await nextTick()
+ initChart()
+ } catch (error) {
+ console.error('鑾峰彇娑堟伅缁熻鏁版嵁澶辫触:', error)
+ messageData.value = []
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鍒濆鍖栧浘琛�
+const initChart = () => {
+ // 妫�鏌ユ槸鍚︽湁鏁版嵁鍙互缁樺埗
+ if (!hasData.value) return
+ // 纭繚 DOM 鍏冪礌瀛樺湪涓斿凡娓叉煋
+ if (!messageChartRef.value) {
+ console.warn('鍥捐〃 DOM 鍏冪礌涓嶅瓨鍦�')
+ return
+ }
+
+ // 閰嶇疆鍥捐〃
+ echarts.use([
+ LineChart,
+ CanvasRenderer,
+ GridComponent,
+ LegendComponent,
+ TooltipComponent,
+ UniversalTransition
+ ])
+ try {
+ const chart = echarts.init(messageChartRef.value)
+ chart.setOption({
+ tooltip: {
+ trigger: 'axis',
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
+ borderColor: '#E5E7EB',
+ textStyle: {
+ color: '#374151'
+ }
+ },
+ legend: {
+ data: ['涓婅娑堟伅閲�', '涓嬭娑堟伅閲�'],
+ textStyle: {
+ color: '#374151',
+ fontWeight: 500
+ }
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: messageData.value.map((item) => item.time),
+ axisLine: {
+ lineStyle: {
+ color: '#E5E7EB'
+ }
+ },
+ axisLabel: {
+ color: '#6B7280'
+ }
+ },
+ yAxis: {
+ type: 'value',
+ axisLine: {
+ lineStyle: {
+ color: '#E5E7EB'
+ }
+ },
+ axisLabel: {
+ color: '#6B7280'
+ },
+ splitLine: {
+ lineStyle: {
+ color: '#F3F4F6'
+ }
+ }
+ },
+ series: [
+ {
+ name: '涓婅娑堟伅閲�',
+ type: 'line',
+ smooth: true,
+ data: messageData.value.map((item) => item.upstreamCount),
+ itemStyle: {
+ color: '#3B82F6'
+ },
+ lineStyle: {
+ width: 2
+ },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(59, 130, 246, 0.2)' },
+ { offset: 1, color: 'rgba(59, 130, 246, 0)' }
+ ])
+ }
+ },
+ {
+ name: '涓嬭娑堟伅閲�',
+ type: 'line',
+ smooth: true,
+ data: messageData.value.map((item) => item.downstreamCount),
+ itemStyle: {
+ color: '#10B981'
+ },
+ lineStyle: {
+ width: 2
+ },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(16, 185, 129, 0.2)' },
+ { offset: 1, color: 'rgba(16, 185, 129, 0)' }
+ ])
+ }
+ }
+ ]
+ })
+ return chart
+ } catch (error) {
+ console.error('鍒濆鍖栧浘琛ㄥけ璐�:', error)
+ return null
+ }
+}
+
+/** 缁勪欢鎸傝浇鏃跺垵濮嬪寲 */
+onMounted(() => {
+ fetchMessageData()
+})
+</script>
diff --git a/src/views/iot/home/index.vue b/src/views/iot/home/index.vue
new file mode 100644
index 0000000..3d60ace
--- /dev/null
+++ b/src/views/iot/home/index.vue
@@ -0,0 +1,110 @@
+<template>
+ <!-- 绗竴琛岋細缁熻鍗$墖琛� -->
+ <el-row :gutter="16" class="mb-4">
+ <el-col :span="6">
+ <ComparisonCard
+ title="鍒嗙被鏁伴噺"
+ :value="statsData.productCategoryCount"
+ :todayCount="statsData.productCategoryTodayCount"
+ icon="ep:menu"
+ iconColor="text-blue-400"
+ :loading="loading"
+ />
+ </el-col>
+ <el-col :span="6">
+ <ComparisonCard
+ title="浜у搧鏁伴噺"
+ :value="statsData.productCount"
+ :todayCount="statsData.productTodayCount"
+ icon="ep:box"
+ iconColor="text-orange-400"
+ :loading="loading"
+ />
+ </el-col>
+ <el-col :span="6">
+ <ComparisonCard
+ title="璁惧鏁伴噺"
+ :value="statsData.deviceCount"
+ :todayCount="statsData.deviceTodayCount"
+ icon="ep:cpu"
+ iconColor="text-purple-400"
+ :loading="loading"
+ />
+ </el-col>
+ <el-col :span="6">
+ <ComparisonCard
+ title="璁惧娑堟伅鏁�"
+ :value="statsData.deviceMessageCount"
+ :todayCount="statsData.deviceMessageTodayCount"
+ icon="ep:message"
+ iconColor="text-teal-400"
+ :loading="loading"
+ />
+ </el-col>
+ </el-row>
+
+ <!-- 绗簩琛岋細鍥捐〃琛� -->
+ <el-row :gutter="16" class="mb-4">
+ <el-col :span="12">
+ <DeviceCountCard :statsData="statsData" :loading="loading" />
+ </el-col>
+ <el-col :span="12">
+ <DeviceStateCountCard :statsData="statsData" :loading="loading" />
+ </el-col>
+ </el-row>
+
+ <!-- 绗笁琛岋細娑堟伅缁熻琛� -->
+ <el-row>
+ <el-col :span="24">
+ <MessageTrendCard />
+ </el-col>
+ </el-row>
+
+ <!-- TODO 绗洓琛岋細鍦板浘 -->
+</template>
+
+<script setup lang="ts" name="Index">
+import { IotStatisticsSummaryRespVO, StatisticsApi } from '@/api/iot/statistics'
+import ComparisonCard from './components/ComparisonCard.vue'
+import DeviceCountCard from './components/DeviceCountCard.vue'
+import DeviceStateCountCard from './components/DeviceStateCountCard.vue'
+import MessageTrendCard from './components/MessageTrendCard.vue'
+
+/** IoT 棣栭〉 */
+defineOptions({ name: 'IoTHome' })
+
+const statsData = ref<IotStatisticsSummaryRespVO>({
+ productCategoryCount: -1,
+ productCount: -1,
+ deviceCount: -1,
+ deviceMessageCount: -1,
+ productCategoryTodayCount: -1,
+ productTodayCount: -1,
+ deviceTodayCount: -1,
+ deviceMessageTodayCount: -1,
+ deviceOnlineCount: -1,
+ deviceOfflineCount: -1,
+ deviceInactiveCount: -1,
+ productCategoryDeviceCounts: {}
+}) // 鍩虹缁熻鏁版嵁
+
+const loading = ref(true) // 鍔犺浇鐘舵��
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const getStats = async () => {
+ loading.value = true
+ try {
+ // 鑾峰彇鍩虹缁熻鏁版嵁
+ statsData.value = await StatisticsApi.getStatisticsSummary()
+ } catch (error) {
+ console.error('鑾峰彇缁熻鏁版嵁鍑洪敊:', error)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getStats()
+})
+</script>
diff --git a/src/views/iot/ota/firmware/OtaFirmwareForm.vue b/src/views/iot/ota/firmware/OtaFirmwareForm.vue
new file mode 100644
index 0000000..9689a97
--- /dev/null
+++ b/src/views/iot/ota/firmware/OtaFirmwareForm.vue
@@ -0,0 +1,169 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍥轰欢鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ浐浠跺悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍥轰欢鎻忚堪" prop="description">
+ <el-input
+ v-model="formData.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ浐浠舵弿杩�"
+ />
+ </el-form-item>
+ <el-form-item label="鎵�灞炰骇鍝�" prop="productId">
+ <el-select
+ v-model="formData.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ clearable
+ class="!w-100%"
+ :disabled="formType === 'update'"
+ >
+ <el-option
+ v-for="product in productList"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐗堟湰鍙�" prop="version" v-if="formType === 'create'">
+ <el-input v-model="formData.version" placeholder="璇疯緭鍏ョ増鏈彿" />
+ </el-form-item>
+ <el-form-item label="鍥轰欢鏂囦欢" prop="fileUrl" v-if="formType === 'create'">
+ <UploadFile
+ v-model="formData.fileUrl"
+ :file-type="['bin', 'zip', 'pdf']"
+ :file-size="50"
+ :limit="1"
+ />
+ </el-form-item>
+ <!-- 鏇存柊鏃舵樉绀哄彧璇讳俊鎭� -->
+ <template v-if="formType === 'update'">
+ <el-form-item label="鐗堟湰鍙�">
+ <el-input v-model="formData.version" readonly />
+ </el-form-item>
+ <el-form-item label="鍥轰欢鏂囦欢">
+ <el-link
+ type="primary"
+ :href="formData.fileUrl"
+ target="_blank"
+ download
+ v-if="formData.fileUrl"
+ >
+ <Icon icon="ep:download" class="mr-5px" />
+ 涓嬭浇鍥轰欢鏂囦欢
+ </el-link>
+ <span v-else>鏃犳枃浠�</span>
+ </el-form-item>
+ </template>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { UploadFile } from '@/components/UploadFile'
+
+/** IoT OTA 鍥轰欢琛ㄥ崟 */
+defineOptions({ name: 'IoTOtaFirmwareForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ version: undefined,
+ productId: undefined,
+ fileUrl: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍥轰欢鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ version: [{ required: true, message: '鐗堟湰鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'change' }],
+ fileUrl: [{ required: true, message: '鍥轰欢鏂囦欢涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await IoTOtaFirmwareApi.getOtaFirmware(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+
+ // 鑾峰彇浜у搧鍒楄〃
+ productList.value = await ProductApi.getSimpleProductList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as IoTOtaFirmware
+ if (formType.value === 'create') {
+ await IoTOtaFirmwareApi.createOtaFirmware(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ // 鏇存柊鏃跺彧鎻愪氦鍙紪杈戠殑瀛楁
+ await IoTOtaFirmwareApi.updateOtaFirmware({
+ id: data.id,
+ name: data.name,
+ description: data.description
+ })
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ version: undefined,
+ productId: undefined,
+ fileUrl: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/ota/firmware/detail/index.vue b/src/views/iot/ota/firmware/detail/index.vue
new file mode 100644
index 0000000..00e7578
--- /dev/null
+++ b/src/views/iot/ota/firmware/detail/index.vue
@@ -0,0 +1,143 @@
+<template>
+ <div class="app-container">
+ <!-- 鍥轰欢淇℃伅 -->
+ <ContentWrap title="鍥轰欢淇℃伅" class="mb-20px">
+ <el-descriptions :column="3" v-loading="firmwareLoading" border>
+ <el-descriptions-item label="鍥轰欢鍚嶇О">
+ {{ firmware?.name }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵�灞炰骇鍝�">
+ {{ firmware?.productName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥轰欢鐗堟湰">
+ {{ firmware?.version }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ firmware?.createTime ? formatDate(firmware.createTime) : '-' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍥轰欢鎻忚堪" :span="2">
+ {{ firmware?.description }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+
+ <!-- 鍗囩骇璁惧缁熻 -->
+ <ContentWrap title="鍗囩骇璁惧缁熻" class="mb-20px">
+ <el-row :gutter="20" class="py-20px" v-loading="firmwareStatisticsLoading">
+ <el-col :span="6">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-blue-500">
+ {{
+ Object.values(firmwareStatistics).reduce((sum, count) => sum + (count || 0), 0) || 0
+ }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇璁惧鎬绘暟</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-gray-400">
+ {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">寰呮帹閫�</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-blue-400">
+ {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">宸叉帹閫�</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-yellow-500">
+ {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">姝e湪鍗囩骇</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-green-500">
+ {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇鎴愬姛</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-red-500">
+ {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇澶辫触</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-gray-400">
+ {{ firmwareStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇鍙栨秷</div>
+ </div>
+ </el-col>
+ </el-row>
+ </ContentWrap>
+
+ <!-- 浠诲姟绠$悊 -->
+ <OtaTaskList
+ :firmware-id="firmwareId"
+ :product-id="firmware?.productId"
+ @success="getStatistics"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import { formatDate } from '@/utils/formatTime'
+import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
+import { IoTOtaTaskRecordApi } from '@/api/iot/ota/task/record'
+import { IoTOtaTaskRecordStatusEnum } from '@/views/iot/utils/constants'
+import OtaTaskList from '../../task/OtaTaskList.vue'
+
+/** IoT OTA 鍥轰欢璇︽儏 */
+defineOptions({ name: 'IoTOtaFirmwareDetail' })
+
+const route = useRoute() // 璺敱
+
+const firmwareId = ref(Number(route.params.id)) // 鍥轰欢缂栧彿
+const firmwareLoading = ref(false) // 鍥轰欢鍔犺浇鐘舵��
+const firmware = ref<IoTOtaFirmware>({} as IoTOtaFirmware) // 鍥轰欢淇℃伅
+
+const firmwareStatisticsLoading = ref(false) // 缁熻淇℃伅鍔犺浇鐘舵��
+const firmwareStatistics = ref<Record<string, number>>({}) // 缁熻淇℃伅
+
+/** 鑾峰彇鍥轰欢淇℃伅 */
+const getFirmwareInfo = async () => {
+ firmwareLoading.value = true
+ try {
+ firmware.value = await IoTOtaFirmwareApi.getOtaFirmware(firmwareId.value)
+ } finally {
+ firmwareLoading.value = false
+ }
+}
+
+/** 鑾峰彇鍗囩骇缁熻 */
+const getStatistics = async () => {
+ firmwareStatisticsLoading.value = true
+ try {
+ firmwareStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
+ firmwareId.value
+ )
+ } finally {
+ firmwareStatisticsLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getFirmwareInfo()
+ getStatistics()
+})
+</script>
diff --git a/src/views/iot/ota/firmware/index.vue b/src/views/iot/ota/firmware/index.vue
new file mode 100644
index 0000000..bfc862b
--- /dev/null
+++ b/src/views/iot/ota/firmware/index.vue
@@ -0,0 +1,232 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍥轰欢鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ浐浠跺悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧" prop="productId">
+ <el-select
+ v-model="queryParams.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="product in productList"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:ota-firmware:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="鍥轰欢缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍥轰欢鍚嶇О" align="center" prop="name" />
+ <el-table-column label="鍥轰欢鐗堟湰" align="center" prop="description" />
+ <el-table-column label="鐗堟湰鍙�" align="center" prop="version" />
+ <el-table-column label="鎵�灞炰骇鍝�" align="center" prop="productId">
+ <template #default="scope">
+ <el-link
+ @click="openProductDetail(scope.row.productId)"
+ v-if="getProductName(scope.row.productId)"
+ >
+ {{ getProductName(scope.row.productId) }}
+ </el-link>
+ <span v-else>鍔犺浇涓�...</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍥轰欢鏂囦欢" align="center" prop="fileUrl">
+ <template #default="scope">
+ <el-link :href="scope.row.fileUrl" target="_blank" download>
+ <Icon icon="ep:download" class="mr-5px" />
+ 涓嬭浇鍥轰欢
+ </el-link>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="180px">
+ <template #default="scope">
+ <el-button
+ link
+ @click="openFirmwareDetail(scope.row.id)"
+ v-hasPermi="['iot:ota-firmware:query']"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['iot:ota-firmware:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:ota-firmware:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <OtaFirmwareForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { IoTOtaFirmwareApi, IoTOtaFirmware } from '@/api/iot/ota/firmware'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import OtaFirmwareForm from './OtaFirmwareForm.vue'
+
+/** IoT OTA 鍥轰欢鍒楄〃 */
+defineOptions({ name: 'IoTOtaFirmware' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<IoTOtaFirmware[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const productList = ref<ProductVO[]>([]) // 浜у搧鍒楄〃
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ productId: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await IoTOtaFirmwareApi.getOtaFirmwarePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鏍规嵁浜у搧缂栧彿锛岃幏鍙栦骇鍝佸悕绉� */
+const getProductName = (productId: number) => {
+ const product = productList.value.find((p) => p.id === productId)
+ return product?.name || ''
+}
+
+/** 鎵撳紑浜у搧璇︽儏 */
+const openProductDetail = (productId: number) => {
+ push({ name: 'IoTProductDetail', params: { id: productId } })
+}
+
+/** 鎵撳紑鍥轰欢璇︽儏 */
+const openFirmwareDetail = (firmwareId: number) => {
+ push({ name: 'IoTOtaFirmwareDetail', params: { id: firmwareId } })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await IoTOtaFirmwareApi.deleteOtaFirmware(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ productList.value = await ProductApi.getSimpleProductList()
+ getList()
+})
+</script>
diff --git a/src/views/iot/ota/task/OtaTaskDetail.vue b/src/views/iot/ota/task/OtaTaskDetail.vue
new file mode 100644
index 0000000..950a3f9
--- /dev/null
+++ b/src/views/iot/ota/task/OtaTaskDetail.vue
@@ -0,0 +1,285 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍗囩骇浠诲姟璇︽儏" width="1200px" append-to-body>
+ <!-- 浠诲姟淇℃伅 -->
+ <ContentWrap title="浠诲姟淇℃伅" class="mb-20px">
+ <el-descriptions :column="3" v-loading="taskLoading" border>
+ <el-descriptions-item label="浠诲姟缂栧彿">{{ task.id }}</el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鍚嶇О">{{ task.name }}</el-descriptions-item>
+ <el-descriptions-item label="鍗囩骇鑼冨洿">
+ <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE" :value="task.deviceScope" />
+ </el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鐘舵��">
+ <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_STATUS" :value="task.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ task.createTime ? formatDate(task.createTime) : '-' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠诲姟鎻忚堪" :span="3">
+ {{ task.description }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+
+ <!-- 浠诲姟鍗囩骇璁惧缁熻 -->
+ <ContentWrap title="鍗囩骇璁惧缁熻" class="mb-20px">
+ <el-row :gutter="20" class="py-20px" v-loading="taskStatisticsLoading">
+ <el-col :span="6">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-blue-500">
+ {{ Object.values(taskStatistics).reduce((sum, count) => sum + (count || 0), 0) || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇璁惧鎬绘暟</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-gray-400">
+ {{ taskStatistics[IoTOtaTaskRecordStatusEnum.PENDING.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">寰呮帹閫�</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-blue-400">
+ {{ taskStatistics[IoTOtaTaskRecordStatusEnum.PUSHED.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">宸叉帹閫�</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-yellow-500">
+ {{ taskStatistics[IoTOtaTaskRecordStatusEnum.UPGRADING.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">姝e湪鍗囩骇</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-green-500">
+ {{ taskStatistics[IoTOtaTaskRecordStatusEnum.SUCCESS.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇鎴愬姛</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-red-500">
+ {{ taskStatistics[IoTOtaTaskRecordStatusEnum.FAILURE.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇澶辫触</div>
+ </div>
+ </el-col>
+ <el-col :span="3">
+ <div class="text-center p-20px border border-solid border-gray-200 rounded bg-gray-50">
+ <div class="text-32px font-bold mb-8px text-gray-400">
+ {{ taskStatistics[IoTOtaTaskRecordStatusEnum.CANCELED.value] || 0 }}
+ </div>
+ <div class="text-14px text-gray-600">鍗囩骇鍙栨秷</div>
+ </div>
+ </el-col>
+ </el-row>
+ </ContentWrap>
+
+ <!-- 璁惧绠$悊 -->
+ <ContentWrap title="鍗囩骇璁惧璁板綍">
+ <!-- Tab 鍒囨崲 -->
+ <el-tabs v-model="activeTab" @tab-click="handleTabClick" class="mb-15px">
+ <el-tab-pane v-for="tab in statusTabs" :key="tab.key" :label="tab.label" :name="tab.key" />
+ </el-tabs>
+ <!-- Tab 鍐呭 -->
+ <div v-for="tab in statusTabs" :key="tab.key" v-show="activeTab === tab.key">
+ <!-- 璁惧鍒楄〃 -->
+ <el-table
+ v-loading="recordLoading"
+ :data="recordList"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="璁惧鍚嶇О" align="center" prop="deviceName" />
+ <el-table-column label="褰撳墠鐗堟湰" align="center" prop="fromFirmwareVersion" />
+ <el-table-column label="鍗囩骇鐘舵��" align="center" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_RECORD_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗囩骇杩涘害" align="center" prop="progress" width="120">
+ <template #default="scope"> {{ scope.row.progress }}% </template>
+ </el-table-column>
+ <el-table-column label="鐘舵�佹弿杩�" align="center" prop="description" />
+ <el-table-column label="鏇存柊鏃堕棿" align="center" prop="updateTime" width="180">
+ <template #default="scope">
+ {{ formatDate(scope.row.updateTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="80">
+ <template #default="scope">
+ <el-button
+ v-if="
+ [
+ IoTOtaTaskRecordStatusEnum.PENDING.value,
+ IoTOtaTaskRecordStatusEnum.PUSHED.value,
+ IoTOtaTaskRecordStatusEnum.UPGRADING.value
+ ].includes(scope.row.status)
+ "
+ link
+ type="danger"
+ @click="handleCancelUpgrade(scope.row)"
+ v-hasPermi="['iot:ota-task-record:cancel']"
+ >
+ 鍙栨秷
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="recordTotal"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getRecordList"
+ />
+ </div>
+ </ContentWrap>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed } from 'vue'
+import { TabsPaneContext } from 'element-plus'
+import { Dialog } from '@/components/Dialog'
+import { ContentWrap } from '@/components/ContentWrap'
+import Pagination from '@/components/Pagination/index.vue'
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { IoTOtaTaskRecordApi, OtaTaskRecord } from '@/api/iot/ota/task/record'
+import { DICT_TYPE } from '@/utils/dict'
+import { IoTOtaTaskRecordStatusEnum } from '@/views/iot/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+
+/** OTA 浠诲姟璇︽儏缁勪欢 */
+defineOptions({ name: 'OtaTaskDetail' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+
+const taskId = ref<number>() // 浠诲姟缂栧彿
+const taskLoading = ref(false) // 浠诲姟鍔犺浇鐘舵��
+const task = ref<OtaTask>({} as OtaTask) // 浠诲姟淇℃伅
+
+const taskStatisticsLoading = ref(false) // 浠诲姟缁熻鍔犺浇鐘舵��
+const taskStatistics = ref<Record<string, number>>({}) // 浠诲姟缁熻鏁版嵁
+
+const recordLoading = ref(false) // 璁板綍鍒楄〃鍔犺浇鐘舵��
+const recordList = ref<OtaTaskRecord[]>([]) // 璁板綍鍒楄〃鏁版嵁
+const recordTotal = ref(0) // 璁板綍鎬绘暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ taskId: undefined as number | undefined,
+ status: undefined as number | undefined
+}) // 鏌ヨ鍙傛暟
+const activeTab = ref('') // 褰撳墠婵�娲荤殑鏍囩椤�
+
+/** 鐘舵�佹爣绛鹃厤缃� */
+const statusTabs = computed(() => {
+ const tabs = [{ key: '', label: '鍏ㄩ儴璁惧' }]
+ Object.values(IoTOtaTaskRecordStatusEnum).forEach((status) => {
+ tabs.push({
+ key: status.value.toString(),
+ label: status.label
+ })
+ })
+ return tabs
+})
+
+/** 鑾峰彇浠诲姟璇︽儏 */
+const getTaskInfo = async () => {
+ if (!taskId.value) {
+ return
+ }
+ taskLoading.value = true
+ try {
+ task.value = await IoTOtaTaskApi.getOtaTask(taskId.value)
+ } finally {
+ taskLoading.value = false
+ }
+}
+
+/** 鑾峰彇缁熻鏁版嵁 */
+const getStatistics = async () => {
+ if (!taskId.value) {
+ return
+ }
+ taskStatisticsLoading.value = true
+ try {
+ taskStatistics.value = await IoTOtaTaskRecordApi.getOtaTaskRecordStatusStatistics(
+ undefined,
+ taskId.value
+ )
+ } finally {
+ taskStatisticsLoading.value = false
+ }
+}
+
+/** 鑾峰彇鍗囩骇璁板綍鍒楄〃 */
+const getRecordList = async () => {
+ if (!taskId.value) {
+ return
+ }
+ recordLoading.value = true
+ try {
+ queryParams.taskId = taskId.value
+ const data = await IoTOtaTaskRecordApi.getOtaTaskRecordPage(queryParams)
+ recordList.value = data.list || []
+ recordTotal.value = data.total || 0
+ } finally {
+ recordLoading.value = false
+ }
+}
+
+/** 鍒囨崲鏍囩 */
+const handleTabClick = (tab: TabsPaneContext) => {
+ const tabKey = tab.paneName as string
+ activeTab.value = tabKey
+ queryParams.pageNo = 1
+ queryParams.status = activeTab.value === '' ? undefined : parseInt(tabKey)
+ getRecordList()
+}
+
+/** 鍙栨秷鍗囩骇 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const handleCancelUpgrade = async (record: OtaTaskRecord) => {
+ try {
+ await message.confirm('纭瑕佸彇娑堣璁惧鐨勫崌绾т换鍔″悧锛�')
+ await IoTOtaTaskRecordApi.cancelOtaTaskRecord(record.id!)
+ message.success('鍙栨秷鎴愬姛')
+ // 鍒锋柊鏁版嵁
+ await getRecordList()
+ await getStatistics()
+ await getTaskInfo()
+ // 閫氱煡鐖剁粍浠跺埛鏂版暟鎹�
+ emit('success')
+ } catch (error) {
+ console.error('鍙栨秷鍗囩骇澶辫触', error)
+ }
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = (id: number) => {
+ taskId.value = id
+ dialogVisible.value = true
+ // 閲嶇疆鏁版嵁
+ activeTab.value = ''
+ queryParams.pageNo = 1
+ queryParams.status = undefined
+
+ // 鍔犺浇鏁版嵁
+ getTaskInfo()
+ getStatistics()
+ getRecordList()
+}
+
+/** 鏆撮湶鏂规硶 */
+defineExpose({ open })
+</script>
diff --git a/src/views/iot/ota/task/OtaTaskForm.vue b/src/views/iot/ota/task/OtaTaskForm.vue
new file mode 100644
index 0000000..6fde972
--- /dev/null
+++ b/src/views/iot/ota/task/OtaTaskForm.vue
@@ -0,0 +1,132 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="鏂板鍗囩骇浠诲姟" width="800px" append-to-body>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="浠诲姟鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヤ换鍔″悕绉�" />
+ </el-form-item>
+ <el-form-item label="浠诲姟鎻忚堪" prop="description">
+ <el-input
+ v-model="formData.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ换鍔℃弿杩�"
+ />
+ </el-form-item>
+ <el-form-item label="鍗囩骇鑼冨洿" prop="deviceScope">
+ <el-select v-model="formData.deviceScope" placeholder="璇烽�夋嫨鍗囩骇鑼冨洿" class="w-full">
+ <el-option
+ v-for="item in Object.values(IoTOtaTaskDeviceScopeEnum)"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ label="閫夋嫨璁惧"
+ prop="deviceIds"
+ v-if="formData.deviceScope === IoTOtaTaskDeviceScopeEnum.SELECT.value"
+ >
+ <el-select
+ v-model="formData.deviceIds"
+ multiple
+ placeholder="璇烽�夋嫨璁惧"
+ class="w-full"
+ filterable
+ reserve-keyword
+ >
+ <el-option
+ v-for="device in devices"
+ :key="device.id"
+ :label="
+ device.nickname ? `${device.deviceName} (${device.nickname})` : device.deviceName
+ "
+ :value="device.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { IoTOtaTaskDeviceScopeEnum } from '@/views/iot/utils/constants'
+import { DeviceApi, DeviceVO } from '@/api/iot/device/device'
+
+/** IoT OTA 鍗囩骇浠诲姟琛ㄥ崟 */
+defineOptions({ name: 'OtaTaskForm' })
+
+const props = defineProps<{
+ firmwareId: number
+ productId: number
+}>()
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛氫慨鏀规椂鐨勬暟鎹姞杞�
+const formData = ref<OtaTask>({
+ name: '',
+ deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
+ firmwareId: props.firmwareId,
+ description: '',
+ deviceIds: []
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const formRules = {
+ name: [{ required: true, message: '璇疯緭鍏ヤ换鍔″悕绉�', trigger: 'blur' }],
+ deviceScope: [{ required: true, message: '璇烽�夋嫨鍗囩骇鑼冨洿', trigger: 'change' }],
+ deviceIds: [{ required: true, message: '璇疯嚦灏戦�夋嫨涓�涓澶�', trigger: 'change' }]
+}
+const devices = ref<DeviceVO[]>([]) // 璁惧閫夋嫨鐩稿叧
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ resetForm()
+ // 鍔犺浇璁惧鍒楄〃
+ devices.value = (await DeviceApi.getDeviceListByProductId(props.productId)) || []
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await IoTOtaTaskApi.createOtaTask(formData.value)
+ message.success('鍒涘缓鎴愬姛')
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ name: '',
+ deviceScope: IoTOtaTaskDeviceScopeEnum.ALL.value,
+ firmwareId: props.firmwareId,
+ description: '',
+ deviceIds: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/ota/task/OtaTaskList.vue b/src/views/iot/ota/task/OtaTaskList.vue
new file mode 100644
index 0000000..f6c3a6b
--- /dev/null
+++ b/src/views/iot/ota/task/OtaTaskList.vue
@@ -0,0 +1,187 @@
+<template>
+ <ContentWrap title="鍗囩骇浠诲姟绠$悊" class="mb-20px">
+ <!-- 鎼滅储鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ @submit.prevent
+ >
+ <el-form-item>
+ <el-button type="primary" @click="openTaskForm" v-hasPermi="['iot:ota-task:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ <el-form-item class="float-right">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ </el-form>
+
+ <!-- 浠诲姟鍒楄〃 -->
+ <el-table
+ v-loading="taskLoading"
+ :data="taskList"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ class="mt-15px"
+ >
+ <el-table-column label="浠诲姟缂栧彿" align="center" prop="id" width="80" />
+ <el-table-column label="浠诲姟鍚嶇О" align="center" prop="name" />
+ <el-table-column label="鍗囩骇鑼冨洿" align="center" prop="deviceScope">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_DEVICE_SCOPE" :value="scope.row.deviceScope" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗囩骇杩涘害" align="center">
+ <template #default="scope">
+ {{ scope.row.deviceSuccessCount }}/{{ scope.row.deviceTotalCount }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="浠诲姟鎻忚堪" align="center" prop="description" show-overflow-tooltip />
+ <el-table-column label="浠诲姟鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_OTA_TASK_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="120">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleTaskDetail(scope.row.id)"> 璇︽儏 </el-button>
+ <el-button
+ v-if="scope.row.status === IoTOtaTaskStatusEnum.IN_PROGRESS.value"
+ link
+ type="danger"
+ @click="handleCancelTask(scope.row.id)"
+ v-hasPermi="['iot:ota-task:cancel']"
+ >
+ 鍙栨秷
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="taskTotal"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getTaskList"
+ />
+
+ <!-- 鏂板浠诲姟寮圭獥 -->
+ <OtaTaskForm
+ ref="taskFormRef"
+ :firmware-id="firmwareId"
+ :product-id="productId"
+ @success="handleTaskCreateSuccess"
+ />
+
+ <!-- 浠诲姟璇︽儏寮圭獥 -->
+ <OtaTaskDetail ref="taskDetailRef" @success="refresh" />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { IoTOtaTaskApi, OtaTask } from '@/api/iot/ota/task'
+import { DICT_TYPE } from '@/utils/dict'
+import { IoTOtaTaskStatusEnum } from '@/views/iot/utils/constants'
+import OtaTaskForm from './OtaTaskForm.vue'
+import OtaTaskDetail from './OtaTaskDetail.vue'
+
+/** IoT OTA 浠诲姟鍒楄〃 */
+defineOptions({ name: 'OtaTaskList' })
+
+const props = defineProps<{
+ firmwareId: number
+ productId: number
+}>()
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 浠诲姟鍒楄〃
+const taskLoading = ref(false)
+const taskList = ref<OtaTask[]>([])
+const taskTotal = ref(0)
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ firmwareId: props.firmwareId
+})
+const queryFormRef = ref() // 鏌ヨ琛ㄥ崟寮曠敤
+const taskFormRef = ref() // 浠诲姟琛ㄥ崟寮曠敤
+const taskDetailRef = ref() // 浠诲姟璇︽儏寮曠敤
+
+/** 鑾峰彇浠诲姟鍒楄〃 */
+const getTaskList = async () => {
+ taskLoading.value = true
+ try {
+ const data = await IoTOtaTaskApi.getOtaTaskPage(queryParams)
+ taskList.value = data.list
+ taskTotal.value = data.total
+ } finally {
+ taskLoading.value = false
+ }
+}
+
+/** 鎼滅储 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getTaskList()
+}
+
+/** 鎵撳紑浠诲姟琛ㄥ崟 */
+const openTaskForm = () => {
+ taskFormRef.value?.open()
+}
+
+/** 澶勭悊浠诲姟鍒涘缓鎴愬姛 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const handleTaskCreateSuccess = () => {
+ getTaskList()
+ emit('success')
+}
+
+/** 鏌ョ湅浠诲姟璇︽儏 */
+const handleTaskDetail = (id: number) => {
+ taskDetailRef.value?.open(id)
+}
+
+/** 鍙栨秷浠诲姟 */
+const handleCancelTask = async (id: number) => {
+ try {
+ await message.confirm('纭瑕佸彇娑堣鍗囩骇浠诲姟鍚楋紵')
+ await IoTOtaTaskApi.cancelOtaTask(id)
+ message.success('鍙栨秷鎴愬姛')
+ // 鍒锋柊鏁版嵁
+ await refresh()
+ } catch (error) {
+ console.error('鍙栨秷浠诲姟澶辫触', error)
+ }
+}
+
+/** 鍒锋柊鏁版嵁 */
+const refresh = async () => {
+ await getTaskList()
+ emit('success')
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getTaskList()
+})
+</script>
diff --git a/src/views/iot/product/category/ProductCategoryForm.vue b/src/views/iot/product/category/ProductCategoryForm.vue
new file mode 100644
index 0000000..ff72192
--- /dev/null
+++ b/src/views/iot/product/category/ProductCategoryForm.vue
@@ -0,0 +1,119 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍒嗙被鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ垎绫诲悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鎺掑簭" prop="sort">
+ <el-input v-model="formData.sort" placeholder="璇疯緭鍏ュ垎绫绘帓搴�" />
+ </el-form-item>
+ <el-form-item label="鍒嗙被鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍒嗙被鎻忚堪" prop="description">
+ <el-input type="textarea" v-model="formData.description" placeholder="璇疯緭鍏ュ垎绫绘弿杩�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** IoT 浜у搧鍒嗙被 琛ㄥ崟 */
+defineOptions({ name: 'ProductCategoryForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ sort: 0,
+ status: CommonStatusEnum.ENABLE,
+ description: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍒嗙被鍚嶅瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鍒嗙被鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鍒嗙被鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductCategoryApi.getProductCategory(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductCategoryVO
+ if (formType.value === 'create') {
+ await ProductCategoryApi.createProductCategory(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductCategoryApi.updateProductCategory(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ sort: 0,
+ status: CommonStatusEnum.ENABLE,
+ description: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/product/category/index.vue b/src/views/iot/product/category/index.vue
new file mode 100644
index 0000000..f421083
--- /dev/null
+++ b/src/views/iot/product/category/index.vue
@@ -0,0 +1,169 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙被鍚嶅瓧" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ垎绫诲悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:product-category:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="ID" align="center" prop="id" />
+ <el-table-column label="鍚嶅瓧" align="center" prop="name" />
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻忚堪" align="center" prop="description" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['iot:product-category:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:product-category:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductCategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category'
+import ProductCategoryForm from './ProductCategoryForm.vue'
+
+/** IoT 浜у搧鍒嗙被鍒楄〃 */
+defineOptions({ name: 'IotProductCategory' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProductCategoryVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductCategoryApi.getProductCategoryPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductCategoryApi.deleteProductCategory(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/product/product/ProductForm.vue b/src/views/iot/product/product/ProductForm.vue
new file mode 100644
index 0000000..5247e92
--- /dev/null
+++ b/src/views/iot/product/product/ProductForm.vue
@@ -0,0 +1,220 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="110px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="ProductKey" prop="productKey">
+ <el-input
+ v-model="formData.productKey"
+ placeholder="璇疯緭鍏� ProductKey"
+ :readonly="formType === 'update'"
+ >
+ <template #append>
+ <el-button @click="generateProductKey" :disabled="formType === 'update'">
+ 閲嶆柊鐢熸垚
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="浜у搧鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�" />
+ </el-form-item>
+ <el-form-item label="浜у搧鍒嗙被" prop="categoryId">
+ <el-select v-model="formData.categoryId" placeholder="璇烽�夋嫨浜у搧鍒嗙被" clearable>
+ <el-option
+ v-for="category in categoryList"
+ :key="category.id"
+ :label="category.name"
+ :value="category.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧绫诲瀷" prop="deviceType">
+ <el-radio-group v-model="formData.deviceType" :disabled="formType === 'update'">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(formData.deviceType!)"
+ label="鑱旂綉鏂瑰紡"
+ prop="netType"
+ >
+ <el-select
+ v-model="formData.netType"
+ placeholder="璇烽�夋嫨鑱旂綉鏂瑰紡"
+ :disabled="formType === 'update'"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_NET_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹氫綅绫诲瀷" prop="locationType">
+ <el-radio-group v-model="formData.locationType" :disabled="formType === 'update'">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_LOCATION_TYPE)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏁版嵁鏍煎紡" prop="codecType">
+ <el-radio-group v-model="formData.codecType" :disabled="formType === 'update'">
+ <el-radio
+ v-for="dict in getStrDictOptions(DICT_TYPE.IOT_CODEC_TYPE)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-collapse>
+ <el-collapse-item title="鏇村閰嶇疆">
+ <el-form-item label="浜у搧鍥炬爣" prop="icon">
+ <UploadImg v-model="formData.icon" :height="'80px'" :width="'80px'" />
+ </el-form-item>
+ <el-form-item label="浜у搧鍥剧墖" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" :height="'120px'" :width="'120px'" />
+ </el-form-item>
+ <el-form-item label="浜у搧鎻忚堪" prop="description">
+ <el-input type="textarea" v-model="formData.description" placeholder="璇疯緭鍏ヤ骇鍝佹弿杩�" />
+ </el-form-item>
+ </el-collapse-item>
+ </el-collapse>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { ProductApi, ProductVO, CodecTypeEnum, DeviceTypeEnum } from '@/api/iot/product/product'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { ProductCategoryApi, ProductCategoryVO } from '@/api/iot/product/category'
+import { UploadImg } from '@/components/UploadFile'
+import { generateRandomStr } from '@/utils'
+
+defineOptions({ name: 'IoTProductForm' })
+
+const { t } = useI18n()
+const message = useMessage()
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const formType = ref('')
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ productKey: '',
+ categoryId: undefined,
+ icon: undefined,
+ picUrl: undefined,
+ description: undefined,
+ deviceType: undefined,
+ locationType: undefined,
+ netType: undefined,
+ codecType: CodecTypeEnum.ALINK
+})
+const formRules = reactive({
+ productKey: [{ required: true, message: 'ProductKey 涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '浜у搧鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ categoryId: [{ required: true, message: '浜у搧鍒嗙被涓嶈兘涓虹┖', trigger: 'change' }],
+ deviceType: [{ required: true, message: '璁惧绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ locationType: [{ required: true, message: '瀹氫綅绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ netType: [
+ {
+ required: true,
+ message: '鑱旂綉鏂瑰紡涓嶈兘涓虹┖',
+ trigger: 'change'
+ }
+ ],
+ codecType: [{ required: true, message: '鏁版嵁鏍煎紡涓嶈兘涓虹┖', trigger: 'change' }]
+})
+const formRef = ref()
+const categoryList = ref<ProductCategoryVO[]>([]) // 浜у搧鍒嗙被鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductApi.getProduct(id)
+ } finally {
+ formLoading.value = false
+ }
+ } else {
+ // 鏂板鏃讹紝鐢熸垚闅忔満 productKey
+ generateProductKey()
+ }
+ // 鍔犺浇鍒嗙被鍒楄〃
+ categoryList.value = await ProductCategoryApi.getSimpleProductCategoryList()
+}
+defineExpose({ open, close: () => (dialogVisible.value = false) })
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+ await formRef.value.validate()
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ProductVO
+ if (formType.value === 'create') {
+ await ProductApi.createProduct(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductApi.updateProduct(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false // 纭繚鍏抽棴寮规
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ productKey: '',
+ categoryId: undefined,
+ icon: undefined,
+ picUrl: undefined,
+ description: undefined,
+ deviceType: undefined,
+ locationType: undefined,
+ netType: undefined,
+ codecType: CodecTypeEnum.ALINK
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鐢熸垚 ProductKey */
+const generateProductKey = () => {
+ formData.value.productKey = generateRandomStr(16)
+}
+</script>
diff --git a/src/views/iot/product/product/components/ProductTableSelect.vue b/src/views/iot/product/product/components/ProductTableSelect.vue
new file mode 100644
index 0000000..a1ab0f2
--- /dev/null
+++ b/src/views/iot/product/product/components/ProductTableSelect.vue
@@ -0,0 +1,220 @@
+<!-- IoT 浜у搧閫夋嫨锛屼娇鐢ㄥ脊绐楀睍绀� -->
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" :appendToBody="true" width="60%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="浜у搧鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="ProductKey" prop="productKey">
+ <el-input
+ v-model="queryParams.productKey"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ骇鍝佹爣璇�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ ref="tableRef"
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @row-click="handleRowClick"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column v-if="multiple" type="selection" width="55" />
+ <el-table-column v-else width="55">
+ <template #default="scope">
+ <el-radio
+ v-model="selectedId"
+ :value="scope.row.id"
+ @change="() => handleRadioChange(scope.row)"
+ >
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍚嶇О" prop="name" />
+ <el-table-column align="center" label="ProductKey" prop="productKey" />
+ <el-table-column align="center" label="鍝佺被" prop="categoryName" />
+ <el-table-column align="center" label="璁惧绫诲瀷" prop="deviceType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浜у搧鍥炬爣" prop="icon">
+ <template #default="scope">
+ <el-image
+ v-if="scope.row.icon"
+ :preview-src-list="[scope.row.icon]"
+ :src="scope.row.icon"
+ class="w-40px h-40px"
+ />
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浜у搧鍥剧墖" prop="picture">
+ <template #default="scope">
+ <el-image
+ v-if="scope.row.picUrl"
+ :preview-src-list="[scope.row.picture]"
+ :src="scope.row.picUrl"
+ class="w-40px h-40px"
+ />
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+
+defineOptions({ name: 'IoTProductTableSelect' })
+
+const props = defineProps({
+ multiple: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const message = useMessage()
+const dialogVisible = ref(false)
+const dialogTitle = ref('浜у搧閫夋嫨鍣�')
+const formLoading = ref(false)
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ProductVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const selectedProducts = ref<ProductVO[]>([]) // 閫変腑鐨勪骇鍝佸垪琛�
+const selectedId = ref<number>() // 鍗曢�夋ā寮忎笅閫変腑鐨処D
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ productKey: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductApi.getProductPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ // 閲嶇疆閫夋嫨鐘舵��
+ selectedProducts.value = []
+ selectedId.value = undefined
+ await getList()
+}
+defineExpose({ open })
+
+/** 澶勭悊琛岀偣鍑讳簨浠� */
+const tableRef = ref()
+const handleRowClick = (row: ProductVO) => {
+ if (props.multiple) {
+ tableRef.value?.toggleRowSelection(row)
+ } else {
+ selectedId.value = row.id
+ selectedProducts.value = [row]
+ }
+}
+
+/** 澶勭悊鍗曢�夊彉鏇翠簨浠� */
+const handleRadioChange = (row: ProductVO) => {
+ selectedProducts.value = [row]
+}
+
+/** 澶勭悊閫夋嫨鍙樻洿浜嬩欢 */
+const handleSelectionChange = (selection: ProductVO[]) => {
+ if (props.multiple) {
+ selectedProducts.value = selection
+ }
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+ if (selectedProducts.value.length === 0) {
+ message.warning(props.multiple ? '璇疯嚦灏戦�夋嫨涓�涓骇鍝�' : '璇烽�夋嫨涓�涓骇鍝�')
+ return
+ }
+ emit('success', props.multiple ? selectedProducts.value : selectedProducts.value[0])
+ dialogVisible.value = false
+}
+</script>
diff --git a/src/views/iot/product/product/detail/ProductDetailsHeader.vue b/src/views/iot/product/product/detail/ProductDetailsHeader.vue
new file mode 100644
index 0000000..311900c
--- /dev/null
+++ b/src/views/iot/product/product/detail/ProductDetailsHeader.vue
@@ -0,0 +1,113 @@
+<template>
+ <div>
+ <div class="flex items-start justify-between">
+ <div>
+ <el-col>
+ <el-row>
+ <span class="text-xl font-bold">{{ product.name }}</span>
+ </el-row>
+ </el-col>
+ </div>
+ <div>
+ <!-- 鍙充笂锛氭寜閽� -->
+ <el-button
+ @click="openForm('update', product.id)"
+ v-hasPermi="['iot:product:update']"
+ :disabled="product.status === 1"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="primary"
+ @click="confirmPublish(product.id)"
+ v-hasPermi="['iot:product:update']"
+ v-if="product.status === 0"
+ >
+ 鍙戝竷
+ </el-button>
+ <el-button
+ type="danger"
+ @click="confirmUnpublish(product.id)"
+ v-hasPermi="['iot:product:update']"
+ v-if="product.status === 1"
+ >
+ 鎾ら攢鍙戝竷
+ </el-button>
+ </div>
+ </div>
+ </div>
+ <ContentWrap class="mt-10px">
+ <el-descriptions :column="1" direction="horizontal">
+ <el-descriptions-item label="ProductKey">
+ {{ product.productKey }}
+ <el-button @click="copyToClipboard(product.productKey)">澶嶅埗</el-button>
+ </el-descriptions-item>
+ <el-descriptions-item label="璁惧鎬绘暟">
+ <span class="ml-20px mr-10px">{{ product.deviceCount ?? '鍔犺浇涓�...' }}</span>
+ <el-button @click="goToDeviceList(product.id)">鍓嶅線绠$悊</el-button>
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductForm ref="formRef" @success="emit('refresh')" />
+</template>
+<script setup lang="ts">
+import ProductForm from '@/views/iot/product/product/ProductForm.vue'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { useClipboard } from '@vueuse/core'
+
+const message = useMessage()
+const { t } = useI18n() // 鍥介檯鍖�
+
+const { product } = defineProps<{ product: ProductVO }>() // 瀹氫箟 Props
+
+/** 澶嶅埗鍒板壀璐存澘鏂规硶 */
+const copyToClipboard = async (text: string) => {
+ const { copy, copied, isSupported } = useClipboard({ legacy: true, source: text })
+ if (!isSupported) {
+ message.error(t('common.copyError'))
+ return
+ }
+ await copy()
+ if (unref(copied)) {
+ message.success(t('common.copySuccess'))
+ }
+}
+
+/** 璺敱璺宠浆鍒拌澶囩鐞� */
+const { push } = useRouter()
+const goToDeviceList = (productId: number) => {
+ push({ name: 'IoTDevice', query: { productId } })
+}
+
+/** 淇敼鎿嶄綔 */
+const emit = defineEmits(['refresh']) // 瀹氫箟 Emits
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍙戝竷鎿嶄綔 */
+const confirmPublish = async (id: number) => {
+ try {
+ await ProductApi.updateProductStatus(id, 1)
+ message.success('鍙戝竷鎴愬姛')
+ formRef.value.close() // 鍏抽棴寮规
+ emit('refresh')
+ } catch (error) {
+ message.error('鍙戝竷澶辫触')
+ }
+}
+
+/** 鎾ら攢鍙戝竷鎿嶄綔 */
+const confirmUnpublish = async (id: number) => {
+ try {
+ await ProductApi.updateProductStatus(id, 0)
+ message.success('鎾ら攢鍙戝竷鎴愬姛')
+ formRef.value.close() // 鍏抽棴寮规
+ emit('refresh')
+ } catch (error) {
+ message.error('鎾ら攢鍙戝竷澶辫触')
+ }
+}
+</script>
diff --git a/src/views/iot/product/product/detail/ProductDetailsInfo.vue b/src/views/iot/product/product/detail/ProductDetailsInfo.vue
new file mode 100644
index 0000000..51ac544
--- /dev/null
+++ b/src/views/iot/product/product/detail/ProductDetailsInfo.vue
@@ -0,0 +1,37 @@
+<template>
+ <ContentWrap>
+ <el-descriptions :column="3" title="浜у搧淇℃伅" border>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{ product.name }}</el-descriptions-item>
+ <el-descriptions-item label="鎵�灞炲垎绫�">{{ product.categoryName }}</el-descriptions-item>
+ <el-descriptions-item label="璁惧绫诲瀷">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="product.deviceType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹氫綅绫诲瀷">
+ <dict-tag :type="DICT_TYPE.IOT_LOCATION_TYPE" :value="product.locationType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(product.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏁版嵁鏍煎紡">
+ <dict-tag :type="DICT_TYPE.IOT_CODEC_TYPE" :value="product.codecType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="浜у搧鐘舵��">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
+ </el-descriptions-item>
+ <el-descriptions-item
+ label="鑱旂綉鏂瑰紡"
+ v-if="[DeviceTypeEnum.DEVICE, DeviceTypeEnum.GATEWAY].includes(product.deviceType)"
+ >
+ <dict-tag :type="DICT_TYPE.IOT_NET_TYPE" :value="product.netType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="浜у搧鎻忚堪">{{ product.description }}</el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { DeviceTypeEnum, ProductVO } from '@/api/iot/product/product'
+import { formatDate } from '@/utils/formatTime'
+
+const { product } = defineProps<{ product: ProductVO }>()
+</script>
diff --git a/src/views/iot/product/product/detail/index.vue b/src/views/iot/product/product/detail/index.vue
new file mode 100644
index 0000000..0e996eb
--- /dev/null
+++ b/src/views/iot/product/product/detail/index.vue
@@ -0,0 +1,76 @@
+<template>
+ <ProductDetailsHeader :loading="loading" :product="product" @refresh="() => getProductData(id)" />
+ <el-col>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="浜у搧淇℃伅" name="info">
+ <ProductDetailsInfo v-if="activeTab === 'info'" :product="product" />
+ </el-tab-pane>
+ <el-tab-pane label="鐗╂ā鍨嬶紙鍔熻兘瀹氫箟锛�" lazy name="thingModel">
+ <IoTProductThingModel ref="thingModelRef" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-col>
+</template>
+<script lang="ts" setup>
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import { DeviceApi } from '@/api/iot/device/device'
+import ProductDetailsHeader from './ProductDetailsHeader.vue'
+import ProductDetailsInfo from './ProductDetailsInfo.vue'
+import IoTProductThingModel from '@/views/iot/thingmodel/index.vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useRouter } from 'vue-router'
+import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
+
+defineOptions({ name: 'IoTProductDetail' })
+
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { currentRoute } = useRouter()
+
+const route = useRoute()
+const message = useMessage()
+const id = route.params.id // 缂栧彿
+const loading = ref(true) // 鍔犺浇涓�
+const product = ref<ProductVO>({} as ProductVO) // 璇︽儏
+const activeTab = ref('info') // 榛樿涓� info 鏍囩椤�
+
+provide(IOT_PROVIDE_KEY.PRODUCT, product) // 鎻愪緵浜у搧淇℃伅缁欎骇鍝佷俊鎭鎯呴〉鐨勬墍鏈夊瓙缁勪欢
+
+/** 鑾峰彇璇︽儏 */
+const getProductData = async (id: number) => {
+ loading.value = true
+ try {
+ product.value = await ProductApi.getProduct(id)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鏌ヨ璁惧鏁伴噺 */
+const getDeviceCount = async (productId: number) => {
+ try {
+ return await DeviceApi.getDeviceCount(productId)
+ } catch (error) {
+ console.error('Error fetching device count:', error, 'productId:', productId)
+ return 0
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ if (!id) {
+ message.warning('鍙傛暟閿欒锛屼骇鍝佷笉鑳戒负绌猴紒')
+ delView(unref(currentRoute))
+ return
+ }
+ await getProductData(id)
+ // 澶勭悊 tab 鍙傛暟
+ const { tab } = route.query
+ if (tab) {
+ activeTab.value = tab as string
+ }
+ // 鏌ヨ璁惧鏁伴噺
+ if (product.value.id) {
+ product.value.deviceCount = await getDeviceCount(product.value.id)
+ }
+})
+</script>
diff --git a/src/views/iot/product/product/index.vue b/src/views/iot/product/product/index.vue
new file mode 100644
index 0000000..ea7c94a
--- /dev/null
+++ b/src/views/iot/product/product/index.vue
@@ -0,0 +1,355 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="浜у搧鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="ProductKey" prop="productKey">
+ <el-input
+ v-model="queryParams.productKey"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ骇鍝佹爣璇�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:product:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:product:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ <!-- 瑙嗗浘鍒囨崲鎸夐挳 -->
+ <el-form-item class="float-right !mr-0 !mb-0">
+ <el-button-group>
+ <el-button :type="viewMode === 'card' ? 'primary' : 'default'" @click="viewMode = 'card'">
+ <Icon icon="ep:grid" />
+ </el-button>
+ <el-button :type="viewMode === 'list' ? 'primary' : 'default'" @click="viewMode = 'list'">
+ <Icon icon="ep:list" />
+ </el-button>
+ </el-button-group>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍗$墖瑙嗗浘 -->
+ <ContentWrap>
+ <el-row v-if="viewMode === 'card'" :gutter="16">
+ <el-col v-for="item in list" :key="item.id" :lg="6" :md="12" :sm="12" :xs="24" class="mb-4">
+ <el-card :body-style="{ padding: '0' }" class="h-full transition-colors">
+ <!-- 鍐呭鍖哄煙 -->
+ <div class="p-4">
+ <!-- 鏍囬鍖哄煙 -->
+ <div class="flex items-center mb-3">
+ <div class="mr-2.5 flex items-center">
+ <el-image :src="item.icon || defaultIconUrl" class="w-[35px] h-[35px]" />
+ </div>
+ <div class="text-[16px] font-600">{{ item.name }}</div>
+ </div>
+
+ <!-- 淇℃伅鍖哄煙 -->
+ <div class="flex items-center text-[14px]">
+ <div class="flex-1">
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">浜у搧鍒嗙被</span>
+ <span class="text-[#0070ff]">{{ item.categoryName }}</span>
+ </div>
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">浜у搧绫诲瀷</span>
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="item.deviceType" />
+ </div>
+ <div class="mb-2.5 last:mb-0">
+ <span class="text-[#717c8e] mr-2.5">浜у搧鏍囪瘑</span>
+ <span class="text-[#0b1d30] inline-block align-middle overflow-hidden text-ellipsis whitespace-nowrap max-w-[140px]">
+ {{ item.productKey }}
+ </span>
+ </div>
+ </div>
+ <div class="w-[100px] h-[100px]">
+ <el-image :src="item.picUrl || defaultPicUrl" class="w-full h-full" />
+ </div>
+ </div>
+
+ <!-- 鍒嗛殧绾� -->
+ <el-divider class="!my-3" />
+
+ <!-- 鎸夐挳缁� -->
+ <div class="flex items-center px-0">
+ <el-button
+ v-hasPermi="['iot:product:update']"
+ class="flex-1 !px-2 !h-[32px] text-[13px]"
+ plain
+ type="primary"
+ @click="openForm('update', item.id)"
+ >
+ <Icon class="mr-1" icon="ep:edit-pen" />
+ 缂栬緫
+ </el-button>
+ <el-button
+ class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
+ plain
+ type="warning"
+ @click="openDetail(item.id)"
+ >
+ <Icon class="mr-1" icon="ep:view" />
+ 璇︽儏
+ </el-button>
+ <el-button
+ class="flex-1 !px-2 !h-[32px] !ml-[10px] text-[13px]"
+ plain
+ type="success"
+ @click="openObjectModel(item)"
+ >
+ <Icon class="mr-1" icon="ep:scale-to-original" />
+ 鐗╂ā鍨�
+ </el-button>
+ <div class="mx-[10px] h-[20px] w-[1px] bg-[#dcdfe6]"></div>
+ <el-button
+ v-hasPermi="['iot:product:delete']"
+ :disabled="item.status === 1"
+ class="!px-2 !h-[32px] text-[13px]"
+ plain
+ type="danger"
+ @click="handleDelete(item.id)"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 鍒楄〃瑙嗗浘 -->
+ <el-table v-else v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="ID" prop="id" />
+ <el-table-column align="center" label="ProductKey" prop="productKey" />
+ <el-table-column align="center" label="鍝佺被" prop="categoryName" />
+ <el-table-column align="center" label="璁惧绫诲瀷" prop="deviceType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_DEVICE_TYPE" :value="scope.row.deviceType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浜у搧鍥炬爣" prop="icon">
+ <template #default="scope">
+ <el-image
+ v-if="scope.row.icon"
+ :preview-src-list="[scope.row.icon]"
+ :src="scope.row.icon"
+ class="w-40px h-40px"
+ />
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浜у搧鍥剧墖" prop="picture">
+ <template #default="scope">
+ <el-image
+ v-if="scope.row.picUrl"
+ :preview-src-list="[scope.row.picture]"
+ :src="scope.row.picUrl"
+ class="w-40px h-40px"
+ />
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['iot:product:query']"
+ link
+ type="primary"
+ @click="openDetail(scope.row.id)"
+ >
+ 鏌ョ湅
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:product:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:product:delete']"
+ :disabled="scope.row.status === 1"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ProductForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import { ProductApi, ProductVO } from '@/api/iot/product/product'
+import ProductForm from './ProductForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import download from '@/utils/download'
+import defaultPicUrl from '@/assets/imgs/iot/device.png'
+import defaultIconUrl from '@/assets/svgs/iot/cube.svg'
+
+/** iot 浜у搧鍒楄〃 */
+defineOptions({ name: 'IoTProduct' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter()
+const route = useRoute()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const activeName = ref('info') // 褰撳墠婵�娲荤殑鏍囩椤�
+const list = ref<ProductVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ productKey: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鍔犺浇涓�
+const viewMode = ref<'card' | 'list'>('card') // 瑙嗗浘妯″紡鐘舵��
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductApi.getProductPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鎵撳紑璇︽儏 */
+const openDetail = (id: number) => {
+ push({ name: 'IoTProductDetail', params: { id } })
+}
+
+/** 鎵撳紑鐗╂ā鍨� */
+const openObjectModel = (item: ProductVO) => {
+ push({
+ name: 'IoTProductDetail',
+ params: { id: item.id },
+ query: { tab: 'thingModel' }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductApi.deleteProduct(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ProductApi.exportProduct(queryParams)
+ download.excel(data, '鐗╄仈缃戜骇鍝�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+ // 澶勭悊 tab 鍙傛暟
+ const { tab } = route.query
+ if (tab) {
+ activeName.value = tab as string
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/index.vue b/src/views/iot/rule/data/index.vue
new file mode 100644
index 0000000..8c88759
--- /dev/null
+++ b/src/views/iot/rule/data/index.vue
@@ -0,0 +1,20 @@
+<template>
+ <el-tabs v-model="activeTab" type="border-card">
+ <el-tab-pane label="瑙勫垯" name="rule">
+ <RuleIndex />
+ </el-tab-pane>
+ <el-tab-pane label="鐩殑" name="sink" lazy>
+ <SinkIndex />
+ </el-tab-pane>
+ </el-tabs>
+</template>
+
+<script setup lang="ts">
+import RuleIndex from './rule/index.vue'
+import SinkIndex from './sink/index.vue'
+
+/** IoT 鏁版嵁娴佽浆 */
+defineOptions({ name: 'IoTDataRule' })
+
+const activeTab = ref('rule')
+</script>
diff --git a/src/views/iot/rule/data/rule/DataRuleForm.vue b/src/views/iot/rule/data/rule/DataRuleForm.vue
new file mode 100644
index 0000000..3adc171
--- /dev/null
+++ b/src/views/iot/rule/data/rule/DataRuleForm.vue
@@ -0,0 +1,158 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="870">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="瑙勫垯鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヨ鍒欏悕绉�" />
+ </el-form-item>
+ <el-form-item label="瑙勫垯鎻忚堪" prop="description">
+ <el-input v-model="formData.description" height="150px" type="textarea" />
+ </el-form-item>
+ <el-form-item label="瑙勫垯鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏁版嵁鐩殑" prop="sinkIds">
+ <el-select
+ v-model="formData.sinkIds"
+ placeholder="璇烽�夋嫨鏁版嵁鐩殑"
+ multiple
+ clearable
+ class="w-1/1"
+ >
+ <el-option
+ v-for="sink in dataSinkList"
+ :key="sink.id"
+ :label="sink.name"
+ :value="sink.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏁版嵁婧�" prop="sourceConfigs">
+ <SourceConfigForm ref="sourceConfigRef" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { DataRuleApi, DataRule } from '@/api/iot/rule/data/rule'
+import { DataSinkApi } from '@/api/iot/rule/data/sink'
+import { CommonStatusEnum } from '@/utils/constants'
+import SourceConfigForm from './components/SourceConfigForm.vue'
+
+/** IoT 鏁版嵁娴佽浆瑙勫垯鐨勮〃鍗� */
+defineOptions({ name: 'DataRuleForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sourceConfigs: [],
+ sinkIds: []
+})
+const formRules = reactive({
+ name: [{ required: true, message: '瑙勫垯鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '瑙勫垯鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ sourceConfigs: [{ required: true, message: '鏁版嵁婧愰厤缃暟缁勪笉鑳戒负绌�', trigger: 'blur' }],
+ sinkIds: [{ required: true, message: '鏁版嵁鐩殑缂栧彿鏁扮粍涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const dataSinkList = ref<any[]>([]) // 鏁版嵁鐩殑鍒楄〃
+const sourceConfigRef = ref() // 鏁版嵁婧愰厤缃粍浠跺紩鐢�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = await DataRuleApi.getDataRule(id)
+ formData.value = data
+ // 璁剧疆鏁版嵁婧愰厤缃�
+ nextTick(() => {
+ sourceConfigRef.value?.setData(data.sourceConfigs || [])
+ })
+ } finally {
+ formLoading.value = false
+ }
+ }
+
+ // 鍔犺浇鏁版嵁鐩殑鍒楄〃
+ dataSinkList.value = await DataSinkApi.getDataSinkSimpleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙鏁版嵁婧愰厤缃�
+ await sourceConfigRef.value?.validate()
+ formData.value.sourceConfigs = sourceConfigRef.value?.getData() || []
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value } as unknown as DataRule
+ if (formType.value === 'create') {
+ await DataRuleApi.createDataRule(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DataRuleApi.updateDataRule(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sourceConfigs: [],
+ sinkIds: []
+ }
+ formRef.value?.resetFields()
+ // 閲嶇疆鏁版嵁婧愰厤缃�
+ await nextTick()
+ sourceConfigRef.value?.setData([])
+}
+</script>
diff --git a/src/views/iot/rule/data/rule/components/SourceConfigForm.vue b/src/views/iot/rule/data/rule/components/SourceConfigForm.vue
new file mode 100644
index 0000000..4e10138
--- /dev/null
+++ b/src/views/iot/rule/data/rule/components/SourceConfigForm.vue
@@ -0,0 +1,262 @@
+<template>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="0px"
+ :inline-message="true"
+ >
+ <el-table :data="formData" class="-mt-10px">
+ <el-table-column label="浜у搧" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.productId`" :rules="formRules.productId" class="mb-0px!">
+ <el-select
+ v-model="row.productId"
+ placeholder="璇烽�夋嫨浜у搧"
+ @change="handleProductChange(row, $index)"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="product in productList"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="璁惧" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.deviceId`" :rules="formRules.deviceId" class="mb-0px!">
+ <el-select
+ v-model="row.deviceId"
+ placeholder="璇烽�夋嫨璁惧"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option label="鍏ㄩ儴璁惧" :value="0" />
+ <el-option
+ v-for="device in getFilteredDevices(row.productId)"
+ :key="device.id"
+ :label="device.deviceName"
+ :value="device.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="娑堟伅" min-width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.method`" :rules="formRules.method" class="mb-0px!">
+ <el-select
+ v-model="row.method"
+ placeholder="璇烽�夋嫨娑堟伅"
+ @change="handleMethodChange(row, $index)"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="method in upstreamMethods"
+ :key="method.method"
+ :label="method.name"
+ :value="method.method"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏍囪瘑绗�" min-width="200">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`${$index}.identifier`" class="mb-0px!">
+ <el-select
+ v-if="shouldShowIdentifierSelect(row)"
+ v-model="row.identifier"
+ placeholder="璇烽�夋嫨鏍囪瘑绗�"
+ clearable
+ filterable
+ style="width: 100%"
+ v-loading="row.identifierLoading"
+ >
+ <el-option
+ v-for="item in getThingModelOptions(row)"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="60">
+ <template #default="{ $index }">
+ <el-button @click="handleDelete($index)" link type="danger">鈥�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <el-row justify="center" class="mt-3">
+ <el-button @click="handleAdd" type="primary" plain round>+ 娣诲姞鏁版嵁婧�</el-button>
+ </el-row>
+ </el-form>
+</template>
+
+<script setup lang="ts">
+import { ProductApi } from '@/api/iot/product/product'
+import { DeviceApi } from '@/api/iot/device/device'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import { IotDeviceMessageMethodEnum, IoTThingModelTypeEnum } from '@/views/iot/utils/constants'
+
+const formData = ref<any[]>([])
+const productList = ref<any[]>([]) // 浜у搧鍒楄〃
+const deviceList = ref<any[]>([]) // 璁惧鍒楄〃
+const thingModelCache = ref<Map<number, any[]>>(new Map()) // 缂撳瓨鐗╂ā鍨嬫暟鎹紝key 涓� productId
+
+const formRules = reactive({
+ productId: [{ required: true, message: '浜у搧涓嶈兘涓虹┖', trigger: 'change' }],
+ deviceId: [{ required: true, message: '璁惧涓嶈兘涓虹┖', trigger: 'change' }],
+ method: [{ required: true, message: '娑堟伅鏂规硶涓嶈兘涓虹┖', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// 鑾峰彇涓婅娑堟伅鏂规硶鍒楄〃
+const upstreamMethods = computed(() => {
+ return Object.values(IotDeviceMessageMethodEnum).filter((item) => item.upstream)
+})
+
+/** 鏍规嵁浜у搧 ID 杩囨护璁惧 */
+const getFilteredDevices = (productId: number) => {
+ if (!productId) return []
+ return deviceList.value.filter((device: any) => device.productId === productId)
+}
+
+/** 鍒ゆ柇鏄惁闇�瑕佹樉绀烘爣璇嗙閫夋嫨鍣� */
+const shouldShowIdentifierSelect = (row: any) => {
+ return [
+ IotDeviceMessageMethodEnum.EVENT_POST.method,
+ IotDeviceMessageMethodEnum.PROPERTY_POST.method
+ ].includes(row.method)
+}
+
+/** 鑾峰彇鐗╂ā鍨嬮�夐」 */
+const getThingModelOptions = (row: any) => {
+ if (!row.productId || !shouldShowIdentifierSelect(row)) {
+ return []
+ }
+ const thingModels: any[] = thingModelCache.value.get(row.productId) || []
+ let filteredModels: any[] = []
+ if (row.method === IotDeviceMessageMethodEnum.EVENT_POST.method) {
+ filteredModels = thingModels.filter((item: any) => item.type === IoTThingModelTypeEnum.EVENT)
+ } else if (row.method === IotDeviceMessageMethodEnum.PROPERTY_POST.method) {
+ filteredModels = thingModels.filter((item: any) => item.type === IoTThingModelTypeEnum.PROPERTY)
+ }
+ return filteredModels.map((item: any) => ({
+ label: `${item.name} (${item.identifier})`,
+ value: item.identifier
+ }))
+}
+
+/** 鍔犺浇浜у搧鍒楄〃 */
+const loadProductList = async () => {
+ try {
+ productList.value = await ProductApi.getSimpleProductList()
+ } catch (error) {
+ console.error('鍔犺浇浜у搧鍒楄〃澶辫触:', error)
+ }
+}
+
+/** 鍔犺浇璁惧鍒楄〃 */
+const loadDeviceList = async () => {
+ try {
+ deviceList.value = await DeviceApi.getSimpleDeviceList()
+ } catch (error) {
+ console.error('鍔犺浇璁惧鍒楄〃澶辫触:', error)
+ }
+}
+
+/** 鍔犺浇鐗╂ā鍨嬫暟鎹� */
+const loadThingModel = async (productId: number) => {
+ // 宸茬紦瀛橈紝鏃犻渶閲嶅鍔犺浇
+ if (thingModelCache.value.has(productId)) {
+ return
+ }
+ try {
+ const thingModels = await ThingModelApi.getThingModelList({ productId })
+ thingModelCache.value.set(productId, thingModels)
+ } catch (error) {
+ console.error('鍔犺浇鐗╂ā鍨嬪け璐�:', error)
+ }
+}
+
+/** 浜у搧鍙樺寲鏃跺鐞� */
+const handleProductChange = async (row: any, _index: number) => {
+ row.deviceId = 0
+ row.method = undefined
+ row.identifier = undefined
+ row.identifierLoading = false
+}
+
+/** 娑堟伅鏂规硶鍙樺寲鏃跺鐞� */
+const handleMethodChange = async (row: any, _index: number) => {
+ // 娓呯┖鏍囪瘑绗�
+ row.identifier = undefined
+ // 濡傛灉闇�瑕佸姞杞界墿妯″瀷鏁版嵁
+ if (shouldShowIdentifierSelect(row) && row.productId) {
+ row.identifierLoading = true
+ await loadThingModel(row.productId)
+ row.identifierLoading = false
+ }
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ const row = {
+ productId: undefined,
+ deviceId: undefined,
+ method: undefined,
+ identifier: undefined,
+ identifierLoading: false
+ }
+ formData.value.push(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = (index: number) => {
+ formData.value.splice(index, 1)
+}
+
+/** 琛ㄥ崟鏍¢獙 */
+const validate = () => {
+ return formRef.value.validate()
+}
+
+/** 琛ㄥ崟鍊� */
+const getData = () => {
+ return formData.value
+}
+
+/** 璁剧疆琛ㄥ崟鍊� */
+const setData = (data: any[]) => {
+ // 纭繚姣忎釜椤归兘鏈夊繀瑕佺殑瀛楁
+ formData.value = (data || []).map((item) => ({
+ ...item,
+ identifierLoading: false
+ }))
+ // 涓哄凡鏈夋暟鎹鍔犺浇鐗╂ā鍨�
+ data?.forEach(async (item) => {
+ if (item.productId && shouldShowIdentifierSelect(item)) {
+ await loadThingModel(item.productId)
+ }
+ })
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await Promise.all([loadProductList(), loadDeviceList()])
+})
+
+defineExpose({ validate, getData, setData })
+</script>
diff --git a/src/views/iot/rule/data/rule/index.vue b/src/views/iot/rule/data/rule/index.vue
new file mode 100644
index 0000000..cce4830
--- /dev/null
+++ b/src/views/iot/rule/data/rule/index.vue
@@ -0,0 +1,196 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="瑙勫垯鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヨ鍒欏悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瑙勫垯鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨瑙勫垯鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['iot:data-rule:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ row-key="id"
+ v-loading="loading"
+ :data="list"
+ :stripe="true"
+ :show-overflow-tooltip="true"
+ >
+ <el-table-column label="瑙勫垯缂栧彿" align="center" prop="id" />
+ <el-table-column label="瑙勫垯鍚嶇О" align="center" prop="name" />
+ <el-table-column label="瑙勫垯鎻忚堪" align="center" prop="description" />
+ <el-table-column label="瑙勫垯鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁版嵁婧�" align="center" prop="sourceConfigs">
+ <template #default="scope"> {{ scope.row.sourceConfigs?.length || 0 }} 涓� </template>
+ </el-table-column>
+ <el-table-column label="鏁版嵁鐩殑" align="center" prop="sinkIds">
+ <template #default="scope"> {{ scope.row.sinkIds?.length || 0 }} 涓� </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="120px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['iot:data-rule:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['iot:data-rule:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DataRuleForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DataRuleApi, DataRule } from '@/api/iot/rule/data/rule'
+import DataRuleForm from './DataRuleForm.vue'
+
+/** IoT 鏁版嵁娴佽浆瑙勫垯鍒楄〃 */
+defineOptions({ name: 'IotDataRule' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<DataRule[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DataRuleApi.getDataRulePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DataRuleApi.deleteDataRule(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/DataSinkForm.vue b/src/views/iot/rule/data/sink/DataSinkForm.vue
new file mode 100644
index 0000000..a497457
--- /dev/null
+++ b/src/views/iot/rule/data/sink/DataSinkForm.vue
@@ -0,0 +1,188 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ >
+ <el-form-item label="鐩殑鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ洰鐨勫悕绉�" />
+ </el-form-item>
+ <el-form-item label="鐩殑鎻忚堪" prop="description">
+ <el-input v-model="formData.description" height="150px" type="textarea" />
+ </el-form-item>
+ <el-form-item label="鐩殑绫诲瀷" prop="type">
+ <el-select v-model="formData.type" @change="handleTypeChange">
+ <el-option
+ v-for="item in getIntDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM)"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <HttpConfigForm v-if="IotDataSinkTypeEnum.HTTP === formData.type" v-model="formData.config" />
+ <MqttConfigForm v-if="IotDataSinkTypeEnum.MQTT === formData.type" v-model="formData.config" />
+ <RocketMQConfigForm
+ v-if="IotDataSinkTypeEnum.ROCKETMQ === formData.type"
+ v-model="formData.config"
+ />
+ <KafkaMQConfigForm
+ v-if="IotDataSinkTypeEnum.KAFKA === formData.type"
+ v-model="formData.config"
+ />
+ <RabbitMQConfigForm
+ v-if="IotDataSinkTypeEnum.RABBITMQ === formData.type"
+ v-model="formData.config"
+ />
+ <RedisStreamConfigForm
+ v-if="IotDataSinkTypeEnum.REDIS_STREAM === formData.type"
+ v-model="formData.config"
+ />
+ <el-form-item label="鐩殑鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { DataSinkApi, DataSinkVO, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
+import {
+ HttpConfigForm,
+ KafkaMQConfigForm,
+ MqttConfigForm,
+ RabbitMQConfigForm,
+ RedisStreamConfigForm,
+ RocketMQConfigForm
+} from './config'
+
+/** IoT 鏁版嵁娴佽浆鐩殑鐨勮〃鍗� */
+defineOptions({ name: 'IoTDataSinkForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<DataSinkVO>({
+ status: CommonStatusEnum.ENABLE,
+ type: IotDataSinkTypeEnum.HTTP,
+ config: {} as any
+})
+const formRules = reactive({
+ // 閫氱敤瀛楁
+ name: [{ required: true, message: '鐩殑鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐩殑鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ type: [{ required: true, message: '鐩殑绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ // HTTP 閰嶇疆
+ 'config.url': [{ required: true, message: '璇锋眰鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.method': [{ required: true, message: '璇锋眰鏂规硶涓嶈兘涓虹┖', trigger: 'blur' }],
+ // MQTT 閰嶇疆
+ 'config.username': [{ required: true, message: '鐢ㄦ埛鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ 'config.password': [{ required: true, message: '瀵嗙爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.clientId': [{ required: true, message: '瀹㈡埛绔� ID 涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.topic': [{ required: true, message: '涓婚涓嶈兘涓虹┖', trigger: 'blur' }],
+ // RocketMQ 閰嶇疆
+ 'config.nameServer': [{ required: true, message: 'NameServer 鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.accessKey': [{ required: true, message: 'AccessKey 涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.secretKey': [{ required: true, message: 'SecretKey 涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.group': [{ required: true, message: '娑堣垂缁勪笉鑳戒负绌�', trigger: 'blur' }],
+ // Kafka 閰嶇疆
+ 'config.bootstrapServers': [{ required: true, message: '鏈嶅姟鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.ssl': [{ required: true, message: 'SSL 閰嶇疆涓嶈兘涓虹┖', trigger: 'change' }],
+ // RabbitMQ 閰嶇疆
+ 'config.host': [{ required: true, message: '涓绘満鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.port': [
+ { required: true, message: '绔彛涓嶈兘涓虹┖', trigger: 'blur' },
+ { type: 'number', min: 1, max: 65535, message: '绔彛鍙疯寖鍥� 1-65535', trigger: 'blur' }
+ ],
+ 'config.virtualHost': [{ required: true, message: '铏氭嫙涓绘満涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.exchange': [{ required: true, message: '浜ゆ崲鏈轰笉鑳戒负绌�', trigger: 'blur' }],
+ 'config.routingKey': [{ required: true, message: '璺敱閿笉鑳戒负绌�', trigger: 'blur' }],
+ 'config.queue': [{ required: true, message: '闃熷垪涓嶈兘涓虹┖', trigger: 'blur' }],
+ // Redis Stream 閰嶇疆
+ 'config.database': [
+ { required: true, message: '鏁版嵁搴撶储寮曚笉鑳戒负绌�', trigger: 'blur' },
+ { type: 'number', min: 0, message: '鏁版嵁搴撶储寮曞繀椤绘槸闈炶礋鏁存暟', trigger: 'blur' }
+ ]
+})
+
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DataSinkApi.getDataSink(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as DataSinkVO
+ if (formType.value === 'create') {
+ await DataSinkApi.createDataSink(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DataSinkApi.updateDataSink(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊绫诲瀷鍒囨崲浜嬩欢 */
+const handleTypeChange = (type: number) => {
+ formData.value.type = type
+ // 鍒囨崲绫诲瀷鏃堕噸缃厤缃�
+ formData.value.config = {} as any
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ status: CommonStatusEnum.ENABLE,
+ type: IotDataSinkTypeEnum.HTTP,
+ config: {} as any
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/rule/data/sink/config/HttpConfigForm.vue b/src/views/iot/rule/data/sink/config/HttpConfigForm.vue
new file mode 100644
index 0000000..5fa759b
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/HttpConfigForm.vue
@@ -0,0 +1,86 @@
+<template>
+ <el-form-item label="璇锋眰鍦板潃" prop="config.url">
+ <el-input v-model="urlPath" placeholder="璇疯緭鍏ヨ姹傚湴鍧�">
+ <template #prepend>
+ <el-select v-model="urlPrefix" placeholder="Select" style="width: 115px">
+ <!--suppress HttpUrlsUsage -->
+ <el-option label="http://" value="http://" />
+ <el-option label="https://" value="https://" />
+ </el-select>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="璇锋眰鏂规硶" prop="config.method">
+ <el-select v-model="config.method" placeholder="璇烽�夋嫨璇锋眰鏂规硶">
+ <el-option label="GET" value="GET" />
+ <el-option label="POST" value="POST" />
+ <el-option label="PUT" value="PUT" />
+ <el-option label="DELETE" value="DELETE" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璇锋眰澶�" prop="config.headers">
+ <key-value-editor v-model="config.headers" add-button-text="娣诲姞璇锋眰澶�" />
+ </el-form-item>
+ <el-form-item label="璇锋眰鍙傛暟" prop="config.query">
+ <key-value-editor v-model="config.query" add-button-text="娣诲姞鍙傛暟" />
+ </el-form-item>
+ <el-form-item label="璇锋眰浣�" prop="config.body">
+ <el-input v-model="config.body" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { HttpConfig, IotDataSinkTypeEnum } from '@/api/iot/rule/data/sink'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+import KeyValueEditor from './components/KeyValueEditor.vue'
+
+defineOptions({ name: 'HttpConfigForm' })
+
+const props = defineProps<{
+ modelValue: any
+}>()
+const emit = defineEmits(['update:modelValue'])
+const config = useVModel(props, 'modelValue', emit) as Ref<HttpConfig>
+
+// noinspection HttpUrlsUsage
+/** URL澶勭悊 */
+const urlPrefix = ref('http://')
+const urlPath = ref('')
+const fullUrl = computed(() => {
+ return urlPath.value ? urlPrefix.value + urlPath.value : ''
+})
+
+/** 鐩戝惉 URL 鍙樺寲 */
+watch([urlPrefix, urlPath], () => {
+ config.value.url = fullUrl.value
+})
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ if (!isEmpty(config.value)) {
+ // 鍒濆鍖� URL
+ if (config.value.url) {
+ if (config.value.url.startsWith('https://')) {
+ urlPrefix.value = 'https://'
+ urlPath.value = config.value.url.substring(8)
+ } else if (config.value.url.startsWith('http://')) {
+ urlPrefix.value = 'http://'
+ urlPath.value = config.value.url.substring(7)
+ } else {
+ urlPath.value = config.value.url
+ }
+ }
+ return
+ }
+
+ config.value = {
+ type: IotDataSinkTypeEnum.HTTP + '', // 搴忓垪鍖栨垚瀵瑰簲绫诲瀷鏃朵娇鐢�
+ url: '',
+ method: 'POST',
+ headers: {},
+ query: {},
+ body: ''
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/config/KafkaMQConfigForm.vue b/src/views/iot/rule/data/sink/config/KafkaMQConfigForm.vue
new file mode 100644
index 0000000..d269277
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/KafkaMQConfigForm.vue
@@ -0,0 +1,45 @@
+<template>
+ <el-form-item label="鏈嶅姟鍦板潃" prop="config.bootstrapServers">
+ <el-input v-model="config.bootstrapServers" placeholder="璇疯緭鍏ユ湇鍔″湴鍧�锛屽锛歭ocalhost:9092" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚�" prop="config.username">
+ <el-input v-model="config.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
+ </el-form-item>
+ <el-form-item label="瀵嗙爜" prop="config.password">
+ <el-input v-model="config.password" placeholder="璇疯緭鍏ュ瘑鐮�" show-password type="password" />
+ </el-form-item>
+ <el-form-item label="鍚敤 SSL" prop="config.ssl">
+ <el-switch v-model="config.ssl" />
+ </el-form-item>
+ <el-form-item label="涓婚" prop="config.topic">
+ <el-input v-model="config.topic" placeholder="璇疯緭鍏ヤ富棰�" />
+ </el-form-item>
+</template>
+<script lang="ts" setup>
+import { IotDataSinkTypeEnum, KafkaMQConfig } from '@/api/iot/rule/data/sink'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'KafkaMQConfigForm' })
+
+const props = defineProps<{
+ modelValue: any
+}>()
+const emit = defineEmits(['update:modelValue'])
+const config = useVModel(props, 'modelValue', emit) as Ref<KafkaMQConfig>
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ if (!isEmpty(config.value)) {
+ return
+ }
+ config.value = {
+ type: IotDataSinkTypeEnum.KAFKA + '', // 搴忓垪鍖栨垚瀵瑰簲绫诲瀷鏃朵娇鐢�
+ bootstrapServers: '',
+ username: '',
+ password: '',
+ ssl: false,
+ topic: ''
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/config/MqttConfigForm.vue b/src/views/iot/rule/data/sink/config/MqttConfigForm.vue
new file mode 100644
index 0000000..e9731f5
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/MqttConfigForm.vue
@@ -0,0 +1,45 @@
+<template>
+ <el-form-item label="鏈嶅姟鍦板潃" prop="config.url">
+ <el-input v-model="config.url" placeholder="璇疯緭鍏QTT鏈嶅姟鍦板潃锛屽锛歮qtt://localhost:1883" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚�" prop="config.username">
+ <el-input v-model="config.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
+ </el-form-item>
+ <el-form-item label="瀵嗙爜" prop="config.password">
+ <el-input v-model="config.password" placeholder="璇疯緭鍏ュ瘑鐮�" show-password type="password" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绔疘D" prop="config.clientId">
+ <el-input v-model="config.clientId" placeholder="璇疯緭鍏ュ鎴风ID" />
+ </el-form-item>
+ <el-form-item label="涓婚" prop="config.topic">
+ <el-input v-model="config.topic" placeholder="璇疯緭鍏ヤ富棰�" />
+ </el-form-item>
+</template>
+<script lang="ts" setup>
+import { IotDataSinkTypeEnum, MqttConfig } from '@/api/iot/rule/data/sink'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'MqttConfigForm' })
+
+const props = defineProps<{
+ modelValue: any
+}>()
+const emit = defineEmits(['update:modelValue'])
+const config = useVModel(props, 'modelValue', emit) as Ref<MqttConfig>
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ if (!isEmpty(config.value)) {
+ return
+ }
+ config.value = {
+ type: IotDataSinkTypeEnum.MQTT + '', // 搴忓垪鍖栨垚瀵瑰簲绫诲瀷鏃朵娇鐢�
+ url: '',
+ username: '',
+ password: '',
+ clientId: '',
+ topic: ''
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/config/RabbitMQConfigForm.vue b/src/views/iot/rule/data/sink/config/RabbitMQConfigForm.vue
new file mode 100644
index 0000000..f6b1d6c
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/RabbitMQConfigForm.vue
@@ -0,0 +1,63 @@
+<template>
+ <el-form-item label="涓绘満鍦板潃" prop="config.host">
+ <el-input v-model="config.host" placeholder="璇疯緭鍏ヤ富鏈哄湴鍧�锛屽锛歭ocalhost" />
+ </el-form-item>
+ <el-form-item label="绔彛" prop="config.port">
+ <el-input-number
+ v-model="config.port"
+ :max="65535"
+ :min="1"
+ controls-position="right"
+ placeholder="璇疯緭鍏ョ鍙�"
+ />
+ </el-form-item>
+ <el-form-item label="铏氭嫙涓绘満" prop="config.virtualHost">
+ <el-input v-model="config.virtualHost" placeholder="璇疯緭鍏ヨ櫄鎷熶富鏈�" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚�" prop="config.username">
+ <el-input v-model="config.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
+ </el-form-item>
+ <el-form-item label="瀵嗙爜" prop="config.password">
+ <el-input v-model="config.password" placeholder="璇疯緭鍏ュ瘑鐮�" show-password type="password" />
+ </el-form-item>
+ <el-form-item label="浜ゆ崲鏈�" prop="config.exchange">
+ <el-input v-model="config.exchange" placeholder="璇疯緭鍏ヤ氦鎹㈡満" />
+ </el-form-item>
+ <el-form-item label="璺敱閿�" prop="config.routingKey">
+ <el-input v-model="config.routingKey" placeholder="璇疯緭鍏ヨ矾鐢遍敭" />
+ </el-form-item>
+ <el-form-item label="闃熷垪" prop="config.queue">
+ <el-input v-model="config.queue" placeholder="璇疯緭鍏ラ槦鍒�" />
+ </el-form-item>
+</template>
+<script lang="ts" setup>
+import { IotDataSinkTypeEnum, RabbitMQConfig } from '@/api/iot/rule/data/sink'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'RabbitMQConfigForm' })
+
+const props = defineProps<{
+ modelValue: any
+}>()
+const emit = defineEmits(['update:modelValue'])
+const config = useVModel(props, 'modelValue', emit) as Ref<RabbitMQConfig>
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ if (!isEmpty(config.value)) {
+ return
+ }
+ config.value = {
+ type: IotDataSinkTypeEnum.RABBITMQ + '', // 搴忓垪鍖栨垚瀵瑰簲绫诲瀷鏃朵娇鐢�
+ host: '',
+ port: 5672,
+ virtualHost: '/',
+ username: '',
+ password: '',
+ exchange: '',
+ routingKey: '',
+ queue: ''
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/config/RedisStreamConfigForm.vue b/src/views/iot/rule/data/sink/config/RedisStreamConfigForm.vue
new file mode 100644
index 0000000..137eee9
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/RedisStreamConfigForm.vue
@@ -0,0 +1,57 @@
+<template>
+ <el-form-item label="涓绘満鍦板潃" prop="config.host">
+ <el-input v-model="config.host" placeholder="璇疯緭鍏ヤ富鏈哄湴鍧�锛屽锛歭ocalhost" />
+ </el-form-item>
+ <el-form-item label="绔彛" prop="config.port">
+ <el-input-number
+ v-model="config.port"
+ :max="65535"
+ :min="1"
+ controls-position="right"
+ placeholder="璇疯緭鍏ョ鍙�"
+ />
+ </el-form-item>
+ <el-form-item label="瀵嗙爜" prop="config.password">
+ <el-input v-model="config.password" placeholder="璇疯緭鍏ュ瘑鐮�" show-password type="password" />
+ </el-form-item>
+ <el-form-item label="鏁版嵁搴�" prop="config.database">
+ <el-input-number
+ v-model="config.database"
+ :max="15"
+ :min="0"
+ controls-position="right"
+ placeholder="璇疯緭鍏ユ暟鎹簱绱㈠紩"
+ />
+ </el-form-item>
+ <el-form-item label="涓婚" prop="config.topic">
+ <el-input v-model="config.topic" placeholder="璇疯緭鍏ヤ富棰�" />
+ </el-form-item>
+</template>
+<script lang="ts" setup>
+import { IotDataSinkTypeEnum, RedisStreamMQConfig } from '@/api/iot/rule/data/sink'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'RedisStreamMQConfigForm' })
+
+const props = defineProps<{
+ modelValue: any
+}>()
+const emit = defineEmits(['update:modelValue'])
+const config = useVModel(props, 'modelValue', emit) as Ref<RedisStreamMQConfig>
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ if (!isEmpty(config.value)) {
+ return
+ }
+ config.value = {
+ type: IotDataSinkTypeEnum.REDIS_STREAM + '', // 搴忓垪鍖栨垚瀵瑰簲绫诲瀷鏃朵娇鐢�
+ host: '',
+ port: 6379,
+ password: '',
+ database: 0,
+ topic: ''
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/config/RocketMQConfigForm.vue b/src/views/iot/rule/data/sink/config/RocketMQConfigForm.vue
new file mode 100644
index 0000000..8ffa882
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/RocketMQConfigForm.vue
@@ -0,0 +1,57 @@
+<template>
+ <el-form-item label="NameServer" prop="config.nameServer">
+ <el-input
+ v-model="config.nameServer"
+ placeholder="璇疯緭鍏� NameServer 鍦板潃锛屽锛�127.0.0.1:9876"
+ />
+ </el-form-item>
+ <el-form-item label="AccessKey" prop="config.accessKey">
+ <el-input v-model="config.accessKey" placeholder="璇疯緭鍏� AccessKey" />
+ </el-form-item>
+ <el-form-item label="SecretKey" prop="config.secretKey">
+ <el-input
+ v-model="config.secretKey"
+ placeholder="璇疯緭鍏� SecretKey"
+ show-password
+ type="password"
+ />
+ </el-form-item>
+ <el-form-item label="娑堣垂缁�" prop="config.group">
+ <el-input v-model="config.group" placeholder="璇疯緭鍏ユ秷璐圭粍" />
+ </el-form-item>
+ <el-form-item label="涓婚" prop="config.topic">
+ <el-input v-model="config.topic" placeholder="璇疯緭鍏ヤ富棰�" />
+ </el-form-item>
+ <el-form-item label="鏍囩" prop="config.tags">
+ <el-input v-model="config.tags" placeholder="璇疯緭鍏ユ爣绛�" />
+ </el-form-item>
+</template>
+<script lang="ts" setup>
+import { IotDataSinkTypeEnum, RocketMQConfig } from '@/api/iot/rule/data/sink'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'RocketMQConfigForm' })
+
+const props = defineProps<{
+ modelValue: any
+}>()
+const emit = defineEmits(['update:modelValue'])
+const config = useVModel(props, 'modelValue', emit) as Ref<RocketMQConfig>
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ if (!isEmpty(config.value)) {
+ return
+ }
+ config.value = {
+ type: IotDataSinkTypeEnum.ROCKETMQ + '', // 搴忓垪鍖栨垚瀵瑰簲绫诲瀷鏃朵娇鐢�
+ nameServer: '',
+ accessKey: '',
+ secretKey: '',
+ group: '',
+ topic: '',
+ tags: ''
+ }
+})
+</script>
diff --git a/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue b/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue
new file mode 100644
index 0000000..d0b115c
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/components/KeyValueEditor.vue
@@ -0,0 +1,73 @@
+<template>
+ <div v-for="(item, index) in items" :key="index" class="flex mb-2 w-full">
+ <el-input v-model="item.key" class="mr-2" placeholder="閿�" />
+ <el-input v-model="item.value" placeholder="鍊�" />
+ <el-button class="ml-2" text type="danger" @click="removeItem(index)">
+ <el-icon>
+ <Delete />
+ </el-icon>
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <el-button text type="primary" @click="addItem">
+ <el-icon>
+ <Plus />
+ </el-icon>
+ {{ addButtonText }}
+ </el-button>
+</template>
+
+<script lang="ts" setup>
+import { Delete, Plus } from '@element-plus/icons-vue'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'KeyValueEditor' })
+
+interface KeyValueItem {
+ key: string
+ value: string
+}
+
+const props = defineProps<{
+ modelValue: Record<string, string>
+ addButtonText: string
+}>()
+const emit = defineEmits(['update:modelValue'])
+const items = ref<KeyValueItem[]>([]) // 鍐呴儴 key-value 椤瑰垪琛�
+
+/** 娣诲姞椤圭洰 */
+const addItem = () => {
+ items.value.push({ key: '', value: '' })
+ updateModelValue()
+}
+
+/** 绉婚櫎椤圭洰 */
+const removeItem = (index: number) => {
+ items.value.splice(index, 1)
+ updateModelValue()
+}
+
+/** 鏇存柊 modelValue */
+const updateModelValue = () => {
+ const result: Record<string, string> = {}
+ items.value.forEach((item) => {
+ if (item.key) {
+ result[item.key] = item.value
+ }
+ })
+ emit('update:modelValue', result)
+}
+
+/** 鐩戝惉椤圭洰鍙樺寲 */
+watch(items, updateModelValue, { deep: true })
+watch(
+ () => props.modelValue,
+ (val) => {
+ // 鍒楄〃鏈夊�煎悗浠ュ垪琛ㄤ腑鐨勫�间负鍑�
+ if (isEmpty(val) || !isEmpty(items.value)) {
+ return
+ }
+ items.value = Object.entries(props.modelValue).map(([key, value]) => ({ key, value }))
+ }
+)
+</script>
diff --git a/src/views/iot/rule/data/sink/config/index.ts b/src/views/iot/rule/data/sink/config/index.ts
new file mode 100644
index 0000000..b3927dc
--- /dev/null
+++ b/src/views/iot/rule/data/sink/config/index.ts
@@ -0,0 +1,15 @@
+import HttpConfigForm from './HttpConfigForm.vue'
+import MqttConfigForm from './MqttConfigForm.vue'
+import RocketMQConfigForm from './RocketMQConfigForm.vue'
+import KafkaMQConfigForm from './KafkaMQConfigForm.vue'
+import RabbitMQConfigForm from './RabbitMQConfigForm.vue'
+import RedisStreamConfigForm from './RedisStreamConfigForm.vue'
+
+export {
+ HttpConfigForm,
+ MqttConfigForm,
+ RocketMQConfigForm,
+ KafkaMQConfigForm,
+ RabbitMQConfigForm,
+ RedisStreamConfigForm
+}
diff --git a/src/views/iot/rule/data/sink/index.vue b/src/views/iot/rule/data/sink/index.vue
new file mode 100644
index 0000000..723abed
--- /dev/null
+++ b/src/views/iot/rule/data/sink/index.vue
@@ -0,0 +1,212 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鐩殑鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ョ洰鐨勫悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐩殑鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鐩殑鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐩殑绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鐩殑绫诲瀷"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-220px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:data-sink:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="鐩殑缂栧彿" prop="id" />
+ <el-table-column align="center" label="鐩殑鍚嶇О" prop="name" />
+ <el-table-column align="center" label="鐩殑鎻忚堪" prop="description" />
+ <el-table-column align="center" label="鐩殑鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐩殑绫诲瀷" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_DATA_SINK_TYPE_ENUM" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="120px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['iot:data-sink:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:data-sink:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DataSinkForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { DataSinkApi, DataSinkVO } from '@/api/iot/rule/data/sink'
+import DataSinkForm from './DataSinkForm.vue'
+
+/** IoT 鏁版嵁娴佽浆鐩殑 鍒楄〃 */
+defineOptions({ name: 'IotDataSink' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<DataSinkVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ type: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DataSinkApi.getDataSinkPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DataSinkApi.deleteDataSink(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/rule/scene/form/RuleSceneForm.vue b/src/views/iot/rule/scene/form/RuleSceneForm.vue
new file mode 100644
index 0000000..22ba268
--- /dev/null
+++ b/src/views/iot/rule/scene/form/RuleSceneForm.vue
@@ -0,0 +1,330 @@
+<template>
+ <el-drawer
+ v-model="drawerVisible"
+ :title="drawerTitle"
+ size="80%"
+ direction="rtl"
+ :close-on-click-modal="false"
+ :close-on-press-escape="false"
+ @close="handleClose"
+ >
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="110px">
+ <!-- 鍩虹淇℃伅閰嶇疆 -->
+ <BasicInfoSection v-model="formData" :rules="formRules" />
+ <!-- 瑙﹀彂鍣ㄩ厤缃� -->
+ <TriggerSection v-model:triggers="formData.triggers" />
+ <!-- 鎵ц鍣ㄩ厤缃� -->
+ <ActionSection v-model:actions="formData.actions" />
+ </el-form>
+ <template #footer>
+ <div class="drawer-footer">
+ <el-button :disabled="submitLoading" type="primary" @click="handleSubmit">
+ <Icon icon="ep:check" />
+ 纭� 瀹�
+ </el-button>
+ <el-button @click="handleClose">
+ <Icon icon="ep:close" />
+ 鍙� 娑�
+ </el-button>
+ </div>
+ </template>
+ </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import BasicInfoSection from './sections/BasicInfoSection.vue'
+import TriggerSection from './sections/TriggerSection.vue'
+import ActionSection from './sections/ActionSection.vue'
+import { IotSceneRule } from '@/api/iot/rule/scene'
+import { RuleSceneApi } from '@/api/iot/rule/scene'
+import {
+ IotRuleSceneTriggerTypeEnum,
+ IotRuleSceneActionTypeEnum,
+ isDeviceTrigger
+} from '@/views/iot/utils/constants'
+import { ElMessage } from 'element-plus'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** IoT 鍦烘櫙鑱斿姩瑙勫垯琛ㄥ崟 - 涓昏〃鍗曠粍浠� */
+defineOptions({ name: 'RuleSceneForm' })
+
+/** 缁勪欢灞炴�у畾涔� */
+const props = defineProps<{
+ /** 鎶藉眽鏄剧ず鐘舵�� */
+ modelValue: boolean
+ /** 缂栬緫鐨勫満鏅仈鍔ㄨ鍒欐暟鎹� */
+ ruleScene?: IotSceneRule
+}>()
+
+/** 缁勪欢浜嬩欢瀹氫箟 */
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: boolean): void
+ (e: 'success'): void
+}>()
+
+const drawerVisible = useVModel(props, 'modelValue', emit) // 鎶藉眽鏄剧ず鐘舵��
+
+/**
+ * 鍒涘缓榛樿鐨勮〃鍗曟暟鎹�
+ * @returns 榛樿琛ㄥ崟鏁版嵁瀵硅薄
+ */
+const createDefaultFormData = (): IotSceneRule => {
+ return {
+ name: '',
+ description: '',
+ status: CommonStatusEnum.ENABLE, // 榛樿鍚敤鐘舵��
+ triggers: [
+ {
+ type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+ productId: undefined,
+ deviceId: undefined,
+ identifier: undefined,
+ operator: undefined,
+ value: undefined,
+ cronExpression: undefined,
+ conditionGroups: [] // 绌虹殑鏉′欢缁勬暟缁�
+ }
+ ],
+ actions: []
+ }
+}
+
+const formRef = ref() // 琛ㄥ崟寮曠敤
+const formData = ref<IotSceneRule>(createDefaultFormData()) // 琛ㄥ崟鏁版嵁
+
+/**
+ * 瑙﹀彂鍣ㄦ牎楠屽櫒
+ * @param _rule 鏍¢獙瑙勫垯锛堟湭浣跨敤锛�
+ * @param value 鏍¢獙鍊�
+ * @param callback 鍥炶皟鍑芥暟
+ */
+const validateTriggers = (_rule: any, value: any, callback: any) => {
+ if (!value || !Array.isArray(value) || value.length === 0) {
+ callback(new Error('鑷冲皯闇�瑕佷竴涓Е鍙戝櫒'))
+ return
+ }
+
+ for (let i = 0; i < value.length; i++) {
+ const trigger = value[i]
+
+ // 鏍¢獙瑙﹀彂鍣ㄧ被鍨�
+ if (!trigger.type) {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: 瑙﹀彂鍣ㄧ被鍨嬩笉鑳戒负绌篳))
+ return
+ }
+
+ // 鏍¢獙璁惧瑙﹀彂鍣�
+ if (isDeviceTrigger(trigger.type)) {
+ if (!trigger.productId) {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: 浜у搧涓嶈兘涓虹┖`))
+ return
+ }
+ if (!trigger.deviceId) {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: 璁惧涓嶈兘涓虹┖`))
+ return
+ }
+ if (!trigger.identifier) {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: 鐗╂ā鍨嬫爣璇嗙涓嶈兘涓虹┖`))
+ return
+ }
+ if (!trigger.operator) {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: 鎿嶄綔绗︿笉鑳戒负绌篳))
+ return
+ }
+ if (trigger.value === undefined || trigger.value === null || trigger.value === '') {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: 鍙傛暟鍊间笉鑳戒负绌篳))
+ return
+ }
+ }
+
+ // 鏍¢獙瀹氭椂瑙﹀彂鍣�
+ if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) {
+ if (!trigger.cronExpression) {
+ callback(new Error(`瑙﹀彂鍣� ${i + 1}: CRON琛ㄨ揪寮忎笉鑳戒负绌篳))
+ return
+ }
+ }
+ }
+
+ callback()
+}
+
+/**
+ * 鎵ц鍣ㄦ牎楠屽櫒
+ * @param _rule 鏍¢獙瑙勫垯锛堟湭浣跨敤锛�
+ * @param value 鏍¢獙鍊�
+ * @param callback 鍥炶皟鍑芥暟
+ */
+const validateActions = (_rule: any, value: any, callback: any) => {
+ if (!value || !Array.isArray(value) || value.length === 0) {
+ callback(new Error('鑷冲皯闇�瑕佷竴涓墽琛屽櫒'))
+ return
+ }
+
+ for (let i = 0; i < value.length; i++) {
+ const action = value[i]
+
+ // 鏍¢獙鎵ц鍣ㄧ被鍨�
+ if (!action.type) {
+ callback(new Error(`鎵ц鍣� ${i + 1}: 鎵ц鍣ㄧ被鍨嬩笉鑳戒负绌篳))
+ return
+ }
+
+ // 鏍¢獙璁惧鎺у埗鎵ц鍣�
+ if (
+ action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET ||
+ action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+ ) {
+ if (!action.productId) {
+ callback(new Error(`鎵ц鍣� ${i + 1}: 浜у搧涓嶈兘涓虹┖`))
+ return
+ }
+ if (!action.deviceId) {
+ callback(new Error(`鎵ц鍣� ${i + 1}: 璁惧涓嶈兘涓虹┖`))
+ return
+ }
+
+ // 鏈嶅姟璋冪敤闇�瑕侀獙璇佹湇鍔℃爣璇嗙
+ if (action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE) {
+ if (!action.identifier) {
+ callback(new Error(`鎵ц鍣� ${i + 1}: 鏈嶅姟涓嶈兘涓虹┖`))
+ return
+ }
+ }
+
+ if (!action.params || Object.keys(action.params).length === 0) {
+ callback(new Error(`鎵ц鍣� ${i + 1}: 鍙傛暟閰嶇疆涓嶈兘涓虹┖`))
+ return
+ }
+ }
+
+ // 鏍¢獙鍛婅鎵ц鍣�
+ if (
+ action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER ||
+ action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER
+ ) {
+ if (!action.alertConfigId) {
+ callback(new Error(`鎵ц鍣� ${i + 1}: 鍛婅閰嶇疆涓嶈兘涓虹┖`))
+ return
+ }
+ }
+ }
+
+ callback()
+}
+
+const formRules = reactive({
+ name: [
+ { required: true, message: '鍦烘櫙鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' },
+ { type: 'string', min: 1, max: 50, message: '鍦烘櫙鍚嶇О闀垮害搴斿湪1-50涓瓧绗︿箣闂�', trigger: 'blur' }
+ ],
+ status: [
+ { required: true, message: '鍦烘櫙鐘舵�佷笉鑳戒负绌�', trigger: 'change' },
+ {
+ type: 'enum',
+ enum: [CommonStatusEnum.ENABLE, CommonStatusEnum.DISABLE],
+ message: '鐘舵�佸�煎繀椤讳负鍚敤鎴栫鐢�',
+ trigger: 'change'
+ }
+ ],
+ description: [
+ { type: 'string', max: 200, message: '鍦烘櫙鎻忚堪涓嶈兘瓒呰繃200涓瓧绗�', trigger: 'blur' }
+ ],
+ triggers: [{ required: true, validator: validateTriggers, trigger: 'change' }],
+ actions: [{ required: true, validator: validateActions, trigger: 'change' }]
+}) // 琛ㄥ崟鏍¢獙瑙勫垯
+
+const submitLoading = ref(false) // 鎻愪氦鍔犺浇鐘舵��
+const isEdit = ref(false) // 鏄惁涓虹紪杈戞ā寮�
+const drawerTitle = computed(() => (isEdit.value ? '缂栬緫鍦烘櫙鑱斿姩瑙勫垯' : '鏂板鍦烘櫙鑱斿姩瑙勫垯')) // 鎶藉眽鏍囬
+
+/** 鎻愪氦琛ㄥ崟 */
+const handleSubmit = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef.value) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+
+ // 鎻愪氦璇锋眰
+ submitLoading.value = true
+ try {
+ if (isEdit.value) {
+ // 鏇存柊鍦烘櫙鑱斿姩瑙勫垯
+ await RuleSceneApi.updateRuleScene(formData.value)
+ ElMessage.success('鏇存柊鎴愬姛')
+ } else {
+ // 鍒涘缓鍦烘櫙鑱斿姩瑙勫垯
+ await RuleSceneApi.createRuleScene(formData.value)
+ ElMessage.success('鍒涘缓鎴愬姛')
+ }
+
+ // 鍏抽棴鎶藉眽骞惰Е鍙戞垚鍔熶簨浠�
+ drawerVisible.value = false
+ emit('success')
+ } catch (error) {
+ console.error('淇濆瓨澶辫触:', error)
+ ElMessage.error(isEdit.value ? '鏇存柊澶辫触' : '鍒涘缓澶辫触')
+ } finally {
+ submitLoading.value = false
+ }
+}
+
+/** 澶勭悊鎶藉眽鍏抽棴浜嬩欢 */
+const handleClose = () => {
+ drawerVisible.value = false
+}
+
+/** 鍒濆鍖栬〃鍗曟暟鎹� */
+const initFormData = () => {
+ if (props.ruleScene) {
+ // 缂栬緫妯″紡锛氭暟鎹粨鏋勫凡瀵归綈锛岀洿鎺ヤ娇鐢ㄥ悗绔暟鎹�
+ isEdit.value = true
+ formData.value = {
+ ...props.ruleScene,
+ // 纭繚瑙﹀彂鍣ㄦ暟缁勪笉涓虹┖
+ triggers: props.ruleScene.triggers?.length
+ ? props.ruleScene.triggers
+ : [
+ {
+ type: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+ productId: undefined,
+ deviceId: undefined,
+ identifier: undefined,
+ operator: undefined,
+ value: undefined,
+ cronExpression: undefined,
+ conditionGroups: []
+ }
+ ],
+ // 纭繚鎵ц鍣ㄦ暟缁勪笉涓虹┖
+ actions: props.ruleScene.actions || []
+ }
+ } else {
+ // 鏂板妯″紡锛氫娇鐢ㄩ粯璁ゆ暟鎹�
+ isEdit.value = false
+ formData.value = createDefaultFormData()
+ }
+}
+
+/** 鐩戝惉鎶藉眽鏄剧ず */
+watch(drawerVisible, async (visible) => {
+ if (visible) {
+ initFormData()
+ // 閲嶇疆琛ㄥ崟楠岃瘉鐘舵��
+ await nextTick()
+ formRef.value?.clearValidate()
+ }
+})
+
+/** 鐩戝惉缂栬緫鏁版嵁鍙樺寲 */
+watch(
+ () => props.ruleScene,
+ () => {
+ if (drawerVisible.value) {
+ initFormData()
+ }
+ },
+ { deep: true }
+)
+</script>
diff --git a/src/views/iot/rule/scene/form/configs/AlertConfig.vue b/src/views/iot/rule/scene/form/configs/AlertConfig.vue
new file mode 100644
index 0000000..8073c35
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/AlertConfig.vue
@@ -0,0 +1,81 @@
+<!-- 鍛婅閰嶇疆缁勪欢 -->
+<template>
+ <div class="w-full">
+ <el-form-item label="鍛婅閰嶇疆" required>
+ <el-select
+ v-model="localValue"
+ placeholder="璇烽�夋嫨鍛婅閰嶇疆"
+ filterable
+ clearable
+ @change="handleChange"
+ class="w-full"
+ :loading="loading"
+ >
+ <el-option
+ v-for="config in alertConfigs"
+ :key="config.id"
+ :label="config.name"
+ :value="config.id"
+ >
+ <div class="flex items-center justify-between">
+ <span>{{ config.name }}</span>
+ <el-tag :type="config.enabled ? 'success' : 'danger'" size="small">
+ {{ config.enabled ? '鍚敤' : '绂佺敤' }}
+ </el-tag>
+ </div>
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { AlertConfigApi } from '@/api/iot/alert/config'
+
+/** 鍛婅閰嶇疆缁勪欢 */
+defineOptions({ name: 'AlertConfig' })
+
+const props = defineProps<{
+ modelValue?: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value?: number): void
+}>()
+
+const localValue = useVModel(props, 'modelValue', emit)
+
+const loading = ref(false) // 鍔犺浇鐘舵��
+const alertConfigs = ref<any[]>([]) // 鍛婅閰嶇疆鍒楄〃
+
+/**
+ * 澶勭悊閫夋嫨鍙樺寲浜嬩欢
+ * @param value 閫変腑鐨勫��
+ */
+const handleChange = (value?: number) => {
+ emit('update:modelValue', value)
+}
+
+/**
+ * 鍔犺浇鍛婅閰嶇疆鍒楄〃
+ */
+const loadAlertConfigs = async () => {
+ loading.value = true
+ try {
+ const data = await AlertConfigApi.getAlertConfigPage({
+ pageNo: 1,
+ pageSize: 100,
+ enabled: true // 鍙姞杞藉惎鐢ㄧ殑閰嶇疆
+ })
+ alertConfigs.value = data.list || []
+ } finally {
+ loading.value = false
+ }
+}
+
+// 缁勪欢鎸傝浇鏃跺姞杞芥暟鎹�
+onMounted(() => {
+ loadAlertConfigs()
+})
+</script>
diff --git a/src/views/iot/rule/scene/form/configs/ConditionConfig.vue b/src/views/iot/rule/scene/form/configs/ConditionConfig.vue
new file mode 100644
index 0000000..7cb9bdb
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/ConditionConfig.vue
@@ -0,0 +1,301 @@
+<!-- 鍗曚釜鏉′欢閰嶇疆缁勪欢 -->
+<template>
+ <div class="flex flex-col gap-16px">
+ <!-- 鏉′欢绫诲瀷閫夋嫨 -->
+ <el-row :gutter="16">
+ <el-col :span="8">
+ <el-form-item label="鏉′欢绫诲瀷" required>
+ <el-select
+ :model-value="condition.type"
+ @update:model-value="(value) => updateConditionField('type', value)"
+ @change="handleConditionTypeChange"
+ placeholder="璇烽�夋嫨鏉′欢绫诲瀷"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in getConditionTypeOptions()"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 浜у搧璁惧閫夋嫨 - 璁惧鐩稿叧鏉′欢鐨勫叕鍏遍儴鍒� -->
+ <el-row v-if="isDeviceCondition" :gutter="16">
+ <el-col :span="12">
+ <el-form-item label="浜у搧" required>
+ <ProductSelector
+ :model-value="condition.productId"
+ @update:model-value="(value) => updateConditionField('productId', value)"
+ @change="handleProductChange"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧" required>
+ <DeviceSelector
+ :model-value="condition.deviceId"
+ @update:model-value="(value) => updateConditionField('deviceId', value)"
+ :product-id="condition.productId"
+ @change="handleDeviceChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 璁惧鐘舵�佹潯浠堕厤缃� -->
+ <div
+ v-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS"
+ class="flex flex-col gap-16px"
+ >
+ <!-- 鐘舵�佸拰鎿嶄綔绗﹂�夋嫨 -->
+ <el-row :gutter="16">
+ <!-- 鎿嶄綔绗﹂�夋嫨 -->
+ <el-col :span="12">
+ <el-form-item label="鎿嶄綔绗�" required>
+ <el-select
+ :model-value="condition.operator"
+ @update:model-value="(value) => updateConditionField('operator', value)"
+ placeholder="璇烽�夋嫨鎿嶄綔绗�"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in statusOperatorOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ <!-- 鐘舵�侀�夋嫨 -->
+ <el-col :span="12">
+ <el-form-item label="璁惧鐘舵��" required>
+ <el-select
+ :model-value="condition.param"
+ @update:model-value="(value) => updateConditionField('param', value)"
+ placeholder="璇烽�夋嫨璁惧鐘舵��"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in deviceStatusOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 璁惧灞炴�ф潯浠堕厤缃� -->
+ <div
+ v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY"
+ class="space-y-16px"
+ >
+ <!-- 灞炴�ч厤缃� -->
+ <el-row :gutter="16">
+ <!-- 灞炴��/浜嬩欢/鏈嶅姟閫夋嫨 -->
+ <el-col :span="6">
+ <el-form-item label="鐩戞帶椤�" required>
+ <PropertySelector
+ :model-value="condition.identifier"
+ @update:model-value="(value) => updateConditionField('identifier', value)"
+ :trigger-type="triggerType"
+ :product-id="condition.productId"
+ :device-id="condition.deviceId"
+ @change="handlePropertyChange"
+ />
+ </el-form-item>
+ </el-col>
+
+ <!-- 鎿嶄綔绗﹂�夋嫨 -->
+ <el-col :span="6">
+ <el-form-item label="鎿嶄綔绗�" required>
+ <OperatorSelector
+ :model-value="condition.operator"
+ @update:model-value="(value) => updateConditionField('operator', value)"
+ :property-type="propertyType"
+ @change="handleOperatorChange"
+ />
+ </el-form-item>
+ </el-col>
+
+ <!-- 鍊艰緭鍏� -->
+ <el-col :span="12">
+ <el-form-item label="姣旇緝鍊�" required>
+ <ValueInput
+ :model-value="condition.param"
+ @update:model-value="(value) => updateConditionField('param', value)"
+ :property-type="propertyType"
+ :operator="condition.operator"
+ :property-config="propertyConfig"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 褰撳墠鏃堕棿鏉′欢閰嶇疆 -->
+ <CurrentTimeConditionConfig
+ v-else-if="condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME"
+ :model-value="condition"
+ @update:model-value="updateCondition"
+ />
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import CurrentTimeConditionConfig from './CurrentTimeConditionConfig.vue'
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import PropertySelector from '../selectors/PropertySelector.vue'
+import OperatorSelector from '../selectors/OperatorSelector.vue'
+import ValueInput from '../inputs/ValueInput.vue'
+import type { TriggerCondition } from '@/api/iot/rule/scene'
+import {
+ IotRuleSceneTriggerConditionTypeEnum,
+ IotRuleSceneTriggerConditionParameterOperatorEnum,
+ getConditionTypeOptions,
+ IoTDeviceStatusEnum
+} from '@/views/iot/utils/constants'
+
+/** 鍗曚釜鏉′欢閰嶇疆缁勪欢 */
+defineOptions({ name: 'ConditionConfig' })
+
+const props = defineProps<{
+ modelValue: TriggerCondition
+ triggerType: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: TriggerCondition): void
+}>()
+
+/** 鑾峰彇璁惧鐘舵�侀�夐」 */
+const deviceStatusOptions = [
+ {
+ value: IoTDeviceStatusEnum.ONLINE.value,
+ label: IoTDeviceStatusEnum.ONLINE.label
+ },
+ {
+ value: IoTDeviceStatusEnum.OFFLINE.value,
+ label: IoTDeviceStatusEnum.OFFLINE.label
+ }
+]
+
+/** 鑾峰彇鐘舵�佹搷浣滅閫夐」 */
+const statusOperatorOptions = [
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name
+ }
+]
+
+const condition = useVModel(props, 'modelValue', emit)
+
+const propertyType = ref<string>('string') // 灞炴�х被鍨�
+const propertyConfig = ref<any>(null) // 灞炴�ч厤缃�
+const isDeviceCondition = computed(() => {
+ return (
+ condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS ||
+ condition.value.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY
+ )
+}) // 璁$畻灞炴�э細鍒ゆ柇鏄惁涓鸿澶囩浉鍏虫潯浠�
+
+/**
+ * 鏇存柊鏉′欢瀛楁
+ * @param field 瀛楁鍚�
+ * @param value 瀛楁鍊�
+ */
+const updateConditionField = (field: any, value: any) => {
+ ;(condition.value as any)[field] = value
+ emit('update:modelValue', condition.value)
+}
+
+/**
+ * 鏇存柊鏁翠釜鏉′欢瀵硅薄
+ * @param newCondition 鏂扮殑鏉′欢瀵硅薄
+ */
+const updateCondition = (newCondition: TriggerCondition) => {
+ condition.value = newCondition
+ emit('update:modelValue', condition.value)
+}
+
+/**
+ * 澶勭悊鏉′欢绫诲瀷鍙樺寲浜嬩欢
+ * @param type 鏉′欢绫诲瀷
+ */
+const handleConditionTypeChange = (type: number) => {
+ // 鏍规嵁鏉′欢绫诲瀷娓呯悊瀛楁
+ const isCurrentTime = type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME
+ const isDeviceStatus = type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS
+
+ // 娓呯悊鏍囪瘑绗﹀瓧娈碉紙鏃堕棿鏉′欢鍜岃澶囩姸鎬佹潯浠堕兘涓嶉渶瑕侊級
+ if (isCurrentTime || isDeviceStatus) {
+ condition.value.identifier = undefined
+ }
+
+ // 娓呯悊璁惧鐩稿叧瀛楁锛堜粎鏃堕棿鏉′欢闇�瑕侊級
+ if (isCurrentTime) {
+ condition.value.productId = undefined
+ condition.value.deviceId = undefined
+ }
+
+ // 璁剧疆榛樿鎿嶄綔绗�
+ condition.value.operator = isCurrentTime
+ ? 'at_time'
+ : IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
+
+ // 娓呯┖鍙傛暟鍊�
+ condition.value.param = ''
+}
+
+/** 澶勭悊浜у搧鍙樺寲浜嬩欢 */
+const handleProductChange = (_: number) => {
+ // 浜у搧鍙樺寲鏃舵竻绌鸿澶囧拰灞炴��
+ condition.value.deviceId = undefined
+ condition.value.identifier = ''
+}
+
+/** 澶勭悊璁惧鍙樺寲浜嬩欢 */
+const handleDeviceChange = (_: number) => {
+ // 璁惧鍙樺寲鏃舵竻绌哄睘鎬�
+ condition.value.identifier = ''
+}
+
+/**
+ * 澶勭悊灞炴�у彉鍖栦簨浠�
+ * @param propertyInfo 灞炴�т俊鎭璞�
+ */
+const handlePropertyChange = (propertyInfo: { type: string; config: any }) => {
+ propertyType.value = propertyInfo.type
+ propertyConfig.value = propertyInfo.config
+
+ // 閲嶇疆鎿嶄綔绗﹀拰鍊�
+ condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
+ condition.value.param = ''
+}
+
+/** 澶勭悊鎿嶄綔绗﹀彉鍖栦簨浠� */
+const handleOperatorChange = () => {
+ // 閲嶇疆鍊�
+ condition.value.param = ''
+}
+</script>
+
+<style scoped>
+:deep(.el-form-item) {
+ margin-bottom: 0;
+}
+</style>
diff --git a/src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue b/src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue
new file mode 100644
index 0000000..9d304b7
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/CurrentTimeConditionConfig.vue
@@ -0,0 +1,234 @@
+<!-- 褰撳墠鏃堕棿鏉′欢閰嶇疆缁勪欢 -->
+<template>
+ <div class="flex flex-col gap-16px">
+ <el-row :gutter="16">
+ <!-- 鏃堕棿鎿嶄綔绗﹂�夋嫨 -->
+ <el-col :span="8">
+ <el-form-item label="鏃堕棿鏉′欢" required>
+ <el-select
+ :model-value="condition.operator"
+ @update:model-value="(value) => updateConditionField('operator', value)"
+ placeholder="璇烽�夋嫨鏃堕棿鏉′欢"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in timeOperatorOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ >
+ <div class="flex items-center justify-between w-full">
+ <div class="flex items-center gap-8px">
+ <Icon :icon="option.icon" :class="option.iconClass" />
+ <span>{{ option.label }}</span>
+ </div>
+ <el-tag :type="option.tag as any" size="small">{{ option.category }}</el-tag>
+ </div>
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ <!-- 鏃堕棿鍊艰緭鍏� -->
+ <el-col :span="8">
+ <el-form-item label="鏃堕棿鍊�" required>
+ <el-time-picker
+ v-if="needsTimeInput"
+ :model-value="timeValue"
+ @update:model-value="handleTimeValueChange"
+ placeholder="璇烽�夋嫨鏃堕棿"
+ format="HH:mm:ss"
+ value-format="HH:mm:ss"
+ class="w-full"
+ />
+ <el-date-picker
+ v-else-if="needsDateInput"
+ :model-value="timeValue"
+ @update:model-value="handleTimeValueChange"
+ type="datetime"
+ placeholder="璇烽�夋嫨鏃ユ湡鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ class="w-full"
+ />
+ <div v-else class="text-[var(--el-text-color-placeholder)] text-14px">
+ 鏃犻渶璁剧疆鏃堕棿鍊�
+ </div>
+ </el-form-item>
+ </el-col>
+
+ <!-- 绗簩涓椂闂村�硷紙鑼冨洿鏉′欢锛� -->
+ <el-col :span="8" v-if="needsSecondTimeInput">
+ <el-form-item label="缁撴潫鏃堕棿" required>
+ <el-time-picker
+ v-if="needsTimeInput"
+ :model-value="timeValue2"
+ @update:model-value="handleTimeValue2Change"
+ placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+ format="HH:mm:ss"
+ value-format="HH:mm:ss"
+ class="w-full"
+ />
+ <el-date-picker
+ v-else
+ :model-value="timeValue2"
+ @update:model-value="handleTimeValue2Change"
+ type="datetime"
+ placeholder="璇烽�夋嫨缁撴潫鏃ユ湡鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ class="w-full"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { IotRuleSceneTriggerTimeOperatorEnum } from '@/views/iot/utils/constants'
+import type { TriggerCondition } from '@/api/iot/rule/scene'
+
+/** 褰撳墠鏃堕棿鏉′欢閰嶇疆缁勪欢 */
+defineOptions({ name: 'CurrentTimeConditionConfig' })
+
+const props = defineProps<{
+ modelValue: TriggerCondition
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: TriggerCondition): void
+}>()
+
+const condition = useVModel(props, 'modelValue', emit)
+
+// 鏃堕棿鎿嶄綔绗﹂�夐」
+const timeOperatorOptions = [
+ {
+ value: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
+ label: IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.name,
+ icon: 'ep:arrow-left',
+ iconClass: 'text-blue-500',
+ tag: 'primary',
+ category: '鏃堕棿鐐�'
+ },
+ {
+ value: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
+ label: IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.name,
+ icon: 'ep:arrow-right',
+ iconClass: 'text-green-500',
+ tag: 'success',
+ category: '鏃堕棿鐐�'
+ },
+ {
+ value: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
+ label: IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.name,
+ icon: 'ep:sort',
+ iconClass: 'text-orange-500',
+ tag: 'warning',
+ category: '鏃堕棿娈�'
+ },
+ {
+ value: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value,
+ label: IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.name,
+ icon: 'ep:position',
+ iconClass: 'text-purple-500',
+ tag: 'info',
+ category: '鏃堕棿鐐�'
+ },
+ {
+ value: IotRuleSceneTriggerTimeOperatorEnum.TODAY.value,
+ label: IotRuleSceneTriggerTimeOperatorEnum.TODAY.name,
+ icon: 'ep:calendar',
+ iconClass: 'text-red-500',
+ tag: 'danger',
+ category: '鏃ユ湡'
+ }
+]
+
+// 璁$畻灞炴�э細鏄惁闇�瑕佹椂闂磋緭鍏�
+const needsTimeInput = computed(() => {
+ const timeOnlyOperators = [
+ IotRuleSceneTriggerTimeOperatorEnum.BEFORE_TIME.value,
+ IotRuleSceneTriggerTimeOperatorEnum.AFTER_TIME.value,
+ IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value,
+ IotRuleSceneTriggerTimeOperatorEnum.AT_TIME.value
+ ]
+ return timeOnlyOperators.includes(condition.value.operator as any)
+})
+
+// 璁$畻灞炴�э細鏄惁闇�瑕佹棩鏈熻緭鍏�
+const needsDateInput = computed(() => {
+ return false // 鏆傛椂涓嶆敮鎸佹棩鏈熻緭鍏ワ紝鍙敮鎸佹椂闂�
+})
+
+// 璁$畻灞炴�э細鏄惁闇�瑕佺浜屼釜鏃堕棿杈撳叆
+const needsSecondTimeInput = computed(() => {
+ return condition.value.operator === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value
+})
+
+// 璁$畻灞炴�э細浠� param 涓В鏋愭椂闂村��
+const timeValue = computed(() => {
+ if (!condition.value.param) return ''
+ const params = condition.value.param.split(',')
+ return params[0] || ''
+})
+
+// 璁$畻灞炴�э細浠� param 涓В鏋愮浜屼釜鏃堕棿鍊�
+const timeValue2 = computed(() => {
+ if (!condition.value.param) return ''
+ const params = condition.value.param.split(',')
+ return params[1] || ''
+})
+
+/**
+ * 鏇存柊鏉′欢瀛楁
+ * @param field 瀛楁鍚�
+ * @param value 瀛楁鍊�
+ */
+const updateConditionField = (field: any, value: any) => {
+ condition.value[field] = value
+}
+
+/**
+ * 澶勭悊绗竴涓椂闂村�煎彉鍖�
+ * @param value 鏃堕棿鍊�
+ */
+const handleTimeValueChange = (value: string) => {
+ const currentParams = condition.value.param ? condition.value.param.split(',') : []
+ currentParams[0] = value || ''
+
+ // 濡傛灉鏄寖鍥存潯浠讹紝淇濈暀绗簩涓�硷紱鍚﹀垯鍙繚鐣欑涓�涓��
+ if (needsSecondTimeInput.value) {
+ condition.value.param = currentParams.slice(0, 2).join(',')
+ } else {
+ condition.value.param = currentParams[0]
+ }
+}
+
+/**
+ * 澶勭悊绗簩涓椂闂村�煎彉鍖�
+ * @param value 鏃堕棿鍊�
+ */
+const handleTimeValue2Change = (value: string) => {
+ const currentParams = condition.value.param ? condition.value.param.split(',') : ['']
+ currentParams[1] = value || ''
+ condition.value.param = currentParams.slice(0, 2).join(',')
+}
+
+/** 鐩戝惉鎿嶄綔绗﹀彉鍖栵紝娓呯悊涓嶇浉鍏崇殑鏃堕棿鍊� */
+watch(
+ () => condition.value.operator,
+ (newOperator) => {
+ if (newOperator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) {
+ // 浠婃棩鏉′欢涓嶉渶瑕佹椂闂村弬鏁�
+ condition.value.param = ''
+ } else if (!needsSecondTimeInput.value) {
+ // 闈炶寖鍥存潯浠跺彧淇濈暀绗竴涓椂闂村��
+ const currentParams = condition.value.param ? condition.value.param.split(',') : []
+ condition.value.param = currentParams[0] || ''
+ }
+ }
+)
+</script>
diff --git a/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue b/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue
new file mode 100644
index 0000000..2cc89c9
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/DeviceControlConfig.vue
@@ -0,0 +1,376 @@
+<!-- 璁惧鎺у埗閰嶇疆缁勪欢 -->
+<template>
+ <div class="flex flex-col gap-16px">
+ <!-- 浜у搧鍜岃澶囬�夋嫨 - 涓庤Е鍙戝櫒淇濇寔涓�鑷寸殑鍒嗙寮忛�夋嫨鍣� -->
+ <el-row :gutter="16">
+ <el-col :span="12">
+ <el-form-item label="浜у搧" required>
+ <ProductSelector v-model="action.productId" @change="handleProductChange" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧" required>
+ <DeviceSelector
+ v-model="action.deviceId"
+ :product-id="action.productId"
+ @change="handleDeviceChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 鏈嶅姟閫夋嫨 - 鏈嶅姟璋冪敤绫诲瀷鏃舵樉绀� -->
+ <div v-if="action.productId && isServiceInvokeAction" class="space-y-16px">
+ <el-form-item label="鏈嶅姟" required>
+ <el-select
+ v-model="action.identifier"
+ placeholder="璇烽�夋嫨鏈嶅姟"
+ filterable
+ clearable
+ class="w-full"
+ :loading="loadingServices"
+ @change="handleServiceChange"
+ >
+ <el-option
+ v-for="service in serviceList"
+ :key="service.identifier"
+ :label="service.name"
+ :value="service.identifier"
+ >
+ <div class="flex items-center justify-between">
+ <span>{{ service.name }}</span>
+ <el-tag :type="service.callType === 'sync' ? 'primary' : 'success'" size="small">
+ {{ service.callType === 'sync' ? '鍚屾' : '寮傛' }}
+ </el-tag>
+ </div>
+ </el-option>
+ </el-select>
+ </el-form-item>
+
+ <!-- 鏈嶅姟鍙傛暟閰嶇疆 -->
+ <div v-if="action.identifier" class="space-y-16px">
+ <el-form-item label="鏈嶅姟鍙傛暟" required>
+ <JsonParamsInput
+ v-model="paramsValue"
+ type="service"
+ :config="{ service: selectedService } as any"
+ placeholder="璇疯緭鍏� JSON 鏍煎紡鐨勬湇鍔″弬鏁�"
+ />
+ </el-form-item>
+ </div>
+ </div>
+
+ <!-- 鎺у埗鍙傛暟閰嶇疆 - 灞炴�ц缃被鍨嬫椂鏄剧ず -->
+ <div v-if="action.productId && isPropertySetAction" class="space-y-16px">
+ <!-- 鍙傛暟閰嶇疆 -->
+ <el-form-item label="鍙傛暟" required>
+ <JsonParamsInput
+ v-model="paramsValue"
+ type="property"
+ :config="{ properties: thingModelProperties }"
+ placeholder="璇疯緭鍏� JSON 鏍煎紡鐨勬帶鍒跺弬鏁�"
+ />
+ </el-form-item>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import JsonParamsInput from '../inputs/JsonParamsInput.vue'
+import type { Action } from '@/api/iot/rule/scene'
+import type { ThingModelProperty, ThingModelService } from '@/api/iot/thingmodel'
+import {
+ IotRuleSceneActionTypeEnum,
+ IoTThingModelAccessModeEnum,
+ IoTDataSpecsDataTypeEnum
+} from '@/views/iot/utils/constants'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+
+/** 璁惧鎺у埗閰嶇疆缁勪欢 */
+defineOptions({ name: 'DeviceControlConfig' })
+
+const props = defineProps<{
+ modelValue: Action
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: Action): void
+}>()
+
+const action = useVModel(props, 'modelValue', emit)
+
+const thingModelProperties = ref<ThingModelProperty[]>([]) // 鐗╂ā鍨嬪睘鎬у垪琛�
+const loadingThingModel = ref(false) // 鐗╂ā鍨嬪姞杞界姸鎬�
+const selectedService = ref<ThingModelService | null>(null) // 閫変腑鐨勬湇鍔″璞�
+const serviceList = ref<ThingModelService[]>([]) // 鏈嶅姟鍒楄〃
+const loadingServices = ref(false) // 鏈嶅姟鍔犺浇鐘舵��
+
+// 鍙傛暟鍊肩殑璁$畻灞炴�э紝鐢ㄤ簬鍙屽悜缁戝畾
+const paramsValue = computed({
+ get: () => {
+ // 濡傛灉 params 鏄璞★紝杞崲涓� JSON 瀛楃涓诧紙鍏煎鏃ф暟鎹級
+ if (action.value.params && typeof action.value.params === 'object') {
+ return JSON.stringify(action.value.params, null, 2)
+ }
+ // 濡傛灉 params 宸茬粡鏄瓧绗︿覆锛岀洿鎺ヨ繑鍥�
+ return action.value.params || ''
+ },
+ set: (value: string) => {
+ // 鐩存帴淇濆瓨涓� JSON 瀛楃涓诧紝涓嶈繘琛岃В鏋愯浆鎹�
+ action.value.params = value.trim() || ''
+ }
+})
+
+// 璁$畻灞炴�э細鏄惁涓哄睘鎬ц缃被鍨�
+const isPropertySetAction = computed(() => {
+ return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET
+})
+
+// 璁$畻灞炴�э細鏄惁涓烘湇鍔¤皟鐢ㄧ被鍨�
+const isServiceInvokeAction = computed(() => {
+ return action.value.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+})
+
+/**
+ * 澶勭悊浜у搧鍙樺寲浜嬩欢
+ * @param productId 浜у搧 ID
+ */
+const handleProductChange = (productId?: number) => {
+ // 褰撲骇鍝佸彉鍖栨椂锛屾竻绌鸿澶囬�夋嫨鍜屽弬鏁伴厤缃�
+ if (action.value.productId !== productId) {
+ action.value.deviceId = undefined
+ action.value.identifier = undefined // 娓呯┖鏈嶅姟鏍囪瘑绗�
+ action.value.params = '' // 娓呯┖鍙傛暟锛屼繚瀛樹负绌哄瓧绗︿覆
+ selectedService.value = null // 娓呯┖閫変腑鐨勬湇鍔�
+ serviceList.value = [] // 娓呯┖鏈嶅姟鍒楄〃
+ }
+
+ // 鍔犺浇鏂颁骇鍝佺殑鐗╂ā鍨嬪睘鎬ф垨鏈嶅姟鍒楄〃
+ if (productId) {
+ if (isPropertySetAction.value) {
+ loadThingModelProperties(productId)
+ } else if (isServiceInvokeAction.value) {
+ loadServiceList(productId)
+ }
+ }
+}
+
+/**
+ * 澶勭悊璁惧鍙樺寲浜嬩欢
+ * @param deviceId 璁惧 ID
+ */
+const handleDeviceChange = (deviceId?: number) => {
+ // 褰撹澶囧彉鍖栨椂锛屾竻绌哄弬鏁伴厤缃�
+ if (action.value.deviceId !== deviceId) {
+ action.value.params = '' // 娓呯┖鍙傛暟锛屼繚瀛樹负绌哄瓧绗︿覆
+ }
+}
+
+/**
+ * 澶勭悊鏈嶅姟鍙樺寲浜嬩欢
+ * @param serviceIdentifier 鏈嶅姟鏍囪瘑绗�
+ */
+const handleServiceChange = (serviceIdentifier?: string) => {
+ // 鏍规嵁鏈嶅姟鏍囪瘑绗︽壘鍒板搴旂殑鏈嶅姟瀵硅薄
+ const service = serviceList.value.find((s) => s.identifier === serviceIdentifier) || null
+ selectedService.value = service
+
+ // 褰撴湇鍔″彉鍖栨椂锛屾竻绌哄弬鏁伴厤缃�
+ action.value.params = ''
+
+ // 濡傛灉閫夋嫨浜嗘湇鍔′笖鏈夎緭鍏ュ弬鏁帮紝鐢熸垚榛樿鍙傛暟缁撴瀯
+ if (service && service.inputParams && service.inputParams.length > 0) {
+ const defaultParams = {}
+ service.inputParams.forEach((param) => {
+ defaultParams[param.identifier] = getDefaultValueForParam(param)
+ })
+ // 灏嗛粯璁ゅ弬鏁拌浆鎹负 JSON 瀛楃涓蹭繚瀛�
+ action.value.params = JSON.stringify(defaultParams, null, 2)
+ }
+}
+
+/**
+ * 鑾峰彇鐗╂ā鍨婽SL鏁版嵁
+ * @param productId 浜у搧ID
+ * @returns 鐗╂ā鍨婽SL鏁版嵁
+ */
+const getThingModelTSL = async (productId: number) => {
+ if (!productId) return null
+
+ try {
+ return await ThingModelApi.getThingModelTSLByProductId(productId)
+ } catch (error) {
+ console.error('鑾峰彇鐗╂ā鍨婽SL鏁版嵁澶辫触:', error)
+ return null
+ }
+}
+
+/**
+ * 鍔犺浇鐗╂ā鍨嬪睘鎬э紙鍙啓灞炴�э級
+ * @param productId 浜у搧ID
+ */
+const loadThingModelProperties = async (productId: number) => {
+ if (!productId) {
+ thingModelProperties.value = []
+ return
+ }
+
+ try {
+ loadingThingModel.value = true
+ const tslData = await getThingModelTSL(productId)
+
+ if (!tslData?.properties) {
+ thingModelProperties.value = []
+ return
+ }
+
+ // 杩囨护鍑哄彲鍐欑殑灞炴�э紙accessMode 鍖呭惈 'w'锛�
+ thingModelProperties.value = tslData.properties.filter(
+ (property: ThingModelProperty) =>
+ property.accessMode &&
+ (property.accessMode === IoTThingModelAccessModeEnum.READ_WRITE.value ||
+ property.accessMode === IoTThingModelAccessModeEnum.WRITE_ONLY.value)
+ )
+ } catch (error) {
+ console.error('鍔犺浇鐗╂ā鍨嬪睘鎬уけ璐�:', error)
+ thingModelProperties.value = []
+ } finally {
+ loadingThingModel.value = false
+ }
+}
+
+/**
+ * 鍔犺浇鏈嶅姟鍒楄〃
+ * @param productId 浜у搧ID
+ */
+const loadServiceList = async (productId: number) => {
+ if (!productId) {
+ serviceList.value = []
+ return
+ }
+
+ try {
+ loadingServices.value = true
+ const tslData = await getThingModelTSL(productId)
+
+ if (!tslData?.services) {
+ serviceList.value = []
+ return
+ }
+
+ serviceList.value = tslData.services
+ } catch (error) {
+ console.error('鍔犺浇鏈嶅姟鍒楄〃澶辫触:', error)
+ serviceList.value = []
+ } finally {
+ loadingServices.value = false
+ }
+}
+
+/**
+ * 浠嶵SL鍔犺浇鏈嶅姟淇℃伅锛堢敤浜庣紪杈戞ā寮忓洖鏄撅級
+ * @param productId 浜у搧ID
+ * @param serviceIdentifier 鏈嶅姟鏍囪瘑绗�
+ */
+const loadServiceFromTSL = async (productId: number, serviceIdentifier: string) => {
+ // 鍏堝姞杞芥湇鍔″垪琛�
+ await loadServiceList(productId)
+
+ // 鐒跺悗璁剧疆閫変腑鐨勬湇鍔�
+ const service = serviceList.value.find((s: any) => s.identifier === serviceIdentifier)
+ if (service) {
+ selectedService.value = service
+ }
+}
+
+/**
+ * 鏍规嵁鍙傛暟绫诲瀷鑾峰彇榛樿鍊�
+ * @param param 鍙傛暟瀵硅薄
+ * @returns 榛樿鍊�
+ */
+const getDefaultValueForParam = (param: any) => {
+ switch (param.dataType) {
+ case IoTDataSpecsDataTypeEnum.INT:
+ return 0
+ case IoTDataSpecsDataTypeEnum.FLOAT:
+ case IoTDataSpecsDataTypeEnum.DOUBLE:
+ return 0.0
+ case IoTDataSpecsDataTypeEnum.BOOL:
+ return false
+ case IoTDataSpecsDataTypeEnum.TEXT:
+ return ''
+ case IoTDataSpecsDataTypeEnum.ENUM:
+ // 濡傛灉鏈夋灇涓惧�硷紝浣跨敤绗竴涓�
+ if (param.dataSpecs?.dataSpecsList && param.dataSpecs.dataSpecsList.length > 0) {
+ return param.dataSpecs.dataSpecsList[0].value
+ }
+ return ''
+ default:
+ return ''
+ }
+}
+
+const isInitialized = ref(false) // 闃叉閲嶅鍒濆鍖栫殑鏍囧織
+
+/**
+ * 鍒濆鍖栫粍浠舵暟鎹�
+ */
+const initializeComponent = async () => {
+ if (isInitialized.value) return
+
+ const currentAction = action.value
+ if (!currentAction) return
+
+ // 濡傛灉宸茬粡閫夋嫨浜嗕骇鍝佷笖鏄睘鎬ц缃被鍨嬶紝鍔犺浇鐗╂ā鍨�
+ if (currentAction.productId && isPropertySetAction.value) {
+ await loadThingModelProperties(currentAction.productId)
+ }
+
+ // 濡傛灉鏄湇鍔¤皟鐢ㄧ被鍨嬩笖宸叉湁鏍囪瘑绗︼紝鍒濆鍖栨湇鍔¢�夋嫨
+ if (currentAction.productId && isServiceInvokeAction.value && currentAction.identifier) {
+ // 鍔犺浇鐗╂ā鍨婽SL浠ヨ幏鍙栨湇鍔′俊鎭�
+ await loadServiceFromTSL(currentAction.productId, currentAction.identifier)
+ }
+
+ isInitialized.value = true
+}
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(() => {
+ initializeComponent()
+})
+
+/** 鐩戝惉鍏抽敭瀛楁鐨勫彉鍖栵紝閬垮厤娣卞害鐩戝惉瀵艰嚧鐨勬�ц兘闂 */
+watch(
+ () => [action.value.productId, action.value.type, action.value.identifier],
+ async ([newProductId, , newIdentifier], [oldProductId, , oldIdentifier]) => {
+ // 閬垮厤鍒濆鍖栨椂鐨勯噸澶嶈皟鐢�
+ if (!isInitialized.value) return
+
+ // 浜у搧鍙樺寲鏃堕噸鏂板姞杞芥暟鎹�
+ if (newProductId !== oldProductId) {
+ if (newProductId && isPropertySetAction.value) {
+ await loadThingModelProperties(newProductId as number)
+ } else if (newProductId && isServiceInvokeAction.value) {
+ await loadServiceList(newProductId as number)
+ }
+ }
+
+ // 鏈嶅姟鏍囪瘑绗﹀彉鍖栨椂鏇存柊閫変腑鐨勬湇鍔�
+ if (
+ newIdentifier !== oldIdentifier &&
+ newProductId &&
+ isServiceInvokeAction.value &&
+ newIdentifier
+ ) {
+ const service = serviceList.value.find((s: any) => s.identifier === newIdentifier)
+ if (service) {
+ selectedService.value = service
+ }
+ }
+ }
+)
+</script>
diff --git a/src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue b/src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue
new file mode 100644
index 0000000..979eff8
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/DeviceTriggerConfig.vue
@@ -0,0 +1,251 @@
+<!-- 璁惧瑙﹀彂閰嶇疆缁勪欢 -->
+<template>
+ <div class="flex flex-col gap-16px">
+ <!-- 涓绘潯浠堕厤缃� - 榛樿鐩存帴灞曠ず -->
+ <div class="space-y-16px">
+ <!-- 涓绘潯浠堕厤缃� -->
+ <div class="flex flex-col gap-16px">
+ <!-- 涓绘潯浠堕厤缃� -->
+ <div class="space-y-16px">
+ <!-- 涓绘潯浠跺ご閮� - 涓庨檮鍔犳潯浠剁粍淇濇寔涓�鑷寸殑缁胯壊椋庢牸 -->
+ <div
+ class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
+ >
+ <div class="flex items-center gap-12px">
+ <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+ <div
+ class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+ >
+ 涓�
+ </div>
+ <span>涓绘潯浠�</span>
+ </div>
+ <el-tag size="small" type="success">蹇呴』婊¤冻</el-tag>
+ </div>
+ </div>
+
+ <!-- 涓绘潯浠跺唴瀹归厤缃� -->
+ <MainConditionInnerConfig
+ :model-value="trigger"
+ @update:model-value="updateCondition"
+ :trigger-type="trigger.type"
+ @trigger-type-change="handleTriggerTypeChange"
+ />
+ </div>
+ </div>
+ </div>
+
+ <!-- 鏉′欢缁勯厤缃� -->
+ <div class="space-y-16px">
+ <!-- 鏉′欢缁勯厤缃� -->
+ <div class="flex flex-col gap-16px">
+ <!-- 鏉′欢缁勫鍣ㄥご閮� -->
+ <div
+ class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border border-green-200 rounded-8px"
+ >
+ <div class="flex items-center gap-12px">
+ <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+ <div
+ class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+ >
+ 缁�
+ </div>
+ <span>闄勫姞鏉′欢缁�</span>
+ </div>
+ <el-tag size="small" type="success">涓�"涓绘潯浠�"涓轰笖鍏崇郴</el-tag>
+ <el-tag size="small" type="info">
+ {{ trigger.conditionGroups?.length || 0 }} 涓瓙鏉′欢缁�
+ </el-tag>
+ </div>
+ <div class="flex items-center gap-8px">
+ <el-button
+ type="primary"
+ size="small"
+ @click="addSubGroup"
+ :disabled="(trigger.conditionGroups?.length || 0) >= maxSubGroups"
+ >
+ <Icon icon="ep:plus" />
+ 娣诲姞瀛愭潯浠剁粍
+ </el-button>
+ <el-button type="danger" size="small" text @click="removeConditionGroup">
+ <Icon icon="ep:delete" />
+ 鍒犻櫎鏉′欢缁�
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 瀛愭潯浠剁粍鍒楄〃 -->
+ <div
+ v-if="trigger.conditionGroups && trigger.conditionGroups.length > 0"
+ class="space-y-16px"
+ >
+ <!-- 閫昏緫鍏崇郴璇存槑 -->
+ <div class="relative">
+ <div
+ v-for="(subGroup, subGroupIndex) in trigger.conditionGroups"
+ :key="`sub-group-${subGroupIndex}`"
+ class="relative"
+ >
+ <!-- 瀛愭潯浠剁粍瀹瑰櫒 -->
+ <div
+ class="border-2 border-orange-200 rounded-8px bg-orange-50 shadow-sm hover:shadow-md transition-shadow"
+ >
+ <div
+ class="flex items-center justify-between p-16px bg-gradient-to-r from-orange-50 to-yellow-50 border-b border-orange-200 rounded-t-6px"
+ >
+ <div class="flex items-center gap-12px">
+ <div class="flex items-center gap-8px text-16px font-600 text-orange-700">
+ <div
+ class="w-24px h-24px bg-orange-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+ >
+ {{ subGroupIndex + 1 }}
+ </div>
+ <span>瀛愭潯浠剁粍 {{ subGroupIndex + 1 }}</span>
+ </div>
+ <el-tag size="small" type="warning" class="font-500">缁勫唴鏉′欢涓�"涓�"鍏崇郴</el-tag>
+ <el-tag size="small" type="info"> {{ subGroup?.length || 0 }}涓潯浠� </el-tag>
+ </div>
+ <el-button
+ type="danger"
+ size="small"
+ text
+ @click="removeSubGroup(subGroupIndex)"
+ class="hover:bg-red-50"
+ >
+ <Icon icon="ep:delete" />
+ 鍒犻櫎缁�
+ </el-button>
+ </div>
+
+ <SubConditionGroupConfig
+ :model-value="subGroup"
+ @update:model-value="(value) => updateSubGroup(subGroupIndex, value)"
+ :trigger-type="trigger.type"
+ :max-conditions="maxConditionsPerGroup"
+ />
+ </div>
+
+ <!-- 瀛愭潯浠剁粍闂寸殑"鎴�"杩炴帴绗� -->
+ <div
+ v-if="subGroupIndex < trigger.conditionGroups!.length - 1"
+ class="flex items-center justify-center py-12px"
+ >
+ <div class="flex items-center gap-8px">
+ <!-- 杩炴帴绾� -->
+ <div class="w-32px h-1px bg-orange-300"></div>
+ <!-- 鎴栨爣绛� -->
+ <div class="px-16px py-6px bg-orange-100 border-2 border-orange-300 rounded-full">
+ <span class="text-14px font-600 text-orange-600">鎴�</span>
+ </div>
+ <!-- 杩炴帴绾� -->
+ <div class="w-32px h-1px bg-orange-300"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 绌虹姸鎬� -->
+ <div
+ v-else
+ class="p-24px border-2 border-dashed border-orange-200 rounded-8px text-center bg-orange-50"
+ >
+ <div class="flex flex-col items-center gap-12px">
+ <Icon icon="ep:plus" class="text-32px text-orange-400" />
+ <div class="text-orange-600">
+ <p class="text-14px font-500 mb-4px">鏆傛棤瀛愭潯浠剁粍</p>
+ <p class="text-12px">鐐瑰嚮涓婃柟"娣诲姞瀛愭潯浠剁粍"鎸夐挳寮�濮嬮厤缃�</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+
+import MainConditionInnerConfig from './MainConditionInnerConfig.vue'
+import SubConditionGroupConfig from './SubConditionGroupConfig.vue'
+import type { Trigger } from '@/api/iot/rule/scene'
+
+/** 璁惧瑙﹀彂閰嶇疆缁勪欢 */
+defineOptions({ name: 'DeviceTriggerConfig' })
+
+const props = defineProps<{
+ modelValue: Trigger
+ index: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: Trigger): void
+ (e: 'trigger-type-change', type: number): void
+}>()
+
+const trigger = useVModel(props, 'modelValue', emit)
+
+const maxSubGroups = 3 // 鏈�澶� 3 涓瓙鏉′欢缁�
+const maxConditionsPerGroup = 3 // 姣忕粍鏈�澶� 3 涓潯浠�
+
+/**
+ * 鏇存柊鏉′欢
+ * @param condition 鏉′欢瀵硅薄
+ */
+const updateCondition = (condition: Trigger) => {
+ trigger.value = condition
+}
+
+/**
+ * 澶勭悊瑙﹀彂鍣ㄧ被鍨嬪彉鍖栦簨浠�
+ * @param type 瑙﹀彂鍣ㄧ被鍨�
+ */
+const handleTriggerTypeChange = (type: number) => {
+ trigger.value.type = type
+ emit('trigger-type-change', type)
+}
+
+/** 娣诲姞瀛愭潯浠剁粍 */
+const addSubGroup = async () => {
+ if (!trigger.value.conditionGroups) {
+ trigger.value.conditionGroups = []
+ }
+
+ // 妫�鏌ユ槸鍚﹁揪鍒版渶澶у瓙缁勬暟閲忛檺鍒�
+ if (trigger.value.conditionGroups?.length >= maxSubGroups) {
+ return
+ }
+
+ // 浣跨敤 nextTick 纭繚鍝嶅簲寮忔洿鏂板畬鎴愬悗鍐嶆坊鍔犳柊鐨勫瓙缁�
+ await nextTick()
+ if (trigger.value.conditionGroups) {
+ trigger.value.conditionGroups.push([])
+ }
+}
+
+/**
+ * 绉婚櫎瀛愭潯浠剁粍
+ * @param index 瀛愭潯浠剁粍绱㈠紩
+ */
+const removeSubGroup = (index: number) => {
+ if (trigger.value.conditionGroups) {
+ trigger.value.conditionGroups.splice(index, 1)
+ }
+}
+
+/**
+ * 鏇存柊瀛愭潯浠剁粍
+ * @param index 瀛愭潯浠剁粍绱㈠紩
+ * @param subGroup 瀛愭潯浠剁粍鏁版嵁
+ */
+const updateSubGroup = (index: number, subGroup: any) => {
+ if (trigger.value.conditionGroups) {
+ trigger.value.conditionGroups[index] = subGroup
+ }
+}
+
+/** 绉婚櫎鏁翠釜鏉′欢缁� */
+const removeConditionGroup = () => {
+ trigger.value.conditionGroups = undefined
+}
+</script>
diff --git a/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue
new file mode 100644
index 0000000..4c61d31
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/MainConditionInnerConfig.vue
@@ -0,0 +1,340 @@
+<template>
+ <div class="space-y-16px">
+ <!-- 瑙﹀彂浜嬩欢绫诲瀷閫夋嫨 -->
+ <el-form-item label="瑙﹀彂浜嬩欢绫诲瀷" required>
+ <el-select
+ :model-value="triggerType"
+ @update:model-value="handleTriggerTypeChange"
+ placeholder="璇烽�夋嫨瑙﹀彂浜嬩欢绫诲瀷"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in triggerTypeOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <!-- 璁惧灞炴�ф潯浠堕厤缃� -->
+ <div v-if="isDevicePropertyTrigger" class="space-y-16px">
+ <!-- 浜у搧璁惧閫夋嫨 -->
+ <el-row :gutter="16">
+ <el-col :span="12">
+ <el-form-item label="浜у搧" required>
+ <ProductSelector
+ :model-value="condition.productId"
+ @update:model-value="(value) => updateConditionField('productId', value)"
+ @change="handleProductChange"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧" required>
+ <DeviceSelector
+ :model-value="condition.deviceId"
+ @update:model-value="(value) => updateConditionField('deviceId', value)"
+ :product-id="condition.productId"
+ @change="handleDeviceChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 灞炴�ч厤缃� -->
+ <el-row :gutter="16">
+ <!-- 灞炴��/浜嬩欢/鏈嶅姟閫夋嫨 -->
+ <el-col :span="6">
+ <el-form-item label="鐩戞帶椤�" required>
+ <PropertySelector
+ :model-value="condition.identifier"
+ @update:model-value="(value) => updateConditionField('identifier', value)"
+ :trigger-type="triggerType"
+ :product-id="condition.productId"
+ :device-id="condition.deviceId"
+ @change="handlePropertyChange"
+ />
+ </el-form-item>
+ </el-col>
+
+ <!-- 鎿嶄綔绗﹂�夋嫨 - 鏈嶅姟璋冪敤鍜屼簨浠朵笂鎶ヤ笉闇�瑕佹搷浣滅 -->
+ <el-col v-if="needsOperatorSelector" :span="6">
+ <el-form-item label="鎿嶄綔绗�" required>
+ <OperatorSelector
+ :model-value="condition.operator"
+ @update:model-value="(value) => updateConditionField('operator', value)"
+ :property-type="propertyType"
+ />
+ </el-form-item>
+ </el-col>
+
+ <!-- 鍊艰緭鍏� -->
+ <el-col :span="isWideValueColumn ? 18 : 12">
+ <el-form-item :label="valueInputLabel" required>
+ <!-- 鏈嶅姟璋冪敤鍙傛暟閰嶇疆 -->
+ <JsonParamsInput
+ v-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE"
+ v-model="condition.value"
+ type="service"
+ :config="serviceConfig"
+ placeholder="璇疯緭鍏� JSON 鏍煎紡鐨勬湇鍔″弬鏁�"
+ />
+ <!-- 浜嬩欢涓婃姤鍙傛暟閰嶇疆 -->
+ <JsonParamsInput
+ v-else-if="triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST"
+ v-model="condition.value"
+ type="event"
+ :config="eventConfig"
+ placeholder="璇疯緭鍏� JSON 鏍煎紡鐨勪簨浠跺弬鏁�"
+ />
+ <!-- 鏅�氬�艰緭鍏� -->
+ <ValueInput
+ v-else
+ :model-value="condition.value"
+ @update:model-value="(value) => updateConditionField('value', value)"
+ :property-type="propertyType"
+ :operator="condition.operator"
+ :property-config="propertyConfig"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 璁惧鐘舵�佹潯浠堕厤缃� -->
+ <div v-else-if="isDeviceStatusTrigger" class="space-y-16px">
+ <!-- 璁惧鐘舵�佽Е鍙戝櫒浣跨敤绠�鍖栫殑閰嶇疆 -->
+ <el-row :gutter="16">
+ <el-col :span="12">
+ <el-form-item label="浜у搧" required>
+ <ProductSelector
+ :model-value="condition.productId"
+ @update:model-value="(value) => updateConditionField('productId', value)"
+ @change="handleProductChange"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧" required>
+ <DeviceSelector
+ :model-value="condition.deviceId"
+ @update:model-value="(value) => updateConditionField('deviceId', value)"
+ :product-id="condition.productId"
+ @change="handleDeviceChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="16">
+ <el-col :span="6">
+ <el-form-item label="鎿嶄綔绗�" required>
+ <el-select
+ :model-value="condition.operator"
+ @update:model-value="(value) => updateConditionField('operator', value)"
+ placeholder="璇烽�夋嫨鎿嶄綔绗�"
+ class="w-full"
+ >
+ <el-option
+ :label="IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name"
+ :value="IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鍙傛暟" required>
+ <el-select
+ :model-value="condition.value"
+ @update:model-value="(value) => updateConditionField('value', value)"
+ placeholder="璇烽�夋嫨鎿嶄綔绗�"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in deviceStatusChangeOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鍏朵粬瑙﹀彂绫诲瀷鐨勬彁绀� -->
+ <div v-else class="text-center py-20px">
+ <p class="text-14px text-[var(--el-text-color-secondary)] mb-4px">
+ 褰撳墠瑙﹀彂浜嬩欢绫诲瀷锛歿{ getTriggerTypeLabel(triggerType) }}
+ </p>
+ <p class="text-12px text-[var(--el-text-color-placeholder)]">
+ 姝よЕ鍙戠被鍨嬫殏涓嶉渶瑕侀厤缃澶栨潯浠�
+ </p>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import ProductSelector from '../selectors/ProductSelector.vue'
+import DeviceSelector from '../selectors/DeviceSelector.vue'
+import PropertySelector from '../selectors/PropertySelector.vue'
+import OperatorSelector from '../selectors/OperatorSelector.vue'
+import ValueInput from '../inputs/ValueInput.vue'
+import JsonParamsInput from '../inputs/JsonParamsInput.vue'
+
+import type { Trigger } from '@/api/iot/rule/scene'
+import {
+ IotRuleSceneTriggerTypeEnum,
+ triggerTypeOptions,
+ getTriggerTypeLabel,
+ IotRuleSceneTriggerConditionParameterOperatorEnum,
+ IoTDeviceStatusEnum
+} from '@/views/iot/utils/constants'
+import { useVModel } from '@vueuse/core'
+
+/** 涓绘潯浠跺唴閮ㄩ厤缃粍浠� */
+defineOptions({ name: 'MainConditionInnerConfig' })
+
+const props = defineProps<{
+ modelValue: Trigger
+ triggerType: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: Trigger): void
+ (e: 'trigger-type-change', value: number): void
+}>()
+
+/** 鑾峰彇璁惧鐘舵�佸彉鏇撮�夐」锛堢敤浜庤Е鍙戝櫒閰嶇疆锛� */
+const deviceStatusChangeOptions = [
+ {
+ label: IoTDeviceStatusEnum.ONLINE.label,
+ value: IoTDeviceStatusEnum.ONLINE.value
+ },
+ {
+ label: IoTDeviceStatusEnum.OFFLINE.label,
+ value: IoTDeviceStatusEnum.OFFLINE.value
+ }
+]
+
+const condition = useVModel(props, 'modelValue', emit)
+const propertyType = ref('') // 灞炴�х被鍨�
+const propertyConfig = ref<any>(null) // 灞炴�ч厤缃�
+
+// 璁$畻灞炴�э細鏄惁涓鸿澶囧睘鎬цЕ鍙戝櫒
+const isDevicePropertyTrigger = computed(() => {
+ return (
+ props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST ||
+ props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
+ props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+ )
+})
+
+// 璁$畻灞炴�э細鏄惁涓鸿澶囩姸鎬佽Е鍙戝櫒
+const isDeviceStatusTrigger = computed(() => {
+ return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE
+})
+
+// 璁$畻灞炴�э細鏄惁闇�瑕佹搷浣滅閫夋嫨锛堟湇鍔¤皟鐢ㄥ拰浜嬩欢涓婃姤涓嶉渶瑕佹搷浣滅锛�
+const needsOperatorSelector = computed(() => {
+ const noOperatorTriggerTypes = [
+ IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+ IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+ ] as number[]
+ return !noOperatorTriggerTypes.includes(props.triggerType)
+})
+
+// 璁$畻灞炴�э細鏄惁闇�瑕佸鍒楀竷灞�锛堟湇鍔¤皟鐢ㄥ拰浜嬩欢涓婃姤涓嶉渶瑕佹搷浣滅鍒楋紝鎵�浠ュ�艰緭鍏ュ垪鏇村锛�
+const isWideValueColumn = computed(() => {
+ const wideColumnTriggerTypes = [
+ IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+ IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST
+ ] as number[]
+ return wideColumnTriggerTypes.includes(props.triggerType)
+})
+
+// 璁$畻灞炴�э細鍊艰緭鍏ュ瓧娈电殑鏍囩鏂囨湰
+const valueInputLabel = computed(() => {
+ return props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+ ? '鏈嶅姟鍙傛暟'
+ : '姣旇緝鍊�'
+})
+
+// 璁$畻灞炴�э細鏈嶅姟閰嶇疆 - 鐢ㄤ簬 JsonParamsInput
+const serviceConfig = computed(() => {
+ if (
+ propertyConfig.value &&
+ props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+ ) {
+ return {
+ service: {
+ name: propertyConfig.value.name || '鏈嶅姟',
+ inputParams: propertyConfig.value.inputParams || []
+ }
+ }
+ }
+ return undefined
+})
+
+// 璁$畻灞炴�э細浜嬩欢閰嶇疆 - 鐢ㄤ簬 JsonParamsInput
+const eventConfig = computed(() => {
+ if (propertyConfig.value && props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
+ return {
+ event: {
+ name: propertyConfig.value.name || '浜嬩欢',
+ outputParams: propertyConfig.value.outputParams || []
+ }
+ }
+ }
+ return undefined
+})
+
+/**
+ * 鏇存柊鏉′欢瀛楁
+ * @param field 瀛楁鍚�
+ * @param value 瀛楁鍊�
+ */
+const updateConditionField = (field: any, value: any) => {
+ condition.value[field] = value
+}
+
+/**
+ * 澶勭悊瑙﹀彂鍣ㄧ被鍨嬪彉鍖栦簨浠�
+ * @param type 瑙﹀彂鍣ㄧ被鍨�
+ */
+const handleTriggerTypeChange = (type: number) => {
+ emit('trigger-type-change', type)
+}
+
+/** 澶勭悊浜у搧鍙樺寲浜嬩欢 */
+const handleProductChange = () => {
+ // 浜у搧鍙樺寲鏃舵竻绌鸿澶囧拰灞炴��
+ condition.value.deviceId = undefined
+ condition.value.identifier = ''
+}
+
+/** 澶勭悊璁惧鍙樺寲浜嬩欢 */
+const handleDeviceChange = () => {
+ // 璁惧鍙樺寲鏃舵竻绌哄睘鎬�
+ condition.value.identifier = ''
+}
+
+/**
+ * 澶勭悊灞炴�у彉鍖栦簨浠�
+ * @param propertyInfo 灞炴�т俊鎭璞�
+ */
+const handlePropertyChange = (propertyInfo: any) => {
+ if (propertyInfo) {
+ propertyType.value = propertyInfo.type
+ propertyConfig.value = propertyInfo.config
+
+ // 瀵逛簬浜嬩欢涓婃姤鍜屾湇鍔¤皟鐢紝鑷姩璁剧疆鎿嶄綔绗︿负 '='
+ if (
+ props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST ||
+ props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+ ) {
+ condition.value.operator = IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value
+ }
+ }
+}
+</script>
diff --git a/src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue b/src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue
new file mode 100644
index 0000000..3097fdc
--- /dev/null
+++ b/src/views/iot/rule/scene/form/configs/SubConditionGroupConfig.vue
@@ -0,0 +1,156 @@
+<template>
+ <div class="p-16px">
+ <!-- 绌虹姸鎬� -->
+ <div v-if="!subGroup || subGroup.length === 0" class="text-center py-24px">
+ <div class="flex flex-col items-center gap-12px">
+ <Icon icon="ep:plus" class="text-32px text-[var(--el-text-color-placeholder)]" />
+ <div class="text-[var(--el-text-color-secondary)]">
+ <p class="text-14px font-500 mb-4px">鏆傛棤鏉′欢</p>
+ <p class="text-12px">鐐瑰嚮涓嬫柟鎸夐挳娣诲姞绗竴涓潯浠�</p>
+ </div>
+ <el-button type="primary" @click="addCondition">
+ <Icon icon="ep:plus" />
+ 娣诲姞鏉′欢
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 鏉′欢鍒楄〃 -->
+ <div v-else class="space-y-16px">
+ <div
+ v-for="(condition, conditionIndex) in subGroup"
+ :key="`condition-${conditionIndex}`"
+ class="relative"
+ >
+ <!-- 鏉′欢閰嶇疆 -->
+ <div
+ class="border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)] shadow-sm"
+ >
+ <div
+ class="flex items-center justify-between p-12px bg-[var(--el-fill-color-light)] border-b border-[var(--el-border-color-lighter)] rounded-t-4px"
+ >
+ <div class="flex items-center gap-8px">
+ <div
+ class="w-20px h-20px bg-blue-500 text-white rounded-full flex items-center justify-center text-10px font-bold"
+ >
+ {{ conditionIndex + 1 }}
+ </div>
+ <span class="text-12px font-500 text-[var(--el-text-color-primary)]"
+ >鏉′欢 {{ conditionIndex + 1 }}</span
+ >
+ </div>
+ <el-button
+ type="danger"
+ size="small"
+ text
+ @click="removeCondition(conditionIndex)"
+ v-if="subGroup!.length > 1"
+ class="hover:bg-red-50"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </div>
+
+ <div class="p-12px">
+ <ConditionConfig
+ :model-value="condition"
+ @update:model-value="(value) => updateCondition(conditionIndex, value)"
+ :trigger-type="triggerType"
+ />
+ </div>
+ </div>
+ </div>
+
+ <!-- 娣诲姞鏉′欢鎸夐挳 -->
+ <div
+ v-if="subGroup && subGroup.length > 0 && subGroup.length < maxConditions"
+ class="text-center py-16px"
+ >
+ <el-button type="primary" plain @click="addCondition">
+ <Icon icon="ep:plus" />
+ 缁х画娣诲姞鏉′欢
+ </el-button>
+ <span class="block mt-8px text-12px text-[var(--el-text-color-secondary)]">
+ 鏈�澶氬彲娣诲姞 {{ maxConditions }} 涓潯浠�
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { nextTick } from 'vue'
+import { useVModel } from '@vueuse/core'
+import ConditionConfig from './ConditionConfig.vue'
+import type { TriggerCondition } from '@/api/iot/rule/scene'
+import {
+ IotRuleSceneTriggerConditionTypeEnum,
+ IotRuleSceneTriggerConditionParameterOperatorEnum
+} from '@/views/iot/utils/constants'
+
+/** 瀛愭潯浠剁粍閰嶇疆缁勪欢 */
+defineOptions({ name: 'SubConditionGroupConfig' })
+
+const props = defineProps<{
+ modelValue: TriggerCondition[]
+ triggerType: number
+ maxConditions?: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: TriggerCondition[]): void
+}>()
+
+const subGroup = useVModel(props, 'modelValue', emit)
+
+const maxConditions = computed(() => props.maxConditions || 3) // 鏈�澶ф潯浠舵暟閲�
+
+/** 娣诲姞鏉′欢 */
+const addCondition = async () => {
+ // 纭繚 subGroup.value 鏄竴涓暟缁�
+ if (!subGroup.value) {
+ subGroup.value = []
+ }
+
+ // 妫�鏌ユ槸鍚﹁揪鍒版渶澶ф潯浠舵暟閲忛檺鍒�
+ if (subGroup.value?.length >= maxConditions.value) {
+ return
+ }
+
+ const newCondition: TriggerCondition = {
+ type: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY, // 榛樿涓鸿澶囧睘鎬�
+ productId: undefined,
+ deviceId: undefined,
+ identifier: '',
+ operator: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value, // 浣跨敤鏋氫妇榛樿鍊�
+ param: ''
+ }
+
+ // 浣跨敤 nextTick 纭繚鍝嶅簲寮忔洿鏂板畬鎴愬悗鍐嶆坊鍔犳柊鏉′欢
+ await nextTick()
+ if (subGroup.value) {
+ subGroup.value.push(newCondition)
+ }
+}
+
+/**
+ * 绉婚櫎鏉′欢
+ * @param index 鏉′欢绱㈠紩
+ */
+const removeCondition = (index: number) => {
+ if (subGroup.value) {
+ subGroup.value.splice(index, 1)
+ }
+}
+
+/**
+ * 鏇存柊鏉′欢
+ * @param index 鏉′欢绱㈠紩
+ * @param condition 鏉′欢瀵硅薄
+ */
+const updateCondition = (index: number, condition: TriggerCondition) => {
+ if (subGroup.value) {
+ subGroup.value[index] = condition
+ }
+}
+</script>
diff --git a/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue b/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue
new file mode 100644
index 0000000..5bfa970
--- /dev/null
+++ b/src/views/iot/rule/scene/form/inputs/JsonParamsInput.vue
@@ -0,0 +1,519 @@
+<!-- JSON鍙傛暟杈撳叆缁勪欢 - 閫氱敤鐗堟湰 -->
+<template>
+ <!-- 鍙傛暟閰嶇疆 -->
+ <div class="w-full space-y-12px">
+ <!-- JSON 杈撳叆妗� -->
+ <div class="relative">
+ <el-input
+ v-model="paramsJson"
+ type="textarea"
+ :rows="4"
+ :placeholder="placeholder"
+ @input="handleParamsChange"
+ :class="{ 'is-error': jsonError }"
+ />
+ <!-- 鏌ョ湅璇︾粏绀轰緥寮瑰嚭灞� -->
+ <div class="absolute top-8px right-8px">
+ <el-popover
+ placement="left-start"
+ :width="450"
+ trigger="click"
+ :show-arrow="true"
+ :offset="8"
+ popper-class="json-params-detail-popover"
+ >
+ <template #reference>
+ <el-button
+ type="info"
+ :icon="InfoFilled"
+ circle
+ size="small"
+ :title="JSON_PARAMS_INPUT_CONSTANTS.VIEW_EXAMPLE_TITLE"
+ />
+ </template>
+
+ <!-- 寮瑰嚭灞傚唴瀹� -->
+ <div class="json-params-detail-content">
+ <div class="flex items-center gap-8px mb-16px">
+ <Icon :icon="titleIcon" class="text-[var(--el-color-primary)] text-18px" />
+ <span class="text-16px font-600 text-[var(--el-text-color-primary)]">
+ {{ title }}
+ </span>
+ </div>
+
+ <div class="space-y-16px">
+ <!-- 鍙傛暟鍒楄〃 -->
+ <div v-if="paramsList.length > 0">
+ <div class="flex items-center gap-8px mb-8px">
+ <Icon :icon="paramsIcon" class="text-[var(--el-color-primary)] text-14px" />
+ <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+ {{ paramsLabel }}
+ </span>
+ </div>
+ <div class="ml-22px space-y-8px">
+ <div
+ v-for="param in paramsList"
+ :key="param.identifier"
+ class="flex items-center justify-between p-8px bg-[var(--el-fill-color-lighter)] rounded-4px"
+ >
+ <div class="flex-1">
+ <div class="text-12px font-500 text-[var(--el-text-color-primary)]">
+ {{ param.name }}
+ <el-tag v-if="param.required" size="small" type="danger" class="ml-4px">
+ {{ JSON_PARAMS_INPUT_CONSTANTS.REQUIRED_TAG }}
+ </el-tag>
+ </div>
+ <div class="text-11px text-[var(--el-text-color-secondary)]">
+ {{ param.identifier }}
+ </div>
+ </div>
+ <div class="flex items-center gap-8px">
+ <el-tag :type="getParamTypeTag(param.dataType)" size="small">
+ {{ getParamTypeName(param.dataType) }}
+ </el-tag>
+ <span class="text-11px text-[var(--el-text-color-secondary)]">
+ {{ getExampleValue(param) }}
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div class="mt-12px ml-22px">
+ <div class="text-12px text-[var(--el-text-color-secondary)] mb-6px">
+ {{ JSON_PARAMS_INPUT_CONSTANTS.COMPLETE_JSON_FORMAT }}
+ </div>
+ <pre
+ class="p-12px bg-[var(--el-fill-color-light)] rounded-4px text-11px text-[var(--el-text-color-primary)] overflow-x-auto border-l-3px border-[var(--el-color-primary)]"
+ >
+ <code>{{ generateExampleJson() }}</code>
+ </pre>
+ </div>
+ </div>
+
+ <!-- 鏃犲弬鏁版彁绀� -->
+ <div v-else>
+ <div class="text-center py-16px">
+ <p class="text-14px text-[var(--el-text-color-secondary)]">{{ emptyMessage }}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-popover>
+ </div>
+ </div>
+
+ <!-- 楠岃瘉鐘舵�佸拰閿欒鎻愮ず -->
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-8px">
+ <Icon
+ :icon="
+ jsonError
+ ? JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.ERROR
+ : JSON_PARAMS_INPUT_ICONS.STATUS_ICONS.SUCCESS
+ "
+ :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
+ class="text-14px"
+ />
+ <span
+ :class="jsonError ? 'text-[var(--el-color-danger)]' : 'text-[var(--el-color-success)]'"
+ class="text-12px"
+ >
+ {{ jsonError || JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_CORRECT }}
+ </span>
+ </div>
+
+ <!-- 蹇�熷~鍏呮寜閽� -->
+ <div v-if="paramsList.length > 0" class="flex items-center gap-8px">
+ <span class="text-12px text-[var(--el-text-color-secondary)]">{{
+ JSON_PARAMS_INPUT_CONSTANTS.QUICK_FILL_LABEL
+ }}</span>
+ <el-button size="small" type="primary" plain @click="fillExampleJson">
+ {{ JSON_PARAMS_INPUT_CONSTANTS.EXAMPLE_DATA_BUTTON }}
+ </el-button>
+ <el-button size="small" type="danger" plain @click="clearParams">{{
+ JSON_PARAMS_INPUT_CONSTANTS.CLEAR_BUTTON
+ }}</el-button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { InfoFilled } from '@element-plus/icons-vue'
+import {
+ IoTDataSpecsDataTypeEnum,
+ JSON_PARAMS_INPUT_CONSTANTS,
+ JSON_PARAMS_INPUT_ICONS,
+ JSON_PARAMS_EXAMPLE_VALUES,
+ JsonParamsInputTypeEnum,
+ type JsonParamsInputType
+} from '@/views/iot/utils/constants'
+
+/** JSON鍙傛暟杈撳叆缁勪欢 - 閫氱敤鐗堟湰 */
+defineOptions({ name: 'JsonParamsInput' })
+
+interface JsonParamsConfig {
+ // 鏈嶅姟閰嶇疆
+ service?: {
+ name: string
+ inputParams?: any[]
+ }
+ // 浜嬩欢閰嶇疆
+ event?: {
+ name: string
+ outputParams?: any[]
+ }
+ // 灞炴�ч厤缃�
+ properties?: any[]
+ // 鑷畾涔夐厤缃�
+ custom?: {
+ name: string
+ params: any[]
+ }
+}
+
+interface Props {
+ modelValue?: string
+ config?: JsonParamsConfig
+ type?: JsonParamsInputType
+ placeholder?: string
+}
+
+interface Emits {
+ (e: 'update:modelValue', value: string): void
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ type: JsonParamsInputTypeEnum.SERVICE,
+ placeholder: JSON_PARAMS_INPUT_CONSTANTS.PLACEHOLDER
+})
+
+const emit = defineEmits<Emits>()
+
+const localValue = useVModel(props, 'modelValue', emit, {
+ defaultValue: ''
+})
+
+const paramsJson = ref('') // JSON鍙傛暟瀛楃涓�
+const jsonError = ref('') // JSON楠岃瘉閿欒淇℃伅
+
+// 璁$畻灞炴�э細鍙傛暟鍒楄〃
+const paramsList = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return props.config?.service?.inputParams || []
+ case JsonParamsInputTypeEnum.EVENT:
+ return props.config?.event?.outputParams || []
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return props.config?.properties || []
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return props.config?.custom?.params || []
+ default:
+ return []
+ }
+})
+
+// 璁$畻灞炴�э細鏍囬
+const title = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return JSON_PARAMS_INPUT_CONSTANTS.TITLES.SERVICE(props.config?.service?.name)
+ case JsonParamsInputTypeEnum.EVENT:
+ return JSON_PARAMS_INPUT_CONSTANTS.TITLES.EVENT(props.config?.event?.name)
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return JSON_PARAMS_INPUT_CONSTANTS.TITLES.PROPERTY
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return JSON_PARAMS_INPUT_CONSTANTS.TITLES.CUSTOM(props.config?.custom?.name)
+ default:
+ return JSON_PARAMS_INPUT_CONSTANTS.TITLES.DEFAULT
+ }
+})
+
+// 璁$畻灞炴�э細鏍囬鍥炬爣
+const titleIcon = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.SERVICE
+ case JsonParamsInputTypeEnum.EVENT:
+ return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.EVENT
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.PROPERTY
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.CUSTOM
+ default:
+ return JSON_PARAMS_INPUT_ICONS.TITLE_ICONS.DEFAULT
+ }
+})
+
+// 璁$畻灞炴�э細鍙傛暟鍥炬爣
+const paramsIcon = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.SERVICE
+ case JsonParamsInputTypeEnum.EVENT:
+ return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.EVENT
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.PROPERTY
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.CUSTOM
+ default:
+ return JSON_PARAMS_INPUT_ICONS.PARAMS_ICONS.DEFAULT
+ }
+})
+
+// 璁$畻灞炴�э細鍙傛暟鏍囩
+const paramsLabel = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.SERVICE
+ case JsonParamsInputTypeEnum.EVENT:
+ return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.EVENT
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.PROPERTY
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.CUSTOM
+ default:
+ return JSON_PARAMS_INPUT_CONSTANTS.PARAMS_LABELS.DEFAULT
+ }
+})
+
+// 璁$畻灞炴�э細绌虹姸鎬佹秷鎭�
+const emptyMessage = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.SERVICE
+ case JsonParamsInputTypeEnum.EVENT:
+ return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.EVENT
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.PROPERTY
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.CUSTOM
+ default:
+ return JSON_PARAMS_INPUT_CONSTANTS.EMPTY_MESSAGES.DEFAULT
+ }
+})
+
+// 璁$畻灞炴�э細鏃犻厤缃秷鎭�
+const noConfigMessage = computed(() => {
+ switch (props.type) {
+ case JsonParamsInputTypeEnum.SERVICE:
+ return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.SERVICE
+ case JsonParamsInputTypeEnum.EVENT:
+ return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.EVENT
+ case JsonParamsInputTypeEnum.PROPERTY:
+ return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.PROPERTY
+ case JsonParamsInputTypeEnum.CUSTOM:
+ return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.CUSTOM
+ default:
+ return JSON_PARAMS_INPUT_CONSTANTS.NO_CONFIG_MESSAGES.DEFAULT
+ }
+})
+
+/**
+ * 澶勭悊鍙傛暟鍙樺寲浜嬩欢
+ */
+const handleParamsChange = () => {
+ try {
+ jsonError.value = '' // 娓呴櫎涔嬪墠鐨勯敊璇�
+
+ if (paramsJson.value.trim()) {
+ const parsed = JSON.parse(paramsJson.value)
+ localValue.value = paramsJson.value
+
+ // 棰濆鐨勫弬鏁伴獙璇�
+ if (typeof parsed !== 'object' || parsed === null) {
+ jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAMS_MUST_BE_OBJECT
+ return
+ }
+
+ // 楠岃瘉蹇呭~鍙傛暟
+ for (const param of paramsList.value) {
+ if (param.required && (!parsed[param.identifier] || parsed[param.identifier] === '')) {
+ jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.PARAM_REQUIRED_ERROR(param.name)
+ return
+ }
+ }
+ } else {
+ localValue.value = ''
+ }
+
+ // 楠岃瘉閫氳繃
+ jsonError.value = ''
+ } catch (error) {
+ jsonError.value = JSON_PARAMS_INPUT_CONSTANTS.JSON_FORMAT_ERROR(
+ error instanceof Error ? error.message : JSON_PARAMS_INPUT_CONSTANTS.UNKNOWN_ERROR
+ )
+ }
+}
+
+/**
+ * 蹇�熷~鍏呯ず渚嬫暟鎹�
+ */
+const fillExampleJson = () => {
+ paramsJson.value = generateExampleJson()
+ handleParamsChange()
+}
+
+/**
+ * 娓呯┖鍙傛暟
+ */
+const clearParams = () => {
+ paramsJson.value = ''
+ localValue.value = ''
+ jsonError.value = ''
+}
+
+/**
+ * 鑾峰彇鍙傛暟绫诲瀷鍚嶇О
+ * @param dataType 鏁版嵁绫诲瀷
+ * @returns 绫诲瀷鍚嶇О
+ */
+const getParamTypeName = (dataType: string) => {
+ // 浣跨敤 constants.ts 涓凡鏈夌殑 getDataTypeName 鍑芥暟閫昏緫
+ const typeMap = {
+ [IoTDataSpecsDataTypeEnum.INT]: '鏁存暟',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: '娴偣鏁�',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: '鍙岀簿搴�',
+ [IoTDataSpecsDataTypeEnum.TEXT]: '瀛楃涓�',
+ [IoTDataSpecsDataTypeEnum.BOOL]: '甯冨皵鍊�',
+ [IoTDataSpecsDataTypeEnum.ENUM]: '鏋氫妇',
+ [IoTDataSpecsDataTypeEnum.DATE]: '鏃ユ湡',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: '缁撴瀯浣�',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: '鏁扮粍'
+ }
+ return typeMap[dataType] || dataType
+}
+
+/**
+ * 鑾峰彇鍙傛暟绫诲瀷鏍囩鏍峰紡
+ * @param dataType 鏁版嵁绫诲瀷
+ * @returns 鏍囩鏍峰紡
+ */
+const getParamTypeTag = (dataType: string) => {
+ const tagMap = {
+ [IoTDataSpecsDataTypeEnum.INT]: 'primary',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
+ [IoTDataSpecsDataTypeEnum.TEXT]: 'info',
+ [IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
+ [IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
+ [IoTDataSpecsDataTypeEnum.DATE]: 'primary',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: 'warning'
+ }
+ return tagMap[dataType] || 'info'
+}
+
+/**
+ * 鑾峰彇绀轰緥鍊�
+ * @param param 鍙傛暟瀵硅薄
+ * @returns 绀轰緥鍊�
+ */
+const getExampleValue = (param: any) => {
+ const exampleConfig =
+ JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
+ return exampleConfig.display
+}
+
+/**
+ * 鐢熸垚绀轰緥JSON
+ * @returns JSON瀛楃涓�
+ */
+const generateExampleJson = () => {
+ if (paramsList.value.length === 0) {
+ return '{}'
+ }
+
+ const example = {}
+ paramsList.value.forEach((param) => {
+ const exampleConfig =
+ JSON_PARAMS_EXAMPLE_VALUES[param.dataType] || JSON_PARAMS_EXAMPLE_VALUES.DEFAULT
+ example[param.identifier] = exampleConfig.value
+ })
+
+ return JSON.stringify(example, null, 2)
+}
+
+/**
+ * 澶勭悊鏁版嵁鍥炴樉
+ * @param value 鍊煎瓧绗︿覆
+ */
+const handleDataDisplay = (value: string) => {
+ if (!value || !value.trim()) {
+ paramsJson.value = ''
+ jsonError.value = ''
+ return
+ }
+
+ try {
+ // 灏濊瘯瑙f瀽JSON锛屽鏋滄垚鍔熷垯鏍煎紡鍖�
+ const parsed = JSON.parse(value)
+ paramsJson.value = JSON.stringify(parsed, null, 2)
+ jsonError.value = ''
+ } catch {
+ // 濡傛灉涓嶆槸鏈夋晥鐨凧SON锛岀洿鎺ヤ娇鐢ㄥ師瀛楃涓�
+ paramsJson.value = value
+ jsonError.value = ''
+ }
+}
+
+// 鐩戝惉澶栭儴鍊煎彉鍖栵紙缂栬緫妯″紡鏁版嵁鍥炴樉锛�
+watch(
+ () => localValue.value,
+ async (newValue, oldValue) => {
+ // 閬垮厤寰幆鏇存柊
+ if (newValue === oldValue) return
+
+ // 浣跨敤 nextTick 纭繚鍦ㄤ笅涓�涓� tick 涓鐞嗘暟鎹�
+ await nextTick()
+ handleDataDisplay(newValue || '')
+ },
+ { immediate: true }
+)
+
+// 缁勪欢鎸傝浇鍚庝篃灏濊瘯澶勭悊涓�娆℃暟鎹洖鏄�
+onMounted(async () => {
+ await nextTick()
+ if (localValue.value) {
+ handleDataDisplay(localValue.value)
+ }
+})
+
+// 鐩戝惉閰嶇疆鍙樺寲
+watch(
+ () => props.config,
+ (newConfig, oldConfig) => {
+ // 鍙湁鍦ㄩ厤缃湡姝e彉鍖栨椂鎵嶆竻绌烘暟鎹�
+ if (JSON.stringify(newConfig) !== JSON.stringify(oldConfig)) {
+ // 濡傛灉娌℃湁澶栭儴浼犲叆鐨勫�硷紝鎵嶆竻绌烘暟鎹�
+ if (!localValue.value) {
+ paramsJson.value = ''
+ jsonError.value = ''
+ }
+ }
+ }
+)
+</script>
+
+<style scoped>
+/* 寮瑰嚭灞傚唴瀹规牱寮� */
+.json-params-detail-content {
+ padding: 4px 0;
+}
+
+/* 寮瑰嚭灞傝嚜瀹氫箟鏍峰紡 */
+:global(.json-params-detail-popover) {
+ max-width: 500px !important;
+}
+
+:global(.json-params-detail-popover .el-popover__content) {
+ padding: 16px !important;
+}
+
+/* JSON 浠g爜鍧楁牱寮� */
+.json-params-detail-content pre {
+ max-height: 200px;
+ overflow-y: auto;
+}
+</style>
diff --git a/src/views/iot/rule/scene/form/inputs/ValueInput.vue b/src/views/iot/rule/scene/form/inputs/ValueInput.vue
new file mode 100644
index 0000000..40548bd
--- /dev/null
+++ b/src/views/iot/rule/scene/form/inputs/ValueInput.vue
@@ -0,0 +1,266 @@
+<!-- 鍊艰緭鍏ョ粍浠� -->
+<template>
+ <div class="w-full min-w-0">
+ <!-- 甯冨皵鍊奸�夋嫨 -->
+ <el-select
+ v-if="propertyType === IoTDataSpecsDataTypeEnum.BOOL"
+ v-model="localValue"
+ placeholder="璇烽�夋嫨甯冨皵鍊�"
+ class="w-full!"
+ >
+ <el-option label="鐪� (true)" value="true" />
+ <el-option label="鍋� (false)" value="false" />
+ </el-select>
+
+ <!-- 鏋氫妇鍊奸�夋嫨 -->
+ <el-select
+ v-else-if="propertyType === IoTDataSpecsDataTypeEnum.ENUM && enumOptions.length > 0"
+ v-model="localValue"
+ placeholder="璇烽�夋嫨鏋氫妇鍊�"
+ class="w-full!"
+ >
+ <el-option
+ v-for="option in enumOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+
+ <!-- 鑼冨洿杈撳叆 (between 鎿嶄綔绗�) -->
+ <div
+ v-else-if="operator === IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value"
+ class="w-full! flex items-center gap-8px"
+ >
+ <el-input
+ v-model="rangeStart"
+ :type="getInputType()"
+ placeholder="鏈�灏忓��"
+ @input="handleRangeChange"
+ class="flex-1 min-w-0"
+ style="width: auto !important"
+ />
+ <span class="text-12px text-[var(--el-text-color-secondary)] whitespace-nowrap">鑷�</span>
+ <el-input
+ v-model="rangeEnd"
+ :type="getInputType()"
+ placeholder="鏈�澶у��"
+ @input="handleRangeChange"
+ class="flex-1 min-w-0"
+ />
+ </div>
+
+ <!-- 鍒楄〃杈撳叆 (in 鎿嶄綔绗�) -->
+ <div
+ v-else-if="operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value"
+ class="w-full!"
+ >
+ <el-input v-model="localValue" placeholder="璇疯緭鍏ュ�煎垪琛紝鐢ㄩ�楀彿鍒嗛殧" class="w-full!">
+ <template #suffix>
+ <el-tooltip content="澶氫釜鍊肩敤閫楀彿鍒嗛殧锛屽锛�1,2,3" placement="top">
+ <Icon
+ icon="ep:question-filled"
+ class="text-[var(--el-text-color-placeholder)] cursor-help"
+ />
+ </el-tooltip>
+ </template>
+ </el-input>
+ <div v-if="listPreview.length > 0" class="mt-8px flex items-center gap-6px flex-wrap">
+ <span class="text-12px text-[var(--el-text-color-secondary)]">瑙f瀽缁撴灉锛�</span>
+ <el-tag v-for="(item, index) in listPreview" :key="index" size="small" class="m-0">
+ {{ item }}
+ </el-tag>
+ </div>
+ </div>
+
+ <!-- 鏃ユ湡鏃堕棿杈撳叆 -->
+ <el-date-picker
+ v-else-if="propertyType === IoTDataSpecsDataTypeEnum.DATE"
+ v-model="dateValue"
+ type="datetime"
+ placeholder="璇烽�夋嫨鏃ユ湡鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ @change="handleDateChange"
+ class="w-full!"
+ />
+
+ <!-- 鏁板瓧杈撳叆 -->
+ <el-input-number
+ v-else-if="isNumericType()"
+ v-model="numberValue"
+ :precision="getPrecision()"
+ :step="getStep()"
+ :min="getMin()"
+ :max="getMax()"
+ placeholder="璇疯緭鍏ユ暟鍊�"
+ @change="handleNumberChange"
+ class="w-full!"
+ />
+
+ <!-- 鏂囨湰杈撳叆 -->
+ <el-input
+ v-else
+ v-model="localValue"
+ :type="getInputType()"
+ :placeholder="getPlaceholder()"
+ class="w-full!"
+ >
+ <template #suffix>
+ <el-tooltip
+ v-if="propertyConfig?.unit"
+ :content="`鍗曚綅锛�${propertyConfig.unit}`"
+ placement="top"
+ >
+ <span class="text-12px text-[var(--el-text-color-secondary)] px-4px">
+ {{ propertyConfig.unit }}
+ </span>
+ </el-tooltip>
+ </template>
+ </el-input>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import {
+ IoTDataSpecsDataTypeEnum,
+ IotRuleSceneTriggerConditionParameterOperatorEnum
+} from '@/views/iot/utils/constants'
+
+/** 鍊艰緭鍏ョ粍浠� */
+defineOptions({ name: 'ValueInput' })
+
+interface Props {
+ modelValue?: string
+ propertyType?: string
+ operator?: string
+ propertyConfig?: any
+}
+
+interface Emits {
+ (e: 'update:modelValue', value: string): void
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<Emits>()
+
+const localValue = useVModel(props, 'modelValue', emit, {
+ defaultValue: ''
+})
+
+const rangeStart = ref('') // 鑼冨洿寮�濮嬪��
+const rangeEnd = ref('') // 鑼冨洿缁撴潫鍊�
+const dateValue = ref('') // 鏃ユ湡鍊�
+const numberValue = ref<number>() // 鏁板瓧鍊�
+
+/** 璁$畻灞炴�э細鏋氫妇閫夐」 */
+const enumOptions = computed(() => {
+ if (props.propertyConfig?.enum) {
+ return props.propertyConfig.enum.map((item: any) => ({
+ label: item.name || item.label || item.value,
+ value: item.value
+ }))
+ }
+ return []
+})
+
+/** 璁$畻灞炴�э細鍒楄〃棰勮 */
+const listPreview = computed(() => {
+ if (
+ props.operator === IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value &&
+ localValue.value
+ ) {
+ return localValue.value
+ .split(',')
+ .map((item) => item.trim())
+ .filter((item) => item)
+ }
+ return []
+})
+
+/** 鍒ゆ柇鏄惁涓烘暟瀛楃被鍨� */
+const isNumericType = () => {
+ return [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE
+ ].includes((props.propertyType || '') as any)
+}
+
+/** 鑾峰彇杈撳叆妗嗙被鍨� */
+const getInputType = () => {
+ switch (props.propertyType) {
+ case IoTDataSpecsDataTypeEnum.INT:
+ case IoTDataSpecsDataTypeEnum.FLOAT:
+ case IoTDataSpecsDataTypeEnum.DOUBLE:
+ return 'number'
+ default:
+ return 'text'
+ }
+}
+
+/** 鑾峰彇鍗犱綅绗︽枃鏈� */
+const getPlaceholder = () => {
+ const typeMap = {
+ [IoTDataSpecsDataTypeEnum.TEXT]: '璇疯緭鍏ュ瓧绗︿覆',
+ [IoTDataSpecsDataTypeEnum.INT]: '璇疯緭鍏ユ暣鏁�',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: '璇疯緭鍏ユ诞鐐规暟',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: '璇疯緭鍏ュ弻绮惧害鏁�',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: '璇疯緭鍏� JSON 鏍煎紡鏁版嵁',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: '璇疯緭鍏ユ暟缁勬牸寮忔暟鎹�'
+ }
+ return typeMap[props.propertyType || ''] || '璇疯緭鍏ュ��'
+}
+
+/** 鑾峰彇鏁板瓧绮惧害 */
+const getPrecision = () => {
+ return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 0 : 2
+}
+
+/** 鑾峰彇鏁板瓧姝ラ暱 */
+const getStep = () => {
+ return props.propertyType === IoTDataSpecsDataTypeEnum.INT ? 1 : 0.1
+}
+
+/** 鑾峰彇鏈�灏忓�� */
+const getMin = () => {
+ return props.propertyConfig?.min || undefined
+}
+
+/** 鑾峰彇鏈�澶у�� */
+const getMax = () => {
+ return props.propertyConfig?.max || undefined
+}
+
+/** 澶勭悊鑼冨洿鍙樺寲浜嬩欢 */
+const handleRangeChange = () => {
+ if (rangeStart.value && rangeEnd.value) {
+ localValue.value = `${rangeStart.value},${rangeEnd.value}`
+ } else {
+ localValue.value = ''
+ }
+}
+
+/** 澶勭悊鏃ユ湡鍙樺寲浜嬩欢 */
+const handleDateChange = (value: string) => {
+ localValue.value = value || ''
+}
+
+/** 澶勭悊鏁板瓧鍙樺寲浜嬩欢 */
+const handleNumberChange = (value: number | undefined) => {
+ localValue.value = value?.toString() || ''
+}
+
+/** 鐩戝惉鎿嶄綔绗﹀彉鍖� */
+watch(
+ () => props.operator,
+ () => {
+ localValue.value = ''
+ rangeStart.value = ''
+ rangeEnd.value = ''
+ dateValue.value = ''
+ numberValue.value = undefined
+ }
+)
+</script>
diff --git a/src/views/iot/rule/scene/form/sections/ActionSection.vue b/src/views/iot/rule/scene/form/sections/ActionSection.vue
new file mode 100644
index 0000000..6357afe
--- /dev/null
+++ b/src/views/iot/rule/scene/form/sections/ActionSection.vue
@@ -0,0 +1,272 @@
+<!-- 鎵ц鍣ㄩ厤缃粍浠� -->
+<template>
+ <el-card class="border border-[var(--el-border-color-light)] rounded-8px" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-8px">
+ <Icon icon="ep:setting" class="text-[var(--el-color-primary)] text-18px" />
+ <span class="text-16px font-600 text-[var(--el-text-color-primary)]">鎵ц鍣ㄩ厤缃�</span>
+ <el-tag size="small" type="info">{{ actions.length }} 涓墽琛屽櫒</el-tag>
+ </div>
+ <div class="flex items-center gap-8px">
+ <el-button type="primary" size="small" @click="addAction">
+ <Icon icon="ep:plus" />
+ 娣诲姞鎵ц鍣�
+ </el-button>
+ </div>
+ </div>
+ </template>
+
+ <div class="p-0">
+ <!-- 绌虹姸鎬� -->
+ <div v-if="actions.length === 0">
+ <el-empty description="鏆傛棤鎵ц鍣ㄩ厤缃�">
+ <el-button type="primary" @click="addAction">
+ <Icon icon="ep:plus" />
+ 娣诲姞绗竴涓墽琛屽櫒
+ </el-button>
+ </el-empty>
+ </div>
+
+ <!-- 鎵ц鍣ㄥ垪琛� -->
+ <div v-else class="space-y-24px">
+ <div
+ v-for="(action, index) in actions"
+ :key="`action-${index}`"
+ class="border-2 border-blue-200 rounded-8px bg-blue-50 shadow-sm hover:shadow-md transition-shadow"
+ >
+ <!-- 鎵ц鍣ㄥご閮� - 钃濊壊涓婚 -->
+ <div
+ class="flex items-center justify-between p-16px bg-gradient-to-r from-blue-50 to-sky-50 border-b border-blue-200 rounded-t-6px"
+ >
+ <div class="flex items-center gap-12px">
+ <div class="flex items-center gap-8px text-16px font-600 text-blue-700">
+ <div
+ class="w-24px h-24px bg-blue-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+ >
+ {{ index + 1 }}
+ </div>
+ <span>鎵ц鍣� {{ index + 1 }}</span>
+ </div>
+ <el-tag :type="getActionTypeTag(action.type)" size="small" class="font-500">
+ {{ getActionTypeLabel(action.type) }}
+ </el-tag>
+ </div>
+ <div class="flex items-center gap-8px">
+ <el-button
+ v-if="actions.length > 1"
+ type="danger"
+ size="small"
+ text
+ @click="removeAction(index)"
+ class="hover:bg-red-50"
+ >
+ <Icon icon="ep:delete" />
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 鎵ц鍣ㄥ唴瀹瑰尯鍩� -->
+ <div class="p-16px space-y-16px">
+ <!-- 鎵ц绫诲瀷閫夋嫨 -->
+ <div class="w-full">
+ <el-form-item label="鎵ц绫诲瀷" required>
+ <el-select
+ :model-value="action.type"
+ @update:model-value="(value) => updateActionType(index, value)"
+ @change="(value) => onActionTypeChange(action, value)"
+ placeholder="璇烽�夋嫨鎵ц绫诲瀷"
+ class="w-full"
+ >
+ <el-option
+ v-for="option in getActionTypeOptions()"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ </div>
+
+ <!-- 璁惧鎺у埗閰嶇疆 -->
+ <DeviceControlConfig
+ v-if="isDeviceAction(action.type)"
+ :model-value="action"
+ @update:model-value="(value) => updateAction(index, value)"
+ />
+
+ <!-- 鍛婅閰嶇疆 - 鍙湁鎭㈠鍛婅鏃舵墠鏄剧ず -->
+ <AlertConfig
+ v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER"
+ :model-value="action.alertConfigId"
+ @update:model-value="(value) => updateActionAlertConfig(index, value)"
+ />
+
+ <!-- 瑙﹀彂鍛婅鎻愮ず - 瑙﹀彂鍛婅鏃舵樉绀� -->
+ <div
+ v-if="action.type === IotRuleSceneActionTypeEnum.ALERT_TRIGGER"
+ class="border border-[var(--el-border-color-light)] rounded-6px p-16px bg-[var(--el-fill-color-blank)]"
+ >
+ <div class="flex items-center gap-8px mb-8px">
+ <Icon icon="ep:warning" class="text-[var(--el-color-warning)] text-16px" />
+ <span class="text-14px font-600 text-[var(--el-text-color-primary)]">瑙﹀彂鍛婅</span>
+ <el-tag size="small" type="warning">鑷姩鎵ц</el-tag>
+ </div>
+ <div class="text-12px text-[var(--el-text-color-secondary)] leading-relaxed">
+ 褰撹Е鍙戞潯浠舵弧瓒虫椂锛岀郴缁熷皢鑷姩鍙戦�佸憡璀﹂�氱煡锛屽彲鍦ㄨ彍鍗� [鍛婅涓績 -> 鍛婅閰嶇疆] 绠$悊銆�
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 娣诲姞鎻愮ず -->
+ <div v-if="actions.length > 0" class="text-center py-16px">
+ <el-button type="primary" plain @click="addAction">
+ <Icon icon="ep:plus" />
+ 缁х画娣诲姞鎵ц鍣�
+ </el-button>
+ </div>
+ </div>
+ </el-card>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import DeviceControlConfig from '../configs/DeviceControlConfig.vue'
+import AlertConfig from '../configs/AlertConfig.vue'
+import type { Action } from '@/api/iot/rule/scene'
+import {
+ getActionTypeLabel,
+ getActionTypeOptions,
+ IotRuleSceneActionTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** 鎵ц鍣ㄩ厤缃粍浠� */
+defineOptions({ name: 'ActionSection' })
+
+const props = defineProps<{
+ actions: Action[]
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:actions', value: Action[]): void
+}>()
+
+const actions = useVModel(props, 'actions', emit)
+
+/** 鑾峰彇鎵ц鍣ㄦ爣绛剧被鍨嬶紙鐢ㄤ簬 el-tag 鐨� type 灞炴�э級 */
+const getActionTypeTag = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
+ const actionTypeTags = {
+ [IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET]: 'primary',
+ [IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE]: 'success',
+ [IotRuleSceneActionTypeEnum.ALERT_TRIGGER]: 'danger',
+ [IotRuleSceneActionTypeEnum.ALERT_RECOVER]: 'warning'
+ } as const
+ return actionTypeTags[type] || 'info'
+}
+
+/** 鍒ゆ柇鏄惁涓鸿澶囨墽琛屽櫒绫诲瀷 */
+const isDeviceAction = (type: number): boolean => {
+ const deviceActionTypes = [
+ IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+ IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE
+ ] as number[]
+ return deviceActionTypes.includes(type)
+}
+
+/** 鍒ゆ柇鏄惁涓哄憡璀︽墽琛屽櫒绫诲瀷 */
+const isAlertAction = (type: number): boolean => {
+ const alertActionTypes = [
+ IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+ IotRuleSceneActionTypeEnum.ALERT_RECOVER
+ ] as number[]
+ return alertActionTypes.includes(type)
+}
+
+/**
+ * 鍒涘缓榛樿鐨勬墽琛屽櫒鏁版嵁
+ * @returns 榛樿鎵ц鍣ㄥ璞�
+ */
+const createDefaultActionData = (): Action => {
+ return {
+ type: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET, // 榛樿涓鸿澶囧睘鎬ц缃�
+ productId: undefined,
+ deviceId: undefined,
+ identifier: undefined, // 鐗╂ā鍨嬫爣璇嗙锛堟湇鍔¤皟鐢ㄦ椂浣跨敤锛�
+ params: undefined,
+ alertConfigId: undefined
+ }
+}
+
+/**
+ * 娣诲姞鎵ц鍣�
+ */
+const addAction = () => {
+ const newAction = createDefaultActionData()
+ actions.value.push(newAction)
+}
+
+/**
+ * 鍒犻櫎鎵ц鍣�
+ * @param index 鎵ц鍣ㄧ储寮�
+ */
+const removeAction = (index: number) => {
+ actions.value.splice(index, 1)
+}
+
+/**
+ * 鏇存柊鎵ц鍣ㄧ被鍨�
+ * @param index 鎵ц鍣ㄧ储寮�
+ * @param type 鎵ц鍣ㄧ被鍨�
+ */
+const updateActionType = (index: number, type: number) => {
+ actions.value[index].type = type
+ onActionTypeChange(actions.value[index], type)
+}
+
+/**
+ * 鏇存柊鎵ц鍣�
+ * @param index 鎵ц鍣ㄧ储寮�
+ * @param action 鎵ц鍣ㄥ璞�
+ */
+const updateAction = (index: number, action: Action) => {
+ actions.value[index] = action
+}
+
+/**
+ * 鏇存柊鍛婅閰嶇疆
+ * @param index 鎵ц鍣ㄧ储寮�
+ * @param alertConfigId 鍛婅閰嶇疆ID
+ */
+const updateActionAlertConfig = (index: number, alertConfigId?: number) => {
+ actions.value[index].alertConfigId = alertConfigId
+}
+
+/**
+ * 鐩戝惉鎵ц鍣ㄧ被鍨嬪彉鍖�
+ * @param action 鎵ц鍣ㄥ璞�
+ * @param type 鎵ц鍣ㄧ被鍨�
+ */
+const onActionTypeChange = (action: Action, type: number) => {
+ // 娓呯悊涓嶇浉鍏崇殑閰嶇疆锛岀‘淇濇暟鎹粨鏋勫共鍑�
+ if (isDeviceAction(type)) {
+ // 璁惧鎺у埗绫诲瀷锛氭竻鐞嗗憡璀﹂厤缃紝纭繚璁惧鍙傛暟瀛樺湪
+ action.alertConfigId = undefined
+ if (!action.params) {
+ action.params = ''
+ }
+ // 濡傛灉浠庡叾浠栫被鍨嬪垏鎹㈠埌璁惧鎺у埗绫诲瀷锛屾竻绌篿dentifier锛堣鐢ㄦ埛閲嶆柊閫夋嫨锛�
+ if (action.identifier && type !== action.type) {
+ action.identifier = undefined
+ }
+ } else if (isAlertAction(type)) {
+ action.productId = undefined
+ action.deviceId = undefined
+ action.identifier = undefined // 娓呯悊鏈嶅姟鏍囪瘑绗�
+ action.params = undefined
+ action.alertConfigId = undefined
+ }
+}
+</script>
diff --git a/src/views/iot/rule/scene/form/sections/BasicInfoSection.vue b/src/views/iot/rule/scene/form/sections/BasicInfoSection.vue
new file mode 100644
index 0000000..4e77053
--- /dev/null
+++ b/src/views/iot/rule/scene/form/sections/BasicInfoSection.vue
@@ -0,0 +1,86 @@
+<!-- 鍩虹淇℃伅閰嶇疆缁勪欢 -->
+<template>
+ <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-8px">
+ <Icon icon="ep:info-filled" class="text-[var(--el-color-primary)] text-18px" />
+ <span class="text-16px font-600 text-[var(--el-text-color-primary)]">鍩虹淇℃伅</span>
+ </div>
+ <div class="flex items-center gap-8px">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData.status" />
+ </div>
+ </div>
+ </template>
+
+ <div class="p-0">
+ <el-row :gutter="24" class="mb-24px">
+ <el-col :span="12">
+ <el-form-item label="鍦烘櫙鍚嶇О" prop="name" required>
+ <el-input
+ v-model="formData.name"
+ placeholder="璇疯緭鍏ュ満鏅悕绉�"
+ maxlength="50"
+ show-word-limit
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍦烘櫙鐘舵��" prop="status" required>
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍦烘櫙鎻忚堪" prop="description">
+ <el-input
+ v-model="formData.description"
+ type="textarea"
+ placeholder="璇疯緭鍏ュ満鏅弿杩帮紙鍙�夛級"
+ :rows="3"
+ maxlength="200"
+ show-word-limit
+ resize="none"
+ />
+ </el-form-item>
+ </div>
+ </el-card>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import type { IotSceneRule } from '@/api/iot/rule/scene'
+
+/** 鍩虹淇℃伅閰嶇疆缁勪欢 */
+defineOptions({ name: 'BasicInfoSection' })
+
+const props = defineProps<{
+ modelValue: IotSceneRule
+ rules?: any
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: IotSceneRule): void
+}>()
+
+const formData = useVModel(props, 'modelValue', emit) // 琛ㄥ崟鏁版嵁
+</script>
+
+<style scoped>
+:deep(.el-form-item) {
+ margin-bottom: 20px;
+}
+
+:deep(.el-form-item:last-child) {
+ margin-bottom: 0;
+}
+</style>
diff --git a/src/views/iot/rule/scene/form/sections/TriggerSection.vue b/src/views/iot/rule/scene/form/sections/TriggerSection.vue
new file mode 100644
index 0000000..144d53c
--- /dev/null
+++ b/src/views/iot/rule/scene/form/sections/TriggerSection.vue
@@ -0,0 +1,222 @@
+<template>
+ <el-card class="border border-[var(--el-border-color-light)] rounded-8px mb-10px" shadow="never">
+ <template #header>
+ <div class="flex items-center justify-between">
+ <div class="flex items-center gap-8px">
+ <Icon icon="ep:lightning" class="text-[var(--el-color-primary)] text-18px" />
+ <span class="text-16px font-600 text-[var(--el-text-color-primary)]">瑙﹀彂鍣ㄩ厤缃�</span>
+ <el-tag size="small" type="info">{{ triggers.length }} 涓Е鍙戝櫒</el-tag>
+ </div>
+ <el-button type="primary" size="small" @click="addTrigger">
+ <Icon icon="ep:plus" />
+ 娣诲姞瑙﹀彂鍣�
+ </el-button>
+ </div>
+ </template>
+
+ <div class="p-16px space-y-24px">
+ <!-- 瑙﹀彂鍣ㄥ垪琛� -->
+ <div v-if="triggers.length > 0" class="space-y-24px">
+ <div
+ v-for="(triggerItem, index) in triggers"
+ :key="`trigger-${index}`"
+ class="border-2 border-green-200 rounded-8px bg-green-50 shadow-sm hover:shadow-md transition-shadow"
+ >
+ <!-- 瑙﹀彂鍣ㄥご閮� - 缁胯壊涓婚 -->
+ <div
+ class="flex items-center justify-between p-16px bg-gradient-to-r from-green-50 to-emerald-50 border-b border-green-200 rounded-t-6px"
+ >
+ <div class="flex items-center gap-12px">
+ <div class="flex items-center gap-8px text-16px font-600 text-green-700">
+ <div
+ class="w-24px h-24px bg-green-500 text-white rounded-full flex items-center justify-center text-12px font-bold"
+ >
+ {{ index + 1 }}
+ </div>
+ <span>瑙﹀彂鍣� {{ index + 1 }}</span>
+ </div>
+ <el-tag size="small" :type="getTriggerTagType(triggerItem.type)" class="font-500">
+ {{ getTriggerTypeLabel(triggerItem.type) }}
+ </el-tag>
+ </div>
+ <div class="flex items-center gap-8px">
+ <el-button
+ v-if="triggers.length > 1"
+ type="danger"
+ size="small"
+ text
+ @click="removeTrigger(index)"
+ class="hover:bg-red-50"
+ >
+ <Icon icon="ep:delete" />
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 瑙﹀彂鍣ㄥ唴瀹瑰尯鍩� -->
+ <div class="p-16px space-y-16px">
+ <!-- 璁惧瑙﹀彂閰嶇疆 -->
+ <DeviceTriggerConfig
+ v-if="isDeviceTrigger(triggerItem.type)"
+ :model-value="triggerItem"
+ :index="index"
+ @update:model-value="(value) => updateTriggerDeviceConfig(index, value)"
+ @trigger-type-change="(type) => updateTriggerType(index, type)"
+ />
+
+ <!-- 瀹氭椂瑙﹀彂閰嶇疆 -->
+ <div
+ v-else-if="triggerItem.type === IotRuleSceneTriggerTypeEnum.TIMER"
+ class="flex flex-col gap-16px"
+ >
+ <div
+ class="flex items-center gap-8px p-12px px-16px bg-[var(--el-fill-color-light)] rounded-6px border border-[var(--el-border-color-lighter)]"
+ >
+ <Icon icon="ep:timer" class="text-[var(--el-color-danger)] text-18px" />
+ <span class="text-14px font-500 text-[var(--el-text-color-primary)]"
+ >瀹氭椂瑙﹀彂閰嶇疆</span
+ >
+ </div>
+
+ <!-- CRON 琛ㄨ揪寮忛厤缃� -->
+ <div
+ class="p-16px border border-[var(--el-border-color-lighter)] rounded-6px bg-[var(--el-fill-color-blank)]"
+ >
+ <el-form-item label="CRON琛ㄨ揪寮�" required>
+ <Crontab
+ :model-value="triggerItem.cronExpression || '0 0 12 * * ?'"
+ @update:model-value="(value) => updateTriggerCronConfig(index, value)"
+ />
+ </el-form-item>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 绌虹姸鎬� -->
+ <div v-else class="py-40px text-center">
+ <el-empty description="鏆傛棤瑙﹀彂鍣�">
+ <template #description>
+ <div class="space-y-8px">
+ <p class="text-[var(--el-text-color-secondary)]">鏆傛棤瑙﹀彂鍣ㄩ厤缃�</p>
+ <p class="text-12px text-[var(--el-text-color-placeholder)]">
+ 璇蜂娇鐢ㄤ笂鏂圭殑"娣诲姞瑙﹀彂鍣�"鎸夐挳鏉ヨ缃Е鍙戣鍒�
+ </p>
+ </div>
+ </template>
+ </el-empty>
+ </div>
+ </div>
+ </el-card>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import DeviceTriggerConfig from '../configs/DeviceTriggerConfig.vue'
+import { Crontab } from '@/components/Crontab'
+import type { Trigger } from '@/api/iot/rule/scene'
+import {
+ getTriggerTypeLabel,
+ IotRuleSceneTriggerTypeEnum,
+ isDeviceTrigger
+} from '@/views/iot/utils/constants'
+
+/** 瑙﹀彂鍣ㄩ厤缃粍浠� */
+defineOptions({ name: 'TriggerSection' })
+
+const props = defineProps<{
+ triggers: Trigger[]
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:triggers', value: Trigger[]): void
+}>()
+
+const triggers = useVModel(props, 'triggers', emit)
+
+/** 鑾峰彇瑙﹀彂鍣ㄦ爣绛剧被鍨嬶紙鐢ㄤ簬 el-tag 鐨� type 灞炴�э級 */
+const getTriggerTagType = (type: number): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
+ if (type === IotRuleSceneTriggerTypeEnum.TIMER) {
+ return 'warning'
+ }
+ return isDeviceTrigger(type) ? 'success' : 'info'
+}
+
+/** 娣诲姞瑙﹀彂鍣� */
+const addTrigger = () => {
+ const newTrigger: Trigger = {
+ type: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+ productId: undefined,
+ deviceId: undefined,
+ identifier: undefined,
+ operator: undefined,
+ value: undefined,
+ cronExpression: undefined,
+ conditionGroups: [] // 绌虹殑鏉′欢缁勬暟缁�
+ }
+ triggers.value.push(newTrigger)
+}
+
+/**
+ * 鍒犻櫎瑙﹀彂鍣�
+ * @param index 瑙﹀彂鍣ㄧ储寮�
+ */
+const removeTrigger = (index: number) => {
+ if (triggers.value.length > 1) {
+ triggers.value.splice(index, 1)
+ }
+}
+
+/**
+ * 鏇存柊瑙﹀彂鍣ㄧ被鍨�
+ * @param index 瑙﹀彂鍣ㄧ储寮�
+ * @param type 瑙﹀彂鍣ㄧ被鍨�
+ */
+const updateTriggerType = (index: number, type: number) => {
+ triggers.value[index].type = type
+ onTriggerTypeChange(index, type)
+}
+
+/**
+ * 鏇存柊瑙﹀彂鍣ㄨ澶囬厤缃�
+ * @param index 瑙﹀彂鍣ㄧ储寮�
+ * @param newTrigger 鏂扮殑瑙﹀彂鍣ㄥ璞�
+ */
+const updateTriggerDeviceConfig = (index: number, newTrigger: Trigger) => {
+ triggers.value[index] = newTrigger
+}
+
+/**
+ * 鏇存柊瑙﹀彂鍣� CRON 閰嶇疆
+ * @param index 瑙﹀彂鍣ㄧ储寮�
+ * @param cronExpression CRON 琛ㄨ揪寮�
+ */
+const updateTriggerCronConfig = (index: number, cronExpression?: string) => {
+ triggers.value[index].cronExpression = cronExpression
+}
+
+/**
+ * 澶勭悊瑙﹀彂鍣ㄧ被鍨嬪彉鍖栦簨浠�
+ * @param index 瑙﹀彂鍣ㄧ储寮�
+ * @param _ 瑙﹀彂鍣ㄧ被鍨嬶紙鏈娇鐢級
+ */
+const onTriggerTypeChange = (index: number, _: number) => {
+ const triggerItem = triggers.value[index]
+ triggerItem.productId = undefined
+ triggerItem.deviceId = undefined
+ triggerItem.identifier = undefined
+ triggerItem.operator = undefined
+ triggerItem.value = undefined
+ triggerItem.cronExpression = undefined
+ triggerItem.conditionGroups = []
+}
+
+/** 鍒濆鍖栵細纭繚鑷冲皯鏈変竴涓Е鍙戝櫒 */
+onMounted(() => {
+ if (triggers.value.length === 0) {
+ addTrigger()
+ }
+})
+</script>
diff --git a/src/views/iot/rule/scene/form/selectors/DeviceSelector.vue b/src/views/iot/rule/scene/form/selectors/DeviceSelector.vue
new file mode 100644
index 0000000..0aa9cdd
--- /dev/null
+++ b/src/views/iot/rule/scene/form/selectors/DeviceSelector.vue
@@ -0,0 +1,103 @@
+<!-- 璁惧閫夋嫨鍣ㄧ粍浠� -->
+<template>
+ <el-select
+ :model-value="modelValue"
+ @update:model-value="handleChange"
+ placeholder="璇烽�夋嫨璁惧"
+ filterable
+ clearable
+ class="w-full"
+ :loading="deviceLoading"
+ :disabled="!productId"
+ >
+ <el-option
+ v-for="device in deviceList"
+ :key="device.id"
+ :label="device.deviceName"
+ :value="device.id"
+ >
+ <div class="flex items-center justify-between w-full py-4px">
+ <div class="flex-1">
+ <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
+ {{ device.deviceName }}
+ </div>
+ <div class="text-12px text-[var(--el-text-color-secondary)]">{{ device.deviceKey }}</div>
+ </div>
+ <div class="flex items-center gap-4px" v-if="device.id > 0">
+ <dict-tag :type="DICT_TYPE.IOT_DEVICE_STATE" :value="device.state" />
+ </div>
+ </div>
+ </el-option>
+ </el-select>
+</template>
+
+<script setup lang="ts">
+import { DeviceApi } from '@/api/iot/device/device'
+import { DEVICE_SELECTOR_OPTIONS } from '@/views/iot/utils/constants'
+import { DICT_TYPE } from '@/utils/dict'
+
+/** 璁惧閫夋嫨鍣ㄧ粍浠� */
+defineOptions({ name: 'DeviceSelector' })
+
+const props = defineProps<{
+ modelValue?: number
+ productId?: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value?: number): void
+ (e: 'change', value?: number): void
+}>()
+
+const deviceLoading = ref(false) // 璁惧鍔犺浇鐘舵��
+const deviceList = ref<any[]>([]) // 璁惧鍒楄〃
+
+/**
+ * 澶勭悊閫夋嫨鍙樺寲浜嬩欢
+ * @param value 閫変腑鐨勮澶嘔D
+ */
+const handleChange = (value?: number) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+
+/**
+ * 鑾峰彇璁惧鍒楄〃
+ */
+const getDeviceList = async () => {
+ if (!props.productId) {
+ deviceList.value = []
+ return
+ }
+
+ try {
+ deviceLoading.value = true
+ const res = await DeviceApi.getDeviceListByProductId(props.productId)
+ deviceList.value = res || []
+ } catch (error) {
+ console.error('鑾峰彇璁惧鍒楄〃澶辫触:', error)
+ deviceList.value = []
+ } finally {
+ deviceList.value.unshift(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES)
+ deviceLoading.value = false
+ }
+}
+
+// 鐩戝惉浜у搧鍙樺寲
+watch(
+ () => props.productId,
+ (newProductId) => {
+ if (newProductId) {
+ getDeviceList()
+ } else {
+ deviceList.value = []
+ // 娓呯┖褰撳墠閫夋嫨鐨勮澶�
+ if (props.modelValue) {
+ emit('update:modelValue', undefined)
+ emit('change', undefined)
+ }
+ }
+ },
+ { immediate: true }
+)
+</script>
diff --git a/src/views/iot/rule/scene/form/selectors/OperatorSelector.vue b/src/views/iot/rule/scene/form/selectors/OperatorSelector.vue
new file mode 100644
index 0000000..df98de6
--- /dev/null
+++ b/src/views/iot/rule/scene/form/selectors/OperatorSelector.vue
@@ -0,0 +1,264 @@
+<!-- 鎿嶄綔绗﹂�夋嫨鍣ㄧ粍浠� -->
+<template>
+ <div class="w-full">
+ <el-select
+ v-model="localValue"
+ placeholder="璇烽�夋嫨鎿嶄綔绗�"
+ @change="handleChange"
+ class="w-full"
+ >
+ <el-option
+ v-for="operator in availableOperators"
+ :key="operator.value"
+ :label="operator.label"
+ :value="operator.value"
+ >
+ <div class="flex items-center justify-between w-full py-4px">
+ <div class="flex items-center gap-8px">
+ <div class="text-14px font-500 text-[var(--el-text-color-primary)]">
+ {{ operator.label }}
+ </div>
+ <div
+ class="text-12px text-[var(--el-color-primary)] bg-[var(--el-color-primary-light-9)] px-6px py-2px rounded-4px font-mono"
+ >
+ {{ operator.symbol }}
+ </div>
+ </div>
+ <div class="text-12px text-[var(--el-text-color-secondary)]">
+ {{ operator.description }}
+ </div>
+ </div>
+ </el-option>
+ </el-select>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import {
+ IotRuleSceneTriggerConditionParameterOperatorEnum,
+ IoTDataSpecsDataTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** 鎿嶄綔绗﹂�夋嫨鍣ㄧ粍浠� */
+defineOptions({ name: 'OperatorSelector' })
+
+const props = defineProps<{
+ modelValue?: string
+ propertyType?: string
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: string): void
+ (e: 'change', value: string): void
+}>()
+
+const localValue = useVModel(props, 'modelValue', emit)
+
+// 鍩轰簬鏋氫妇鐨勬搷浣滅瀹氫箟
+const allOperators = [
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.name,
+ symbol: '=',
+ description: '鍊煎畬鍏ㄧ浉绛夋椂瑙﹀彂',
+ example: 'temperature = 25',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.TEXT,
+ IoTDataSpecsDataTypeEnum.BOOL,
+ IoTDataSpecsDataTypeEnum.ENUM
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.name,
+ symbol: '鈮�',
+ description: '鍊间笉鐩哥瓑鏃惰Е鍙�',
+ example: 'power != false',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.TEXT,
+ IoTDataSpecsDataTypeEnum.BOOL,
+ IoTDataSpecsDataTypeEnum.ENUM
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.name,
+ symbol: '>',
+ description: '鍊煎ぇ浜庢寚瀹氬�兼椂瑙﹀彂',
+ example: 'temperature > 30',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.name,
+ symbol: '鈮�',
+ description: '鍊煎ぇ浜庢垨绛変簬鎸囧畾鍊兼椂瑙﹀彂',
+ example: 'humidity >= 80',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.name,
+ symbol: '<',
+ description: '鍊煎皬浜庢寚瀹氬�兼椂瑙﹀彂',
+ example: 'temperature < 10',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.name,
+ symbol: '鈮�',
+ description: '鍊煎皬浜庢垨绛変簬鎸囧畾鍊兼椂瑙﹀彂',
+ example: 'battery <= 20',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.IN.name,
+ symbol: '鈭�',
+ description: '鍊煎湪鎸囧畾鍒楄〃涓椂瑙﹀彂',
+ example: 'status in [1,2,3]',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.TEXT,
+ IoTDataSpecsDataTypeEnum.ENUM
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.name,
+ symbol: '鈭�',
+ description: '鍊间笉鍦ㄦ寚瀹氬垪琛ㄤ腑鏃惰Е鍙�',
+ example: 'status not in [1,2,3]',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.TEXT,
+ IoTDataSpecsDataTypeEnum.ENUM
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.name,
+ symbol: '鈯�',
+ description: '鍊煎湪鎸囧畾鑼冨洿鍐呮椂瑙﹀彂',
+ example: 'temperature between 20,30',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.name,
+ symbol: '鈯�',
+ description: '鍊间笉鍦ㄦ寚瀹氳寖鍥村唴鏃惰Е鍙�',
+ example: 'temperature not between 20,30',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.LIKE.name,
+ symbol: '鈮�',
+ description: '瀛楃涓插尮閰嶆寚瀹氭ā寮忔椂瑙﹀彂',
+ example: 'message like "%error%"',
+ supportedTypes: [IoTDataSpecsDataTypeEnum.TEXT]
+ },
+ {
+ value: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.value,
+ label: IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_NULL.name,
+ symbol: '鈮犫垍',
+ description: '鍊奸潪绌烘椂瑙﹀彂',
+ example: 'data not null',
+ supportedTypes: [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.FLOAT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.TEXT,
+ IoTDataSpecsDataTypeEnum.BOOL,
+ IoTDataSpecsDataTypeEnum.ENUM,
+ IoTDataSpecsDataTypeEnum.DATE
+ ]
+ }
+]
+
+// 璁$畻灞炴�э細鍙敤鐨勬搷浣滅
+const availableOperators = computed(() => {
+ if (!props.propertyType) {
+ return allOperators
+ }
+ return allOperators.filter((op) =>
+ (op.supportedTypes as any[]).includes(props.propertyType || '')
+ )
+})
+
+// 璁$畻灞炴�э細褰撳墠閫変腑鐨勬搷浣滅
+const selectedOperator = computed(() => {
+ return allOperators.find((op) => op.value === localValue.value)
+})
+
+/**
+ * 澶勭悊閫夋嫨鍙樺寲浜嬩欢
+ * @param value 閫変腑鐨勬搷浣滅鍊�
+ */
+const handleChange = (value: string) => {
+ emit('change', value)
+}
+
+/** 鐩戝惉灞炴�х被鍨嬪彉鍖� */
+watch(
+ () => props.propertyType,
+ () => {
+ // 濡傛灉褰撳墠閫夋嫨鐨勬搷浣滅涓嶆敮鎸佹柊鐨勫睘鎬х被鍨嬶紝鍒欐竻绌洪�夋嫨
+ if (
+ localValue.value &&
+ selectedOperator.value &&
+ !(selectedOperator.value.supportedTypes as any[]).includes(props.propertyType || '')
+ ) {
+ localValue.value = ''
+ }
+ }
+)
+</script>
+
+<style scoped>
+:deep(.el-select-dropdown__item) {
+ height: auto;
+ padding: 8px 20px;
+}
+</style>
diff --git a/src/views/iot/rule/scene/form/selectors/ProductSelector.vue b/src/views/iot/rule/scene/form/selectors/ProductSelector.vue
new file mode 100644
index 0000000..ab76d63
--- /dev/null
+++ b/src/views/iot/rule/scene/form/selectors/ProductSelector.vue
@@ -0,0 +1,79 @@
+<!-- 浜у搧閫夋嫨鍣ㄧ粍浠� -->
+<template>
+ <el-select
+ :model-value="modelValue"
+ @update:model-value="handleChange"
+ placeholder="璇烽�夋嫨浜у搧"
+ filterable
+ clearable
+ class="w-full"
+ :loading="productLoading"
+ >
+ <el-option
+ v-for="product in productList"
+ :key="product.id"
+ :label="product.name"
+ :value="product.id"
+ >
+ <div class="flex items-center justify-between w-full py-4px">
+ <div class="flex-1">
+ <div class="text-14px font-500 text-[var(--el-text-color-primary)] mb-2px">
+ {{ product.name }}
+ </div>
+ <div class="text-12px text-[var(--el-text-color-secondary)]">
+ {{ product.productKey }}
+ </div>
+ </div>
+ <dict-tag :type="DICT_TYPE.IOT_PRODUCT_STATUS" :value="product.status" />
+ </div>
+ </el-option>
+ </el-select>
+</template>
+
+<script setup lang="ts">
+import { ProductApi } from '@/api/iot/product/product'
+import { DICT_TYPE } from '@/utils/dict'
+
+/** 浜у搧閫夋嫨鍣ㄧ粍浠� */
+defineOptions({ name: 'ProductSelector' })
+
+defineProps<{
+ modelValue?: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value?: number): void
+ (e: 'change', value?: number): void
+}>()
+
+const productLoading = ref(false) // 浜у搧鍔犺浇鐘舵��
+const productList = ref<any[]>([]) // 浜у搧鍒楄〃
+
+/**
+ * 澶勭悊閫夋嫨鍙樺寲浜嬩欢
+ * @param value 閫変腑鐨勪骇鍝� ID
+ */
+const handleChange = (value?: number) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+
+/** 鑾峰彇浜у搧鍒楄〃 */
+const getProductList = async () => {
+ try {
+ productLoading.value = true
+ const res = await ProductApi.getSimpleProductList()
+ productList.value = res || []
+ } catch (error) {
+ console.error('鑾峰彇浜у搧鍒楄〃澶辫触:', error)
+ productList.value = []
+ } finally {
+ productLoading.value = false
+ }
+}
+
+// 缁勪欢鎸傝浇鏃惰幏鍙栦骇鍝佸垪琛�
+onMounted(() => {
+ getProductList()
+})
+</script>
diff --git a/src/views/iot/rule/scene/form/selectors/PropertySelector.vue b/src/views/iot/rule/scene/form/selectors/PropertySelector.vue
new file mode 100644
index 0000000..51f2117
--- /dev/null
+++ b/src/views/iot/rule/scene/form/selectors/PropertySelector.vue
@@ -0,0 +1,437 @@
+<!-- 灞炴�ч�夋嫨鍣ㄧ粍浠� -->
+<template>
+ <div class="flex items-center gap-8px">
+ <el-select
+ v-model="localValue"
+ placeholder="璇烽�夋嫨鐩戞帶椤�"
+ filterable
+ clearable
+ @change="handleChange"
+ class="!w-150px"
+ :loading="loading"
+ >
+ <el-option-group v-for="group in propertyGroups" :key="group.label" :label="group.label">
+ <el-option
+ v-for="property in group.options"
+ :key="property.identifier"
+ :label="property.name"
+ :value="property.identifier"
+ >
+ <div class="flex items-center justify-between w-full py-2px">
+ <span class="text-14px font-500 text-[var(--el-text-color-primary)] flex-1 truncate">
+ {{ property.name }}
+ </span>
+ <el-tag
+ :type="getDataTypeTagType(property.dataType)"
+ size="small"
+ class="ml-8px flex-shrink-0"
+ >
+ {{ property.identifier }}
+ </el-tag>
+ </div>
+ </el-option>
+ </el-option-group>
+ </el-select>
+
+ <!-- 灞炴�ц鎯呭脊鍑哄眰 -->
+ <el-popover
+ v-if="selectedProperty"
+ placement="right-start"
+ :width="350"
+ trigger="click"
+ :show-arrow="true"
+ :offset="8"
+ popper-class="property-detail-popover"
+ >
+ <template #reference>
+ <el-button
+ type="info"
+ :icon="InfoFilled"
+ circle
+ size="small"
+ class="flex-shrink-0"
+ title="鏌ョ湅灞炴�ц鎯�"
+ />
+ </template>
+
+ <!-- 寮瑰嚭灞傚唴瀹� -->
+ <div class="property-detail-content">
+ <div class="flex items-center gap-8px mb-12px">
+ <Icon icon="ep:info-filled" class="text-[var(--el-color-info)] text-16px" />
+ <span class="text-14px font-500 text-[var(--el-text-color-primary)]">
+ {{ selectedProperty.name }}
+ </span>
+ <el-tag :type="getDataTypeTagType(selectedProperty.dataType)" size="small">
+ {{ getDataTypeName(selectedProperty.dataType) }}
+ </el-tag>
+ </div>
+
+ <div class="space-y-8px ml-24px">
+ <div class="flex items-start gap-8px">
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 鏍囪瘑绗︼細
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ selectedProperty.identifier }}
+ </span>
+ </div>
+
+ <div v-if="selectedProperty.description" class="flex items-start gap-8px">
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 鎻忚堪锛�
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ selectedProperty.description }}
+ </span>
+ </div>
+
+ <div v-if="selectedProperty.unit" class="flex items-start gap-8px">
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 鍗曚綅锛�
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ selectedProperty.unit }}
+ </span>
+ </div>
+
+ <div v-if="selectedProperty.range" class="flex items-start gap-8px">
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 鍙栧�艰寖鍥达細
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ selectedProperty.range }}
+ </span>
+ </div>
+
+ <!-- 鏍规嵁灞炴�х被鍨嬫樉绀洪澶栦俊鎭� -->
+ <div
+ v-if="
+ selectedProperty.type === IoTThingModelTypeEnum.PROPERTY &&
+ selectedProperty.accessMode
+ "
+ class="flex items-start gap-8px"
+ >
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 璁块棶妯″紡锛�
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ getAccessModeLabel(selectedProperty.accessMode) }}
+ </span>
+ </div>
+
+ <div
+ v-if="
+ selectedProperty.type === IoTThingModelTypeEnum.EVENT && selectedProperty.eventType
+ "
+ class="flex items-start gap-8px"
+ >
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 浜嬩欢绫诲瀷锛�
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ getEventTypeLabel(selectedProperty.eventType) }}
+ </span>
+ </div>
+
+ <div
+ v-if="
+ selectedProperty.type === IoTThingModelTypeEnum.SERVICE && selectedProperty.callType
+ "
+ class="flex items-start gap-8px"
+ >
+ <span class="text-12px text-[var(--el-text-color-secondary)] min-w-60px flex-shrink-0">
+ 璋冪敤绫诲瀷锛�
+ </span>
+ <span class="text-12px text-[var(--el-text-color-primary)] flex-1">
+ {{ getThingModelServiceCallTypeLabel(selectedProperty.callType) }}
+ </span>
+ </div>
+ </div>
+ </div>
+ </el-popover>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core'
+import { InfoFilled } from '@element-plus/icons-vue'
+import {
+ IotRuleSceneTriggerTypeEnum,
+ IoTThingModelTypeEnum,
+ getAccessModeLabel,
+ getEventTypeLabel,
+ getThingModelServiceCallTypeLabel,
+ getDataTypeName,
+ getDataTypeTagType,
+ THING_MODEL_GROUP_LABELS
+} from '@/views/iot/utils/constants'
+import type {
+ IotThingModelTSLResp,
+ ThingModelEvent,
+ ThingModelParam,
+ ThingModelProperty,
+ ThingModelService
+} from '@/api/iot/thingmodel'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+
+/** 灞炴�ч�夋嫨鍣ㄧ粍浠� */
+defineOptions({ name: 'PropertySelector' })
+
+/** 灞炴�ч�夋嫨鍣ㄥ唴閮ㄤ娇鐢ㄧ殑缁熶竴鏁版嵁缁撴瀯 */
+interface PropertySelectorItem {
+ identifier: string
+ name: string
+ description?: string
+ dataType: string
+ type: number // IoTThingModelTypeEnum
+ accessMode?: string
+ required?: boolean
+ unit?: string
+ range?: string
+ eventType?: string
+ callType?: string
+ inputParams?: ThingModelParam[]
+ outputParams?: ThingModelParam[]
+ property?: ThingModelProperty
+ event?: ThingModelEvent
+ service?: ThingModelService
+}
+
+const props = defineProps<{
+ modelValue?: string
+ triggerType: number
+ productId?: number
+ deviceId?: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', value: string): void
+ (e: 'change', value: { type: string; config: any }): void
+}>()
+
+const localValue = useVModel(props, 'modelValue', emit)
+
+const loading = ref(false) // 鍔犺浇鐘舵��
+const propertyList = ref<PropertySelectorItem[]>([]) // 灞炴�у垪琛�
+const thingModelTSL = ref<IotThingModelTSLResp | null>(null) // 鐗╂ā鍨婽SL鏁版嵁
+
+// 璁$畻灞炴�э細灞炴�у垎缁�
+const propertyGroups = computed(() => {
+ const groups: { label: string; options: any[] }[] = []
+
+ if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST) {
+ groups.push({
+ label: THING_MODEL_GROUP_LABELS.PROPERTY,
+ options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.PROPERTY)
+ })
+ }
+
+ if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST) {
+ groups.push({
+ label: THING_MODEL_GROUP_LABELS.EVENT,
+ options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.EVENT)
+ })
+ }
+
+ if (props.triggerType === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE) {
+ groups.push({
+ label: THING_MODEL_GROUP_LABELS.SERVICE,
+ options: propertyList.value.filter((p) => p.type === IoTThingModelTypeEnum.SERVICE)
+ })
+ }
+
+ return groups.filter((group) => group.options.length > 0)
+})
+
+// 璁$畻灞炴�э細褰撳墠閫変腑鐨勫睘鎬�
+const selectedProperty = computed(() => {
+ return propertyList.value.find((p) => p.identifier === localValue.value)
+})
+
+/**
+ * 澶勭悊閫夋嫨鍙樺寲浜嬩欢
+ * @param value 閫変腑鐨勫睘鎬ф爣璇嗙
+ */
+const handleChange = (value: string) => {
+ const property = propertyList.value.find((p) => p.identifier === value)
+ if (property) {
+ emit('change', {
+ type: property.dataType,
+ config: property
+ })
+ }
+}
+
+/**
+ * 鑾峰彇鐗╂ā鍨婽SL鏁版嵁
+ */
+const getThingModelTSL = async () => {
+ if (!props.productId) {
+ thingModelTSL.value = null
+ propertyList.value = []
+ return
+ }
+
+ loading.value = true
+ try {
+ const tslData = await ThingModelApi.getThingModelTSLByProductId(props.productId)
+
+ if (tslData) {
+ thingModelTSL.value = tslData
+ parseThingModelData()
+ } else {
+ console.error('鑾峰彇鐗╂ā鍨婽SL澶辫触: 杩斿洖鏁版嵁涓虹┖')
+ propertyList.value = []
+ }
+ } catch (error) {
+ console.error('鑾峰彇鐗╂ā鍨婽SL澶辫触:', error)
+ propertyList.value = []
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 瑙f瀽鐗╂ā鍨� TSL 鏁版嵁 */
+const parseThingModelData = () => {
+ const tsl = thingModelTSL.value
+ const properties: PropertySelectorItem[] = []
+
+ if (!tsl) {
+ propertyList.value = properties
+ return
+ }
+ // 瑙f瀽灞炴��
+ if (tsl.properties && Array.isArray(tsl.properties)) {
+ tsl.properties.forEach((prop) => {
+ properties.push({
+ identifier: prop.identifier,
+ name: prop.name,
+ description: prop.description,
+ dataType: prop.dataType,
+ type: IoTThingModelTypeEnum.PROPERTY,
+ accessMode: prop.accessMode,
+ required: prop.required,
+ unit: getPropertyUnit(prop),
+ range: getPropertyRange(prop),
+ property: prop
+ })
+ })
+ }
+
+ // 瑙f瀽浜嬩欢
+ if (tsl.events && Array.isArray(tsl.events)) {
+ tsl.events.forEach((event) => {
+ properties.push({
+ identifier: event.identifier,
+ name: event.name,
+ description: event.description,
+ dataType: 'struct',
+ type: IoTThingModelTypeEnum.EVENT,
+ eventType: event.type,
+ required: event.required,
+ outputParams: event.outputParams,
+ event: event
+ })
+ })
+ }
+
+ // 瑙f瀽鏈嶅姟
+ if (tsl.services && Array.isArray(tsl.services)) {
+ tsl.services.forEach((service) => {
+ properties.push({
+ identifier: service.identifier,
+ name: service.name,
+ description: service.description,
+ dataType: 'struct',
+ type: IoTThingModelTypeEnum.SERVICE,
+ callType: service.callType,
+ required: service.required,
+ inputParams: service.inputParams,
+ outputParams: service.outputParams,
+ service: service
+ })
+ })
+ }
+ propertyList.value = properties
+}
+
+/**
+ * 鑾峰彇灞炴�у崟浣�
+ * @param property 灞炴�у璞�
+ * @returns 灞炴�у崟浣�
+ */
+const getPropertyUnit = (property: any) => {
+ if (!property) return undefined
+
+ // 鏁板�煎瀷鏁版嵁鐨勫崟浣�
+ if (property.dataSpecs && property.dataSpecs.unit) {
+ return property.dataSpecs.unit
+ }
+
+ return undefined
+}
+
+/**
+ * 鑾峰彇灞炴�ц寖鍥存弿杩�
+ * @param property 灞炴�у璞�
+ * @returns 灞炴�ц寖鍥存弿杩�
+ */
+const getPropertyRange = (property: any) => {
+ if (!property) return undefined
+
+ // 鏁板�煎瀷鏁版嵁鐨勮寖鍥�
+ if (property.dataSpecs) {
+ const specs = property.dataSpecs
+ if (specs.min !== undefined && specs.max !== undefined) {
+ return `${specs.min}~${specs.max}`
+ }
+ }
+
+ // 鏋氫妇鍨嬪拰甯冨皵鍨嬫暟鎹殑閫夐」
+ if (property.dataSpecsList && Array.isArray(property.dataSpecsList)) {
+ return property.dataSpecsList.map((item: any) => `${item.name}(${item.value})`).join(', ')
+ }
+
+ return undefined
+}
+
+/** 鐩戝惉浜у搧鍙樺寲 */
+watch(
+ () => props.productId,
+ () => {
+ getThingModelTSL()
+ },
+ { immediate: true }
+)
+
+/** 鐩戝惉瑙﹀彂绫诲瀷鍙樺寲 */
+watch(
+ () => props.triggerType,
+ () => {
+ localValue.value = ''
+ }
+)
+</script>
+
+<style scoped>
+/* 涓嬫媺閫夐」鏍峰紡 */
+:deep(.el-select-dropdown__item) {
+ height: auto;
+ padding: 6px 20px;
+}
+
+/* 寮瑰嚭灞傚唴瀹规牱寮� */
+.property-detail-content {
+ padding: 4px 0;
+}
+
+/* 寮瑰嚭灞傝嚜瀹氫箟鏍峰紡 */
+:global(.property-detail-popover) {
+ /* 鍙互鍦ㄨ繖閲屾坊鍔犲叏灞�寮瑰嚭灞傛牱寮� */
+ max-width: 400px !important;
+}
+
+:global(.property-detail-popover .el-popover__content) {
+ padding: 16px !important;
+}
+</style>
diff --git a/src/views/iot/rule/scene/index.vue b/src/views/iot/rule/scene/index.vue
new file mode 100644
index 0000000..f4a7b55
--- /dev/null
+++ b/src/views/iot/rule/scene/index.vue
@@ -0,0 +1,492 @@
+<template>
+ <ContentWrap>
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="flex justify-between items-start mb-20px">
+ <div class="flex-1">
+ <h2 class="flex items-center m-0 mb-8px text-24px font-600 text-[#303133]">
+ <Icon icon="ep:connection" class="ml-5px mr-12px text-[#409eff]" />
+ 鍦烘櫙鑱斿姩瑙勫垯
+ </h2>
+ <p class="m-0 text-[#606266] text-14px">
+ 閫氳繃閰嶇疆瑙﹀彂鏉′欢鍜屾墽琛屽姩浣滐紝瀹炵幇璁惧闂寸殑鏅鸿兘鑱斿姩鎺у埗
+ </p>
+ </div>
+ <div>
+ <el-button type="primary" @click="handleAdd">
+ <Icon icon="ep:plus" />
+ 鏂板瑙勫垯
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 鎼滅储鍜岀瓫閫� -->
+ <el-card class="mb-16px" shadow="never">
+ <el-form
+ ref="queryFormRef"
+ :model="queryParams"
+ :inline="true"
+ label-width="80px"
+ @submit.prevent
+ >
+ <el-form-item label="瑙勫垯鍚嶇О">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヨ鍒欏悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="瑙勫垯鐘舵��">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">
+ <Icon icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 缁熻鍗$墖 -->
+ <el-row :gutter="16" class="mb-16px">
+ <el-col :span="6">
+ <el-card
+ class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+ shadow="hover"
+ >
+ <div class="flex items-center">
+ <div
+ class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#667eea] to-[#764ba2]"
+ >
+ <Icon icon="ep:document" />
+ </div>
+ <div>
+ <div class="text-24px font-600 text-[#303133] leading-none">
+ {{ statistics.total }}
+ </div>
+ <div class="text-14px text-[#909399] mt-4px">鎬昏鍒欐暟</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card
+ class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+ shadow="hover"
+ >
+ <div class="flex items-center">
+ <div
+ class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#f093fb] to-[#f5576c]"
+ >
+ <Icon icon="ep:check" />
+ </div>
+ <div>
+ <div class="text-24px font-600 text-[#303133] leading-none">
+ {{ statistics.enabled }}
+ </div>
+ <div class="text-14px text-[#909399] mt-4px">鍚敤瑙勫垯</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card
+ class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+ shadow="hover"
+ >
+ <div class="flex items-center">
+ <div
+ class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#4facfe] to-[#00f2fe]"
+ >
+ <Icon icon="ep:close" />
+ </div>
+ <div>
+ <div class="text-24px font-600 text-[#303133] leading-none">
+ {{ statistics.disabled }}
+ </div>
+ <div class="text-14px text-[#909399] mt-4px">绂佺敤瑙勫垯</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card
+ class="cursor-pointer transition-all duration-300 hover:transform hover:-translate-y-2px"
+ shadow="hover"
+ >
+ <div class="flex items-center">
+ <div
+ class="w-48px h-48px rounded-8px flex items-center justify-center text-24px text-white mr-16px bg-gradient-to-br from-[#43e97b] to-[#38f9d7]"
+ >
+ <Icon icon="ep:timer" />
+ </div>
+ <div>
+ <div class="text-24px font-600 text-[#303133] leading-none">
+ {{ statistics.timerRules }}
+ </div>
+ <div class="text-14px text-[#909399] mt-4px">瀹氭椂瑙勫垯</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <el-card class="mb-20px" shadow="never">
+ <el-table v-loading="loading" :data="list" stripe @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="瑙勫垯鍚嶇О" prop="name" min-width="200">
+ <template #default="{ row }">
+ <div class="flex items-center gap-8px">
+ <span class="font-500 text-[#303133]">{{ row.name }}</span>
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="row.status" />
+ </div>
+ <div v-if="row.description" class="text-12px text-[#909399] mt-4px">
+ {{ row.description }}
+ </div>
+ </template>
+ </el-table-column>
+ <!-- 瑙﹀彂鏉′欢鍒� -->
+ <el-table-column label="瑙﹀彂鏉′欢" min-width="280">
+ <template #default="{ row }">
+ <div class="space-y-4px">
+ <div class="flex flex-wrap gap-4px">
+ <el-tag type="primary" size="small" class="m-0">
+ {{ getTriggerSummary(row) }}
+ </el-tag>
+ </div>
+ <!-- 鏄剧ず瀹氭椂瑙﹀彂鍣ㄧ殑棰濆淇℃伅 -->
+ <div v-if="hasTimerTrigger(row)" class="mt-4px">
+ <el-tooltip :content="getCronExpression(row)" placement="top">
+ <el-tag size="small" type="info" class="mr-4px">
+ <Icon icon="ep:timer" class="mr-2px" />
+ {{ getCronFrequency(row) }}
+ </el-tag>
+ </el-tooltip>
+ <div v-if="getNextExecutionTime(row)" class="text-12px text-[#909399] mt-2px">
+ <Icon icon="ep:clock" class="mr-2px" />
+ 涓嬫鎵ц: {{ formatDate(getNextExecutionTime(row)!) }}
+ </div>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <!-- 鎵ц鍔ㄤ綔鍒� -->
+ <el-table-column label="鎵ц鍔ㄤ綔" min-width="250">
+ <template #default="{ row }">
+ <div class="flex flex-wrap gap-4px">
+ <el-tag type="success" size="small" class="m-0">
+ {{ getActionSummary(row) }}
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈�杩戣Е鍙�" prop="lastTriggeredTime" width="180">
+ <template #default="{ row }">
+ <span v-if="row.lastTriggeredTime">
+ {{ formatDate(row.lastTriggeredTime) }}
+ </span>
+ <span v-else class="text-gray-400">鏈Е鍙�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" width="180">
+ <template #default="{ row }">
+ {{ formatDate(row.createTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="210" fixed="right">
+ <template #default="{ row }">
+ <div>
+ <el-button type="primary" link @click="handleEdit(row)">
+ <Icon icon="ep:edit" />
+ 缂栬緫
+ </el-button>
+ <el-button
+ :type="row.status === 0 ? 'warning' : 'success'"
+ link
+ @click="handleToggleStatus(row)"
+ >
+ <Icon :icon="row.status === 0 ? 'ep:video-pause' : 'ep:video-play'" />
+ {{ getDictLabel(DICT_TYPE.COMMON_STATUS, row.status) }}
+ </el-button>
+ <el-button type="danger" class="!mr-10px" link @click="handleDelete(row.id)">
+ <Icon icon="ep:delete" />
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 琛ㄥ崟瀵硅瘽妗� -->
+ <RuleSceneForm v-model="formVisible" :rule-scene="currentRule" @success="getList" />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getDictLabel, getIntDictOptions } from '@/utils/dict'
+import { ContentWrap } from '@/components/ContentWrap'
+import RuleSceneForm from './form/RuleSceneForm.vue'
+import { IotSceneRule, RuleSceneApi } from '@/api/iot/rule/scene'
+import {
+ getActionTypeLabel,
+ getTriggerTypeLabel,
+ IotRuleSceneTriggerTypeEnum
+} from '@/views/iot/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import { CommonStatusEnum } from '@/utils/constants'
+import { CronUtils } from '@/utils/cron'
+
+/** 鍦烘櫙鑱斿姩瑙勫垯绠$悊椤甸潰 */
+defineOptions({ name: 'IoTSceneRule' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+/** 鏌ヨ鍙傛暟 */
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ status: undefined
+})
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<IotSceneRule[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const selectedRows = ref<IotSceneRule[]>([]) // 閫変腑鐨勮鏁版嵁
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 琛ㄥ崟鐘舵�� */
+const formVisible = ref(false) // 鏄惁鍙
+const currentRule = ref<IotSceneRule>() // 琛ㄥ崟鏁版嵁
+
+/** 缁熻鏁版嵁 */
+const statistics = ref({
+ total: 0,
+ enabled: 0,
+ disabled: 0,
+ timerRules: 0 // 瀹氭椂瑙勫垯鏁伴噺
+})
+
+/** 鑾峰彇瑙勫垯鎽樿淇℃伅 */
+const getRuleSceneSummary = (rule: IotSceneRule) => {
+ const triggerSummary =
+ rule.triggers?.map((trigger: any) => {
+ // 鏋勫缓鍩虹鎻忚堪
+ let description = getTriggerTypeLabel(trigger.type)
+ switch (trigger.type) {
+ case IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE:
+ break
+ case IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST:
+ case IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST:
+ case IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE:
+ if (trigger.identifier) {
+ description += ` (${trigger.identifier})`
+ }
+ break
+ case IotRuleSceneTriggerTypeEnum.TIMER:
+ description = `${getTriggerTypeLabel(trigger.type)} (${CronUtils.format(trigger.cronExpression || '')})`
+ break
+ default:
+ description = getTriggerTypeLabel(trigger.type)
+ }
+ // 娣诲姞璁惧淇℃伅锛堝鏋滄湁锛�
+ if (trigger.deviceId) {
+ description += ` [璁惧 ID: ${trigger.deviceId}]`
+ } else if (trigger.productId) {
+ description += ` [浜у搧 ID: ${trigger.productId}]`
+ }
+ return description
+ }) || []
+
+ const actionSummary =
+ rule.actions?.map((action: any) => {
+ // 鏋勫缓鍩虹鎻忚堪
+ let description = getActionTypeLabel(action.type)
+ // 娣诲姞璁惧淇℃伅锛堝鏋滄湁锛�
+ if (action.deviceId) {
+ description += ` [璁惧 ID: ${action.deviceId}]`
+ } else if (action.productId) {
+ description += ` [浜у搧 ID: ${action.productId}]`
+ }
+ // 娣诲姞鍛婅閰嶇疆淇℃伅锛堝鏋滄湁锛�
+ if (action.alertConfigId) {
+ description += ` [鍛婅閰嶇疆 ID: ${action.alertConfigId}]`
+ }
+ return description
+ }) || []
+
+ return {
+ triggerSummary: triggerSummary.join(', ') || '鏃犺Е鍙戝櫒',
+ actionSummary: actionSummary.join(', ') || '鏃犳墽琛屽櫒'
+ }
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await RuleSceneApi.getRuleScenePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ // 鏇存柊缁熻鏁版嵁
+ updateStatistics()
+ loading.value = false
+ }
+}
+
+/** 鏇存柊缁熻鏁版嵁 */
+const updateStatistics = () => {
+ statistics.value = {
+ total: list.value.length,
+ enabled: list.value.filter((item) => item.status === CommonStatusEnum.ENABLE).length,
+ disabled: list.value.filter((item) => item.status === CommonStatusEnum.DISABLE).length,
+ timerRules: list.value.filter((item) => hasTimerTrigger(item)).length
+ }
+}
+
+/** 鑾峰彇瑙﹀彂鍣ㄦ憳瑕� */
+const getTriggerSummary = (rule: IotSceneRule) => {
+ return getRuleSceneSummary(rule).triggerSummary
+}
+
+/** 鑾峰彇鎵ц鍣ㄦ憳瑕� */
+const getActionSummary = (rule: IotSceneRule) => {
+ return getRuleSceneSummary(rule).actionSummary
+}
+
+/** 妫�鏌ヨ鍒欐槸鍚﹀寘鍚畾鏃惰Е鍙戝櫒 */
+const hasTimerTrigger = (rule: IotSceneRule): boolean => {
+ return (
+ rule.triggers?.some((trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) || false
+ )
+}
+
+/** 鑾峰彇 CRON 琛ㄨ揪寮忕殑鎵ц棰戠巼鎻忚堪 */
+const getCronFrequency = (rule: IotSceneRule): string => {
+ const timerTrigger = rule.triggers?.find(
+ (trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+ )
+ if (timerTrigger?.cronExpression) {
+ return CronUtils.getFrequencyDescription(timerTrigger.cronExpression)
+ }
+ return ''
+}
+
+/** 鑾峰彇涓嬫鎵ц鏃堕棿 */
+const getNextExecutionTime = (rule: IotSceneRule): Date | null => {
+ const timerTrigger = rule.triggers?.find(
+ (trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+ )
+ if (timerTrigger?.cronExpression) {
+ return CronUtils.getNextExecutionTime(timerTrigger.cronExpression)
+ }
+ return null
+}
+
+/** 鑾峰彇 CRON 琛ㄨ揪寮忓師濮嬪�� */
+const getCronExpression = (rule: IotSceneRule): string => {
+ const timerTrigger = rule.triggers?.find(
+ (trigger) => trigger.type === IotRuleSceneTriggerTypeEnum.TIMER
+ )
+ return timerTrigger?.cronExpression || ''
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.name = ''
+ queryParams.status = undefined
+ handleQuery()
+}
+
+/** 娣诲姞鎿嶄綔 */
+const handleAdd = () => {
+ currentRule.value = undefined
+ formVisible.value = true
+}
+
+/** 淇敼鎿嶄綔 */
+const handleEdit = (row: IotSceneRule) => {
+ currentRule.value = row
+ formVisible.value = true
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await RuleSceneApi.deleteRuleScene(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch (error) {}
+}
+
+/** 淇敼鐘舵�� */
+const handleToggleStatus = async (row: IotSceneRule) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.status === CommonStatusEnum.ENABLE ? '绂佺敤' : '鍚敤'
+ await message.confirm('纭瑕�' + text + '"' + row.name + '"鍚�?')
+ // 鍙戣捣淇敼鐘舵��
+ await RuleSceneApi.updateRuleSceneStatus(
+ row.id!,
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ )
+ message.success(text + '鎴愬姛')
+ // 鍒锋柊
+ await getList()
+ } catch {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+const handleSelectionChange = (selection: IotSceneRule[]) => {
+ selectedRows.value = selection
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/thingmodel/ThingModelEvent.vue b/src/views/iot/thingmodel/ThingModelEvent.vue
new file mode 100644
index 0000000..2704f4b
--- /dev/null
+++ b/src/views/iot/thingmodel/ThingModelEvent.vue
@@ -0,0 +1,58 @@
+<!-- 浜у搧鐨勭墿妯″瀷琛ㄥ崟锛坋vent 椤癸級 -->
+<template>
+ <el-form-item
+ :rules="[{ required: true, message: '璇烽�夋嫨浜嬩欢绫诲瀷', trigger: 'change' }]"
+ label="浜嬩欢绫诲瀷"
+ prop="event.type"
+ >
+ <el-radio-group v-model="thingModelEvent.type">
+ <el-radio
+ v-for="eventType in Object.values(IoTThingModelEventTypeEnum)"
+ :key="eventType.value"
+ :value="eventType.value"
+ >
+ {{ eventType.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="杈撳嚭鍙傛暟">
+ <ThingModelInputOutputParam
+ v-model="thingModelEvent.outputParams"
+ :direction="IoTThingModelParamDirectionEnum.OUTPUT"
+ />
+ </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
+import { useVModel } from '@vueuse/core'
+import { ThingModelEvent } from '@/api/iot/thingmodel'
+import { isEmpty } from '@/utils/is'
+import {
+ IoTThingModelEventTypeEnum,
+ IoTThingModelParamDirectionEnum
+} from '@/views/iot/utils/constants'
+
+/** IoT 鐗╂ā鍨嬩簨浠� */
+defineOptions({ name: 'ThingModelEvent' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const thingModelEvent = useVModel(props, 'modelValue', emits) as Ref<ThingModelEvent>
+
+// 榛樿閫変腑锛孖NFO 淇℃伅
+watch(
+ () => thingModelEvent.value.type,
+ (val: string) =>
+ isEmpty(val) && (thingModelEvent.value.type = IoTThingModelEventTypeEnum.INFO.value),
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+ .el-form-item {
+ margin-bottom: 0;
+ }
+}
+</style>
diff --git a/src/views/iot/thingmodel/ThingModelForm.vue b/src/views/iot/thingmodel/ThingModelForm.vue
new file mode 100644
index 0000000..cc049fe
--- /dev/null
+++ b/src/views/iot/thingmodel/ThingModelForm.vue
@@ -0,0 +1,221 @@
+<!-- 浜у搧鐨勭墿妯″瀷琛ㄥ崟 -->
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="ThingModelFormRules"
+ label-width="100px"
+ >
+ <el-form-item label="鍔熻兘绫诲瀷" prop="type">
+ <el-radio-group v-model="formData.type">
+ <el-radio-button
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍔熻兘鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ姛鑳藉悕绉�" />
+ </el-form-item>
+ <el-form-item label="鏍囪瘑绗�" prop="identifier">
+ <el-input v-model="formData.identifier" placeholder="璇疯緭鍏ユ爣璇嗙" />
+ </el-form-item>
+ <!-- 灞炴�ч厤缃� -->
+ <ThingModelProperty
+ v-if="formData.type === IoTThingModelTypeEnum.PROPERTY"
+ v-model="formData.property"
+ />
+ <!-- 鏈嶅姟閰嶇疆 -->
+ <ThingModelService
+ v-if="formData.type === IoTThingModelTypeEnum.SERVICE"
+ v-model="formData.service"
+ />
+ <!-- 浜嬩欢閰嶇疆 -->
+ <ThingModelEvent
+ v-if="formData.type === IoTThingModelTypeEnum.EVENT"
+ v-model="formData.event"
+ />
+ <el-form-item label="鎻忚堪" prop="description">
+ <el-input
+ v-model="formData.description"
+ :maxlength="200"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ睘鎬ф弿杩�"
+ type="textarea"
+ />
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ProductVO } from '@/api/iot/product/product'
+import ThingModelProperty from './ThingModelProperty.vue'
+import ThingModelService from './ThingModelService.vue'
+import ThingModelEvent from './ThingModelEvent.vue'
+import { ThingModelApi, ThingModelData, ThingModelFormRules } from '@/api/iot/thingmodel'
+import {
+ IOT_PROVIDE_KEY,
+ IoTDataSpecsDataTypeEnum,
+ IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+import { cloneDeep } from 'lodash-es'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { isEmpty } from '@/utils/is'
+
+/** IoT 鐗╂ā鍨嬫暟鎹〃鍗� */
+defineOptions({ name: 'IoTThingModelForm' })
+
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 娉ㄥ叆浜у搧淇℃伅
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<ThingModelData>({
+ type: IoTThingModelTypeEnum.PROPERTY,
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ property: {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ },
+ service: {},
+ event: {}
+})
+
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ThingModelApi.getThingModel(id)
+ // 鎯呭喌涓�锛氬睘鎬у垵濮嬪寲
+ if (isEmpty(formData.value.property)) {
+ formData.value.dataType = IoTDataSpecsDataTypeEnum.INT
+ formData.value.property = {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ }
+ }
+ // 鎯呭喌浜岋細鏈嶅姟鍒濆鍖�
+ if (isEmpty(formData.value.service)) {
+ formData.value.service = {}
+ }
+ // 鎯呭喌涓夛細浜嬩欢鍒濆鍖�
+ if (isEmpty(formData.value.event)) {
+ formData.value.event = {}
+ }
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open, close: () => (dialogVisible.value = false) })
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success'])
+const submitForm = async () => {
+ await formRef.value.validate()
+ formLoading.value = true
+ try {
+ const data = cloneDeep(formData.value) as ThingModelData
+ // 淇℃伅琛ュ叏
+ data.productId = product!.value.id
+ data.productKey = product!.value.productKey
+ fillExtraAttributes(data)
+ if (formType.value === 'create') {
+ await ThingModelApi.createThingModel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ThingModelApi.updateThingModel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 濉啓棰濆鐨勫睘鎬э紙澶勭悊涓嶅悓绫诲瀷鐨勬儏鍐碉級 */
+const fillExtraAttributes = (data: any) => {
+ // 灞炴��
+ if (data.type === IoTThingModelTypeEnum.PROPERTY) {
+ removeDataSpecs(data.property)
+ data.dataType = data.property.dataType
+ data.property.identifier = data.identifier
+ data.property.name = data.name
+ delete data.service
+ delete data.event
+ }
+ // 鏈嶅姟
+ if (data.type === IoTThingModelTypeEnum.SERVICE) {
+ removeDataSpecs(data.service)
+ data.dataType = data.service.dataType
+ data.service.identifier = data.identifier
+ data.service.name = data.name
+ delete data.property
+ delete data.event
+ }
+ // 浜嬩欢
+ if (data.type === IoTThingModelTypeEnum.EVENT) {
+ removeDataSpecs(data.event)
+ data.dataType = data.event.dataType
+ data.event.identifier = data.identifier
+ data.event.name = data.name
+ delete data.property
+ delete data.service
+ }
+}
+
+/** 澶勭悊 dataSpecs 涓虹┖鐨勬儏鍐� */
+const removeDataSpecs = (val: any) => {
+ if (isEmpty(val.dataSpecs)) {
+ delete val.dataSpecs
+ }
+ if (isEmpty(val.dataSpecsList)) {
+ delete val.dataSpecsList
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ type: IoTThingModelTypeEnum.PROPERTY,
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ property: {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ },
+ service: {},
+ event: {}
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/iot/thingmodel/ThingModelInputOutputParam.vue b/src/views/iot/thingmodel/ThingModelInputOutputParam.vue
new file mode 100644
index 0000000..04bc950
--- /dev/null
+++ b/src/views/iot/thingmodel/ThingModelInputOutputParam.vue
@@ -0,0 +1,151 @@
+<!-- 浜у搧鐨勭墿妯″瀷琛ㄥ崟锛坋vent銆乻ervice 椤归噷鐨勫弬鏁帮級 -->
+<template>
+ <div
+ v-for="(item, index) in thingModelParams"
+ :key="index"
+ class="w-1/1 param-item flex justify-between px-10px mb-10px"
+ >
+ <span>鍙傛暟鍚嶇О锛歿{ item.name }}</span>
+ <div class="btn">
+ <el-button link type="primary" @click="openParamForm(item)">缂栬緫</el-button>
+ <el-divider direction="vertical" />
+ <el-button link type="danger" @click="deleteParamItem(index)">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <el-button link type="primary" @click="openParamForm(null)">+鏂板鍙傛暟</el-button>
+
+ <!-- param 琛ㄥ崟 -->
+ <Dialog v-model="dialogVisible" title="鏂板鍙傛暟" append-to-body>
+ <el-form
+ ref="paramFormRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="ThingModelFormRules"
+ label-width="100px"
+ >
+ <el-form-item label="鍙傛暟鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ姛鑳藉悕绉�" />
+ </el-form-item>
+ <el-form-item label="鏍囪瘑绗�" prop="identifier">
+ <el-input v-model="formData.identifier" placeholder="璇疯緭鍏ユ爣璇嗙" />
+ </el-form-item>
+ <!-- 灞炴�ч厤缃� -->
+ <ThingModelProperty v-model="formData.property" is-params />
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelProperty from './ThingModelProperty.vue'
+import { isEmpty } from '@/utils/is'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+import { ThingModelFormRules } from '@/api/iot/thingmodel'
+
+/** 杈撳叆杈撳嚭鍙傛暟閰嶇疆缁勪欢 */
+defineOptions({ name: 'ThingModelInputOutputParam' })
+
+const props = defineProps<{ modelValue: any; direction: string }>()
+const emits = defineEmits(['update:modelValue'])
+const thingModelParams = useVModel(props, 'modelValue', emits) as Ref<any[]>
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const paramFormRef = ref() // 琛ㄥ崟 ref
+const formData = ref<any>({
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ property: {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ }
+})
+
+/** 鎵撳紑 param 琛ㄥ崟 */
+const openParamForm = (val: any) => {
+ dialogVisible.value = true
+ resetForm()
+ if (isEmpty(val)) {
+ return
+ }
+ // 缂栬緫鏃跺洖鏄炬暟鎹�
+ formData.value = {
+ identifier: val.identifier,
+ name: val.name,
+ description: val.description,
+ property: {
+ dataType: val.dataType,
+ dataSpecs: val.dataSpecs,
+ dataSpecsList: val.dataSpecsList
+ }
+ }
+}
+
+/** 鍒犻櫎 param 椤� */
+const deleteParamItem = (index: number) => {
+ thingModelParams.value.splice(index, 1)
+}
+
+/** 娣诲姞鍙傛暟 */
+const submitForm = async () => {
+ // 鍒濆鍖栧弬鏁板垪琛�
+ if (isEmpty(thingModelParams.value)) {
+ thingModelParams.value = []
+ }
+ // 鏍¢獙鍙傛暟
+ await paramFormRef.value.validate()
+ try {
+ // 鏋勫缓鏁版嵁瀵硅薄
+ const data = unref(formData)
+ const item = {
+ identifier: data.identifier,
+ name: data.name,
+ description: data.description,
+ dataType: data.property.dataType,
+ paraOrder: 0, // TODO @puhui999: 鍏堝啓姝婚粯璁ょ湅鐪嬪悗缁�
+ direction: props.direction,
+ dataSpecs:
+ !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
+ ? data.property.dataSpecs
+ : undefined,
+ dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList
+ }
+
+ // 鏂板鎴栦慨鏀瑰悓 identifier 鐨勫弬鏁�
+ const existingIndex = thingModelParams.value.findIndex(
+ (spec) => spec.identifier === data.identifier
+ )
+ if (existingIndex > -1) {
+ thingModelParams.value[existingIndex] = item
+ } else {
+ thingModelParams.value.push(item)
+ }
+ } finally {
+ dialogVisible.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ property: {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ }
+ }
+ paramFormRef.value?.resetFields()
+}
+</script>
+
+<style lang="scss" scoped>
+.param-item {
+ background-color: #e4f2fd;
+}
+</style>
diff --git a/src/views/iot/thingmodel/ThingModelProperty.vue b/src/views/iot/thingmodel/ThingModelProperty.vue
new file mode 100644
index 0000000..490f364
--- /dev/null
+++ b/src/views/iot/thingmodel/ThingModelProperty.vue
@@ -0,0 +1,177 @@
+<!-- 浜у搧鐨勭墿妯″瀷琛ㄥ崟锛坧roperty 椤癸級 -->
+<template>
+ <el-form-item
+ :rules="[{ required: true, message: '璇烽�夋嫨鏁版嵁绫诲瀷', trigger: 'change' }]"
+ label="鏁版嵁绫诲瀷"
+ prop="property.dataType"
+ >
+ <el-select v-model="property.dataType" placeholder="璇烽�夋嫨鏁版嵁绫诲瀷" @change="handleChange">
+ <!-- ARRAY 鍜� STRUCT 绫诲瀷鏁版嵁鐩镐簰宓屽鏃讹紝鏈�澶氭敮鎸侀�掑綊宓屽 2 灞傦紙鐖跺拰瀛愶級 -->
+ <el-option
+ v-for="option in getDataTypeOptions2"
+ :key="option.value"
+ :label="`${option.value}(${option.label})`"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ <!-- 鏁板�煎瀷閰嶇疆 -->
+ <ThingModelNumberDataSpecs
+ v-if="
+ [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.FLOAT
+ ].includes(property.dataType || '')
+ "
+ v-model="property.dataSpecs"
+ />
+ <!-- 鏋氫妇鍨嬮厤缃� -->
+ <ThingModelEnumDataSpecs
+ v-if="property.dataType === IoTDataSpecsDataTypeEnum.ENUM"
+ v-model="property.dataSpecsList"
+ />
+ <!-- 甯冨皵鍨嬮厤缃� -->
+ <el-form-item v-if="property.dataType === IoTDataSpecsDataTypeEnum.BOOL" label="甯冨皵鍊�">
+ <template v-for="(item, index) in property.dataSpecsList" :key="item.value">
+ <div class="flex items-center justify-start w-1/1 mb-5px">
+ <span>{{ item.value }}</span>
+ <span class="mx-2">-</span>
+ <el-form-item
+ :prop="`property.dataSpecsList[${index}].name`"
+ :rules="[
+ { required: true, message: '鏋氫妇鎻忚堪涓嶈兘涓虹┖' },
+ { validator: validateBoolName, trigger: 'blur' }
+ ]"
+ class="flex-1 mb-0"
+ >
+ <el-input
+ v-model="item.name"
+ :placeholder="`濡傦細${item.value === 0 ? '鍏�' : '寮�'}`"
+ class="w-255px!"
+ />
+ </el-form-item>
+ </div>
+ </template>
+ </el-form-item>
+ <!-- 鏂囨湰鍨嬮厤缃� -->
+ <el-form-item
+ v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
+ label="鏁版嵁闀垮害"
+ prop="property.dataSpecs.length"
+ >
+ <el-input v-model="property.dataSpecs.length" class="w-255px!" placeholder="璇疯緭鍏ユ枃鏈瓧鑺傞暱搴�">
+ <template #append>瀛楄妭</template>
+ </el-input>
+ </el-form-item>
+ <!-- 鏃堕棿鍨嬮厤缃� -->
+ <el-form-item
+ v-if="property.dataType === IoTDataSpecsDataTypeEnum.DATE"
+ label="鏃堕棿鏍煎紡"
+ prop="date"
+ >
+ <el-input class="w-255px!" disabled placeholder="String 绫诲瀷鐨� UTC 鏃堕棿鎴筹紙姣锛�" />
+ </el-form-item>
+ <!-- 鏁扮粍鍨嬮厤缃�-->
+ <ThingModelArrayDataSpecs
+ v-if="property.dataType === IoTDataSpecsDataTypeEnum.ARRAY"
+ v-model="property.dataSpecs"
+ />
+ <!-- Struct 鍨嬮厤缃�-->
+ <ThingModelStructDataSpecs
+ v-if="property.dataType === IoTDataSpecsDataTypeEnum.STRUCT"
+ v-model="property.dataSpecsList"
+ />
+ <el-form-item v-if="!isStructDataSpecs && !isParams" label="璇诲啓绫诲瀷" prop="property.accessMode">
+ <el-radio-group v-model="property.accessMode">
+ <el-radio
+ v-for="accessMode in Object.values(IoTThingModelAccessModeEnum)"
+ :key="accessMode.value"
+ :label="accessMode.value"
+ >
+ {{ accessMode.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import {
+ ThingModelArrayDataSpecs,
+ ThingModelEnumDataSpecs,
+ ThingModelNumberDataSpecs,
+ ThingModelStructDataSpecs
+} from './dataSpecs'
+import { ThingModelProperty, validateBoolName } from '@/api/iot/thingmodel'
+import { isEmpty } from '@/utils/is'
+import {
+ getDataTypeOptions,
+ IoTDataSpecsDataTypeEnum,
+ IoTThingModelAccessModeEnum
+} from '@/views/iot/utils/constants'
+
+/** IoT 鐗╂ā鍨嬪睘鎬� */
+defineOptions({ name: 'ThingModelProperty' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean; isParams?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const property = useVModel(props, 'modelValue', emits) as Ref<ThingModelProperty>
+const getDataTypeOptions2 = computed(() => {
+ if (!props.isStructDataSpecs) {
+ return getDataTypeOptions()
+ }
+ const excludedTypes = [IoTDataSpecsDataTypeEnum.STRUCT, IoTDataSpecsDataTypeEnum.ARRAY]
+ return getDataTypeOptions().filter((item: any) => !excludedTypes.includes(item.value))
+}) // 鑾峰緱鏁版嵁绫诲瀷鍒楄〃
+
+/** 灞炴�у�肩殑鏁版嵁绫诲瀷鍒囨崲鏃跺垵濮嬪寲鐩稿叧鏁版嵁 */
+const handleChange = (dataType: any) => {
+ property.value.dataSpecs = {}
+ property.value.dataSpecsList = []
+ // 涓嶆槸鍒楄〃鍨嬫暟鎹墠璁剧疆 dataSpecs.dataType
+ ![
+ IoTDataSpecsDataTypeEnum.ENUM,
+ IoTDataSpecsDataTypeEnum.BOOL,
+ IoTDataSpecsDataTypeEnum.STRUCT
+ ].includes(dataType) && (property.value.dataSpecs.dataType = dataType)
+ switch (dataType) {
+ case IoTDataSpecsDataTypeEnum.ENUM:
+ property.value.dataSpecsList.push({
+ dataType: IoTDataSpecsDataTypeEnum.ENUM,
+ name: '', // 鏋氫妇椤圭殑鍚嶇О
+ value: undefined // 鏋氫妇鍊�
+ })
+ break
+ case IoTDataSpecsDataTypeEnum.BOOL:
+ for (let i = 0; i < 2; i++) {
+ property.value.dataSpecsList.push({
+ dataType: IoTDataSpecsDataTypeEnum.BOOL,
+ name: '', // 甯冨皵鍊肩殑鍚嶇О
+ value: i // 甯冨皵鍊�
+ })
+ }
+ break
+ }
+}
+
+/** 榛樿閫変腑璇诲啓 */
+watch(
+ () => property.value.accessMode,
+ (val: string) => {
+ if (props.isStructDataSpecs || props.isParams) {
+ return
+ }
+ isEmpty(val) && (property.value.accessMode = IoTThingModelAccessModeEnum.READ_WRITE.value)
+ },
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+ .el-form-item {
+ margin-bottom: 0;
+ }
+}
+</style>
diff --git a/src/views/iot/thingmodel/ThingModelService.vue b/src/views/iot/thingmodel/ThingModelService.vue
new file mode 100644
index 0000000..35f7c98
--- /dev/null
+++ b/src/views/iot/thingmodel/ThingModelService.vue
@@ -0,0 +1,64 @@
+<!-- 浜у搧鐨勭墿妯″瀷琛ㄥ崟锛坰ervice 椤癸級 -->
+<template>
+ <el-form-item
+ :rules="[{ required: true, message: '璇烽�夋嫨璋冪敤鏂瑰紡', trigger: 'change' }]"
+ label="璋冪敤鏂瑰紡"
+ prop="service.callType"
+ >
+ <el-radio-group v-model="service.callType">
+ <el-radio
+ v-for="callType in Object.values(IoTThingModelServiceCallTypeEnum)"
+ :key="callType.value"
+ :value="callType.value"
+ >
+ {{ callType.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="杈撳叆鍙傛暟">
+ <ThingModelInputOutputParam
+ v-model="service.inputParams"
+ :direction="IoTThingModelParamDirectionEnum.INPUT"
+ />
+ </el-form-item>
+ <el-form-item label="杈撳嚭鍙傛暟">
+ <ThingModelInputOutputParam
+ v-model="service.outputParams"
+ :direction="IoTThingModelParamDirectionEnum.OUTPUT"
+ />
+ </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import ThingModelInputOutputParam from './ThingModelInputOutputParam.vue'
+import { useVModel } from '@vueuse/core'
+import { ThingModelService } from '@/api/iot/thingmodel'
+import { isEmpty } from '@/utils/is'
+import {
+ IoTThingModelParamDirectionEnum,
+ IoTThingModelServiceCallTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** IoT 鐗╂ā鍨嬫湇鍔� */
+defineOptions({ name: 'ThingModelService' })
+
+const props = defineProps<{ modelValue: any; isStructDataSpecs?: boolean }>()
+const emits = defineEmits(['update:modelValue'])
+const service = useVModel(props, 'modelValue', emits) as Ref<ThingModelService>
+
+/** 榛樿閫変腑锛孉SYNC 寮傛 */
+watch(
+ () => service.value.callType,
+ (val: string) =>
+ isEmpty(val) && (service.value.callType = IoTThingModelServiceCallTypeEnum.ASYNC.value),
+ { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+ .el-form-item {
+ margin-bottom: 0;
+ }
+}
+</style>
diff --git a/src/views/iot/thingmodel/ThingModelTSL.vue b/src/views/iot/thingmodel/ThingModelTSL.vue
new file mode 100644
index 0000000..210dbd4
--- /dev/null
+++ b/src/views/iot/thingmodel/ThingModelTSL.vue
@@ -0,0 +1,50 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <JsonEditor
+ v-model="thingModelTSL"
+ :mode="viewMode === 'editor' ? 'code' : 'view'"
+ height="600px"
+ />
+ <template #footer>
+ <el-radio-group v-model="viewMode" size="small">
+ <el-radio-button label="code">浠g爜瑙嗗浘</el-radio-button>
+ <el-radio-button label="editor">缂栬緫鍣ㄨ鍥�</el-radio-button>
+ </el-radio-group>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import hljs from 'highlight.js' // 瀵煎叆浠g爜楂樹寒鏂囦欢
+import 'highlight.js/styles/github.css' // 瀵煎叆浠g爜楂樹寒鏍峰紡
+import json from 'highlight.js/lib/languages/json'
+import { ThingModelApi } from '@/api/iot/thingmodel'
+import { ProductVO } from '@/api/iot/product/product'
+import { IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
+
+defineOptions({ name: 'ThingModelTSL' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('鐗╂ā鍨� TSL') // 寮圭獥鐨勬爣棰�
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 娉ㄥ叆浜у搧淇℃伅
+const viewMode = ref('code') // 鏌ョ湅妯″紡锛歝ode-浠g爜瑙嗗浘锛宔ditor-缂栬緫鍣ㄨ鍥�
+
+/** 鎵撳紑寮圭獥 */
+const open = () => {
+ dialogVisible.value = true
+}
+defineExpose({ open })
+
+/** 鑾峰彇 TSL */
+const thingModelTSL = ref({})
+const getTsl = async () => {
+ thingModelTSL.value = await ThingModelApi.getThingModelTSLByProductId(product?.value?.id || 0)
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 娉ㄥ唽浠g爜楂樹寒鐨勫悇绉嶈瑷�
+ hljs.registerLanguage('json', json)
+ await getTsl()
+})
+</script>
diff --git a/src/views/iot/thingmodel/components/DataDefinition.vue b/src/views/iot/thingmodel/components/DataDefinition.vue
new file mode 100644
index 0000000..22e6e3d
--- /dev/null
+++ b/src/views/iot/thingmodel/components/DataDefinition.vue
@@ -0,0 +1,73 @@
+<template>
+ <!-- 灞炴�� -->
+ <template v-if="data.type === IoTThingModelTypeEnum.PROPERTY">
+ <!-- 闈炲垪琛ㄥ瀷锛氭暟鍊� -->
+ <div
+ v-if="
+ [
+ IoTDataSpecsDataTypeEnum.INT,
+ IoTDataSpecsDataTypeEnum.DOUBLE,
+ IoTDataSpecsDataTypeEnum.FLOAT
+ ].includes(data.property.dataType)
+ "
+ >
+ 鍙栧�艰寖鍥达細{{ `${data.property.dataSpecs.min}~${data.property.dataSpecs.max}` }}
+ </div>
+ <!-- 闈炲垪琛ㄥ瀷锛氭枃鏈� -->
+ <div v-if="IoTDataSpecsDataTypeEnum.TEXT === data.property.dataType">
+ 鏁版嵁闀垮害锛歿{ data.property.dataSpecs.length }}
+ </div>
+ <!-- 鍒楄〃鍨�: 鏁扮粍銆佺粨鏋勩�佹椂闂达紙鐗规畩锛� -->
+ <div
+ v-if="
+ [
+ IoTDataSpecsDataTypeEnum.ARRAY,
+ IoTDataSpecsDataTypeEnum.STRUCT,
+ IoTDataSpecsDataTypeEnum.DATE
+ ].includes(data.property.dataType)
+ "
+ >
+ -
+ </div>
+ <!-- 鍒楄〃鍨�: 甯冨皵鍊笺�佹灇涓� -->
+ <div
+ v-if="
+ [IoTDataSpecsDataTypeEnum.BOOL, IoTDataSpecsDataTypeEnum.ENUM].includes(
+ data.property.dataType
+ )
+ "
+ >
+ <div>
+ {{ IoTDataSpecsDataTypeEnum.BOOL === data.property.dataType ? '甯冨皵鍊�' : '鏋氫妇鍊�' }}锛�
+ </div>
+ <div v-for="item in data.property.dataSpecsList" :key="item.value">
+ {{ `${item.name}-${item.value}` }}
+ </div>
+ </div>
+ </template>
+ <!-- 鏈嶅姟 -->
+ <div v-if="data.type === IoTThingModelTypeEnum.SERVICE">
+ 璋冪敤鏂瑰紡锛歿{ getThingModelServiceCallTypeLabel(data.service!.callType) }}
+ </div>
+ <!-- 浜嬩欢 -->
+ <div v-if="data.type === IoTThingModelTypeEnum.EVENT">
+ 浜嬩欢绫诲瀷锛歿{ getEventTypeLabel(data.event!.type) }}
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { ThingModelData } from '@/api/iot/thingmodel'
+import {
+ getEventTypeLabel,
+ getThingModelServiceCallTypeLabel,
+ IoTDataSpecsDataTypeEnum,
+ IoTThingModelTypeEnum
+} from '@/views/iot/utils/constants'
+
+/** 鏁版嵁瀹氫箟灞曠ず缁勪欢 */
+defineOptions({ name: 'DataDefinition' })
+
+defineProps<{ data: ThingModelData }>()
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/iot/thingmodel/components/index.ts b/src/views/iot/thingmodel/components/index.ts
new file mode 100644
index 0000000..66c692b
--- /dev/null
+++ b/src/views/iot/thingmodel/components/index.ts
@@ -0,0 +1,3 @@
+import DataDefinition from './DataDefinition.vue'
+
+export { DataDefinition }
diff --git a/src/views/iot/thingmodel/dataSpecs/ThingModelArrayDataSpecs.vue b/src/views/iot/thingmodel/dataSpecs/ThingModelArrayDataSpecs.vue
new file mode 100644
index 0000000..888df2b
--- /dev/null
+++ b/src/views/iot/thingmodel/dataSpecs/ThingModelArrayDataSpecs.vue
@@ -0,0 +1,56 @@
+<!-- dataType锛歛rray 鏁扮粍绫诲瀷 -->
+<template>
+ <el-form-item label="鍏冪礌绫诲瀷" prop="property.dataSpecs.childDataType">
+ <el-radio-group v-model="dataSpecs.childDataType" @change="handleChange">
+ <template v-for="item in getDataTypeOptions()" :key="item.value">
+ <el-radio
+ v-if="
+ !(
+ [
+ IoTDataSpecsDataTypeEnum.ENUM,
+ IoTDataSpecsDataTypeEnum.ARRAY,
+ IoTDataSpecsDataTypeEnum.DATE
+ ] as any[]
+ ).includes(item.value)
+ "
+ :value="item.value"
+ class="w-1/3"
+ >
+ {{ `${item.value}(${item.label})` }}
+ </el-radio>
+ </template>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍏冪礌涓暟" prop="property.dataSpecs.size">
+ <el-input v-model="dataSpecs.size" placeholder="璇疯緭鍏ユ暟缁勪腑鐨勫厓绱犱釜鏁�" />
+ </el-form-item>
+ <!-- Struct 鍨嬮厤缃�-->
+ <ThingModelStructDataSpecs
+ v-if="dataSpecs.childDataType === IoTDataSpecsDataTypeEnum.STRUCT"
+ v-model="dataSpecs.dataSpecsList"
+ />
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
+import { getDataTypeOptions, IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+
+/** 鏁扮粍鍨嬬殑 dataSpecs 閰嶇疆缁勪欢 */
+defineOptions({ name: 'ThingModelArrayDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<any>
+
+/** 鍏冪礌绫诲瀷鏀瑰彉鏃堕棿銆傚綋鍊间负 struct 鏃讹紝瀵� dataSpecs 涓殑 dataSpecsList 杩涜鍒濆鍖� */
+const handleChange = (val: string) => {
+ if (val !== IoTDataSpecsDataTypeEnum.STRUCT) {
+ return
+ }
+
+ dataSpecs.value.dataSpecsList = []
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/iot/thingmodel/dataSpecs/ThingModelEnumDataSpecs.vue b/src/views/iot/thingmodel/dataSpecs/ThingModelEnumDataSpecs.vue
new file mode 100644
index 0000000..71d4778
--- /dev/null
+++ b/src/views/iot/thingmodel/dataSpecs/ThingModelEnumDataSpecs.vue
@@ -0,0 +1,160 @@
+<!-- dataType锛歟num 鏁扮粍绫诲瀷 -->
+<template>
+ <el-form-item
+ :rules="[{ required: true, validator: validateEnumList, trigger: 'change' }]"
+ label="鏋氫妇椤�"
+ >
+ <div class="flex flex-col">
+ <div class="flex items-center">
+ <span class="flex-1"> 鍙傛暟鍊� </span>
+ <span class="flex-1"> 鍙傛暟鎻忚堪 </span>
+ </div>
+ <div
+ v-for="(item, index) in dataSpecsList"
+ :key="index"
+ class="flex items-center justify-between mb-5px"
+ >
+ <el-form-item
+ :prop="`property.dataSpecsList[${index}].value`"
+ :rules="[
+ { required: true, message: '鏋氫妇鍊间笉鑳戒负绌�' },
+ { validator: validateEnumValue, trigger: 'blur' }
+ ]"
+ class="flex-1 mb-0"
+ >
+ <el-input v-model="item.value" placeholder="璇疯緭鍏ユ灇涓惧��,濡�'0'" />
+ </el-form-item>
+ <span class="mx-2">~</span>
+ <el-form-item
+ :prop="`property.dataSpecsList[${index}].name`"
+ :rules="[
+ { required: true, message: '鏋氫妇鎻忚堪涓嶈兘涓虹┖' },
+ { validator: validateEnumName, trigger: 'blur' }
+ ]"
+ class="flex-1 mb-0"
+ >
+ <el-input v-model="item.name" placeholder="瀵硅鏋氫妇椤圭殑鎻忚堪" />
+ </el-form-item>
+ <el-button class="ml-10px" link type="primary" @click="deleteEnum(index)">鍒犻櫎</el-button>
+ </div>
+ <el-button link type="primary" @click="addEnum">+娣诲姞鏋氫妇椤�</el-button>
+ </div>
+ </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+import { DataSpecsEnumOrBoolData } from '@/api/iot/thingmodel'
+
+/** 鏋氫妇鍨嬬殑 dataSpecs 閰嶇疆缁勪欢 */
+defineOptions({ name: 'ThingModelEnumDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<DataSpecsEnumOrBoolData[]>
+const message = useMessage()
+
+/** 娣诲姞鏋氫妇椤� */
+const addEnum = () => {
+ dataSpecsList.value.push({
+ dataType: IoTDataSpecsDataTypeEnum.ENUM,
+ name: '', // 鏋氫妇椤圭殑鍚嶇О
+ value: undefined // 鏋氫妇鍊�
+ })
+}
+
+/** 鍒犻櫎鏋氫妇椤� */
+const deleteEnum = (index: number) => {
+ if (dataSpecsList.value.length === 1) {
+ message.warning('鑷冲皯闇�瑕佷竴涓灇涓鹃」')
+ return
+ }
+ dataSpecsList.value.splice(index, 1)
+}
+
+/** 鏍¢獙鏋氫妇鍊� */
+const validateEnumValue = (_: any, value: any, callback: any) => {
+ if (isEmpty(value)) {
+ callback(new Error('鏋氫妇鍊间笉鑳戒负绌�'))
+ return
+ }
+ if (isNaN(Number(value))) {
+ callback(new Error('鏋氫妇鍊煎繀椤绘槸鏁板瓧'))
+ return
+ }
+ // 妫�鏌ユ灇涓惧�兼槸鍚﹂噸澶�
+ const values = dataSpecsList.value.map((item) => item.value)
+ if (values.filter((v) => v === value).length > 1) {
+ callback(new Error('鏋氫妇鍊间笉鑳介噸澶�'))
+ return
+ }
+ callback()
+}
+
+/** 鏍¢獙鏋氫妇鎻忚堪 */
+const validateEnumName = (_: any, value: string, callback: any) => {
+ if (isEmpty(value)) {
+ callback(new Error('鏋氫妇鎻忚堪涓嶈兘涓虹┖'))
+ return
+ }
+ // 妫�鏌ュ紑澶村瓧绗�
+ if (!/^[\u4e00-\u9fa5a-zA-Z0-9]/.test(value)) {
+ callback(new Error('鏋氫妇鎻忚堪蹇呴』浠ヤ腑鏂囥�佽嫳鏂囧瓧姣嶆垨鏁板瓧寮�澶�'))
+ return
+ }
+ // 妫�鏌ユ暣浣撴牸寮�
+ if (!/^[\u4e00-\u9fa5a-zA-Z0-9][a-zA-Z0-9\u4e00-\u9fa5_-]*$/.test(value)) {
+ callback(new Error('鏋氫妇鎻忚堪鍙兘鍖呭惈涓枃銆佽嫳鏂囧瓧姣嶃�佹暟瀛椼�佷笅鍒掔嚎鍜岀煭鍒掔嚎'))
+ return
+ }
+ // 妫�鏌ラ暱搴︼紙涓�涓腑鏂囩畻涓�涓瓧绗︼級
+ if (value.length > 20) {
+ callback(new Error('鏋氫妇鎻忚堪闀垮害涓嶈兘瓒呰繃20涓瓧绗�'))
+ return
+ }
+ callback()
+}
+
+/** 鏍¢獙鏁翠釜鏋氫妇鍒楄〃 */
+const validateEnumList = (_: any, __: any, callback: any) => {
+ if (isEmpty(dataSpecsList.value)) {
+ callback(new Error('璇疯嚦灏戞坊鍔犱竴涓灇涓鹃」'))
+ return
+ }
+
+ // 妫�鏌ユ槸鍚﹀瓨鍦ㄧ┖鍊�
+ const hasEmptyValue = dataSpecsList.value.some(
+ (item) => isEmpty(item.value) || isEmpty(item.name)
+ )
+ if (hasEmptyValue) {
+ callback(new Error('瀛樺湪鏈~鍐欑殑鏋氫妇鍊兼垨鎻忚堪'))
+ return
+ }
+
+ // 妫�鏌ユ灇涓惧�兼槸鍚﹂兘鏄暟瀛�
+ const hasInvalidNumber = dataSpecsList.value.some((item) => isNaN(Number(item.value)))
+ if (hasInvalidNumber) {
+ callback(new Error('瀛樺湪闈炴暟瀛楃殑鏋氫妇鍊�'))
+ return
+ }
+
+ // 妫�鏌ユ槸鍚︽湁閲嶅鐨勬灇涓惧��
+ const values = dataSpecsList.value.map((item) => item.value)
+ const uniqueValues = new Set(values)
+ if (values.length !== uniqueValues.size) {
+ callback(new Error('瀛樺湪閲嶅鐨勬灇涓惧��'))
+ return
+ }
+ callback()
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+ .el-form-item {
+ margin-bottom: 0;
+ }
+}
+</style>
diff --git a/src/views/iot/thingmodel/dataSpecs/ThingModelNumberDataSpecs.vue b/src/views/iot/thingmodel/dataSpecs/ThingModelNumberDataSpecs.vue
new file mode 100644
index 0000000..d7a47c3
--- /dev/null
+++ b/src/views/iot/thingmodel/dataSpecs/ThingModelNumberDataSpecs.vue
@@ -0,0 +1,139 @@
+<!-- dataType锛歯umber 鏁扮粍绫诲瀷 -->
+<template>
+ <el-form-item label="鍙栧�艰寖鍥�">
+ <div class="flex items-center justify-between">
+ <el-form-item
+ :rules="[
+ { required: true, message: '鏈�灏忓�间笉鑳戒负绌�' },
+ { validator: validateMin, trigger: 'blur' }
+ ]"
+ class="mb-0"
+ prop="property.dataSpecs.min"
+ >
+ <el-input v-model="dataSpecs.min" placeholder="璇疯緭鍏ユ渶灏忓��" />
+ </el-form-item>
+ <span class="mx-2">~</span>
+ <el-form-item
+ :rules="[
+ { required: true, message: '鏈�澶у�间笉鑳戒负绌�' },
+ { validator: validateMax, trigger: 'blur' }
+ ]"
+ class="mb-0"
+ prop="property.dataSpecs.max"
+ >
+ <el-input v-model="dataSpecs.max" placeholder="璇疯緭鍏ユ渶澶у��" />
+ </el-form-item>
+ </div>
+ </el-form-item>
+ <el-form-item
+ :rules="[
+ { required: true, message: '姝ラ暱涓嶈兘涓虹┖' },
+ { validator: validateStep, trigger: 'blur' }
+ ]"
+ label="姝ラ暱"
+ prop="property.dataSpecs.step"
+ >
+ <el-input v-model="dataSpecs.step" placeholder="璇疯緭鍏ユ闀�" />
+ </el-form-item>
+ <el-form-item
+ :rules="[{ required: true, message: '璇烽�夋嫨鍗曚綅' }]"
+ label="鍗曚綅"
+ prop="property.dataSpecs.unit"
+ >
+ <el-select
+ :model-value="dataSpecs.unit ? dataSpecs.unitName + '-' + dataSpecs.unit : ''"
+ filterable
+ placeholder="璇烽�夋嫨鍗曚綅"
+ class="w-1/1"
+ @change="unitChange"
+ >
+ <el-option
+ v-for="(item, index) in getStrDictOptions(DICT_TYPE.IOT_THING_MODEL_UNIT)"
+ :key="index"
+ :label="item.label + '-' + item.value"
+ :value="item.label + '-' + item.value"
+ />
+ </el-select>
+ </el-form-item>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DataSpecsNumberData } from '@/api/iot/thingmodel'
+
+/** 鏁板�煎瀷鐨� dataSpecs 閰嶇疆缁勪欢 */
+defineOptions({ name: 'ThingModelNumberDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecs = useVModel(props, 'modelValue', emits) as Ref<DataSpecsNumberData>
+
+/** 鍗曚綅鍙戠敓鍙樺寲鏃惰Е鍙� */
+const unitChange = (UnitSpecs: string) => {
+ const [unitName, unit] = UnitSpecs.split('-')
+ dataSpecs.value.unitName = unitName
+ dataSpecs.value.unit = unit
+}
+
+/** 鏍¢獙鏈�灏忓�� */
+const validateMin = (_: any, __: any, callback: any) => {
+ const min = Number(dataSpecs.value.min)
+ const max = Number(dataSpecs.value.max)
+ if (isNaN(min)) {
+ callback(new Error('璇疯緭鍏ユ湁鏁堢殑鏁板��'))
+ return
+ }
+ if (max !== undefined && !isNaN(max) && min >= max) {
+ callback(new Error('鏈�灏忓�煎繀椤诲皬浜庢渶澶у��'))
+ return
+ }
+
+ callback()
+}
+
+/** 鏍¢獙鏈�澶у�� */
+const validateMax = (_: any, __: any, callback: any) => {
+ const min = Number(dataSpecs.value.min)
+ const max = Number(dataSpecs.value.max)
+ if (isNaN(max)) {
+ callback(new Error('璇疯緭鍏ユ湁鏁堢殑鏁板��'))
+ return
+ }
+ if (min !== undefined && !isNaN(min) && max <= min) {
+ callback(new Error('鏈�澶у�煎繀椤诲ぇ浜庢渶灏忓��'))
+ return
+ }
+
+ callback()
+}
+
+/** 鏍¢獙姝ラ暱 */
+const validateStep = (_: any, __: any, callback: any) => {
+ const step = Number(dataSpecs.value.step)
+ if (isNaN(step)) {
+ callback(new Error('璇疯緭鍏ユ湁鏁堢殑鏁板��'))
+ return
+ }
+ if (step <= 0) {
+ callback(new Error('姝ラ暱蹇呴』澶т簬0'))
+ return
+ }
+ const min = Number(dataSpecs.value.min)
+ const max = Number(dataSpecs.value.max)
+ if (!isNaN(min) && !isNaN(max) && step > max - min) {
+ callback(new Error('姝ラ暱涓嶈兘澶т簬鏈�澶у�煎拰鏈�灏忓�肩殑宸��'))
+ return
+ }
+
+ callback()
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-form-item) {
+ .el-form-item {
+ margin-bottom: 0;
+ }
+}
+</style>
diff --git a/src/views/iot/thingmodel/dataSpecs/ThingModelStructDataSpecs.vue b/src/views/iot/thingmodel/dataSpecs/ThingModelStructDataSpecs.vue
new file mode 100644
index 0000000..529a798
--- /dev/null
+++ b/src/views/iot/thingmodel/dataSpecs/ThingModelStructDataSpecs.vue
@@ -0,0 +1,167 @@
+<!-- dataType锛歴truct 鏁扮粍绫诲瀷 -->
+<template>
+ <!-- struct 鏁版嵁灞曠ず -->
+ <el-form-item
+ :rules="[{ required: true, validator: validateList, trigger: 'change' }]"
+ label="JSON 瀵硅薄"
+ >
+ <div
+ v-for="(item, index) in dataSpecsList"
+ :key="index"
+ class="w-1/1 struct-item flex justify-between px-10px mb-10px"
+ >
+ <span>鍙傛暟鍚嶇О锛歿{ item.name }}</span>
+ <div class="btn">
+ <el-button link type="primary" @click="openStructForm(item)">缂栬緫</el-button>
+ <el-divider direction="vertical" />
+ <el-button link type="danger" @click="deleteStructItem(index)">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <el-button link type="primary" @click="openStructForm(null)">+鏂板鍙傛暟</el-button>
+ </el-form-item>
+
+ <!-- struct 琛ㄥ崟 -->
+ <Dialog v-model="dialogVisible" :title="dialogTitle" append-to-body>
+ <el-form
+ ref="structFormRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="ThingModelFormRules"
+ label-width="100px"
+ >
+ <el-form-item label="鍙傛暟鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ姛鑳藉悕绉�" />
+ </el-form-item>
+ <el-form-item label="鏍囪瘑绗�" prop="identifier">
+ <el-input v-model="formData.identifier" placeholder="璇疯緭鍏ユ爣璇嗙" />
+ </el-form-item>
+ <!-- 灞炴�ч厤缃� -->
+ <ThingModelProperty v-model="formData.property" is-struct-data-specs />
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { useVModel } from '@vueuse/core'
+import ThingModelProperty from '../ThingModelProperty.vue'
+import { isEmpty } from '@/utils/is'
+import { IoTDataSpecsDataTypeEnum } from '@/views/iot/utils/constants'
+import { ThingModelFormRules } from '@/api/iot/thingmodel'
+
+/** Struct 鍨嬬殑 dataSpecs 閰嶇疆缁勪欢 */
+defineOptions({ name: 'ThingModelStructDataSpecs' })
+
+const props = defineProps<{ modelValue: any }>()
+const emits = defineEmits(['update:modelValue'])
+const dataSpecsList = useVModel(props, 'modelValue', emits) as Ref<any[]>
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('鏂板鍙傛暟') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const structFormRef = ref() // 琛ㄥ崟 ref
+const formData = ref<any>({
+ property: {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ }
+})
+
+/** 鎵撳紑 struct 琛ㄥ崟 */
+const openStructForm = (val: any) => {
+ dialogVisible.value = true
+ resetForm()
+ if (isEmpty(val)) {
+ return
+ }
+ // 缂栬緫鏃跺洖鏄炬暟鎹�
+ formData.value = {
+ identifier: val.identifier,
+ name: val.name,
+ description: val.description,
+ property: {
+ dataType: val.childDataType,
+ dataSpecs: val.dataSpecs,
+ dataSpecsList: val.dataSpecsList
+ }
+ }
+}
+
+/** 鍒犻櫎 struct 椤� */
+const deleteStructItem = (index: number) => {
+ dataSpecsList.value.splice(index, 1)
+}
+
+/** 娣诲姞鍙傛暟 */
+const submitForm = async () => {
+ await structFormRef.value.validate()
+
+ try {
+ const data = unref(formData)
+ // 鏋勫缓鏁版嵁瀵硅薄
+ const item = {
+ identifier: data.identifier,
+ name: data.name,
+ description: data.description,
+ dataType: IoTDataSpecsDataTypeEnum.STRUCT,
+ childDataType: data.property.dataType,
+ dataSpecs:
+ !!data.property.dataSpecs && Object.keys(data.property.dataSpecs).length > 1
+ ? data.property.dataSpecs
+ : undefined,
+ dataSpecsList: isEmpty(data.property.dataSpecsList) ? undefined : data.property.dataSpecsList
+ }
+
+ // 鏂板鎴栦慨鏀瑰悓 identifier 鐨勫弬鏁�
+ const existingIndex = dataSpecsList.value.findIndex(
+ (spec) => spec.identifier === data.identifier
+ )
+ if (existingIndex > -1) {
+ dataSpecsList.value[existingIndex] = item
+ } else {
+ dataSpecsList.value.push(item)
+ }
+ } finally {
+ dialogVisible.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ property: {
+ dataType: IoTDataSpecsDataTypeEnum.INT,
+ dataSpecs: {
+ dataType: IoTDataSpecsDataTypeEnum.INT
+ }
+ }
+ }
+ structFormRef.value?.resetFields()
+}
+
+/** 鏍¢獙 struct 涓嶈兘涓虹┖ */
+const validateList = (_: any, __: any, callback: any) => {
+ if (isEmpty(dataSpecsList.value)) {
+ callback(new Error('struct 涓嶈兘涓虹┖'))
+ return
+ }
+ callback()
+}
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(async () => {
+ await nextTick()
+ // 棰勯槻 dataSpecsList 绌烘寚閽�
+ isEmpty(dataSpecsList.value) && (dataSpecsList.value = [])
+})
+</script>
+
+<style lang="scss" scoped>
+.struct-item {
+ background-color: #e4f2fd;
+}
+</style>
diff --git a/src/views/iot/thingmodel/dataSpecs/index.ts b/src/views/iot/thingmodel/dataSpecs/index.ts
new file mode 100644
index 0000000..30151ae
--- /dev/null
+++ b/src/views/iot/thingmodel/dataSpecs/index.ts
@@ -0,0 +1,11 @@
+import ThingModelEnumDataSpecs from './ThingModelEnumDataSpecs.vue'
+import ThingModelNumberDataSpecs from './ThingModelNumberDataSpecs.vue'
+import ThingModelArrayDataSpecs from './ThingModelArrayDataSpecs.vue'
+import ThingModelStructDataSpecs from './ThingModelStructDataSpecs.vue'
+
+export {
+ ThingModelEnumDataSpecs,
+ ThingModelNumberDataSpecs,
+ ThingModelArrayDataSpecs,
+ ThingModelStructDataSpecs
+}
diff --git a/src/views/iot/thingmodel/index.vue b/src/views/iot/thingmodel/index.vue
new file mode 100644
index 0000000..3502bfa
--- /dev/null
+++ b/src/views/iot/thingmodel/index.vue
@@ -0,0 +1,176 @@
+<!-- 浜у搧鐨勭墿妯″瀷鍒楄〃 -->
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍔熻兘绫诲瀷" prop="name">
+ <el-select
+ v-model="queryParams.type"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鍔熻兘绫诲瀷"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.IOT_THING_MODEL_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ v-hasPermi="[`iot:thing-model:create`]"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 娣诲姞鍔熻兘
+ </el-button>
+ <el-button v-hasPermi="[`iot:thing-model:query`]" plain type="success" @click="openTSL">
+ TSL
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="鍔熻兘绫诲瀷" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.IOT_THING_MODEL_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍔熻兘鍚嶇О" prop="name" />
+ <el-table-column align="center" label="鏍囪瘑绗�" prop="identifier" />
+ <el-table-column align="center" label="鏁版嵁绫诲瀷" prop="identifier">
+ <template #default="{ row }">
+ {{ getDataTypeOptionsLabel(row.property?.dataType) ?? '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column align="left" label="鏁版嵁瀹氫箟" prop="identifier">
+ <template #default="{ row }">
+ <DataDefinition :data="row" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="[`iot:thing-model:update`]"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['iot:thing-model:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </el-tabs>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ThingModelForm ref="formRef" @success="getList" />
+ <ThingModelTSL ref="tslRef" />
+</template>
+<script lang="ts" setup>
+import { ThingModelApi, ThingModelData } from '@/api/iot/thingmodel'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import ThingModelForm from './ThingModelForm.vue'
+import ThingModelTSL from './ThingModelTSL.vue'
+import { ProductVO } from '@/api/iot/product/product'
+import { getDataTypeOptionsLabel, IOT_PROVIDE_KEY } from '@/views/iot/utils/constants'
+import { DataDefinition } from './components'
+
+defineOptions({ name: 'IoTThingModel' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<ThingModelData[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: undefined,
+ productId: -1
+})
+
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const product = inject<Ref<ProductVO>>(IOT_PROVIDE_KEY.PRODUCT) // 娉ㄥ叆浜у搧淇℃伅
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ queryParams.productId = product?.value?.id || -1
+ const data = await ThingModelApi.getThingModelPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 灞曠ず鐗╂ā鍨� TSL */
+const tslRef = ref()
+const openTSL = () => {
+ tslRef.value?.open()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ThingModelApi.deleteThingModel(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/iot/utils/constants.ts b/src/views/iot/utils/constants.ts
new file mode 100644
index 0000000..fe6db18
--- /dev/null
+++ b/src/views/iot/utils/constants.ts
@@ -0,0 +1,543 @@
+import { isEmpty } from '@/utils/is'
+
+/** IoT 渚濊禆娉ㄥ叆 KEY */
+export const IOT_PROVIDE_KEY = {
+ PRODUCT: 'IOT_PRODUCT'
+}
+
+/** IoT 浜у搧鐗╂ā鍨嬬被鍨嬫灇涓剧被 */
+export const IoTThingModelTypeEnum = {
+ PROPERTY: 1, // 灞炴��
+ SERVICE: 2, // 鏈嶅姟
+ EVENT: 3 // 浜嬩欢
+} as const
+
+/** IoT 璁惧娑堟伅鐨勬柟娉曟灇涓� */
+export const IotDeviceMessageMethodEnum = {
+ // ========== 璁惧鐘舵�� ==========
+ STATE_UPDATE: {
+ method: 'thing.state.update',
+ name: '璁惧鐘舵�佸彉鏇�',
+ upstream: true
+ },
+
+ // ========== 璁惧灞炴�� ==========
+ PROPERTY_POST: {
+ method: 'thing.property.post',
+ name: '灞炴�т笂鎶�',
+ upstream: true
+ },
+ PROPERTY_SET: {
+ method: 'thing.property.set',
+ name: '灞炴�ц缃�',
+ upstream: false
+ },
+
+ // ========== 璁惧浜嬩欢 ==========
+ EVENT_POST: {
+ method: 'thing.event.post',
+ name: '浜嬩欢涓婃姤',
+ upstream: true
+ },
+
+ // ========== 鏈嶅姟璋冪敤 ==========
+ SERVICE_INVOKE: {
+ method: 'thing.service.invoke',
+ name: '鏈嶅姟璋冪敤',
+ upstream: false
+ },
+
+ // ========== 璁惧閰嶇疆 ==========
+ CONFIG_PUSH: {
+ method: 'thing.config.push',
+ name: '閰嶇疆鎺ㄩ��',
+ upstream: false
+ }
+}
+
+// IoT 浜у搧鐗╂ā鍨嬫湇鍔¤皟鐢ㄦ柟寮忔灇涓�
+export const IoTThingModelServiceCallTypeEnum = {
+ ASYNC: {
+ label: '寮傛',
+ value: 'async'
+ },
+ SYNC: {
+ label: '鍚屾',
+ value: 'sync'
+ }
+} as const
+export const getThingModelServiceCallTypeLabel = (value: string): string | undefined =>
+ Object.values(IoTThingModelServiceCallTypeEnum).find((type) => type.value === value)?.label
+
+// IoT 浜у搧鐗╂ā鍨嬩簨浠剁被鍨嬫灇涓�
+export const IoTThingModelEventTypeEnum = {
+ INFO: {
+ label: '淇℃伅',
+ value: 'info'
+ },
+ ALERT: {
+ label: '鍛婅',
+ value: 'alert'
+ },
+ ERROR: {
+ label: '鏁呴殰',
+ value: 'error'
+ }
+} as const
+export const getEventTypeLabel = (value: string): string | undefined =>
+ Object.values(IoTThingModelEventTypeEnum).find((type) => type.value === value)?.label
+
+// IoT 浜у搧鐗╂ā鍨嬪弬鏁版槸杈撳叆鍙傛暟杩樻槸杈撳嚭鍙傛暟
+export const IoTThingModelParamDirectionEnum = {
+ INPUT: 'input', // 杈撳叆鍙傛暟
+ OUTPUT: 'output' // 杈撳嚭鍙傛暟
+} as const
+
+// IoT 浜у搧鐗╂ā鍨嬭闂ā寮忔灇涓剧被
+export const IoTThingModelAccessModeEnum = {
+ READ_WRITE: {
+ label: '璇诲啓',
+ value: 'rw'
+ },
+ READ_ONLY: {
+ label: '鍙',
+ value: 'r'
+ },
+ WRITE_ONLY: {
+ label: '鍙啓',
+ value: 'w'
+ }
+} as const
+
+/** 鑾峰彇璁块棶妯″紡鏍囩 */
+export const getAccessModeLabel = (value: string): string => {
+ const mode = Object.values(IoTThingModelAccessModeEnum).find((mode) => mode.value === value)
+ return mode?.label || value
+}
+
+/** 灞炴�у�肩殑鏁版嵁绫诲瀷 */
+export const IoTDataSpecsDataTypeEnum = {
+ INT: 'int',
+ FLOAT: 'float',
+ DOUBLE: 'double',
+ ENUM: 'enum',
+ BOOL: 'bool',
+ TEXT: 'text',
+ DATE: 'date',
+ STRUCT: 'struct',
+ ARRAY: 'array'
+} as const
+
+export const getDataTypeOptions = () => {
+ return [
+ { value: IoTDataSpecsDataTypeEnum.INT, label: '鏁存暟鍨�' },
+ { value: IoTDataSpecsDataTypeEnum.FLOAT, label: '鍗曠簿搴︽诞鐐瑰瀷' },
+ { value: IoTDataSpecsDataTypeEnum.DOUBLE, label: '鍙岀簿搴︽诞鐐瑰瀷' },
+ { value: IoTDataSpecsDataTypeEnum.ENUM, label: '鏋氫妇鍨�' },
+ { value: IoTDataSpecsDataTypeEnum.BOOL, label: '甯冨皵鍨�' },
+ { value: IoTDataSpecsDataTypeEnum.TEXT, label: '鏂囨湰鍨�' },
+ { value: IoTDataSpecsDataTypeEnum.DATE, label: '鏃堕棿鍨�' },
+ { value: IoTDataSpecsDataTypeEnum.STRUCT, label: '缁撴瀯浣�' },
+ { value: IoTDataSpecsDataTypeEnum.ARRAY, label: '鏁扮粍' }
+ ]
+}
+
+/** 鑾峰緱鐗╀綋妯″瀷鏁版嵁绫诲瀷閰嶇疆椤瑰悕绉� */
+export const getDataTypeOptionsLabel = (value: string) => {
+ if (isEmpty(value)) {
+ return value
+ }
+ const dataType = getDataTypeOptions().find((option) => option.value === value)
+ return dataType && `${dataType.value}(${dataType.label})`
+}
+
+/** 鑾峰彇鏁版嵁绫诲瀷鏄剧ず鍚嶇О锛堢敤浜庡睘鎬ч�夋嫨鍣級 */
+export const getDataTypeName = (dataType: string): string => {
+ const typeMap = {
+ [IoTDataSpecsDataTypeEnum.INT]: '鏁存暟',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: '娴偣鏁�',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: '鍙岀簿搴�',
+ [IoTDataSpecsDataTypeEnum.TEXT]: '瀛楃涓�',
+ [IoTDataSpecsDataTypeEnum.BOOL]: '甯冨皵鍊�',
+ [IoTDataSpecsDataTypeEnum.ENUM]: '鏋氫妇',
+ [IoTDataSpecsDataTypeEnum.DATE]: '鏃ユ湡',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: '缁撴瀯浣�',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: '鏁扮粍'
+ }
+ return typeMap[dataType] || dataType
+}
+
+/** 鑾峰彇鏁版嵁绫诲瀷鏍囩绫诲瀷锛堢敤浜� el-tag 鐨� type 灞炴�э級 */
+export const getDataTypeTagType = (
+ dataType: string
+): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
+ const tagMap = {
+ [IoTDataSpecsDataTypeEnum.INT]: 'primary',
+ [IoTDataSpecsDataTypeEnum.FLOAT]: 'success',
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: 'success',
+ [IoTDataSpecsDataTypeEnum.TEXT]: 'info',
+ [IoTDataSpecsDataTypeEnum.BOOL]: 'warning',
+ [IoTDataSpecsDataTypeEnum.ENUM]: 'danger',
+ [IoTDataSpecsDataTypeEnum.DATE]: 'primary',
+ [IoTDataSpecsDataTypeEnum.STRUCT]: 'info',
+ [IoTDataSpecsDataTypeEnum.ARRAY]: 'warning'
+ } as const
+ return tagMap[dataType] || 'info'
+}
+
+/** 鐗╂ā鍨嬬粍鏍囩甯搁噺 */
+export const THING_MODEL_GROUP_LABELS = {
+ PROPERTY: '璁惧灞炴��',
+ EVENT: '璁惧浜嬩欢',
+ SERVICE: '璁惧鏈嶅姟'
+} as const
+
+// IoT OTA 浠诲姟璁惧鑼冨洿鏋氫妇
+export const IoTOtaTaskDeviceScopeEnum = {
+ ALL: {
+ label: '鍏ㄩ儴璁惧',
+ value: 1
+ },
+ SELECT: {
+ label: '鎸囧畾璁惧',
+ value: 2
+ }
+} as const
+
+// IoT OTA 浠诲姟鐘舵�佹灇涓�
+export const IoTOtaTaskStatusEnum = {
+ IN_PROGRESS: {
+ label: '杩涜涓�',
+ value: 10
+ },
+ END: {
+ label: '宸茬粨鏉�',
+ value: 20
+ },
+ CANCELED: {
+ label: '宸插彇娑�',
+ value: 30
+ }
+} as const
+
+// IoT OTA 鍗囩骇璁板綍鐘舵�佹灇涓�
+export const IoTOtaTaskRecordStatusEnum = {
+ PENDING: {
+ label: '寰呮帹閫�',
+ value: 0
+ },
+ PUSHED: {
+ label: '宸叉帹閫�',
+ value: 10
+ },
+ UPGRADING: {
+ label: '鍗囩骇涓�',
+ value: 20
+ },
+ SUCCESS: {
+ label: '鍗囩骇鎴愬姛',
+ value: 30
+ },
+ FAILURE: {
+ label: '鍗囩骇澶辫触',
+ value: 40
+ },
+ CANCELED: {
+ label: '鍗囩骇鍙栨秷',
+ value: 50
+ }
+} as const
+
+// ========== 鍦烘櫙鑱斿姩瑙勫垯鐩稿叧甯搁噺 ==========
+
+/** IoT 鍦烘櫙鑱斿姩瑙﹀彂鍣ㄧ被鍨嬫灇涓� */
+export const IotRuleSceneTriggerTypeEnum = {
+ DEVICE_STATE_UPDATE: 1, // 璁惧涓婁笅绾垮彉鏇�
+ DEVICE_PROPERTY_POST: 2, // 鐗╂ā鍨嬪睘鎬т笂鎶�
+ DEVICE_EVENT_POST: 3, // 璁惧浜嬩欢涓婃姤
+ DEVICE_SERVICE_INVOKE: 4, // 璁惧鏈嶅姟璋冪敤
+ TIMER: 100 // 瀹氭椂瑙﹀彂
+} as const
+
+/** 瑙﹀彂鍣ㄧ被鍨嬮�夐」閰嶇疆 */
+export const triggerTypeOptions = [
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+ label: '璁惧鐘舵�佸彉鏇�'
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+ label: '璁惧灞炴�т笂鎶�'
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+ label: '璁惧浜嬩欢涓婃姤'
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE,
+ label: '璁惧鏈嶅姟璋冪敤'
+ },
+ {
+ value: IotRuleSceneTriggerTypeEnum.TIMER,
+ label: '瀹氭椂瑙﹀彂'
+ }
+]
+
+/** 鍒ゆ柇鏄惁涓鸿澶囪Е鍙戝櫒绫诲瀷 */
+export const isDeviceTrigger = (type: number): boolean => {
+ const deviceTriggerTypes = [
+ IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE,
+ IotRuleSceneTriggerTypeEnum.DEVICE_PROPERTY_POST,
+ IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST,
+ IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE
+ ] as number[]
+ return deviceTriggerTypes.includes(type)
+}
+
+// ========== 鍦烘櫙鑱斿姩瑙勫垯鎵ц鍣ㄧ浉鍏冲父閲� ==========
+
+/** IoT 鍦烘櫙鑱斿姩鎵ц鍣ㄧ被鍨嬫灇涓� */
+export const IotRuleSceneActionTypeEnum = {
+ DEVICE_PROPERTY_SET: 1, // 璁惧灞炴�ц缃�
+ DEVICE_SERVICE_INVOKE: 2, // 璁惧鏈嶅姟璋冪敤
+ ALERT_TRIGGER: 100, // 鍛婅瑙﹀彂
+ ALERT_RECOVER: 101 // 鍛婅鎭㈠
+} as const
+
+/** 鎵ц鍣ㄧ被鍨嬮�夐」閰嶇疆 */
+export const getActionTypeOptions = () => [
+ {
+ value: IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET,
+ label: '璁惧灞炴�ц缃�'
+ },
+ {
+ value: IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE,
+ label: '璁惧鏈嶅姟璋冪敤'
+ },
+ {
+ value: IotRuleSceneActionTypeEnum.ALERT_TRIGGER,
+ label: '瑙﹀彂鍛婅'
+ },
+ {
+ value: IotRuleSceneActionTypeEnum.ALERT_RECOVER,
+ label: '鎭㈠鍛婅'
+ }
+]
+
+/** 鑾峰彇鎵ц鍣ㄧ被鍨嬫爣绛� */
+export const getActionTypeLabel = (type: number): string => {
+ const option = getActionTypeOptions().find((opt) => opt.value === type)
+ return option?.label || '鏈煡绫诲瀷'
+}
+
+/** IoT 鍦烘櫙鑱斿姩瑙﹀彂鏉′欢鍙傛暟鎿嶄綔绗︽灇涓� */
+export const IotRuleSceneTriggerConditionParameterOperatorEnum = {
+ EQUALS: { name: '绛変簬', value: '=' }, // 绛変簬
+ NOT_EQUALS: { name: '涓嶇瓑浜�', value: '!=' }, // 涓嶇瓑浜�
+ GREATER_THAN: { name: '澶т簬', value: '>' }, // 澶т簬
+ GREATER_THAN_OR_EQUALS: { name: '澶т簬绛変簬', value: '>=' }, // 澶т簬绛変簬
+ LESS_THAN: { name: '灏忎簬', value: '<' }, // 灏忎簬
+ LESS_THAN_OR_EQUALS: { name: '灏忎簬绛変簬', value: '<=' }, // 灏忎簬绛変簬
+ IN: { name: '鍦�...涔嬩腑', value: 'in' }, // 鍦�...涔嬩腑
+ NOT_IN: { name: '涓嶅湪...涔嬩腑', value: 'not in' }, // 涓嶅湪...涔嬩腑
+ BETWEEN: { name: '鍦�...涔嬮棿', value: 'between' }, // 鍦�...涔嬮棿
+ NOT_BETWEEN: { name: '涓嶅湪...涔嬮棿', value: 'not between' }, // 涓嶅湪...涔嬮棿
+ LIKE: { name: '瀛楃涓插尮閰�', value: 'like' }, // 瀛楃涓插尮閰�
+ NOT_NULL: { name: '闈炵┖', value: 'not null' } // 闈炵┖
+} as const
+
+/** IoT 鍦烘櫙鑱斿姩瑙﹀彂鏉′欢绫诲瀷鏋氫妇 */
+export const IotRuleSceneTriggerConditionTypeEnum = {
+ DEVICE_STATUS: 1, // 璁惧鐘舵��
+ DEVICE_PROPERTY: 2, // 璁惧灞炴��
+ CURRENT_TIME: 3 // 褰撳墠鏃堕棿
+} as const
+
+/** 鑾峰彇鏉′欢绫诲瀷閫夐」 */
+export const getConditionTypeOptions = () => [
+ {
+ value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS,
+ label: '璁惧鐘舵��'
+ },
+ {
+ value: IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY,
+ label: '璁惧灞炴��'
+ },
+ {
+ value: IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME,
+ label: '褰撳墠鏃堕棿'
+ }
+]
+
+/** 璁惧鐘舵�佹灇涓� - 缁熶竴鐨勮澶囩姸鎬佺鐞� */
+export const IoTDeviceStatusEnum = {
+ // 鍦ㄧ嚎鐘舵��
+ ONLINE: {
+ label: '鍦ㄧ嚎',
+ value: 'online',
+ tagType: 'success'
+ },
+ OFFLINE: {
+ label: '绂荤嚎',
+ value: 'offline',
+ tagType: 'danger'
+ },
+ // 鍚敤鐘舵��
+ ENABLED: {
+ label: '姝e父',
+ value: 0,
+ value2: 'enabled',
+ tagType: 'success'
+ },
+ DISABLED: {
+ label: '绂佺敤',
+ value: 1,
+ value2: 'disabled',
+ tagType: 'danger'
+ },
+ // 婵�娲荤姸鎬�
+ ACTIVATED: {
+ label: '宸叉縺娲�',
+ value2: 'activated',
+ tagType: 'success'
+ },
+ NOT_ACTIVATED: {
+ label: '鏈縺娲�',
+ value2: 'not_activated',
+ tagType: 'info'
+ }
+} as const
+
+/** 璁惧閫夋嫨鍣ㄧ壒娈婇�夐」 */
+export const DEVICE_SELECTOR_OPTIONS = {
+ ALL_DEVICES: {
+ id: 0,
+ deviceName: '鍏ㄩ儴璁惧'
+ }
+} as const
+
+/** IoT 鍦烘櫙鑱斿姩瑙﹀彂鏃堕棿鎿嶄綔绗︽灇涓� */
+export const IotRuleSceneTriggerTimeOperatorEnum = {
+ BEFORE_TIME: { name: '鍦ㄦ椂闂翠箣鍓�', value: 'before_time' }, // 鍦ㄦ椂闂翠箣鍓�
+ AFTER_TIME: { name: '鍦ㄦ椂闂翠箣鍚�', value: 'after_time' }, // 鍦ㄦ椂闂翠箣鍚�
+ BETWEEN_TIME: { name: '鍦ㄦ椂闂翠箣闂�', value: 'between_time' }, // 鍦ㄦ椂闂翠箣闂�
+ AT_TIME: { name: '鍦ㄦ寚瀹氭椂闂�', value: 'at_time' }, // 鍦ㄦ寚瀹氭椂闂�
+ BEFORE_TODAY: { name: '鍦ㄤ粖鏃ヤ箣鍓�', value: 'before_today' }, // 鍦ㄤ粖鏃ヤ箣鍓�
+ AFTER_TODAY: { name: '鍦ㄤ粖鏃ヤ箣鍚�', value: 'after_today' }, // 鍦ㄤ粖鏃ヤ箣鍚�
+ TODAY: { name: '鍦ㄤ粖鏃ヤ箣闂�', value: 'today' } // 鍦ㄤ粖鏃ヤ箣闂�
+} as const
+
+/** 鑾峰彇瑙﹀彂鍣ㄧ被鍨嬫爣绛� */
+export const getTriggerTypeLabel = (type: number): string => {
+ const option = triggerTypeOptions.find((item) => item.value === type)
+ return option?.label || '鏈煡绫诲瀷'
+}
+
+// ========== JSON 鍙傛暟杈撳叆缁勪欢鐩稿叧甯搁噺 ==========
+
+/** JSON 鍙傛暟杈撳叆缁勪欢绫诲瀷鏋氫妇 */
+export const JsonParamsInputTypeEnum = {
+ SERVICE: 'service',
+ EVENT: 'event',
+ PROPERTY: 'property',
+ CUSTOM: 'custom'
+} as const
+
+/** JSON 鍙傛暟杈撳叆缁勪欢绫诲瀷 */
+export type JsonParamsInputType =
+ (typeof JsonParamsInputTypeEnum)[keyof typeof JsonParamsInputTypeEnum]
+
+/** JSON 鍙傛暟杈撳叆缁勪欢鏂囨湰甯搁噺 */
+export const JSON_PARAMS_INPUT_CONSTANTS = {
+ // 鍩虹鏂囨湰
+ PLACEHOLDER: '璇疯緭鍏SON鏍煎紡鐨勫弬鏁�',
+ JSON_FORMAT_CORRECT: 'JSON 鏍煎紡姝g‘',
+ QUICK_FILL_LABEL: '蹇�熷~鍏咃細',
+ EXAMPLE_DATA_BUTTON: '绀轰緥鏁版嵁',
+ CLEAR_BUTTON: '娓呯┖',
+ VIEW_EXAMPLE_TITLE: '鏌ョ湅鍙傛暟绀轰緥',
+ COMPLETE_JSON_FORMAT: '瀹屾暣 JSON 鏍煎紡锛�',
+ REQUIRED_TAG: '蹇呭~',
+
+ // 閿欒淇℃伅
+ PARAMS_MUST_BE_OBJECT: '鍙傛暟蹇呴』鏄竴涓湁鏁堢殑 JSON 瀵硅薄',
+ PARAM_REQUIRED_ERROR: (paramName: string) => `鍙傛暟 ${paramName} 涓哄繀濉」`,
+ JSON_FORMAT_ERROR: (error: string) => `JSON鏍煎紡閿欒: ${error}`,
+ UNKNOWN_ERROR: '鏈煡閿欒',
+
+ // 绫诲瀷鐩稿叧鏍囬
+ TITLES: {
+ SERVICE: (name?: string) => `${name || '鏈嶅姟'} - 杈撳叆鍙傛暟绀轰緥`,
+ EVENT: (name?: string) => `${name || '浜嬩欢'} - 杈撳嚭鍙傛暟绀轰緥`,
+ PROPERTY: '灞炴�ц缃� - 鍙傛暟绀轰緥',
+ CUSTOM: (name?: string) => `${name || '鑷畾涔�'} - 鍙傛暟绀轰緥`,
+ DEFAULT: '鍙傛暟绀轰緥'
+ },
+
+ // 鍙傛暟鏍囩
+ PARAMS_LABELS: {
+ SERVICE: '杈撳叆鍙傛暟',
+ EVENT: '杈撳嚭鍙傛暟',
+ PROPERTY: '灞炴�у弬鏁�',
+ CUSTOM: '鍙傛暟鍒楄〃',
+ DEFAULT: '鍙傛暟'
+ },
+
+ // 绌虹姸鎬佹秷鎭�
+ EMPTY_MESSAGES: {
+ SERVICE: '姝ゆ湇鍔℃棤闇�杈撳叆鍙傛暟',
+ EVENT: '姝や簨浠舵棤杈撳嚭鍙傛暟',
+ PROPERTY: '鏃犲彲璁剧疆鐨勫睘鎬�',
+ CUSTOM: '鏃犲弬鏁伴厤缃�',
+ DEFAULT: '鏃犲弬鏁�'
+ },
+
+ // 鏃犻厤缃秷鎭�
+ NO_CONFIG_MESSAGES: {
+ SERVICE: '璇峰厛閫夋嫨鏈嶅姟',
+ EVENT: '璇峰厛閫夋嫨浜嬩欢',
+ PROPERTY: '璇峰厛閫夋嫨浜у搧',
+ CUSTOM: '璇峰厛杩涜閰嶇疆',
+ DEFAULT: '璇峰厛杩涜閰嶇疆'
+ }
+} as const
+
+/** JSON 鍙傛暟杈撳叆缁勪欢鍥炬爣甯搁噺 */
+export const JSON_PARAMS_INPUT_ICONS = {
+ // 鏍囬鍥炬爣
+ TITLE_ICONS: {
+ SERVICE: 'ep:service',
+ EVENT: 'ep:bell',
+ PROPERTY: 'ep:edit',
+ CUSTOM: 'ep:document',
+ DEFAULT: 'ep:document'
+ },
+
+ // 鍙傛暟鍥炬爣
+ PARAMS_ICONS: {
+ SERVICE: 'ep:edit',
+ EVENT: 'ep:upload',
+ PROPERTY: 'ep:setting',
+ CUSTOM: 'ep:list',
+ DEFAULT: 'ep:edit'
+ },
+
+ // 鐘舵�佸浘鏍�
+ STATUS_ICONS: {
+ ERROR: 'ep:warning',
+ SUCCESS: 'ep:circle-check'
+ }
+} as const
+
+/** JSON 鍙傛暟杈撳叆缁勪欢绀轰緥鍊煎父閲� */
+export const JSON_PARAMS_EXAMPLE_VALUES = {
+ [IoTDataSpecsDataTypeEnum.INT]: { display: '25', value: 25 },
+ [IoTDataSpecsDataTypeEnum.FLOAT]: { display: '25.5', value: 25.5 },
+ [IoTDataSpecsDataTypeEnum.DOUBLE]: { display: '25.5', value: 25.5 },
+ [IoTDataSpecsDataTypeEnum.BOOL]: { display: 'false', value: false },
+ [IoTDataSpecsDataTypeEnum.TEXT]: { display: '"auto"', value: 'auto' },
+ [IoTDataSpecsDataTypeEnum.ENUM]: { display: '"option1"', value: 'option1' },
+ [IoTDataSpecsDataTypeEnum.STRUCT]: { display: '{}', value: {} },
+ [IoTDataSpecsDataTypeEnum.ARRAY]: { display: '[]', value: [] },
+ DEFAULT: { display: '""', value: '' }
+} as const
diff --git a/src/views/mall/home/components/ComparisonCard.vue b/src/views/mall/home/components/ComparisonCard.vue
new file mode 100644
index 0000000..ee1c2f0
--- /dev/null
+++ b/src/views/mall/home/components/ComparisonCard.vue
@@ -0,0 +1,42 @@
+<template>
+ <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+ <div class="flex items-center justify-between text-gray-500">
+ <span>{{ title }}</span>
+ <el-tag>{{ tag }}</el-tag>
+ </div>
+ <div class="flex flex-row items-baseline justify-between">
+ <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" class="text-3xl" />
+ <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
+ {{ Math.abs(toNumber(percent)) }}%
+ <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
+ </span>
+ </div>
+ <el-divider class="mb-1! mt-2!" />
+ <div class="flex flex-row items-center justify-between text-sm">
+ <span class="text-gray-500">鏄ㄦ棩鏁版嵁</span>
+ <span>{{ prefix || '' }}{{ reference }}</span>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+import { calculateRelativeRate } from '@/utils'
+
+/** 浜ゆ槗瀵圭収鍗$墖 */
+defineOptions({ name: 'ComparisonCard' })
+
+const props = defineProps({
+ title: propTypes.string.def('').isRequired,
+ tag: propTypes.string.def(''),
+ prefix: propTypes.string.def(''),
+ value: propTypes.number.def(0).isRequired,
+ reference: propTypes.number.def(0).isRequired,
+ decimals: propTypes.number.def(0)
+})
+
+// 璁$畻鐜瘮
+const percent = computed(() =>
+ calculateRelativeRate(props.value as number, props.reference as number)
+)
+</script>
diff --git a/src/views/mall/home/components/MemberStatisticsCard.vue b/src/views/mall/home/components/MemberStatisticsCard.vue
new file mode 100644
index 0000000..2f9d7ab
--- /dev/null
+++ b/src/views/mall/home/components/MemberStatisticsCard.vue
@@ -0,0 +1,91 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <CardTitle title="鐢ㄦ埛缁熻" />
+ </template>
+ <!-- 鎶樼嚎鍥� -->
+ <Echart :height="300" :options="lineChartOptions" />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import dayjs from 'dayjs'
+import { EChartsOption } from 'echarts'
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import { formatDate } from '@/utils/formatTime'
+import { CardTitle } from '@/components/Card'
+
+/** 浼氬憳鐢ㄦ埛缁熻鍗$墖 */
+defineOptions({ name: 'MemberStatisticsCard' })
+
+const loading = ref(true) // 鍔犺浇涓�
+/** 鎶樼嚎鍥鹃厤缃� */
+const lineChartOptions = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['date', 'count'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [{ name: '娉ㄥ唽閲�', type: 'line', smooth: true, areaStyle: {} }],
+ toolbox: {
+ feature: {
+ // 鏁版嵁鍖哄煙缂╂斁
+ dataZoom: {
+ yAxisIndex: false // Y杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '浼氬憳缁熻' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ },
+ padding: [5, 10]
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ axisTick: {
+ show: false
+ },
+ axisLabel: {
+ formatter: (date: string) => formatDate(date, 'MM-DD')
+ }
+ },
+ yAxis: {
+ axisTick: {
+ show: false
+ }
+ }
+}) as EChartsOption
+
+const getMemberRegisterCountList = async () => {
+ loading.value = true
+ // 鏌ヨ鏈�杩戜竴鏈堟暟鎹�
+ const beginTime = dayjs().subtract(30, 'd').startOf('d')
+ const endTime = dayjs().endOf('d')
+ const list = await MemberStatisticsApi.getMemberRegisterCountList(beginTime, endTime)
+ // 鏇存柊 Echarts 鏁版嵁
+ if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+ lineChartOptions.dataset['source'] = list
+ }
+ loading.value = false
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getMemberRegisterCountList()
+})
+</script>
diff --git a/src/views/mall/home/components/OperationDataCard.vue b/src/views/mall/home/components/OperationDataCard.vue
new file mode 100644
index 0000000..1a1ffb9
--- /dev/null
+++ b/src/views/mall/home/components/OperationDataCard.vue
@@ -0,0 +1,106 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <CardTitle title="杩愯惀鏁版嵁" />
+ </template>
+ <div class="flex flex-row flex-wrap items-center gap-8 p-4">
+ <div
+ v-for="item in data"
+ :key="item.name"
+ class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
+ @click="handleClick(item.routerName)"
+ >
+ <CountTo
+ :decimals="item.decimals"
+ :end-val="item.value"
+ :prefix="item.prefix"
+ class="text-3xl"
+ />
+ <span class="text-center">{{ item.name }}</span>
+ </div>
+ </div>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as TradeStatisticsApi from '@/api/mall/statistics/trade'
+import * as PayStatisticsApi from '@/api/mall/statistics/pay'
+import { CardTitle } from '@/components/Card'
+
+/** 杩愯惀鏁版嵁鍗$墖 */
+defineOptions({ name: 'OperationDataCard' })
+
+const router = useRouter() // 璺敱
+
+/** 鏁版嵁 */
+const data = reactive({
+ orderUndelivered: { name: '寰呭彂璐ц鍗�', value: 9, routerName: 'TradeOrder' },
+ orderAfterSaleApply: { name: '閫�娆句腑璁㈠崟', value: 4, routerName: 'TradeAfterSale' },
+ orderWaitePickUp: { name: '寰呮牳閿�璁㈠崟', value: 0, routerName: 'TradeOrder' },
+ productAlertStock: { name: '搴撳瓨棰勮', value: 0, routerName: 'ProductSpu' },
+ productForSale: { name: '涓婃灦鍟嗗搧', value: 0, routerName: 'ProductSpu' },
+ productInWarehouse: { name: '浠撳簱鍟嗗搧', value: 0, routerName: 'ProductSpu' },
+ withdrawAuditing: { name: '鎻愮幇寰呭鏍�', value: 0, routerName: 'TradeBrokerageWithdraw' },
+ rechargePrice: {
+ name: '璐︽埛鍏呭��',
+ value: 0.0,
+ prefix: '锟�',
+ decimals: 2,
+ routerName: 'PayWalletRecharge'
+ }
+})
+
+/** 鏌ヨ璁㈠崟鏁版嵁 */
+const getOrderData = async () => {
+ const orderCount = await TradeStatisticsApi.getOrderCount()
+ if (orderCount.undelivered != null) {
+ data.orderUndelivered.value = orderCount.undelivered
+ }
+ if (orderCount.afterSaleApply != null) {
+ data.orderAfterSaleApply.value = orderCount.afterSaleApply
+ }
+ if (orderCount.pickUp != null) {
+ data.orderWaitePickUp.value = orderCount.pickUp
+ }
+ if (orderCount.auditingWithdraw != null) {
+ data.withdrawAuditing.value = orderCount.auditingWithdraw
+ }
+}
+
+/** 鏌ヨ鍟嗗搧鏁版嵁 */
+const getProductData = async () => {
+ const productCount = await ProductSpuApi.getTabsCount()
+ data.productForSale.value = productCount['0']
+ data.productInWarehouse.value = productCount['1']
+ data.productAlertStock.value = productCount['3']
+}
+
+/** 鏌ヨ閽卞寘鍏呭�兼暟鎹� */
+const getWalletRechargeData = async () => {
+ const paySummary = await PayStatisticsApi.getWalletRechargePrice()
+ data.rechargePrice.value = paySummary.rechargePrice
+}
+
+/**
+ * 璺宠浆鍒板搴旈〉闈�
+ *
+ * @param routerName 璺敱椤甸潰缁勪欢鐨勫悕绉�
+ */
+const handleClick = (routerName: string) => {
+ router.push({ name: routerName })
+}
+
+/** 婵�娲绘椂 */
+onActivated(() => {
+ getOrderData()
+ getProductData()
+ getWalletRechargeData()
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getOrderData()
+ getProductData()
+ getWalletRechargeData()
+})
+</script>
diff --git a/src/views/mall/home/components/ShortcutCard.vue b/src/views/mall/home/components/ShortcutCard.vue
new file mode 100644
index 0000000..cea9113
--- /dev/null
+++ b/src/views/mall/home/components/ShortcutCard.vue
@@ -0,0 +1,82 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <CardTitle title="蹇嵎鍏ュ彛" />
+ </template>
+ <div class="flex flex-row flex-wrap gap-8 p-4">
+ <div
+ v-for="menu in menuList"
+ :key="menu.name"
+ class="h-20 w-20% flex flex-col cursor-pointer items-center justify-center gap-2"
+ @click="handleMenuClick(menu.routerName)"
+ >
+ <div
+ :class="menu.bgColor"
+ class="h-48px w-48px flex items-center justify-center rounded text-white"
+ >
+ <Icon :icon="menu.icon" class="text-7.5!" />
+ </div>
+ <span>{{ menu.name }}</span>
+ </div>
+ </div>
+ </el-card>
+</template>
+<script lang="ts" setup>
+/** 蹇嵎鍏ュ彛鍗$墖 */
+import { CardTitle } from '@/components/Card'
+
+defineOptions({ name: 'ShortcutCard' })
+
+const router = useRouter() // 璺敱
+
+/** 鑿滃崟鍒楄〃 */
+const menuList = [
+ { name: '鐢ㄦ埛绠$悊', icon: 'ep:user-filled', bgColor: 'bg-red-400', routerName: 'MemberUser' },
+ {
+ name: '鍟嗗搧绠$悊',
+ icon: 'fluent-mdl2:product',
+ bgColor: 'bg-orange-400',
+ routerName: 'ProductSpu'
+ },
+ { name: '璁㈠崟绠$悊', icon: 'ep:list', bgColor: 'bg-yellow-500', routerName: 'TradeOrder' },
+ {
+ name: '鍞悗绠$悊',
+ icon: 'ri:refund-2-line',
+ bgColor: 'bg-green-600',
+ routerName: 'TradeAfterSale'
+ },
+ {
+ name: '鍒嗛攢绠$悊',
+ icon: 'fa-solid:project-diagram',
+ bgColor: 'bg-cyan-500',
+ routerName: 'TradeBrokerageUser'
+ },
+ {
+ name: '浼樻儬鍒�',
+ icon: 'ep:ticket',
+ bgColor: 'bg-blue-500',
+ routerName: 'PromotionCoupon'
+ },
+ {
+ name: '鎷煎洟娲诲姩',
+ icon: 'fa:group',
+ bgColor: 'bg-purple-500',
+ routerName: 'PromotionBargainActivity'
+ },
+ {
+ name: '浣i噾鎻愮幇',
+ icon: 'vaadin:money-withdraw',
+ bgColor: 'bg-rose-500',
+ routerName: 'TradeBrokerageWithdraw'
+ }
+]
+
+/**
+ * 璺宠浆鍒拌彍鍗曞搴旈〉闈�
+ *
+ * @param routerName 璺敱椤甸潰缁勪欢鐨勫悕绉�
+ */
+const handleMenuClick = (routerName: string) => {
+ router.push({ name: routerName })
+}
+</script>
diff --git a/src/views/mall/home/components/TradeTrendCard.vue b/src/views/mall/home/components/TradeTrendCard.vue
new file mode 100644
index 0000000..0bf284e
--- /dev/null
+++ b/src/views/mall/home/components/TradeTrendCard.vue
@@ -0,0 +1,208 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <div class="flex flex-row items-center justify-between">
+ <CardTitle title="浜ゆ槗閲忚秼鍔�" />
+ <!-- 鏌ヨ鏉′欢 -->
+ <div class="flex flex-row items-center gap-2">
+ <el-radio-group v-model="timeRangeType" @change="handleTimeRangeTypeChange">
+ <el-radio-button v-for="[key, value] in timeRange.entries()" :key="key" :value="key">
+ {{ value.name }}
+ </el-radio-button>
+ </el-radio-group>
+ </div>
+ </div>
+ </template>
+ <!-- 鎶樼嚎鍥� -->
+ <Echart :height="300" :options="eChartOptions" />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import dayjs, { Dayjs } from 'dayjs'
+import { EChartsOption } from 'echarts'
+import * as TradeStatisticsApi from '@/api/mall/statistics/trade'
+import { fenToYuan } from '@/utils'
+import { formatDate } from '@/utils/formatTime'
+import { CardTitle } from '@/components/Card'
+
+/** 浜ゆ槗閲忚秼鍔� */
+defineOptions({ name: 'TradeTrendCard' })
+
+enum TimeRangeTypeEnum {
+ DAY30 = 1,
+ WEEK = 7,
+ MONTH = 30,
+ YEAR = 365
+} // 鏃ユ湡绫诲瀷
+const timeRangeType = ref(TimeRangeTypeEnum.DAY30) // 鏃ユ湡蹇嵎閫夋嫨鎸夐挳, 榛樿30澶�
+const loading = ref(true) // 鍔犺浇涓�
+// 鏃堕棿鑼冨洿 Map
+const timeRange = new Map()
+ .set(TimeRangeTypeEnum.DAY30, {
+ name: '30澶�',
+ series: [
+ { name: '璁㈠崟閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '璁㈠崟鏁伴噺', type: 'line', smooth: true, data: [] }
+ ]
+ })
+ .set(TimeRangeTypeEnum.WEEK, {
+ name: '鍛�',
+ series: [
+ { name: '涓婂懆閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '鏈懆閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '涓婂懆鏁伴噺', type: 'line', smooth: true, data: [] },
+ { name: '鏈懆鏁伴噺', type: 'line', smooth: true, data: [] }
+ ]
+ })
+ .set(TimeRangeTypeEnum.MONTH, {
+ name: '鏈�',
+ series: [
+ { name: '涓婃湀閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '鏈湀閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '涓婃湀鏁伴噺', type: 'line', smooth: true, data: [] },
+ { name: '鏈湀鏁伴噺', type: 'line', smooth: true, data: [] }
+ ]
+ })
+ .set(TimeRangeTypeEnum.YEAR, {
+ name: '骞�',
+ series: [
+ { name: '鍘诲勾閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '浠婂勾閲戦', type: 'bar', smooth: true, data: [] },
+ { name: '鍘诲勾鏁伴噺', type: 'line', smooth: true, data: [] },
+ { name: '浠婂勾鏁伴噺', type: 'line', smooth: true, data: [] }
+ ]
+ })
+/** 鍥捐〃閰嶇疆 */
+const eChartOptions = reactive<EChartsOption>({
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ },
+ legend: {
+ top: 50,
+ data: []
+ },
+ series: [],
+ toolbox: {
+ feature: {
+ // 鏁版嵁鍖哄煙缂╂斁
+ dataZoom: {
+ yAxisIndex: false // Y杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '璁㈠崟閲忚秼鍔�' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ },
+ padding: [5, 10]
+ },
+ xAxis: {
+ type: 'category',
+ inverse: true,
+ boundaryGap: false,
+ axisTick: {
+ show: false
+ },
+ data: [],
+ axisLabel: {
+ formatter: (date: string) => {
+ switch (timeRangeType.value) {
+ case TimeRangeTypeEnum.DAY30:
+ return formatDate(date, 'MM-DD')
+ case TimeRangeTypeEnum.WEEK:
+ let weekDay = formatDate(date, 'ddd')
+ if (weekDay == '0') weekDay = '鏃�'
+ return '鍛�' + weekDay
+ case TimeRangeTypeEnum.MONTH:
+ return formatDate(date, 'D')
+ case TimeRangeTypeEnum.YEAR:
+ return formatDate(date, 'M') + '鏈�'
+ default:
+ return date
+ }
+ }
+ }
+ },
+ yAxis: {
+ axisTick: {
+ show: false
+ }
+ }
+}) as EChartsOption
+
+/** 鏃堕棿鑼冨洿绫诲瀷鍗曢�夋寜閽�変腑 */
+const handleTimeRangeTypeChange = async () => {
+ // 璁剧疆鏃堕棿鑼冨洿
+ let beginTime: Dayjs
+ let endTime: Dayjs
+ switch (timeRangeType.value) {
+ case TimeRangeTypeEnum.WEEK:
+ beginTime = dayjs().startOf('week')
+ endTime = dayjs().endOf('week')
+ break
+ case TimeRangeTypeEnum.MONTH:
+ beginTime = dayjs().startOf('month')
+ endTime = dayjs().endOf('month')
+ break
+ case TimeRangeTypeEnum.YEAR:
+ beginTime = dayjs().startOf('year')
+ endTime = dayjs().endOf('year')
+ break
+ case TimeRangeTypeEnum.DAY30:
+ default:
+ beginTime = dayjs().subtract(30, 'day').startOf('d')
+ endTime = dayjs().endOf('d')
+ break
+ }
+ // 鍙戦�佹椂闂磋寖鍥撮�変腑浜嬩欢
+ await getOrderCountTrendComparison(beginTime, endTime)
+}
+
+/** 鏌ヨ璁㈠崟鏁伴噺瓒嬪娍瀵圭収鏁版嵁 */
+const getOrderCountTrendComparison = async (
+ beginTime: dayjs.ConfigType,
+ endTime: dayjs.ConfigType
+) => {
+ loading.value = true
+ // 鏌ヨ鏁版嵁
+ const list = await TradeStatisticsApi.getOrderCountTrendComparison(
+ timeRangeType.value,
+ beginTime,
+ endTime
+ )
+ // 澶勭悊鏁版嵁
+ const dates: string[] = []
+ const series = [...timeRange.get(timeRangeType.value).series]
+ for (let item of list) {
+ dates.push(item.value.date)
+ if (series.length === 2) {
+ series[0].data.push(fenToYuan(item?.value?.orderPayPrice || 0)) // 褰撳墠閲戦
+ series[1].data.push(item?.value?.orderPayCount || 0) // 褰撳墠鏁伴噺
+ } else {
+ series[0].data.push(fenToYuan(item?.reference?.orderPayPrice || 0)) // 瀵圭収閲戦
+ series[1].data.push(fenToYuan(item?.value?.orderPayPrice || 0)) // 褰撳墠閲戦
+ series[2].data.push(item?.reference?.orderPayCount || 0) // 瀵圭収鏁伴噺
+ series[3].data.push(item?.value?.orderPayCount || 0) // 褰撳墠鏁伴噺
+ }
+ }
+ eChartOptions.xAxis!['data'] = dates
+ eChartOptions.series = series
+ // legend 鍦� 4 涓垏鎹㈠埌 2 涓殑鏃跺�欙紝杩樻槸鏄剧ず鎴� 4 涓紝闇�瑕佹墜鍔ㄩ厤缃竴涓�
+ eChartOptions.legend['data'] = series.map((item) => item.name)
+ loading.value = false
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ handleTimeRangeTypeChange()
+})
+</script>
diff --git a/src/views/mall/home/index.vue b/src/views/mall/home/index.vue
new file mode 100644
index 0000000..89baf33
--- /dev/null
+++ b/src/views/mall/home/index.vue
@@ -0,0 +1,113 @@
+<template>
+ <doc-alert title="鍟嗗煄鎵嬪唽锛堝姛鑳藉紑鍚級" url="https://doc.iocoder.cn/mall/build/" />
+
+ <div class="flex flex-col">
+ <!-- 鏁版嵁瀵圭収 -->
+ <el-row :gutter="16" class="row">
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <ComparisonCard
+ tag="浠婃棩"
+ title="閿�鍞"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(orderComparison?.value?.orderPayPrice || 0)"
+ :reference="fenToYuan(orderComparison?.reference?.orderPayPrice || 0)"
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <ComparisonCard
+ tag="浠婃棩"
+ title="鐢ㄦ埛璁块棶閲�"
+ :value="userComparison?.value?.visitUserCount || 0"
+ :reference="userComparison?.reference?.visitUserCount || 0"
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <ComparisonCard
+ tag="浠婃棩"
+ title="璁㈠崟閲�"
+ :value="orderComparison?.value?.orderPayCount || 0"
+ :reference="orderComparison?.reference?.orderPayCount || 0"
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24" :loading="loading">
+ <ComparisonCard
+ tag="浠婃棩"
+ title="鏂板鐢ㄦ埛"
+ :value="userComparison?.value?.registerUserCount || 0"
+ :reference="userComparison?.reference?.registerUserCount || 0"
+ />
+ </el-col>
+ </el-row>
+ <el-row :gutter="16" class="row">
+ <el-col :md="12">
+ <!-- 蹇嵎鍏ュ彛 -->
+ <ShortcutCard />
+ </el-col>
+ <el-col :md="12">
+ <!-- 杩愯惀鏁版嵁 -->
+ <OperationDataCard />
+ </el-col>
+ </el-row>
+ <el-row :gutter="16" class="mb-4">
+ <el-col :md="18" :sm="24">
+ <!-- 浼氬憳姒傝 -->
+ <MemberFunnelCard />
+ </el-col>
+ <el-col :md="6" :sm="24">
+ <!-- 浼氬憳缁堢 -->
+ <MemberTerminalCard />
+ </el-col>
+ </el-row>
+ <!-- 浜ゆ槗閲忚秼鍔� -->
+ <TradeTrendCard class="mb-4" />
+ <!-- 浼氬憳缁熻 -->
+ <MemberStatisticsCard />
+ </div>
+</template>
+<script lang="ts" setup>
+import * as TradeStatisticsApi from '@/api/mall/statistics/trade'
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+import { TradeOrderSummaryRespVO } from '@/api/mall/statistics/trade'
+import { MemberCountRespVO } from '@/api/mall/statistics/member'
+import { fenToYuan } from '@/utils'
+import ComparisonCard from './components/ComparisonCard.vue'
+import MemberStatisticsCard from './components/MemberStatisticsCard.vue'
+import OperationDataCard from './components/OperationDataCard.vue'
+import ShortcutCard from './components/ShortcutCard.vue'
+import TradeTrendCard from './components/TradeTrendCard.vue'
+import MemberTerminalCard from '@/views/mall/statistics/member/components/MemberTerminalCard.vue'
+import MemberFunnelCard from '@/views/mall/statistics/member/components/MemberFunnelCard.vue'
+
+/** 鍟嗗煄棣栭〉 */
+defineOptions({ name: 'MallHome' })
+
+const loading = ref(true) // 鍔犺浇涓�
+const orderComparison = ref<DataComparisonRespVO<TradeOrderSummaryRespVO>>() // 浜ゆ槗瀵圭収鏁版嵁
+const userComparison = ref<DataComparisonRespVO<MemberCountRespVO>>() // 鐢ㄦ埛瀵圭収鏁版嵁
+
+/** 鏌ヨ浜ゆ槗瀵圭収鍗$墖鏁版嵁 */
+const getOrderComparison = async () => {
+ orderComparison.value = await TradeStatisticsApi.getOrderComparison()
+}
+
+/** 鏌ヨ浼氬憳鐢ㄦ埛鏁伴噺瀵圭収鍗$墖鏁版嵁 */
+const getUserCountComparison = async () => {
+ userComparison.value = await MemberStatisticsApi.getUserCountComparison()
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ loading.value = true
+ await Promise.all([getOrderComparison(), getUserCountComparison()])
+ loading.value = false
+})
+</script>
+<style lang="scss" scoped>
+.row {
+ .el-col {
+ margin-bottom: 1rem;
+ }
+}
+</style>
diff --git a/src/views/mall/product/brand/BrandForm.vue b/src/views/mall/product/brand/BrandForm.vue
new file mode 100644
index 0000000..7486dbe
--- /dev/null
+++ b/src/views/mall/product/brand/BrandForm.vue
@@ -0,0 +1,123 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍝佺墝鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ搧鐗屽悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍝佺墝鍥剧墖" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" />
+ </el-form-item>
+ <el-form-item label="鍝佺墝鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="鍝佺墝鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍝佺墝鎻忚堪">
+ <el-input v-model="formData.description" type="textarea" placeholder="璇疯緭鍏ュ搧鐗屾弿杩�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as ProductBrandApi from '@/api/mall/product/brand'
+
+defineOptions({ name: 'ProductBrandForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ picUrl: '',
+ status: CommonStatusEnum.ENABLE,
+ description: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍝佺墝鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ picUrl: [{ required: true, message: '鍝佺墝鍥剧墖涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鍝佺墝鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductBrandApi.getBrand(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as ProductBrandApi.BrandVO
+ if (formType.value === 'create') {
+ await ProductBrandApi.createBrand(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductBrandApi.updateBrand(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ picUrl: '',
+ status: CommonStatusEnum.ENABLE,
+ description: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/product/brand/index.vue b/src/views/mall/product/brand/index.vue
new file mode 100644
index 0000000..3e34b93
--- /dev/null
+++ b/src/views/mall/product/brand/index.vue
@@ -0,0 +1,182 @@
+<template>
+ <doc-alert title="鍟嗗煄鎵嬪唽锛堝姛鑳藉紑鍚級" url="https://doc.iocoder.cn/mall/build/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍝佺墝鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ搧鐗屽悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['product:brand:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
+ <el-table-column label="鍝佺墝鍚嶇О" prop="name" sortable />
+ <el-table-column label="鍝佺墝鍥剧墖" align="center" prop="picUrl">
+ <template #default="scope">
+ <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="鍝佺墝鍥剧墖" class="h-30px" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍝佺墝鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="寮�鍚姸鎬�" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['product:brand:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['product:brand:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BrandForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProductBrandApi from '@/api/mall/product/brand'
+import BrandForm from './BrandForm.vue'
+
+defineOptions({ name: 'ProductBrand' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductBrandApi.getBrandParam(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductBrandApi.deleteBrand(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/product/category/CategoryForm.vue b/src/views/mall/product/category/CategoryForm.vue
new file mode 100644
index 0000000..44240ae
--- /dev/null
+++ b/src/views/mall/product/category/CategoryForm.vue
@@ -0,0 +1,139 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="涓婄骇鍒嗙被" prop="parentId">
+ <el-select v-model="formData.parentId" placeholder="璇烽�夋嫨涓婄骇鍒嗙被">
+ <el-option :key="0" label="椤剁骇鍒嗙被" :value="0" />
+ <el-option
+ v-for="item in categoryList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒嗙被鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ垎绫诲悕绉�" />
+ </el-form-item>
+ <el-form-item label="绉诲姩绔垎绫诲浘" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" />
+ <div style="font-size: 10px" class="pl-10px">鎺ㄨ崘 180x180 鍥剧墖鍒嗚鲸鐜�</div>
+ </el-form-item>
+ <el-form-item label="鍒嗙被鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+
+defineOptions({ name: 'ProductCategory' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ parentId: 0,
+ name: '',
+ picUrl: '',
+ sort: 0,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ parentId: [{ required: true, message: '璇烽�夋嫨涓婄骇鍒嗙被', trigger: 'blur' }],
+ name: [{ required: true, message: '鍒嗙被鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ picUrl: [{ required: true, message: '鍒嗙被鍥剧墖涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鍒嗙被鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const categoryList = ref<any[]>([]) // 鍒嗙被鏍�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ProductCategoryApi.getCategory(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鍒嗙被鏍�
+ categoryList.value = await ProductCategoryApi.getCategoryList({ parentId: 0 })
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as ProductCategoryApi.CategoryVO
+ if (formType.value === 'create') {
+ await ProductCategoryApi.createCategory(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductCategoryApi.updateCategory(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ parentId: 0,
+ name: '',
+ picUrl: '',
+ sort: 0,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/product/category/components/ProductCategorySelect.vue b/src/views/mall/product/category/components/ProductCategorySelect.vue
new file mode 100644
index 0000000..c1810f5
--- /dev/null
+++ b/src/views/mall/product/category/components/ProductCategorySelect.vue
@@ -0,0 +1,51 @@
+<template>
+ <el-tree-select
+ v-model="selectCategoryId"
+ :data="categoryList"
+ :props="defaultProps"
+ :multiple="multiple"
+ :show-checkbox="multiple"
+ class="w-1/1"
+ node-key="id"
+ placeholder="璇烽�夋嫨鍟嗗搧鍒嗙被"
+ />
+</template>
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import { oneOfType } from 'vue-types'
+import { propTypes } from '@/utils/propTypes'
+
+/** 鍟嗗搧鍒嗙被閫夋嫨缁勪欢 */
+defineOptions({ name: 'ProductCategorySelect' })
+
+const props = defineProps({
+ // 閫変腑鐨処D
+ modelValue: oneOfType<number | number[]>([Number, Array<Number>]),
+ // 鏄惁澶氶��
+ multiple: propTypes.bool.def(false),
+ // 涓婄骇鍝佺被鐨勭紪鍙�
+ parentId: propTypes.number.def(undefined)
+})
+
+/** 閫変腑鐨勫垎绫� ID */
+const selectCategoryId = computed({
+ get: () => {
+ return props.modelValue
+ },
+ set: (val: number | number[]) => {
+ emit('update:modelValue', val)
+ }
+})
+
+/** 鍒嗙被閫夋嫨 */
+const emit = defineEmits(['update:modelValue'])
+
+/** 鍒濆鍖� **/
+const categoryList = ref<ProductCategoryApi.CategoryVO[]>([]) // 鍒嗙被鏍�
+onMounted(async () => {
+ // 鑾峰緱鍒嗙被鏍�
+ const data = await ProductCategoryApi.getCategoryList({ parentId: props.parentId })
+ categoryList.value = handleTree(data, 'id', 'parentId')
+})
+</script>
diff --git a/src/views/mall/product/category/index.vue b/src/views/mall/product/category/index.vue
new file mode 100644
index 0000000..a47684b
--- /dev/null
+++ b/src/views/mall/product/category/index.vue
@@ -0,0 +1,167 @@
+<template>
+ <doc-alert title="銆愬晢鍝併�戝晢鍝佸垎绫�" url="https://doc.iocoder.cn/mall/product-category/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙被鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ垎绫诲悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['product:category:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
+ <el-table-column label="鍚嶇О" min-width="240" prop="name" sortable />
+ <el-table-column label="鍒嗙被鍥炬爣" align="center" min-width="80" prop="picUrl">
+ <template #default="scope">
+ <img :src="scope.row.picUrl" alt="绉诲姩绔垎绫诲浘" class="h-36px" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掑簭" align="center" min-width="150" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" min-width="150" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="180">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['product:category:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ v-if="scope.row.parentId > 0"
+ @click="handleViewSpu(scope.row.id)"
+ v-hasPermi="['product:spu:query']"
+ >
+ 鏌ョ湅鍟嗗搧
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['product:category:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CategoryForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { handleTree } from '@/utils/tree'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import CategoryForm from './CategoryForm.vue'
+
+defineOptions({ name: 'ProductCategory' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ name: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductCategoryApi.getCategoryList(queryParams)
+ list.value = handleTree(data, 'id', 'parentId')
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductCategoryApi.deleteCategory(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鏌ョ湅鍟嗗搧鎿嶄綔 */
+const router = useRouter() // 璺敱
+const handleViewSpu = (id: number) => {
+ router.push({
+ name: 'ProductSpu',
+ query: { categoryId: id }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/product/comment/CommentForm.vue b/src/views/mall/product/comment/CommentForm.vue
new file mode 100644
index 0000000..b8d700c
--- /dev/null
+++ b/src/views/mall/product/comment/CommentForm.vue
@@ -0,0 +1,167 @@
+<template>
+ <Dialog v-model="dialogVisible" title="娣诲姞铏氭嫙璇勮">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="鍟嗗搧" prop="spuId">
+ <SpuShowcase v-model="formData.spuId" :limit="1" />
+ </el-form-item>
+ <el-form-item v-if="formData.spuId" label="鍟嗗搧瑙勬牸" prop="skuId">
+ <div class="h-60px w-60px" @click="handleSelectSku">
+ <div v-if="skuData && skuData.picUrl">
+ <el-image :src="skuData.picUrl" />
+ </div>
+ <div v-else class="select-box">
+ <Icon icon="ep:plus" />
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛澶村儚" prop="userAvatar">
+ <UploadImg v-model="formData.userAvatar" height="60px" width="60px" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="userNickname">
+ <el-input v-model="formData.userNickname" placeholder="璇疯緭鍏ョ敤鎴峰悕绉�" />
+ </el-form-item>
+ <el-form-item label="璇勮鍐呭" prop="content">
+ <el-input v-model="formData.content" type="textarea" />
+ </el-form-item>
+ <el-form-item label="鎻忚堪鏄熺骇" prop="descriptionScores">
+ <el-rate v-model="formData.descriptionScores" />
+ </el-form-item>
+ <el-form-item label="鏈嶅姟鏄熺骇" prop="benefitScores">
+ <el-rate v-model="formData.benefitScores" />
+ </el-form-item>
+ <el-form-item label="璇勮鍥剧墖" prop="picUrls">
+ <UploadImgs v-model="formData.picUrls" :limit="9" height="60px" width="60px" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SkuTableSelect ref="skuTableSelectRef" :spu-id="formData.spuId" @change="handleSkuChange" />
+</template>
+<script lang="ts" setup>
+import * as CommentApi from '@/api/mall/product/comment'
+import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import SkuTableSelect from '@/views/mall/product/spu/components/SkuTableSelect.vue'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ userId: undefined,
+ userNickname: undefined,
+ userAvatar: undefined,
+ spuId: 0,
+ skuId: undefined,
+ descriptionScores: 5,
+ benefitScores: 5,
+ content: undefined,
+ picUrls: []
+})
+const formRules = reactive({
+ spuId: [{ required: true, message: '鍟嗗搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ skuId: [{ required: true, message: '瑙勬牸涓嶈兘涓虹┖', trigger: 'blur' }],
+ userAvatar: [{ required: true, message: '鐢ㄦ埛澶村儚涓嶈兘涓虹┖', trigger: 'blur' }],
+ userNickname: [{ required: true, message: '鐢ㄦ埛鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ content: [{ required: true, message: '璇勮鍐呭涓嶈兘涓虹┖', trigger: 'blur' }],
+ descriptionScores: [{ required: true, message: '鎻忚堪鏄熺骇涓嶈兘涓虹┖', trigger: 'blur' }],
+ benefitScores: [{ required: true, message: '鏈嶅姟鏄熺骇涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const skuData = ref({
+ id: -1,
+ name: '',
+ picUrl: ''
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await CommentApi.getComment(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ if (formType.value === 'create') {
+ await CommentApi.createComment(unref(formData.value) as any)
+ message.success(t('common.createSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ userId: undefined,
+ userNickname: undefined,
+ userAvatar: undefined,
+ spuId: 0,
+ skuId: undefined,
+ descriptionScores: 5,
+ benefitScores: 5,
+ content: undefined,
+ picUrls: []
+ }
+ formRef.value?.resetFields()
+}
+
+/** SKU 琛ㄦ牸閫夋嫨 */
+const skuTableSelectRef = ref()
+const handleSelectSku = () => {
+ skuTableSelectRef.value.open()
+}
+const handleSkuChange = (sku: ProductSpuApi.Sku) => {
+ skuData.value = sku
+ formData.value.skuId = sku.id
+}
+</script>
+<style>
+.select-box {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+ align-items: center;
+ justify-content: center;
+}
+</style>
diff --git a/src/views/mall/product/comment/ReplyForm.vue b/src/views/mall/product/comment/ReplyForm.vue
new file mode 100644
index 0000000..4c8bd4d
--- /dev/null
+++ b/src/views/mall/product/comment/ReplyForm.vue
@@ -0,0 +1,76 @@
+<template>
+ <Dialog title="鍥炲" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍥炲鍐呭" prop="replyContent">
+ <el-input type="textarea" v-model="formData.replyContent" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitReplyForm" type="primary" :disabled="formLoading">纭� 瀹� </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as CommentApi from '@/api/mall/product/comment'
+import { ElInput } from 'element-plus'
+
+defineOptions({ name: 'ProductComment' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ replyContent: undefined
+})
+const formRules = reactive({
+ replyContent: [{ required: true, message: '鍥炲鍐呭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id?: number) => {
+ resetForm()
+ formData.value.id = id
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitReplyForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ const valid = await formRef?.value?.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await CommentApi.replyComment(formData.value)
+ message.success(t('common.createSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ replyContent: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/product/comment/index.vue b/src/views/mall/product/comment/index.vue
new file mode 100644
index 0000000..c598974
--- /dev/null
+++ b/src/views/mall/product/comment/index.vue
@@ -0,0 +1,259 @@
+<template>
+ <doc-alert title="銆愬晢鍝併�戝晢鍝佽瘎浠�" url="https://doc.iocoder.cn/mall/product-comment/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍥炲鐘舵��" prop="replyStatus">
+ <el-select v-model="queryParams.replyStatus" class="!w-240px">
+ <el-option label="宸插洖澶�" :value="true" />
+ <el-option label="鏈洖澶�" :value="false" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="spuName">
+ <el-input
+ v-model="queryParams.spuName"
+ placeholder="璇疯緭鍏ュ晢鍝佸悕绉�"
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="userNickname">
+ <el-input
+ v-model="queryParams.userNickname"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="璁㈠崟缂栧彿" prop="orderId">
+ <el-input
+ v-model="queryParams.orderId"
+ placeholder="璇疯緭鍏ヨ鍗曠紪鍙�"
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="璇勮鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['product:comment:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />
+ 娣诲姞铏氭嫙璇勮
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="false">
+ <el-table-column label="璇勮缂栧彿" align="center" prop="id" min-width="80" />
+ <el-table-column label="鍟嗗搧淇℃伅" align="center" min-width="400">
+ <template #default="scope">
+ <div class="row flex items-center gap-x-4px">
+ <el-image
+ v-if="scope.row.skuPicUrl"
+ :src="scope.row.skuPicUrl"
+ :preview-src-list="[scope.row.skuPicUrl]"
+ class="h-40px w-40px shrink-0"
+ preview-teleported
+ />
+ <div>{{ scope.row.spuName }}</div>
+ <el-tag
+ v-for="property in scope.row.skuProperties"
+ :key="property.propertyId"
+ class="mr-10px"
+ >
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛鍚嶇О" align="center" prop="userNickname" width="100" />
+ <el-table-column label="鍟嗗搧璇勫垎" align="center" prop="descriptionScores" width="90" />
+ <el-table-column label="鏈嶅姟璇勫垎" align="center" prop="benefitScores" width="90" />
+ <el-table-column label="璇勮鍐呭" align="center" prop="content" min-width="210">
+ <template #default="scope">
+ <p>{{ scope.row.content }}</p>
+ <div class="flex justify-center gap-x-4px">
+ <el-image
+ v-for="(picUrl, index) in scope.row.picUrls"
+ :key="index"
+ :src="picUrl"
+ :preview-src-list="scope.row.picUrls"
+ :initial-index="index"
+ class="h-40px w-40px"
+ preview-teleported
+ />
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍥炲鍐呭"
+ align="center"
+ prop="replyContent"
+ min-width="250"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="璇勮鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鏄惁灞曠ず" align="center" width="80px">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.visible"
+ :active-value="true"
+ :inactive-value="false"
+ v-hasPermi="['product:comment:update']"
+ @change="handleVisibleChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" min-width="60px" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="handleReply(scope.row.id)"
+ v-hasPermi="['product:comment:update']"
+ >
+ 鍥炲
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CommentForm ref="formRef" @success="getList" />
+ <!-- 鍥炲琛ㄥ崟寮圭獥 -->
+ <ReplyForm ref="replyFormRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as CommentApi from '@/api/mall/product/comment'
+import CommentForm from './CommentForm.vue'
+import ReplyForm from './ReplyForm.vue'
+
+defineOptions({ name: 'ProductComment' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ replyStatus: null,
+ spuName: null,
+ userNickname: null,
+ orderId: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CommentApi.getCommentPage(queryParams)
+ // visible 濡傛灉涓� null锛屼細瀵艰嚧鍒锋柊鐨勬椂鍊欒Е鍙� e-switch 鐨� change 浜嬩欢
+ data.list.forEach((item) => {
+ if (!item.visible) {
+ item.visible = false
+ }
+ })
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍥炲鎸夐挳鎿嶄綔 **/
+const replyFormRef = ref()
+const handleReply = (id: number) => {
+ replyFormRef.value.open(id)
+}
+
+/** 鏄剧ず/闅愯棌 **/
+const handleVisibleChange = async (row: CommentApi.CommentVO) => {
+ if (loading.value) {
+ return
+ }
+ let changedValue = row.visible
+ try {
+ await message.confirm(changedValue ? '鏄惁鏄剧ず璇勮锛�' : '鏄惁闅愯棌璇勮锛�')
+ await CommentApi.updateCommentVisible({ id: row.id, visible: changedValue })
+ await getList()
+ } catch {
+ row.visible = !changedValue
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/product/property/PropertyForm.vue b/src/views/mall/product/property/PropertyForm.vue
new file mode 100644
index 0000000..db90beb
--- /dev/null
+++ b/src/views/mall/product/property/PropertyForm.vue
@@ -0,0 +1,96 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as PropertyApi from '@/api/mall/product/property'
+
+defineOptions({ name: 'ProductPropertyForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await PropertyApi.getProperty(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as PropertyApi.PropertyVO
+ if (formType.value === 'create') {
+ await PropertyApi.createProperty(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PropertyApi.updateProperty(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/product/property/index.vue b/src/views/mall/product/property/index.vue
new file mode 100644
index 0000000..ac3401a
--- /dev/null
+++ b/src/views/mall/product/property/index.vue
@@ -0,0 +1,177 @@
+<template>
+ <doc-alert title="銆愬晢鍝併�戝晢鍝佸睘鎬�" url="https://doc.iocoder.cn/mall/product-property/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['product:property:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="缂栧彿" min-width="60" prop="id" />
+ <el-table-column align="center" label="灞炴�у悕绉�" prop="name" min-width="150" />
+ <el-table-column :show-overflow-tooltip="true" align="center" label="澶囨敞" prop="remark" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['product:property:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button link type="primary" @click="goValueList(scope.row.id)">灞炴�у��</el-button>
+ <el-button
+ v-hasPermi="['product:property:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <PropertyForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as PropertyApi from '@/api/mall/product/property'
+import PropertyForm from './PropertyForm.vue'
+
+const { push } = useRouter()
+
+defineOptions({ name: 'ProductProperty' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PropertyApi.getPropertyPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PropertyApi.deleteProperty(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 璺宠浆鍟嗗搧灞炴�у垪琛� */
+const goValueList = (id: number) => {
+ push({ name: 'ProductPropertyValue', params: { propertyId: id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/product/property/value/ValueForm.vue b/src/views/mall/product/property/value/ValueForm.vue
new file mode 100644
index 0000000..9e72c09
--- /dev/null
+++ b/src/views/mall/product/property/value/ValueForm.vue
@@ -0,0 +1,105 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="灞炴�х紪鍙�" prop="category">
+ <el-input v-model="formData.propertyId" disabled="" />
+ </el-form-item>
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as PropertyApi from '@/api/mall/product/property'
+
+defineOptions({ name: 'ProductPropertyValueForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ propertyId: undefined,
+ name: '',
+ remark: ''
+})
+const formRules = reactive({
+ propertyId: [{ required: true, message: '灞炴�т笉鑳戒负绌�', trigger: 'blur' }],
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, propertyId: number, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ formData.value.propertyId = propertyId
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await PropertyApi.getPropertyValue(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as PropertyApi.PropertyValueVO
+ if (formType.value === 'create') {
+ await PropertyApi.createPropertyValue(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PropertyApi.updatePropertyValue(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ propertyId: undefined,
+ name: '',
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/product/property/value/index.vue b/src/views/mall/product/property/value/index.vue
new file mode 100644
index 0000000..f0a6ef4
--- /dev/null
+++ b/src/views/mall/product/property/value/index.vue
@@ -0,0 +1,163 @@
+<template>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="灞炴�ч」" prop="propertyId">
+ <el-select v-model="queryParams.propertyId" class="!w-240px" disabled>
+ <el-option
+ v-for="item in propertyOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ plain
+ type="primary"
+ @click="openForm('create')"
+ v-hasPermi="['product:property:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" min-width="60" prop="id" />
+ <el-table-column label="灞炴�у�煎悕绉�" align="center" min-width="150" prop="name" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['product:property:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['product:property:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ValueForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as PropertyApi from '@/api/mall/product/property'
+import ValueForm from './ValueForm.vue'
+
+defineOptions({ name: 'ProductPropertyValue' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const { params } = useRoute() // 鏌ヨ鍙傛暟
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ propertyId: params.propertyId,
+ name: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const propertyOptions = ref([]) // 灞炴�ч」鐨勫垪琛�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PropertyApi.getPropertyValuePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, queryParams.propertyId, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PropertyApi.deletePropertyValue(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 灞炴�ч」涓嬫媺妗嗘暟鎹�
+ propertyOptions.value.push(await PropertyApi.getProperty(queryParams.propertyId))
+})
+</script>
diff --git a/src/views/mall/product/spu/components/SkuList.vue b/src/views/mall/product/spu/components/SkuList.vue
new file mode 100644
index 0000000..c41da4b
--- /dev/null
+++ b/src/views/mall/product/spu/components/SkuList.vue
@@ -0,0 +1,583 @@
+<template>
+ <!-- 鎯呭喌涓�锛氭坊鍔�/淇敼 -->
+ <el-table
+ v-if="!isDetail && !isActivityComponent"
+ :data="isBatch ? skuList : formData!.skus!"
+ border
+ class="tabNumWidth"
+ max-height="500"
+ size="small"
+ >
+ <el-table-column align="center" label="鍥剧墖" min-width="65">
+ <template #default="{ row }">
+ <UploadImg v-model="row.picUrl" height="50px" width="50px" />
+ </template>
+ </el-table-column>
+ <template v-if="formData!.specType && !isBatch">
+ <!-- 鏍规嵁鍟嗗搧灞炴�у姩鎬佹坊鍔� -->
+ <el-table-column
+ v-for="(item, index) in tableHeaders"
+ :key="index"
+ :label="item.label"
+ align="center"
+ min-width="120"
+ >
+ <template #default="{ row }">
+ <span style="font-weight: bold; color: #40aaff">
+ {{ row.properties?.[index]?.valueName }}
+ </span>
+ </template>
+ </el-table-column>
+ </template>
+ <el-table-column align="center" label="鍟嗗搧鏉$爜" min-width="168">
+ <template #default="{ row }">
+ <el-input v-model="row.barCode" class="w-100%" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�鍞环" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.price"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="甯傚満浠�" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.marketPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愭湰浠�" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.costPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="搴撳瓨" min-width="168">
+ <template #default="{ row }">
+ <el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閲嶉噺(kg)" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.weight"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浣撶Н(m^3)" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.volume"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ <template v-if="formData!.subCommissionType">
+ <el-table-column align="center" label="涓�绾ц繑浣�(鍏�)" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.firstBrokeragePrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浜岀骇杩斾剑(鍏�)" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.secondBrokeragePrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ controls-position="right"
+ />
+ </template>
+ </el-table-column>
+ </template>
+ <el-table-column v-if="formData?.specType" align="center" fixed="right" label="鎿嶄綔" width="80">
+ <template #default="{ row }">
+ <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
+ 鎵归噺娣诲姞
+ </el-button>
+ <el-button v-else link size="small" type="primary" @click="deleteSku(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鎯呭喌浜岋細璇︽儏 -->
+ <el-table
+ v-if="isDetail"
+ ref="activitySkuListRef"
+ :data="formData!.skus!"
+ border
+ max-height="500"
+ size="small"
+ style="width: 99%"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column v-if="isComponent" type="selection" width="45" />
+ <el-table-column align="center" label="鍥剧墖" min-width="80">
+ <template #default="{ row }">
+ <el-image
+ v-if="row.picUrl"
+ :src="row.picUrl"
+ class="h-50px w-50px"
+ @click="imagePreview(row.picUrl)"
+ />
+ </template>
+ </el-table-column>
+ <template v-if="formData!.specType && !isBatch">
+ <!-- 鏍规嵁鍟嗗搧灞炴�у姩鎬佹坊鍔� -->
+ <el-table-column
+ v-for="(item, index) in tableHeaders"
+ :key="index"
+ :label="item.label"
+ align="center"
+ min-width="80"
+ >
+ <template #default="{ row }">
+ <span style="font-weight: bold; color: #40aaff">
+ {{ row.properties?.[index]?.valueName }}
+ </span>
+ </template>
+ </el-table-column>
+ </template>
+ <el-table-column align="center" label="鍟嗗搧鏉$爜" min-width="100">
+ <template #default="{ row }">
+ {{ row.barCode }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�鍞环(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ row.price }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="甯傚満浠�(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ row.marketPrice }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愭湰浠�(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ row.costPrice }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="搴撳瓨" min-width="80">
+ <template #default="{ row }">
+ {{ row.stock }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閲嶉噺(kg)" min-width="80">
+ <template #default="{ row }">
+ {{ row.weight }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浣撶Н(m^3)" min-width="80">
+ <template #default="{ row }">
+ {{ row.volume }}
+ </template>
+ </el-table-column>
+ <template v-if="formData!.subCommissionType">
+ <el-table-column align="center" label="涓�绾ц繑浣�(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ row.firstBrokeragePrice }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浜岀骇杩斾剑(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ row.secondBrokeragePrice }}
+ </template>
+ </el-table-column>
+ </template>
+ </el-table>
+
+ <!-- 鎯呭喌涓夛細浣滀负娲诲姩缁勪欢 -->
+ <el-table
+ v-if="isActivityComponent"
+ :data="formData!.skus!"
+ border
+ max-height="500"
+ size="small"
+ style="width: 99%"
+ >
+ <el-table-column v-if="isComponent" type="selection" width="45" />
+ <el-table-column align="center" label="鍥剧墖" min-width="80">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-60px w-60px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <template v-if="formData!.specType">
+ <!-- 鏍规嵁鍟嗗搧灞炴�у姩鎬佹坊鍔� -->
+ <el-table-column
+ v-for="(item, index) in tableHeaders"
+ :key="index"
+ :label="item.label"
+ align="center"
+ min-width="80"
+ >
+ <template #default="{ row }">
+ <span style="font-weight: bold; color: #40aaff">
+ {{ row.properties?.[index]?.valueName }}
+ </span>
+ </template>
+ </el-table-column>
+ </template>
+ <el-table-column align="center" label="鍟嗗搧鏉$爜" min-width="100">
+ <template #default="{ row }">
+ {{ row.barCode }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�鍞环(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ formatToFraction(row.price) }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="甯傚満浠�(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ formatToFraction(row.marketPrice) }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎴愭湰浠�(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ formatToFraction(row.costPrice) }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="搴撳瓨" min-width="80">
+ <template #default="{ row }">
+ {{ row.stock }}
+ </template>
+ </el-table-column>
+ <!-- 鏂逛究鎵╁睍姣忎釜娲诲姩閰嶇疆鐨勫睘鎬т笉涓�鏍� -->
+ <slot name="extension"></slot>
+ </el-table>
+</template>
+<script lang="ts" setup>
+import { PropType, Ref } from 'vue'
+import { copyValueToTarget, formatToFraction } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { UploadImg } from '@/components/UploadFile'
+import type { Property, Sku, Spu } from '@/api/mall/product/spu'
+import { createImageViewer } from '@/components/ImageViewer'
+import { RuleConfig } from '@/views/mall/product/spu/components/index'
+import { PropertyAndValues } from './index'
+import { ElTable } from 'element-plus'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'SkuList' })
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => {}
+ },
+ propertyList: {
+ type: Array as PropType<PropertyAndValues[]>,
+ default: () => []
+ },
+ ruleConfig: {
+ type: Array as PropType<RuleConfig[]>,
+ default: () => []
+ },
+ isBatch: propTypes.bool.def(false), // 鏄惁浣滀负鎵归噺鎿嶄綔缁勪欢
+ isDetail: propTypes.bool.def(false), // 鏄惁浣滀负 sku 璇︽儏缁勪欢
+ isComponent: propTypes.bool.def(false), // 鏄惁浣滀负 sku 閫夋嫨缁勪欢
+ isActivityComponent: propTypes.bool.def(false) // 鏄惁浣滀负 sku 娲诲姩閰嶇疆缁勪欢
+})
+const formData: Ref<Spu | undefined> = ref<Spu>() // 琛ㄥ崟鏁版嵁
+const skuList = ref<Sku[]>([
+ {
+ price: 0, // 鍟嗗搧浠锋牸
+ marketPrice: 0, // 甯傚満浠�
+ costPrice: 0, // 鎴愭湰浠�
+ barCode: '', // 鍟嗗搧鏉$爜
+ picUrl: '', // 鍥剧墖鍦板潃
+ stock: 0, // 搴撳瓨
+ weight: 0, // 鍟嗗搧閲嶉噺
+ volume: 0, // 鍟嗗搧浣撶Н
+ firstBrokeragePrice: 0, // 涓�绾у垎閿�鐨勪剑閲�
+ secondBrokeragePrice: 0 // 浜岀骇鍒嗛攢鐨勪剑閲�
+ }
+]) // 鎵归噺娣诲姞鏃剁殑涓存椂鏁版嵁
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ zIndex: 9999999,
+ urlList: [imgUrl]
+ })
+}
+
+/** 鎵归噺娣诲姞 */
+const batchAdd = () => {
+ validateProperty()
+ formData.value!.skus!.forEach((item) => {
+ copyValueToTarget(item, skuList.value[0])
+ })
+}
+/** 鏍¢獙鍟嗗搧灞炴�у睘鎬у�� */
+const validateProperty = () => {
+ // 鏍¢獙鍟嗗搧灞炴�у睘鎬у�兼槸鍚︿负绌猴紝鏈変竴涓负绌洪兘涓嶇粰杩�
+ const warningInfo = '瀛樺湪灞炴�у睘鎬у�间负绌猴紝璇峰厛妫�鏌ュ畬鍠勫睘鎬у�煎悗閲嶈瘯锛侊紒锛�'
+ for (const item of props.propertyList) {
+ if (!item.values || isEmpty(item.values)) {
+ message.warning(warningInfo)
+ throw new Error(warningInfo)
+ }
+ }
+}
+/** 鍒犻櫎 sku */
+const deleteSku = (row) => {
+ const index = formData.value!.skus!.findIndex(
+ // 鐩存帴鎶婂垪琛ㄨ浆鎴愬瓧绗︿覆姣旇緝
+ (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+ )
+ formData.value!.skus!.splice(index, 1)
+}
+const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 澶氬睘鎬ц〃澶�
+/**
+ * 淇濆瓨鏃讹紝姣忎釜鍟嗗搧瑙勬牸鐨勮〃鍗曡鏍¢獙涓嬨�備緥濡傝锛岄攢鍞噾棰濇渶浣庢槸 0.01 杩欑銆�
+ */
+const validateSku = () => {
+ validateProperty()
+ let warningInfo = '璇锋鏌ュ晢鍝佸悇琛岀浉鍏冲睘鎬ч厤缃紝'
+ let validate = true // 榛樿閫氳繃
+ for (const sku of formData.value!.skus!) {
+ // 浣滀负娲诲姩缁勪欢鐨勬牎楠�
+ for (const rule of props?.ruleConfig) {
+ const arg = getValue(sku, rule.name)
+ if (!rule.rule(arg)) {
+ validate = false // 鍙鏈変竴涓笉閫氳繃鍒欑洿鎺ヤ笉閫氳繃
+ warningInfo += rule.message
+ break
+ }
+ }
+ // 鍙鏈変竴涓笉閫氳繃鍒欑粨鏉熷悗缁殑鏍¢獙
+ if (!validate) {
+ message.warning(warningInfo)
+ throw new Error(warningInfo)
+ }
+ }
+}
+const getValue = (obj, arg) => {
+ const keys = arg.split('.')
+ let value = obj
+ for (const key of keys) {
+ if (value && typeof value === 'object' && key in value) {
+ value = value[key]
+ } else {
+ value = undefined
+ break
+ }
+ }
+ return value
+}
+
+const emit = defineEmits<{
+ (e: 'selectionChange', value: Sku[]): void
+}>()
+/**
+ * 閫夋嫨鏃惰Е鍙�
+ * @param Sku 浼犻�掕繃鏉ョ殑閫変腑鐨� sku 鏄竴涓暟缁�
+ */
+const handleSelectionChange = (val: Sku[]) => {
+ emit('selectionChange', val)
+}
+
+/**
+ * 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 skuList
+ */
+watch(
+ () => props.propFormData,
+ (data) => {
+ if (!data) return
+ formData.value = data
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+/** 鐢熸垚琛ㄦ暟鎹� */
+const generateTableData = (propertyList: any[]) => {
+ // 鏋勫缓鏁版嵁缁撴瀯
+ const propertyValues = propertyList.map((item) =>
+ item.values.map((v: any) => ({
+ propertyId: item.id,
+ propertyName: item.name,
+ valueId: v.id,
+ valueName: v.name
+ }))
+ )
+ const buildSkuList = build(propertyValues)
+ // 濡傛灉鍥炴樉鐨� sku 灞炴�у拰娣诲姞鐨勫睘鎬т笉涓�鑷村垯閲嶇疆 skus 鍒楄〃
+ if (!validateData(propertyList)) {
+ // 濡傛灉涓嶄竴鑷村垯閲嶇疆琛ㄦ暟鎹紝榛樿娣诲姞鏂扮殑灞炴�ч噸鏂扮敓鎴� sku 鍒楄〃
+ formData.value!.skus = []
+ }
+ for (const item of buildSkuList) {
+ const row = {
+ properties: Array.isArray(item) ? item : [item], // 濡傛灉鍙湁涓�涓睘鎬х殑璇濊繑鍥炵殑鏄竴涓� property 瀵硅薄
+ price: 0,
+ marketPrice: 0,
+ costPrice: 0,
+ barCode: '',
+ picUrl: '',
+ stock: 0,
+ weight: 0,
+ volume: 0,
+ firstBrokeragePrice: 0,
+ secondBrokeragePrice: 0
+ }
+ // 濡傛灉瀛樺湪灞炴�х浉鍚岀殑 sku 鍒欎笉鍋氬鐞�
+ const index = formData.value!.skus!.findIndex(
+ (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+ )
+ if (index !== -1) {
+ continue
+ }
+ formData.value!.skus!.push(row)
+ }
+}
+
+/**
+ * 鐢熸垚 skus 鍓嶇疆鏍¢獙
+ */
+const validateData = (propertyList: any[]) => {
+ const skuPropertyIds: number[] = []
+ formData.value!.skus!.forEach((sku) =>
+ sku.properties
+ ?.map((property) => property.propertyId)
+ ?.forEach((propertyId) => {
+ if (skuPropertyIds.indexOf(propertyId!) === -1) {
+ skuPropertyIds.push(propertyId!)
+ }
+ })
+ )
+ const propertyIds = propertyList.map((item) => item.id)
+ return skuPropertyIds.length === propertyIds.length
+}
+
+/** 鏋勫缓鎵�鏈夋帓鍒楃粍鍚� */
+const build = (propertyValuesList: Property[][]) => {
+ if (propertyValuesList.length === 0) {
+ return []
+ } else if (propertyValuesList.length === 1) {
+ return propertyValuesList[0]
+ } else {
+ const result: Property[][] = []
+ const rest = build(propertyValuesList.slice(1))
+ for (let i = 0; i < propertyValuesList[0].length; i++) {
+ for (let j = 0; j < rest.length; j++) {
+ // 绗竴娆′笉鏄暟缁勭粨鏋勶紝鍚庨潰鐨勯兘鏄暟缁勭粨鏋�
+ if (Array.isArray(rest[j])) {
+ result.push([propertyValuesList[0][i], ...rest[j]])
+ } else {
+ result.push([propertyValuesList[0][i], rest[j]])
+ }
+ }
+ }
+ return result
+ }
+}
+
+/** 鐩戝惉灞炴�у垪琛紝鐢熸垚鐩稿叧鍙傛暟鍜岃〃澶� */
+watch(
+ () => props.propertyList,
+ (propertyList: PropertyAndValues[]) => {
+ // 濡傛灉涓嶆槸澶氳鏍煎垯缁撴潫
+ if (!formData.value!.specType) {
+ return
+ }
+ // 濡傛灉褰撳墠缁勪欢浣滀负鎵归噺娣诲姞鏁版嵁浣跨敤锛屽垯閲嶇疆琛ㄦ暟鎹�
+ if (props.isBatch) {
+ skuList.value = [
+ {
+ price: 0,
+ marketPrice: 0,
+ costPrice: 0,
+ barCode: '',
+ picUrl: '',
+ stock: 0,
+ weight: 0,
+ volume: 0,
+ firstBrokeragePrice: 0,
+ secondBrokeragePrice: 0
+ }
+ ]
+ }
+
+ // 鍒ゆ柇浠g悊瀵硅薄鏄惁涓虹┖
+ if (JSON.stringify(propertyList) === '[]') {
+ return
+ }
+ // 閲嶇疆琛ㄥご
+ tableHeaders.value = []
+ // 鐢熸垚琛ㄥご
+ propertyList.forEach((item, index) => {
+ // name鍔犲睘鎬ч」index鍖哄垎灞炴�у��
+ tableHeaders.value.push({ prop: `name${index}`, label: item.name })
+ })
+ // 濡傛灉鍥炴樉鐨� sku 灞炴�у拰娣诲姞鐨勫睘鎬т竴鑷村垯涓嶅鐞�
+ if (validateData(propertyList)) {
+ return
+ }
+ // 娣诲姞鏂板睘鎬ф病鏈夊睘鎬у�间篃涓嶅仛澶勭悊
+ if (propertyList.some((item) => !item.values || isEmpty(item.values))) {
+ return
+ }
+ // 鐢熸垚 table 鏁版嵁锛屽嵆 sku 鍒楄〃
+ generateTableData(propertyList)
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+const activitySkuListRef = ref<InstanceType<typeof ElTable>>()
+
+const getSkuTableRef = () => {
+ return activitySkuListRef.value
+}
+// 鏆撮湶鍑虹敓鎴� sku 鏂规硶锛岀粰娣诲姞灞炴�ф垚鍔熸椂璋冪敤
+defineExpose({ generateTableData, validateSku, getSkuTableRef })
+</script>
+<style>
+// 閬垮厤婊氬姩鏉¢伄鎸℃渶鍚庝竴琛屾暟鎹�
+/*noinspection CssUnusedSymbol*/
+.el-table.tabNumWidth .el-scrollbar {
+ padding-bottom: 10px;
+}
+</style>
diff --git a/src/views/mall/product/spu/components/SkuTableSelect.vue b/src/views/mall/product/spu/components/SkuTableSelect.vue
new file mode 100644
index 0000000..307208c
--- /dev/null
+++ b/src/views/mall/product/spu/components/SkuTableSelect.vue
@@ -0,0 +1,95 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" title="閫夋嫨瑙勬牸" width="700">
+ <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+ <el-table-column label="#" width="55">
+ <template #default="{ row }">
+ <el-radio :value="row.id" v-model="selectedSkuId" @change="handleSelected(row)"
+ >
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍥剧墖" min-width="80">
+ <template #default="{ row }">
+ <el-image
+ :src="row.picUrl"
+ class="h-30px w-30px"
+ :preview-src-list="[row.picUrl]"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙勬牸" align="center" min-width="80">
+ <template #default="{ row }">
+ {{ row.properties?.map((p) => p.valueName)?.join(' ') }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�鍞环(鍏�)" min-width="80">
+ <template #default="{ row }">
+ {{ fenToYuan(row.price) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { propTypes } from '@/utils/propTypes'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'SkuTableSelect' })
+
+const props = defineProps({
+ spuId: propTypes.number.def(null)
+})
+
+const message = useMessage() // 娑堟伅寮圭獥
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+
+const selectedSkuId = ref() // 閫変腑鐨勫晢鍝� spuId
+
+/** 閫変腑鏃惰Е鍙� */
+const handleSelected = (row: ProductSpuApi.Sku) => {
+ emits('change', row)
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ selectedSkuId.value = undefined
+}
+
+// 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢
+const emits = defineEmits<{
+ (e: 'change', spu: ProductSpuApi.Sku): void
+}>()
+
+/** 鎵撳紑寮圭獥 */
+const open = () => {
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getSpuDetail = async () => {
+ loading.value = true
+ try {
+ const spu = await ProductSpuApi.getSpu(props.spuId)
+ list.value = spu.skus
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {})
+watch(
+ () => props.spuId,
+ () => {
+ if (!props.spuId) {
+ return
+ }
+ getSpuDetail()
+ }
+)
+</script>
diff --git a/src/views/mall/product/spu/components/SpuShowcase.vue b/src/views/mall/product/spu/components/SpuShowcase.vue
new file mode 100644
index 0000000..b8527e0
--- /dev/null
+++ b/src/views/mall/product/spu/components/SpuShowcase.vue
@@ -0,0 +1,144 @@
+<template>
+ <div class="flex flex-wrap items-center gap-8px">
+ <div v-for="(spu, index) in productSpus" :key="spu.id" class="select-box spu-pic">
+ <el-tooltip :content="spu.name">
+ <div class="relative h-full w-full">
+ <el-image :src="spu.picUrl" class="h-full w-full" />
+ <Icon
+ v-show="!disabled"
+ class="del-icon"
+ icon="ep:circle-close-filled"
+ @click="handleRemoveSpu(index)"
+ />
+ </div>
+ </el-tooltip>
+ </div>
+ <el-tooltip content="閫夋嫨鍟嗗搧" v-if="canAdd">
+ <div class="select-box" @click="openSpuTableSelect">
+ <Icon icon="ep:plus" />
+ </div>
+ </el-tooltip>
+ </div>
+ <!-- 鍟嗗搧閫夋嫨瀵硅瘽妗嗭紙琛ㄦ牸褰㈠紡锛� -->
+ <SpuTableSelect ref="spuTableSelectRef" :multiple="limit != 1" @change="handleSpuSelected" />
+</template>
+<script lang="ts" setup>
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue'
+import { propTypes } from '@/utils/propTypes'
+import { oneOfType } from 'vue-types'
+import { isArray } from '@/utils/is'
+
+// 鍟嗗搧姗辩獥锛屼竴鑸敤浜庝笌鍟嗗搧寤虹珛鍏崇郴鏃朵娇鐢�
+// 鎻愪緵鍔熻兘锛氬睍绀哄晢鍝佸垪琛ㄣ�佹坊鍔犲晢鍝併�佺Щ闄ゅ晢鍝�
+defineOptions({ name: 'SpuShowcase' })
+
+const props = defineProps({
+ modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
+ // 闄愬埗鏁伴噺锛氶粯璁や笉闄愬埗
+ limit: propTypes.number.def(Number.MAX_VALUE),
+ disabled: propTypes.bool.def(false)
+})
+
+// 璁$畻鏄惁鍙互娣诲姞
+const canAdd = computed(() => {
+ // 鎯呭喌涓�锛氱鐢ㄦ椂涓嶅彲浠ユ坊鍔�
+ if (props.disabled) return false
+ // 鎯呭喌浜岋細鏈寚瀹氶檺鍒舵暟閲忔椂锛屽彲浠ユ坊鍔�
+ if (!props.limit) return true
+ // 鎯呭喌涓夛細妫�鏌ュ凡娣诲姞鏁伴噺鏄惁灏忎簬闄愬埗鏁伴噺
+ return productSpus.value.length < props.limit
+})
+
+// 鍟嗗搧鍒楄〃
+const productSpus = ref<ProductSpuApi.Spu[]>([])
+
+watch(
+ () => props.modelValue,
+ async () => {
+ const ids = isArray(props.modelValue)
+ ? // 鎯呭喌涓�锛氬閫�
+ props.modelValue
+ : // 鎯呭喌浜岋細鍗曢��
+ props.modelValue
+ ? [props.modelValue]
+ : []
+ // 涓嶉渶瑕佽繑鏄�
+ if (ids.length === 0) {
+ productSpus.value = []
+ return
+ }
+ // 鍙湁鍟嗗搧鍙戠敓鍙樺寲涔嬪悗锛屾墠鍘绘煡璇㈠晢鍝�
+ if (productSpus.value.length === 0 || productSpus.value.some((spu) => !ids.includes(spu.id!))) {
+ productSpus.value = await ProductSpuApi.getSpuDetailList(ids)
+ }
+ },
+ { immediate: true }
+)
+
+/** 鍟嗗搧琛ㄦ牸閫夋嫨瀵硅瘽妗� */
+const spuTableSelectRef = ref()
+// 鎵撳紑瀵硅瘽妗�
+const openSpuTableSelect = () => {
+ spuTableSelectRef.value.open(productSpus.value)
+}
+
+/**
+ * 閫夋嫨鍟嗗搧鍚庤Е鍙�
+ *
+ * @param spus 閫変腑鐨勫晢鍝佸垪琛�
+ */
+const handleSpuSelected = (spus: ProductSpuApi.Spu | ProductSpuApi.Spu[]) => {
+ productSpus.value = isArray(spus) ? spus : [spus]
+ emitSpuChange()
+}
+
+/**
+ * 鍒犻櫎鍟嗗搧
+ *
+ * @param index 鍟嗗搧绱㈠紩
+ */
+const handleRemoveSpu = (index: number) => {
+ productSpus.value.splice(index, 1)
+ emitSpuChange()
+}
+const emit = defineEmits(['update:modelValue', 'change'])
+const emitSpuChange = () => {
+ if (props.limit === 1) {
+ const spu = productSpus.value.length > 0 ? productSpus.value[0] : null
+ emit('update:modelValue', spu?.id || 0)
+ emit('change', spu)
+ } else {
+ emit(
+ 'update:modelValue',
+ productSpus.value.map((spu) => spu.id)
+ )
+ emit('change', productSpus.value)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.select-box {
+ display: flex;
+ width: 60px;
+ height: 60px;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+ align-items: center;
+ justify-content: center;
+}
+
+.spu-pic {
+ position: relative;
+}
+
+.del-icon {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ z-index: 1;
+ width: 20px !important;
+ height: 20px !important;
+}
+</style>
diff --git a/src/views/mall/product/spu/components/SpuTableSelect.vue b/src/views/mall/product/spu/components/SpuTableSelect.vue
new file mode 100644
index 0000000..4775e11
--- /dev/null
+++ b/src/views/mall/product/spu/components/SpuTableSelect.vue
@@ -0,0 +1,303 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" title="閫夋嫨鍟嗗搧" width="70%">
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ晢鍝佸悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍒嗙被" prop="categoryId">
+ <el-tree-select
+ v-model="queryParams.categoryId"
+ :data="categoryTreeList"
+ :props="defaultProps"
+ check-strictly
+ class="!w-240px"
+ node-key="id"
+ placeholder="璇烽�夋嫨鍟嗗搧鍒嗙被"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+ <!-- 1. 澶氶�夋ā寮忥紙涓嶈兘浣跨敤type="selection"锛孍lement浼氬拷鐣eader鎻掓Ы锛� -->
+ <el-table-column width="55" v-if="multiple">
+ <template #header>
+ <el-checkbox
+ v-model="isCheckAll"
+ :indeterminate="isIndeterminate"
+ @change="handleCheckAll"
+ />
+ </template>
+ <template #default="{ row }">
+ <el-checkbox
+ v-model="checkedStatus[row.id]"
+ @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+ />
+ </template>
+ </el-table-column>
+ <!-- 2. 鍗曢�夋ā寮� -->
+ <el-table-column label="#" width="55" v-else>
+ <template #default="{ row }">
+ <el-radio :value="row.id" v-model="selectedSpuId" @change="handleSingleSelected(row)">
+ <!-- 绌烘牸涓嶈兘鐪佺暐锛屾槸涓轰簡璁╁崟閫夋涓嶆樉绀簂abel锛屽鏋滀笉鎸囧畾label涓嶄細鏈夐�変腑鐨勬晥鏋� -->
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column key="id" align="center" label="鍟嗗搧缂栧彿" prop="id" min-width="60" />
+ <el-table-column label="鍟嗗搧鍥�" min-width="80">
+ <template #default="{ row }">
+ <el-image
+ :src="row.picUrl"
+ class="h-30px w-30px"
+ :preview-src-list="[row.picUrl]"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍚嶇О" min-width="200" prop="name" />
+ <el-table-column label="鍟嗗搧鍒嗙被" min-width="100" prop="categoryId">
+ <template #default="{ row }">
+ <span>{{ categoryList?.find((c) => c.id === row.categoryId)?.name }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer v-if="multiple">
+ <el-button type="primary" @click="handleEmitChange">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { propTypes } from '@/utils/propTypes'
+import { CHANGE_EVENT } from 'element-plus'
+
+type Spu = Required<ProductSpuApi.Spu>
+
+/**
+ * 鍟嗗搧琛ㄦ牸閫夋嫨瀵硅瘽妗�
+ * 1. 鍗曢�夋ā寮忥細
+ * 1.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫崟閫夋鏃讹紝缁撴潫閫夋嫨锛屽苟鍏抽棴瀵硅瘽妗�
+ * 1.2 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ * 2. 澶氶�夋ā寮忥細
+ * 2.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫閫夋鏃讹紝璁板綍閫変腑鐨勫晢鍝�
+ * 2.2 鍒囨崲鍒嗛〉鏃讹紝淇濇寔鍟嗗搧鐨勯�変腑鐨勭姸鎬�
+ * 2.3 鐐瑰嚮鍙充笅瑙掔殑纭畾鎸夐挳鏃讹紝缁撴潫閫夋嫨锛屽叧闂璇濇
+ * 2.4 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ */
+defineOptions({ name: 'SpuTableSelect' })
+
+defineProps({
+ // 澶氶�夋ā寮�
+ multiple: propTypes.bool.def(false)
+})
+
+// 鍒楄〃鐨勬�婚〉鏁�
+const total = ref(0)
+// 鍒楄〃鐨勬暟鎹�
+const list = ref<Spu[]>([])
+// 鍒楄〃鐨勫姞杞戒腑
+const loading = ref(false)
+// 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogVisible = ref(false)
+// 鏌ヨ鍙傛暟
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ // 榛樿鑾峰彇涓婃灦鐨勫晢鍝�
+ tabType: 0,
+ name: '',
+ categoryId: null,
+ createTime: []
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = (spuList?: Spu[]) => {
+ // 閲嶇疆
+ checkedSpus.value = []
+ checkedStatus.value = {}
+ isCheckAll.value = false
+ isIndeterminate.value = false
+
+ // 澶勭悊宸查�変腑
+ if (spuList && spuList.length > 0) {
+ checkedSpus.value = [...spuList]
+ checkedStatus.value = Object.fromEntries(spuList.map((spu) => [spu.id, true]))
+ }
+
+ dialogVisible.value = true
+ resetQuery()
+}
+// 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+defineExpose({ open })
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductSpuApi.getSpuPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ // checkbox缁戝畾undefined浼氭湁闂锛岄渶瑕佺粰涓�涓猙ool鍊�
+ list.value.forEach(
+ (spu) => (checkedStatus.value[spu.id] = checkedStatus.value[spu.id] || false)
+ )
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ calculateIsCheckAll()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.value = {
+ pageNo: 1,
+ pageSize: 10,
+ // 榛樿鑾峰彇涓婃灦鐨勫晢鍝�
+ tabType: 0,
+ name: '',
+ categoryId: null,
+ createTime: []
+ }
+ getList()
+}
+
+// 鏄惁鍏ㄩ��
+const isCheckAll = ref(false)
+// 鍏ㄩ�夋鏄惁澶勪簬涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+const isIndeterminate = ref(false)
+// 閫変腑鐨勫晢鍝�
+const checkedSpus = ref<Spu[]>([])
+// 閫変腑鐘舵�侊細key涓哄晢鍝両D锛寁alue涓烘槸鍚﹂�変腑
+const checkedStatus = ref<Record<string, boolean>>({})
+
+// 閫変腑鐨勫晢鍝� spuId
+const selectedSpuId = ref()
+/** 鍗曢�変腑鏃惰Е鍙� */
+const handleSingleSelected = (spu: Spu) => {
+ emits(CHANGE_EVENT, spu)
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ // 璁颁綇涓婃閫夋嫨鐨処D
+ selectedSpuId.value = spu.id
+}
+
+/** 澶氶�夊畬鎴� */
+const handleEmitChange = () => {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ emits(CHANGE_EVENT, [...checkedSpus.value])
+}
+
+/** 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢 */
+const emits = defineEmits<{
+ change: [spu: Spu | Spu[] | any]
+}>()
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckAll = (checked: boolean) => {
+ isCheckAll.value = checked
+ isIndeterminate.value = false
+
+ list.value.forEach((spu) => handleCheckOne(checked, spu, false))
+}
+
+/**
+ * 閫変腑涓�琛�
+ * @param checked 鏄惁閫変腑
+ * @param spu 鍟嗗搧
+ * @param isCalcCheckAll 鏄惁璁$畻鍏ㄩ��
+ */
+const handleCheckOne = (checked: boolean, spu: Spu, isCalcCheckAll: boolean) => {
+ if (checked) {
+ checkedSpus.value.push(spu)
+ checkedStatus.value[spu.id] = true
+ } else {
+ const index = findCheckedIndex(spu)
+ if (index > -1) {
+ checkedSpus.value.splice(index, 1)
+ checkedStatus.value[spu.id] = false
+ isCheckAll.value = false
+ }
+ }
+
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ if (isCalcCheckAll) {
+ calculateIsCheckAll()
+ }
+}
+
+// 鏌ユ壘鍟嗗搧鍦ㄥ凡閫変腑鍟嗗搧鍒楄〃涓殑绱㈠紩
+const findCheckedIndex = (spu: Spu) => checkedSpus.value.findIndex((item) => item.id === spu.id)
+
+// 璁$畻鍏ㄩ�夋鐘舵��
+const calculateIsCheckAll = () => {
+ isCheckAll.value = list.value.every((spu) => checkedStatus.value[spu.id])
+ // 璁$畻涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+ isIndeterminate.value = !isCheckAll.value && list.value.some((spu) => checkedStatus.value[spu.id])
+}
+
+// 鍒嗙被鍒楄〃
+const categoryList = ref()
+// 鍒嗙被鏍�
+const categoryTreeList = ref()
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱鍒嗙被鏍�
+ categoryList.value = await ProductCategoryApi.getCategoryList({})
+ categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId')
+})
+</script>
diff --git a/src/views/mall/product/spu/components/index.ts b/src/views/mall/product/spu/components/index.ts
new file mode 100644
index 0000000..e2cbe73
--- /dev/null
+++ b/src/views/mall/product/spu/components/index.ts
@@ -0,0 +1,54 @@
+import SkuList from './SkuList.vue'
+import { Spu } from '@/api/mall/product/spu'
+
+interface PropertyAndValues {
+ id: number
+ name: string
+ values?: PropertyAndValues[]
+}
+
+interface RuleConfig {
+ // 闇�瑕佹牎楠岀殑瀛楁
+ // 渚嬶細name: 'name' 鍒欒〃绀烘牎楠� sku.name 鐨勫��
+ // 渚嬶細name: 'productConfig.stock' 鍒欒〃绀烘牎楠� sku.productConfig.name 鐨勫��,姝ゅ productConfig 琛ㄧず鎴戝湪 Sku 涓婃墿灞曠殑灞炴��
+ name: string
+ // 鏍¢獙瑙勬牸涓轰竴涓瘉鎺夊嚱鏁帮紝鍏朵腑 arg 涓洪渶瑕佹牎楠岀殑瀛楁鐨勫�笺��
+ // 渚嬶細闇�瑕佹牎楠屼环鏍煎繀椤诲ぇ浜�0.01
+ // {
+ // name:'price',
+ // rule:(arg: number) => arg > 0.01
+ // }
+ rule: (arg: any) => boolean
+ // 鏍¢獙涓嶉�氳繃鏃剁殑娑堟伅鎻愮ず
+ message: string
+}
+
+/**
+ * 鑾峰緱鍟嗗搧鐨勮鏍煎垪琛� - 鍟嗗搧鐩稿叧鐨勫叕鍏卞嚱鏁�
+ *
+ * @param spu
+ * @return PropertyAndValues 瑙勬牸鍒楄〃
+ */
+const getPropertyList = (spu: Spu): PropertyAndValues[] => {
+ // 鐩存帴鎷胯繑鍥炵殑 skus 灞炴�ч�嗗悜鐢熸垚鍑� propertyList
+ const properties: PropertyAndValues[] = []
+ // 鍙湁鏄瑙勬牸鎵嶅鐞�
+ if (spu.specType) {
+ spu.skus?.forEach((sku) => {
+ sku.properties?.forEach(({ propertyId, propertyName, valueId, valueName }) => {
+ // 娣诲姞灞炴��
+ if (!properties?.some((item) => item.id === propertyId)) {
+ properties.push({ id: propertyId!, name: propertyName!, values: [] })
+ }
+ // 娣诲姞灞炴�у��
+ const index = properties?.findIndex((item) => item.id === propertyId)
+ if (!properties[index].values?.some((value) => value.id === valueId)) {
+ properties[index].values?.push({ id: valueId!, name: valueName! })
+ }
+ })
+ })
+ }
+ return properties
+}
+
+export { SkuList, PropertyAndValues, RuleConfig, getPropertyList }
diff --git a/src/views/mall/product/spu/form/DeliveryForm.vue b/src/views/mall/product/spu/form/DeliveryForm.vue
new file mode 100644
index 0000000..6800d23
--- /dev/null
+++ b/src/views/mall/product/spu/form/DeliveryForm.vue
@@ -0,0 +1,96 @@
+<!-- 鍟嗗搧鍙戝竷 - 鐗╂祦璁剧疆 -->
+<template>
+ <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+ <el-form-item label="閰嶉�佹柟寮�" prop="deliveryTypes">
+ <el-checkbox-group v-model="formData.deliveryTypes" class="w-80">
+ <el-checkbox
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ <el-form-item
+ label="杩愯垂妯℃澘"
+ prop="deliveryTemplateId"
+ v-if="formData.deliveryTypes?.includes(DeliveryTypeEnum.EXPRESS.type)"
+ >
+ <el-select placeholder="璇烽�夋嫨杩愯垂妯℃澘" v-model="formData.deliveryTemplateId" class="w-80">
+ <el-option
+ v-for="item in deliveryTemplateList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import type { Spu } from '@/api/mall/product/spu'
+import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DeliveryTypeEnum } from '@/utils/constants'
+
+defineOptions({ name: 'ProductDeliveryForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => {}
+ },
+ isDetail: propTypes.bool.def(false) // 鏄惁浣滀负璇︽儏缁勪欢
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const formData = reactive<Spu>({
+ deliveryTypes: [], // 閰嶉�佹柟寮�
+ deliveryTemplateId: undefined // 杩愯垂妯$増
+})
+const rules = reactive({
+ deliveryTypes: [required],
+ deliveryTemplateId: [required]
+})
+
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 formData */
+watch(
+ () => props.propFormData,
+ (data) => {
+ if (!data) {
+ return
+ }
+ copyValueToTarget(formData, data)
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+ if (!formRef) return
+ try {
+ await unref(formRef)?.validate()
+ // 鏍¢獙閫氳繃鏇存柊鏁版嵁
+ Object.assign(props.propFormData, formData)
+ } catch (e) {
+ message.error('銆愮墿娴佽缃�戜笉瀹屽杽锛岃濉啓鐩稿叧淇℃伅')
+ emit('update:activeName', 'delivery')
+ throw e // 鐩殑鎴柇涔嬪悗鐨勬牎楠�
+ }
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+const deliveryTemplateList = ref([]) // 杩愯垂妯$増
+onMounted(async () => {
+ deliveryTemplateList.value = await ExpressTemplateApi.getSimpleTemplateList()
+})
+</script>
diff --git a/src/views/mall/product/spu/form/DescriptionForm.vue b/src/views/mall/product/spu/form/DescriptionForm.vue
new file mode 100644
index 0000000..63bc9d8
--- /dev/null
+++ b/src/views/mall/product/spu/form/DescriptionForm.vue
@@ -0,0 +1,81 @@
+<!-- 鍟嗗搧鍙戝竷 - 鍟嗗搧璇︽儏 -->
+<template>
+ <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+ <!--瀵屾枃鏈紪杈戝櫒缁勪欢-->
+ <el-form-item label="鍟嗗搧璇︽儏" prop="description">
+ <Editor :readonly="isDetail" v-model:modelValue="formData.description" />
+ </el-form-item>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import type { Spu } from '@/api/mall/product/spu'
+import { Editor } from '@/components/Editor'
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { copyValueToTarget } from '@/utils'
+
+defineOptions({ name: 'ProductDescriptionForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => {}
+ },
+ activeName: propTypes.string.def(''),
+ isDetail: propTypes.bool.def(false) // 鏄惁浣滀负璇︽儏缁勪欢
+})
+const formRef = ref() // 琛ㄥ崟Ref
+const formData = ref<Spu>({
+ description: '' // 鍟嗗搧璇︽儏
+})
+// 琛ㄥ崟瑙勫垯
+const rules = reactive({
+ description: [required]
+})
+
+/** 瀵屾枃鏈紪杈戝櫒濡傛灉杈撳叆杩囧啀娓呯┖浼氭湁娈嬬暀锛岄渶鍐嶉噸缃竴娆� */
+watch(
+ () => formData.value.description,
+ (newValue) => {
+ if ('<p><br></p>' === newValue) {
+ formData.value.description = ''
+ }
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 formData */
+watch(
+ () => props.propFormData,
+ (data) => {
+ if (!data) return
+ // fix锛氫笁涓〃鍗曠粍浠剁洃鍚祴鍊煎繀椤讳娇鐢� copyValueToTarget 浣跨敤 formData.value = data 浼氱洃鍚潪甯稿娆�
+ copyValueToTarget(formData.value, data)
+ },
+ {
+ // fix: 鍘绘帀娣卞害鐩戝惉鍙湁瀵硅薄寮曠敤鍙戠敓鏀瑰彉鐨勬椂鍊欐墠鎵ц,瑙e喅鏀逛竴鍔ㄥ鐨勯棶棰�
+ immediate: true
+ }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+ if (!formRef) return
+ try {
+ await unref(formRef)?.validate()
+ // 鏍¢獙閫氳繃鏇存柊鏁版嵁
+ Object.assign(props.propFormData, formData.value)
+ } catch (e) {
+ message.error('銆愬晢鍝佽鎯呫�戜笉瀹屽杽锛岃濉啓鐩稿叧淇℃伅')
+ emit('update:activeName', 'description')
+ throw e // 鐩殑鎴柇涔嬪悗鐨勬牎楠�
+ }
+}
+defineExpose({ validate })
+</script>
diff --git a/src/views/mall/product/spu/form/InfoForm.vue b/src/views/mall/product/spu/form/InfoForm.vue
new file mode 100644
index 0000000..9a9fbfd
--- /dev/null
+++ b/src/views/mall/product/spu/form/InfoForm.vue
@@ -0,0 +1,153 @@
+<!-- 鍟嗗搧鍙戝竷 - 鍩虹璁剧疆 -->
+<template>
+ <el-form ref="formRef" :disabled="isDetail" :model="formData" :rules="rules" label-width="120px">
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="name">
+ <el-input
+ v-model="formData.name"
+ :autosize="{ minRows: 2, maxRows: 2 }"
+ :clearable="true"
+ :show-word-limit="true"
+ class="w-80!"
+ maxlength="64"
+ placeholder="璇疯緭鍏ュ晢鍝佸悕绉�"
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍒嗙被" prop="categoryId">
+ <el-cascader
+ v-model="formData.categoryId"
+ :options="categoryList"
+ :props="defaultProps"
+ class="w-80!"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍟嗗搧鍒嗙被"
+ />
+ <el-button :icon="RefreshRight" @click="refreshCategoryList" class="ml-1" size="small" />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍝佺墝" prop="brandId">
+ <el-select v-model="formData.brandId" class="w-80!" placeholder="璇烽�夋嫨鍟嗗搧鍝佺墝">
+ <el-option
+ v-for="item in brandList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id as number"
+ />
+ </el-select>
+ <el-button :icon="RefreshRight" @click="refreshBrandList" class="ml-1" size="small" />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍏抽敭瀛�" prop="keyword">
+ <el-input v-model="formData.keyword" class="w-80!" placeholder="璇疯緭鍏ュ晢鍝佸叧閿瓧" />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧绠�浠�" prop="introduction">
+ <el-input
+ v-model="formData.introduction"
+ :autosize="{ minRows: 2, maxRows: 2 }"
+ :clearable="true"
+ :show-word-limit="true"
+ class="w-80!"
+ maxlength="128"
+ placeholder="璇疯緭鍏ュ晢鍝佺畝浠�"
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧灏侀潰鍥�" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" :disabled="isDetail" height="80px" />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧杞挱鍥�" prop="sliderPicUrls">
+ <UploadImgs v-model="formData.sliderPicUrls" :disabled="isDetail" />
+ </el-form-item>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { defaultProps, handleTree } from '@/utils/tree'
+import type { Spu } from '@/api/mall/product/spu'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import { CategoryVO } from '@/api/mall/product/category'
+import * as ProductBrandApi from '@/api/mall/product/brand'
+import { BrandVO } from '@/api/mall/product/brand'
+import { RefreshRight } from '@element-plus/icons-vue'
+
+defineOptions({ name: 'ProductSpuInfoForm' })
+const props = defineProps({
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => {}
+ },
+ isDetail: propTypes.bool.def(false) // 鏄惁浣滀负璇︽儏缁勪欢
+})
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const formRef = ref() // 琛ㄥ崟 Ref
+const formData = reactive<Spu>({
+ name: '', // 鍟嗗搧鍚嶇О
+ categoryId: undefined, // 鍟嗗搧鍒嗙被
+ keyword: '', // 鍏抽敭瀛�
+ picUrl: '', // 鍟嗗搧灏侀潰鍥�
+ sliderPicUrls: [], // 鍟嗗搧杞挱鍥�
+ introduction: '', // 鍟嗗搧绠�浠�
+ brandId: undefined // 鍟嗗搧鍝佺墝
+})
+const rules = reactive({
+ name: [required],
+ categoryId: [required],
+ keyword: [required],
+ introduction: [required],
+ picUrl: [required],
+ sliderPicUrls: [required],
+ brandId: [required]
+})
+
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 formData */
+watch(
+ () => props.propFormData,
+ (data) => {
+ if (!data) {
+ return
+ }
+ copyValueToTarget(formData, data)
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+ if (!formRef) return
+ try {
+ await unref(formRef)?.validate()
+ // 鏍¢獙閫氳繃鏇存柊鏁版嵁
+ Object.assign(props.propFormData, formData)
+ } catch (e) {
+ message.error('銆愬熀纭�璁剧疆銆戜笉瀹屽杽锛岃濉啓鐩稿叧淇℃伅')
+ emit('update:activeName', 'info')
+ throw e // 鐩殑鎴柇涔嬪悗鐨勬牎楠�
+ }
+}
+defineExpose({ validate })
+
+/** 鍒濆鍖� */
+const brandList = ref<BrandVO[]>([]) // 鍟嗗搧鍝佺墝鍒楄〃
+const categoryList = ref<CategoryVO[]>([]) // 鍟嗗搧鍒嗙被鏍�
+async function refreshCategoryList() {
+ // 鑾峰緱鍒嗙被鏍�
+ const data = await ProductCategoryApi.getCategoryList({})
+ categoryList.value = handleTree(data, 'id')
+}
+
+async function refreshBrandList() {
+ brandList.value = await ProductBrandApi.getSimpleBrandList()
+}
+
+onMounted(async () => {
+ await refreshCategoryList()
+ // 鑾峰彇鍟嗗搧鍝佺墝鍒楄〃
+ await refreshBrandList()
+})
+</script>
diff --git a/src/views/mall/product/spu/form/OtherForm.vue b/src/views/mall/product/spu/form/OtherForm.vue
new file mode 100644
index 0000000..e7e6358
--- /dev/null
+++ b/src/views/mall/product/spu/form/OtherForm.vue
@@ -0,0 +1,91 @@
+<!-- 鍟嗗搧鍙戝竷 - 鍏跺畠璁剧疆 -->
+<template>
+ <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+ <el-form-item label="鍟嗗搧鎺掑簭" prop="sort">
+ <el-input-number
+ v-model="formData.sort"
+ :min="0"
+ placeholder="璇疯緭鍏ュ晢鍝佹帓搴�"
+ class="w-80!"
+ />
+ </el-form-item>
+ <el-form-item label="璧犻�佺Н鍒�" prop="giveIntegral">
+ <el-input-number
+ v-model="formData.giveIntegral"
+ :min="0"
+ placeholder="璇疯緭鍏ヨ禒閫佺Н鍒�"
+ class="w-80!"
+ />
+ </el-form-item>
+ <el-form-item label="铏氭嫙閿�閲�" prop="virtualSalesCount">
+ <el-input-number
+ v-model="formData.virtualSalesCount"
+ :min="0"
+ placeholder="璇疯緭鍏ヨ櫄鎷熼攢閲�"
+ class="w-80!"
+ />
+ </el-form-item>
+ </el-form>
+</template>
+<script lang="ts" setup>
+import type { Spu } from '@/api/mall/product/spu'
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { copyValueToTarget } from '@/utils'
+
+defineOptions({ name: 'ProductOtherForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => {}
+ },
+ isDetail: propTypes.bool.def(false) // 鏄惁浣滀负璇︽儏缁勪欢
+})
+
+const formRef = ref() // 琛ㄥ崟Ref
+// 琛ㄥ崟鏁版嵁
+const formData = ref<Spu>({
+ sort: 0, // 鍟嗗搧鎺掑簭
+ giveIntegral: 0, // 璧犻�佺Н鍒�
+ virtualSalesCount: 0 // 铏氭嫙閿�閲�
+})
+// 琛ㄥ崟瑙勫垯
+const rules = reactive({
+ sort: [required],
+ giveIntegral: [required],
+ virtualSalesCount: [required]
+})
+
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 formData */
+watch(
+ () => props.propFormData,
+ (data) => {
+ if (!data) {
+ return
+ }
+ copyValueToTarget(formData.value, data)
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+ if (!formRef) return
+ try {
+ await unref(formRef)?.validate()
+ // 鏍¢獙閫氳繃鏇存柊鏁版嵁
+ Object.assign(props.propFormData, formData.value)
+ } catch (e) {
+ message.error('銆愬叾瀹冭缃�戜笉瀹屽杽锛岃濉啓鐩稿叧淇℃伅')
+ emit('update:activeName', 'other')
+ throw e // 鐩殑鎴柇涔嬪悗鐨勬牎楠�
+ }
+}
+defineExpose({ validate })
+</script>
diff --git a/src/views/mall/product/spu/form/ProductAttributes.vue b/src/views/mall/product/spu/form/ProductAttributes.vue
new file mode 100644
index 0000000..1bb24ff
--- /dev/null
+++ b/src/views/mall/product/spu/form/ProductAttributes.vue
@@ -0,0 +1,162 @@
+<!-- 鍟嗗搧鍙戝竷 - 搴撳瓨浠锋牸 - 灞炴�у垪琛� -->
+<template>
+ <el-col v-for="(item, index) in attributeList" :key="index">
+ <div>
+ <el-text class="mx-1">灞炴�у悕锛�</el-text>
+ <el-tag :closable="!isDetail" class="mx-1" type="success" @close="handleCloseProperty(index)">
+ {{ item.name }}
+ </el-tag>
+ </div>
+ <div>
+ <el-text class="mx-1">灞炴�у�硷細</el-text>
+ <el-tag
+ v-for="(value, valueIndex) in item.values"
+ :key="value.id"
+ :closable="!isDetail"
+ class="mx-1"
+ @close="handleCloseValue(index, valueIndex)"
+ >
+ {{ value.name }}
+ </el-tag>
+ <el-select
+ v-show="inputVisible(index)"
+ :id="`input${index}`"
+ :ref="setInputRef"
+ v-model="inputValue"
+ :reserve-keyword="false"
+ allow-create
+ class="!w-30"
+ default-first-option
+ filterable
+ size="small"
+ @blur="handleInputConfirm(index, item.id)"
+ @change="handleInputConfirm(index, item.id)"
+ @keyup.enter="handleInputConfirm(index, item.id)"
+ >
+ <el-option
+ v-for="item2 in attributeOptions"
+ :key="item2.id"
+ :label="item2.name"
+ :value="item2.name"
+ />
+ </el-select>
+ <el-button
+ v-show="!inputVisible(index)"
+ class="button-new-tag ml-1"
+ size="small"
+ @click="showInput(index)"
+ >
+ + 娣诲姞
+ </el-button>
+ </div>
+ <el-divider class="my-10px" />
+ </el-col>
+</template>
+
+<script lang="ts" setup>
+import * as PropertyApi from '@/api/mall/product/property'
+import { PropertyAndValues } from '@/views/mall/product/spu/components'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'ProductAttributes' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const inputValue = ref('') // 杈撳叆妗嗗��
+const attributeIndex = ref<number | null>(null) // 鑾峰彇鐒︾偣鏃惰褰曞綋鍓嶅睘鎬ч」鐨刬ndex
+// 杈撳叆妗嗘樉闅愭帶鍒�
+const inputVisible = computed(() => (index: number) => {
+ if (attributeIndex.value === null) return false
+ if (attributeIndex.value === index) return true
+})
+const inputRef = ref<any[]>([]) //鏍囩杈撳叆妗哛ef
+/** 瑙e喅 ref 鍦� v-for 涓殑鑾峰彇闂*/
+const setInputRef = (el: any) => {
+ if (el === null || typeof el === 'undefined') return
+ // 濡傛灉涓嶅瓨鍦� id 鐩稿悓鐨勫厓绱犳墠娣诲姞
+ if (!inputRef.value.some((item) => item.inputRef?.attributes.id === el.inputRef?.attributes.id)) {
+ inputRef.value.push(el)
+ }
+}
+const attributeList = ref<PropertyAndValues[]>([]) // 鍟嗗搧灞炴�у垪琛�
+const attributeOptions = ref([] as PropertyApi.PropertyValueVO[]) // 鍟嗗搧灞炴�у悕绉颁笅鎷夋
+const props = defineProps({
+ propertyList: {
+ type: Array,
+ default: () => {}
+ },
+ isDetail: propTypes.bool.def(false) // 鏄惁浣滀负璇︽儏缁勪欢
+})
+
+watch(
+ () => props.propertyList,
+ (data) => {
+ if (!data) return
+ attributeList.value = data as any
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+/** 鍒犻櫎灞炴�у��*/
+const handleCloseValue = (index: number, valueIndex: number) => {
+ attributeList.value[index].values?.splice(valueIndex, 1)
+}
+
+/** 鍒犻櫎灞炴��*/
+const handleCloseProperty = (index: number) => {
+ attributeList.value?.splice(index, 1)
+ emit('success', attributeList.value)
+}
+
+/** 鏄剧ず杈撳叆妗嗗苟鑾峰彇鐒︾偣 */
+const showInput = async (index: number) => {
+ attributeIndex.value = index
+ inputRef.value[index].focus()
+ // 鑾峰彇灞炴�т笅鎷夐�夐」
+ await getAttributeOptions(attributeList.value[index].id)
+}
+
+/** 杈撳叆妗嗗け鍘荤劍鐐规垨鐐瑰嚮鍥炶溅鏃惰Е鍙� */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const handleInputConfirm = async (index: number, propertyId: number) => {
+ if (inputValue.value) {
+ // 1. 閲嶅娣诲姞鏍¢獙
+ if (attributeList.value[index].values.find((item) => item.name === inputValue.value)) {
+ message.warning('宸插瓨鍦ㄧ浉鍚屽睘鎬у�硷紝璇烽噸璇�')
+ attributeIndex.value = null
+ inputValue.value = ''
+ return
+ }
+
+ // 2.1 鎯呭喌涓�锛氬睘鎬у�煎凡瀛樺湪锛屽垯鐩存帴浣跨敤骞剁粨鏉�
+ const existValue = attributeOptions.value.find((item) => item.name === inputValue.value)
+ if (existValue) {
+ attributeIndex.value = null
+ inputValue.value = ''
+ attributeList.value[index].values.push({ id: existValue.id, name: existValue.name })
+ emit('success', attributeList.value)
+ return
+ }
+
+ // 2.2 鎯呭喌浜岋細鏂板睘鎬у�硷紝鍒欒繘琛屼繚瀛�
+ try {
+ const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
+ attributeList.value[index].values.push({ id, name: inputValue.value })
+ message.success(t('common.createSuccess'))
+ emit('success', attributeList.value)
+ } catch {
+ message.error('娣诲姞澶辫触锛岃閲嶈瘯')
+ }
+ }
+ attributeIndex.value = null
+ inputValue.value = ''
+}
+
+/** 鑾峰彇鍟嗗搧灞炴�т笅鎷夐�夐」 */
+const getAttributeOptions = async (propertyId: number) => {
+ attributeOptions.value = await PropertyApi.getPropertyValueSimpleList(propertyId)
+}
+</script>
diff --git a/src/views/mall/product/spu/form/ProductPropertyAddForm.vue b/src/views/mall/product/spu/form/ProductPropertyAddForm.vue
new file mode 100644
index 0000000..f45281b
--- /dev/null
+++ b/src/views/mall/product/spu/form/ProductPropertyAddForm.vue
@@ -0,0 +1,148 @@
+<!-- 鍟嗗搧鍙戝竷 - 搴撳瓨浠锋牸 - 娣诲姞灞炴�� -->
+<template>
+ <Dialog v-model="dialogVisible" title="娣诲姞鍟嗗搧灞炴��">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ @keydown.enter.prevent="submitForm"
+ >
+ <el-form-item label="灞炴�у悕绉�" prop="name">
+ <el-select
+ v-model="formData.name"
+ :reserve-keyword="false"
+ allow-create
+ class="!w-360px"
+ default-first-option
+ filterable
+ placeholder="璇烽�夋嫨灞炴�у悕绉般�傚鏋滀笉瀛樺湪锛屽彲鎵嬪姩杈撳叆閫夋嫨"
+ >
+ <el-option
+ v-for="item in attributeOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.name"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as PropertyApi from '@/api/mall/product/property'
+
+defineOptions({ name: 'ProductPropertyForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref({
+ name: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const attributeList = ref([]) // 鍟嗗搧灞炴�у垪琛�
+const attributeOptions = ref([] as PropertyApi.PropertyVO[]) // 鍟嗗搧灞炴�у悕绉颁笅鎷夋
+const props = defineProps({
+ propertyList: {
+ type: Array,
+ default: () => {}
+ }
+})
+
+watch(
+ () => props.propertyList, // 瑙e喅 props 鏃犳硶鐩存帴淇敼鐖剁粍浠剁殑闂
+ (data) => {
+ if (!data) return
+ attributeList.value = data
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ resetForm()
+ // 鍔犺浇鍒楄〃
+ await getAttributeOptions()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 1.1 閲嶅娣诲姞鏍¢獙
+ for (const attrItem of attributeList.value) {
+ if (attrItem.name === formData.value.name) {
+ return message.error('璇ュ睘鎬у凡瀛樺湪锛岃鍕块噸澶嶆坊鍔�')
+ }
+ }
+ // 1.2 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+
+ // 2.1 鎯呭喌涓�锛氬睘鎬у悕宸插瓨鍦紝鍒欑洿鎺ヤ娇鐢ㄥ苟缁撴潫
+ const existProperty = attributeOptions.value.find((item) => item.name === formData.value.name)
+ if (existProperty) {
+ // 娣诲姞鍒板睘鎬у垪琛�
+ attributeList.value.push({
+ id: existProperty.id,
+ ...formData.value,
+ values: []
+ })
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ return
+ }
+
+ // 2.2 鎯呭喌浜岋細濡傛灉鏄笉瀛樺湪鐨勫睘鎬э紝鍒欓渶瑕佹墽琛屾柊澧�
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as PropertyApi.PropertyVO
+ const propertyId = await PropertyApi.createProperty(data)
+ // 娣诲姞鍒板睘鎬у垪琛�
+ attributeList.value.push({
+ id: propertyId,
+ ...formData.value,
+ values: []
+ })
+ // 鍏抽棴寮圭獥
+ message.success(t('common.createSuccess'))
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ name: ''
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鑾峰彇鍟嗗搧灞炴�т笅鎷夐�夐」 */
+const getAttributeOptions = async () => {
+ formLoading.value = true
+ try {
+ attributeOptions.value = await PropertyApi.getPropertySimpleList()
+ } finally {
+ formLoading.value = false
+ }
+}
+</script>
diff --git a/src/views/mall/product/spu/form/SkuForm.vue b/src/views/mall/product/spu/form/SkuForm.vue
new file mode 100644
index 0000000..18cd029
--- /dev/null
+++ b/src/views/mall/product/spu/form/SkuForm.vue
@@ -0,0 +1,194 @@
+<!-- 鍟嗗搧鍙戝竷 - 搴撳瓨浠锋牸 -->
+<template>
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :disabled="isDetail"
+ :model="formData"
+ :rules="rules"
+ label-width="120px"
+ >
+ <el-form-item label="鍒嗛攢绫诲瀷" prop="subCommissionType">
+ <el-radio-group
+ v-model="formData.subCommissionType"
+ class="w-80"
+ @change="changeSubCommissionType"
+ >
+ <el-radio :value="false">榛樿璁剧疆</el-radio>
+ <el-radio :value="true" class="radio">鍗曠嫭璁剧疆</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍟嗗搧瑙勬牸" prop="specType">
+ <el-radio-group v-model="formData.specType" class="w-80" @change="onChangeSpec">
+ <el-radio :value="false" class="radio">鍗曡鏍�</el-radio>
+ <el-radio :value="true">澶氳鏍�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <!-- 澶氳鏍兼坊鍔�-->
+ <el-form-item v-if="!formData.specType">
+ <SkuList
+ ref="skuListRef"
+ :prop-form-data="formData"
+ :property-list="propertyList"
+ :rule-config="ruleConfig"
+ />
+ </el-form-item>
+ <el-form-item v-if="formData.specType" label="鍟嗗搧灞炴��">
+ <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">娣诲姞灞炴��</el-button>
+ <ProductAttributes
+ :is-detail="isDetail"
+ :property-list="propertyList"
+ @success="generateSkus"
+ />
+ </el-form-item>
+ <template v-if="formData.specType && propertyList.length > 0">
+ <el-form-item v-if="!isDetail" label="鎵归噺璁剧疆">
+ <SkuList :is-batch="true" :prop-form-data="formData" :property-list="propertyList" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍒楄〃">
+ <SkuList
+ ref="skuListRef"
+ :is-detail="isDetail"
+ :prop-form-data="formData"
+ :property-list="propertyList"
+ :rule-config="ruleConfig"
+ />
+ </el-form-item>
+ </template>
+ </el-form>
+
+ <!-- 鍟嗗搧灞炴�ф坊鍔� Form 琛ㄥ崟 -->
+ <ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import {
+ getPropertyList,
+ PropertyAndValues,
+ RuleConfig,
+ SkuList
+} from '@/views/mall/product/spu/components/index'
+import ProductAttributes from './ProductAttributes.vue'
+import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
+import type { Spu } from '@/api/mall/product/spu'
+
+defineOptions({ name: 'ProductSpuSkuForm' })
+
+// sku 鐩稿叧灞炴�ф牎楠岃鍒�
+const ruleConfig: RuleConfig[] = [
+ {
+ name: 'stock',
+ rule: (arg) => arg >= 0,
+ message: '鍟嗗搧搴撳瓨蹇呴』澶т簬绛変簬 1 锛侊紒锛�'
+ },
+ {
+ name: 'price',
+ rule: (arg) => arg >= 0.01,
+ message: '鍟嗗搧閿�鍞环鏍煎繀椤诲ぇ浜庣瓑浜� 0.01 鍏冿紒锛侊紒'
+ },
+ {
+ name: 'marketPrice',
+ rule: (arg) => arg >= 0.01,
+ message: '鍟嗗搧甯傚満浠锋牸蹇呴』澶т簬绛変簬 0.01 鍏冿紒锛侊紒'
+ },
+ {
+ name: 'costPrice',
+ rule: (arg) => arg >= 0.01,
+ message: '鍟嗗搧鎴愭湰浠锋牸蹇呴』澶т簬绛変簬 0.00 鍏冿紒锛侊紒'
+ }
+]
+
+const message = useMessage() // 娑堟伅寮圭獥
+const formLoading = ref(false)
+const props = defineProps({
+ propFormData: {
+ type: Object as PropType<Spu>,
+ default: () => {}
+ },
+ isDetail: propTypes.bool.def(false) // 鏄惁浣滀负璇︽儏缁勪欢
+})
+const attributesAddFormRef = ref() // 娣诲姞鍟嗗搧灞炴�ц〃鍗�
+const formRef = ref() // 琛ㄥ崟 Ref
+const propertyList = ref<PropertyAndValues[]>([]) // 鍟嗗搧灞炴�у垪琛�
+const skuListRef = ref() // 鍟嗗搧灞炴�у垪琛� Ref
+const formData = reactive<Spu>({
+ specType: false, // 鍟嗗搧瑙勬牸
+ subCommissionType: false, // 鍒嗛攢绫诲瀷
+ skus: []
+})
+const rules = reactive({
+ specType: [required],
+ subCommissionType: [required]
+})
+
+/** 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 formData */
+watch(
+ () => props.propFormData,
+ (data) => {
+ if (!data) {
+ return
+ }
+ copyValueToTarget(formData, data)
+ // 灏� SKU 鐨勫睘鎬э紝鏁寸悊鎴� PropertyAndValues 鏁扮粍
+ propertyList.value = getPropertyList(data)
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 琛ㄥ崟鏍¢獙 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+ if (!formRef) return
+ try {
+ // 鏍¢獙 sku
+ skuListRef.value.validateSku()
+ await unref(formRef).validate()
+ // 鏍¢獙閫氳繃鏇存柊鏁版嵁
+ Object.assign(props.propFormData, formData)
+ } catch (e) {
+ message.error('銆愬簱瀛樹环鏍笺�戜笉瀹屽杽锛岃濉啓鐩稿叧淇℃伅')
+ emit('update:activeName', 'sku')
+ throw e // 鐩殑鎴柇涔嬪悗鐨勬牎楠�
+ }
+}
+defineExpose({ validate })
+
+/** 鍒嗛攢绫诲瀷 */
+const changeSubCommissionType = () => {
+ // 榛樿涓洪浂锛岀被鍨嬪垏鎹㈠悗涔熻閲嶇疆涓洪浂
+ for (const item of formData.skus!) {
+ item.firstBrokeragePrice = 0
+ item.secondBrokeragePrice = 0
+ }
+}
+
+/** 閫夋嫨瑙勬牸 */
+const onChangeSpec = () => {
+ // 閲嶇疆鍟嗗搧灞炴�у垪琛�
+ propertyList.value = []
+ // 閲嶇疆sku鍒楄〃
+ formData.skus = [
+ {
+ price: 0,
+ marketPrice: 0,
+ costPrice: 0,
+ barCode: '',
+ picUrl: '',
+ stock: 0,
+ weight: 0,
+ volume: 0,
+ firstBrokeragePrice: 0,
+ secondBrokeragePrice: 0
+ }
+ ]
+}
+
+/** 璋冪敤 SkuList generateTableData 鏂规硶*/
+const generateSkus = (propertyList: any[]) => {
+ skuListRef.value.generateTableData(propertyList)
+}
+</script>
diff --git a/src/views/mall/product/spu/form/index.vue b/src/views/mall/product/spu/form/index.vue
new file mode 100644
index 0000000..c4e4b7b
--- /dev/null
+++ b/src/views/mall/product/spu/form/index.vue
@@ -0,0 +1,204 @@
+<template>
+ <ContentWrap v-loading="formLoading">
+ <el-tabs v-model="activeName">
+ <el-tab-pane label="鍩虹璁剧疆" name="info">
+ <InfoForm
+ ref="infoRef"
+ v-model:activeName="activeName"
+ :is-detail="isDetail"
+ :propFormData="formData"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="浠锋牸搴撳瓨" name="sku">
+ <SkuForm
+ ref="skuRef"
+ v-model:activeName="activeName"
+ :is-detail="isDetail"
+ :propFormData="formData"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鐗╂祦璁剧疆" name="delivery">
+ <DeliveryForm
+ ref="deliveryRef"
+ v-model:activeName="activeName"
+ :is-detail="isDetail"
+ :propFormData="formData"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍟嗗搧璇︽儏" name="description">
+ <DescriptionForm
+ ref="descriptionRef"
+ v-model:activeName="activeName"
+ :is-detail="isDetail"
+ :propFormData="formData"
+ />
+ </el-tab-pane>
+ <el-tab-pane label="鍏跺畠璁剧疆" name="other">
+ <OtherForm
+ ref="otherRef"
+ v-model:activeName="activeName"
+ :is-detail="isDetail"
+ :propFormData="formData"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ <el-form>
+ <el-form-item style="float: right">
+ <el-button v-if="!isDetail" :loading="formLoading" type="primary" @click="submitForm">
+ 淇濆瓨
+ </el-button>
+ <el-button @click="close">杩斿洖</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { cloneDeep } from 'lodash-es'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import InfoForm from './InfoForm.vue'
+import DescriptionForm from './DescriptionForm.vue'
+import OtherForm from './OtherForm.vue'
+import SkuForm from './SkuForm.vue'
+import DeliveryForm from './DeliveryForm.vue'
+import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
+
+defineOptions({ name: 'ProductSpuAdd' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const { push, currentRoute } = useRouter() // 璺敱
+const { params, name } = useRoute() // 鏌ヨ鍙傛暟
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const activeName = ref('info') // Tag 婵�娲荤殑绐楀彛
+const isDetail = ref(false) // 鏄惁鏌ョ湅璇︽儏
+const infoRef = ref() // 鍟嗗搧淇℃伅 Ref
+const skuRef = ref() // 鍟嗗搧瑙勬牸 Ref
+const deliveryRef = ref() // 鐗╂祦璁剧疆 Ref
+const descriptionRef = ref() // 鍟嗗搧璇︽儏 Ref
+const otherRef = ref() // 鍏朵粬璁剧疆 Ref
+// SPU 琛ㄥ崟鏁版嵁
+const formData = ref<ProductSpuApi.Spu>({
+ name: '', // 鍟嗗搧鍚嶇О
+ categoryId: undefined, // 鍟嗗搧鍒嗙被
+ keyword: '', // 鍏抽敭瀛�
+ picUrl: '', // 鍟嗗搧灏侀潰鍥�
+ sliderPicUrls: [], // 鍟嗗搧杞挱鍥�
+ introduction: '', // 鍟嗗搧绠�浠�
+ deliveryTypes: [], // 閰嶉�佹柟寮忔暟缁�
+ deliveryTemplateId: undefined, // 杩愯垂妯$増
+ brandId: undefined, // 鍟嗗搧鍝佺墝
+ specType: false, // 鍟嗗搧瑙勬牸
+ subCommissionType: false, // 鍒嗛攢绫诲瀷
+ skus: [
+ {
+ price: 0, // 鍟嗗搧浠锋牸
+ marketPrice: 0, // 甯傚満浠�
+ costPrice: 0, // 鎴愭湰浠�
+ barCode: '', // 鍟嗗搧鏉$爜
+ picUrl: '', // 鍥剧墖鍦板潃
+ stock: 0, // 搴撳瓨
+ weight: 0, // 鍟嗗搧閲嶉噺
+ volume: 0, // 鍟嗗搧浣撶Н
+ firstBrokeragePrice: 0, // 涓�绾у垎閿�鐨勪剑閲�
+ secondBrokeragePrice: 0 // 浜岀骇鍒嗛攢鐨勪剑閲�
+ }
+ ],
+ description: '', // 鍟嗗搧璇︽儏
+ sort: 0, // 鍟嗗搧鎺掑簭
+ giveIntegral: 0, // 璧犻�佺Н鍒�
+ virtualSalesCount: 0 // 铏氭嫙閿�閲�
+})
+
+/** 鑾峰緱璇︽儏 */
+const getDetail = async () => {
+ if ('ProductSpuDetail' === name) {
+ isDetail.value = true
+ }
+ const id = params.id as unknown as number
+ if (id) {
+ formLoading.value = true
+ try {
+ const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
+ res.skus?.forEach((item) => {
+ if (isDetail.value) {
+ item.price = floatToFixed2(item.price)
+ item.marketPrice = floatToFixed2(item.marketPrice)
+ item.costPrice = floatToFixed2(item.costPrice)
+ item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
+ } else {
+ // 鍥炴樉浠锋牸鍒嗚浆鍏�
+ item.price = formatToFraction(item.price)
+ item.marketPrice = formatToFraction(item.marketPrice)
+ item.costPrice = formatToFraction(item.costPrice)
+ item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
+ }
+ })
+ formData.value = res
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+
+/** 鎻愪氦鎸夐挳 */
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鏍¢獙鍚勮〃鍗�
+ await unref(infoRef)?.validate()
+ await unref(skuRef)?.validate()
+ await unref(deliveryRef)?.validate()
+ await unref(descriptionRef)?.validate()
+ await unref(otherRef)?.validate()
+ // 娣辨嫹璐濅竴浠�, 杩欐牱鏈�缁� server 绔笉婊¤冻锛屼笉闇�瑕佸奖鍝嶅師濮嬫暟鎹�
+ const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
+ deepCopyFormData.skus!.forEach((item) => {
+ // 缁檚ku name璧嬪��
+ item.name = deepCopyFormData.name
+ // sku鐩稿叧浠锋牸鍏冭浆鍒�
+ item.price = convertToInteger(item.price)
+ item.marketPrice = convertToInteger(item.marketPrice)
+ item.costPrice = convertToInteger(item.costPrice)
+ item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
+ })
+ // 澶勭悊杞挱鍥惧垪琛�
+ const newSliderPicUrls: any[] = []
+ deepCopyFormData.sliderPicUrls!.forEach((item: any) => {
+ // 濡傛灉鏄墠绔�夌殑鍥�
+ typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
+ })
+ deepCopyFormData.sliderPicUrls = newSliderPicUrls
+ // 鏍¢獙閮介�氳繃鍚庢彁浜よ〃鍗�
+ const data = deepCopyFormData as ProductSpuApi.Spu
+ const id = params.id as unknown as number
+ if (!id) {
+ await ProductSpuApi.createSpu(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ProductSpuApi.updateSpu(data)
+ message.success(t('common.updateSuccess'))
+ }
+ close()
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鍏抽棴鎸夐挳 */
+const close = () => {
+ delView(unref(currentRoute))
+ push({ name: 'ProductSpu' })
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await getDetail()
+})
+</script>
diff --git a/src/views/mall/product/spu/index.vue b/src/views/mall/product/spu/index.vue
new file mode 100644
index 0000000..e12403c
--- /dev/null
+++ b/src/views/mall/product/spu/index.vue
@@ -0,0 +1,457 @@
+<!-- 鍟嗗搧涓績 - 鍟嗗搧鍒楄〃 -->
+<template>
+ <doc-alert title="銆愬晢鍝併�戝晢鍝� SPU 涓� SKU" url="https://doc.iocoder.cn/mall/product-spu-sku/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ晢鍝佸悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗗搧鍒嗙被" prop="categoryId">
+ <el-cascader
+ v-model="queryParams.categoryId"
+ :options="categoryList"
+ :props="defaultProps"
+ class="w-1/1"
+ clearable
+ filterable
+ placeholder="璇烽�夋嫨鍟嗗搧鍒嗙被"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['product:spu:create']"
+ plain
+ type="primary"
+ @click="openForm(undefined)"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['product:spu:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
+ <el-tab-pane
+ v-for="item in tabsData"
+ :key="item.type"
+ :label="item.name + '(' + item.count + ')'"
+ :name="item.type"
+ />
+ </el-tabs>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column type="expand">
+ <template #default="{ row }">
+ <el-form class="spu-table-expand" label-position="left">
+ <el-row>
+ <el-col :span="24">
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="鍟嗗搧鍒嗙被:">
+ <span>{{ formatCategoryName(row.categoryId) }}</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="甯傚満浠�:">
+ <span>{{ fenToYuan(row.marketPrice) }}</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎴愭湰浠�:">
+ <span>{{ fenToYuan(row.costPrice) }}</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-row>
+ <el-col :span="8">
+ <el-form-item label="娴忚閲�:">
+ <span>{{ row.browseCount }}</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="铏氭嫙閿�閲�:">
+ <span>{{ row.virtualSalesCount }}</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-col>
+ </el-row>
+ </el-form>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧缂栧彿" min-width="140" prop="id" />
+ <el-table-column label="鍟嗗搧淇℃伅" min-width="300">
+ <template #default="{ row }">
+ <div class="flex">
+ <el-image
+ fit="cover"
+ :src="row.picUrl"
+ class="flex-none w-50px h-50px"
+ @click="imagePreview(row.picUrl)"
+ />
+ <div class="ml-4 overflow-hidden">
+ <el-tooltip effect="dark" :content="row.name" placement="top">
+ <div>
+ {{ row.name }}
+ </div>
+ </el-tooltip>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="浠锋牸" min-width="160" prop="price">
+ <template #default="{ row }"> 楼 {{ fenToYuan(row.price) }}</template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�閲�" min-width="90" prop="salesCount" />
+ <el-table-column align="center" label="搴撳瓨" min-width="90" prop="stock" />
+ <el-table-column align="center" label="鎺掑簭" min-width="70" prop="sort" />
+ <el-table-column align="center" label="閿�鍞姸鎬�" min-width="80">
+ <template #default="{ row }">
+ <template v-if="row.status >= 0">
+ <el-switch
+ v-model="row.status"
+ :active-value="1"
+ :inactive-value="0"
+ active-text="涓婃灦"
+ inactive-text="涓嬫灦"
+ inline-prompt
+ @change="handleStatusChange(row)"
+ />
+ </template>
+ <template v-else>
+ <el-tag type="info">鍥炴敹绔�</el-tag>
+ </template>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" min-width="200">
+ <template #default="{ row }">
+ <el-button link type="primary" @click="openDetail(row.id)"> 璇︽儏 </el-button>
+ <el-button
+ v-hasPermi="['product:spu:update']"
+ link
+ type="primary"
+ @click="openForm(row.id)"
+ >
+ 淇敼
+ </el-button>
+ <template v-if="queryParams.tabType === 4">
+ <el-button
+ v-hasPermi="['product:spu:delete']"
+ link
+ type="danger"
+ @click="handleDelete(row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ <el-button
+ v-hasPermi="['product:spu:update']"
+ link
+ type="primary"
+ @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
+ >
+ 鎭㈠
+ </el-button>
+ </template>
+ <template v-else>
+ <el-button
+ v-hasPermi="['product:spu:update']"
+ link
+ type="danger"
+ @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
+ >
+ 鍥炴敹
+ </el-button>
+ </template>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { TabsPaneContext } from 'element-plus'
+import { createImageViewer } from '@/components/ImageViewer'
+import { dateFormatter } from '@/utils/formatTime'
+import { defaultProps, handleTree, treeToString } from '@/utils/tree'
+import { ProductSpuStatusEnum } from '@/utils/constants'
+import { fenToYuan } from '@/utils'
+import download from '@/utils/download'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+
+defineOptions({ name: 'ProductSpu' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const route = useRoute() // 璺敱
+const { t } = useI18n() // 鍥介檯鍖�
+const { push } = useRouter() // 璺敱璺宠浆
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<ProductSpuApi.Spu[]>([]) // 鍒楄〃鐨勬暟鎹�
+// tabs 鏁版嵁
+const tabsData = ref([
+ {
+ name: '鍑哄敭涓�',
+ type: 0,
+ count: 0
+ },
+ {
+ name: '浠撳簱涓�',
+ type: 1,
+ count: 0
+ },
+ {
+ name: '宸插敭缃�',
+ type: 2,
+ count: 0
+ },
+ {
+ name: '璀︽垝搴撳瓨',
+ type: 3,
+ count: 0
+ },
+ {
+ name: '鍥炴敹绔�',
+ type: 4,
+ count: 0
+ }
+])
+
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ tabType: 0,
+ name: '',
+ categoryId: undefined,
+ createTime: undefined
+}) // 鏌ヨ鍙傛暟
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗昍ef
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductSpuApi.getSpuPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒囨崲 Tab */
+const handleTabClick = (tab: TabsPaneContext) => {
+ queryParams.value.tabType = tab.paneName as number
+ getList()
+}
+
+/** 鑾峰緱姣忎釜 Tab 鐨勬暟閲� */
+const getTabsCount = async () => {
+ const res = await ProductSpuApi.getTabsCount()
+ for (let objName in res) {
+ tabsData.value[Number(objName)].count = res[objName]
+ }
+}
+
+/** 娣诲姞鍒颁粨搴� / 鍥炴敹绔欑殑鐘舵�� */
+const handleStatus02Change = async (row: any, newStatus: number) => {
+ try {
+ // 浜屾纭
+ const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '鍔犲叆鍒板洖鏀剁珯' : '鎭㈠鍒颁粨搴�'
+ await message.confirm(`纭瑕�"${row.name}"${text}鍚楋紵`)
+ // 鍙戣捣淇敼
+ await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
+ message.success(text + '鎴愬姛')
+ // 鍒锋柊 tabs 鏁版嵁
+ await getTabsCount()
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鏇存柊涓婃灦/涓嬫灦鐘舵�� */
+const handleStatusChange = async (row: any) => {
+ try {
+ // 浜屾纭
+ const text = row.status ? '涓婃灦' : '涓嬫灦'
+ await message.confirm(`纭瑕�${text}"${row.name}"鍚楋紵`)
+ // 鍙戣捣淇敼
+ await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
+ message.success(text + '鎴愬姛')
+ // 鍒锋柊 tabs 鏁版嵁
+ await getTabsCount()
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ // 寮傚父鏃讹紝闇�瑕侀噸缃洖涔嬪墠鐨勫��
+ row.status =
+ row.status === ProductSpuStatusEnum.DISABLE.status
+ ? ProductSpuStatusEnum.ENABLE.status
+ : ProductSpuStatusEnum.DISABLE.status
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ProductSpuApi.deleteSpu(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊tabs鏁版嵁
+ await getTabsCount()
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鏂板鎴栦慨鏀� */
+const openForm = (id?: number) => {
+ // 淇敼
+ if (typeof id === 'number') {
+ push({ name: 'ProductSpuEdit', params: { id } })
+ return
+ }
+ // 鏂板
+ push({ name: 'ProductSpuAdd' })
+}
+
+/** 鏌ョ湅鍟嗗搧璇︽儏 */
+const openDetail = (id: number) => {
+ push({ name: 'ProductSpuDetail', params: { id } })
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await ProductSpuApi.exportSpu(queryParams.value)
+ download.excel(data, '鍟嗗搧鍒楄〃.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鑾峰彇鍒嗙被鐨勮妭鐐圭殑瀹屾暣缁撴瀯 */
+const categoryList = ref() // 鍒嗙被鏍�
+const formatCategoryName = (categoryId: number) => {
+ return treeToString(categoryList.value, categoryId)
+}
+
+/** 婵�娲绘椂 */
+onActivated(() => {
+ getList()
+})
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ // 瑙f瀽璺敱鐨� categoryId
+ if (route.query.categoryId) {
+ queryParams.value.categoryId = route.query.categoryId
+ }
+ // 鑾峰緱鍟嗗搧淇℃伅
+ await getTabsCount()
+ await getList()
+ // 鑾峰緱鍒嗙被鏍�
+ const data = await ProductCategoryApi.getCategoryList({})
+ categoryList.value = handleTree(data, 'id', 'parentId')
+})
+</script>
+<style lang="scss" scoped>
+.spu-table-expand {
+ padding-left: 42px;
+
+ :deep(.el-form-item__label) {
+ width: 82px;
+ font-weight: bold;
+ color: #99a9bf;
+ }
+}
+</style>
diff --git a/src/views/mall/promotion/article/ArticleForm.vue b/src/views/mall/promotion/article/ArticleForm.vue
new file mode 100644
index 0000000..3f269cb
--- /dev/null
+++ b/src/views/mall/promotion/article/ArticleForm.vue
@@ -0,0 +1,225 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="70%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="110px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鏂囩珷鏍囬" prop="title">
+ <el-input v-model="formData.title" placeholder="璇疯緭鍏ユ枃绔犳爣棰�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏂囩珷鍒嗙被" prop="categoryId">
+ <el-select v-model="formData.categoryId" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in categoryList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏂囩珷浣滆��" prop="author">
+ <el-input v-model="formData.author" placeholder="璇疯緭鍏ユ枃绔犱綔鑰�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏂囩珷绠�浠�" prop="introduction">
+ <el-input v-model="formData.introduction" placeholder="璇疯緭鍏ユ枃绔犵畝浠�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鏂囩珷灏侀潰" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" height="80px" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄惁鐑棬" prop="recommendHot">
+ <el-radio-group v-model="formData.recommendHot">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄惁杞挱鍥�" prop="recommendBanner">
+ <el-radio-group v-model="formData.recommendBanner">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鍟嗗搧鍏宠仈" prop="spuId">
+ <el-tag v-if="formData.spuId" class="mr-10px">
+ {{ spuList.find((item) => item.id === formData.spuId)?.name }}
+ </el-tag>
+ <el-button @click="spuSelectRef?.open()">閫夋嫨鍟嗗搧</el-button>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鏂囩珷鍐呭">
+ <Editor v-model="formData.content" height="150px" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SpuSelect ref="spuSelectRef" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import * as ArticleApi from '@/api/mall/promotion/article'
+import * as ArticleCategoryApi from '@/api/mall/promotion/articleCategory'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { SpuSelect } from '@/views/mall/promotion/components'
+
+defineOptions({ name: 'PromotionArticleForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ categoryId: undefined,
+ title: undefined,
+ author: undefined,
+ picUrl: undefined,
+ introduction: undefined,
+ sort: 0,
+ status: 0,
+ spuId: 0,
+ recommendHot: false,
+ recommendBanner: false,
+ content: undefined
+})
+const formRules = reactive({
+ categoryId: [{ required: true, message: '鍒嗙被id涓嶈兘涓虹┖', trigger: 'blur' }],
+ title: [{ required: true, message: '鏂囩珷鏍囬涓嶈兘涓虹┖', trigger: 'blur' }],
+ picUrl: [{ required: true, message: '鏂囩珷灏侀潰鍥剧墖鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ spuId: [{ required: true, message: '鍟嗗搧鍏宠仈id涓嶈兘涓虹┖', trigger: 'blur' }],
+ recommendHot: [{ required: true, message: '鏄惁鐑棬(灏忕▼搴�)涓嶈兘涓虹┖', trigger: 'blur' }],
+ recommendBanner: [{ required: true, message: '鏄惁杞挱鍥�(灏忕▼搴�)涓嶈兘涓虹┖', trigger: 'blur' }],
+ content: [{ required: true, message: '鏂囩珷鍐呭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const spuSelectRef = ref() // 鍟嗗搧鍜屽睘鎬ч�夋嫨 Ref
+const selectSpu = (spuId: number) => {
+ formData.value.spuId = spuId
+}
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ArticleApi.getArticle(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ArticleApi.ArticleVO
+ if (formType.value === 'create') {
+ await ArticleApi.createArticle(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ArticleApi.updateArticle(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ categoryId: undefined,
+ title: undefined,
+ author: undefined,
+ picUrl: undefined,
+ introduction: undefined,
+ sort: 0,
+ status: 0,
+ spuId: 0,
+ recommendHot: false,
+ recommendBanner: false,
+ content: undefined
+ }
+ formRef.value?.resetFields()
+}
+
+const categoryList = ref<ArticleCategoryApi.ArticleCategoryVO[]>([])
+const spuList = ref<ProductSpuApi.Spu[]>([])
+onMounted(async () => {
+ categoryList.value =
+ (await ArticleCategoryApi.getSimpleArticleCategoryList()) as ArticleCategoryApi.ArticleCategoryVO[]
+ spuList.value = (await ProductSpuApi.getSpuSimpleList()) as ProductSpuApi.Spu[]
+})
+</script>
diff --git a/src/views/mall/promotion/article/category/ArticleCategoryForm.vue b/src/views/mall/promotion/article/category/ArticleCategoryForm.vue
new file mode 100644
index 0000000..2e6e9e2
--- /dev/null
+++ b/src/views/mall/promotion/article/category/ArticleCategoryForm.vue
@@ -0,0 +1,122 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戝唴瀹圭鐞�" url="https://doc.iocoder.cn/mall/promotion-content/" />
+
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="鍒嗙被鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ垎绫诲悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍥炬爣鍦板潃" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" height="80px" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as ArticleCategoryApi from '@/api/mall/promotion/articleCategory'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'PromotionArticleCategoryForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ picUrl: undefined,
+ status: undefined,
+ sort: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍒嗙被鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ArticleCategoryApi.getArticleCategory(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ArticleCategoryApi.ArticleCategoryVO
+ if (formType.value === 'create') {
+ await ArticleCategoryApi.createArticleCategory(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ArticleCategoryApi.updateArticleCategory(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ picUrl: undefined,
+ status: CommonStatusEnum.ENABLE,
+ sort: 0
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/article/category/index.vue b/src/views/mall/promotion/article/category/index.vue
new file mode 100644
index 0000000..73d1420
--- /dev/null
+++ b/src/views/mall/promotion/article/category/index.vue
@@ -0,0 +1,199 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙被鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ垎绫诲悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:article-category:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="缂栧彿" prop="id" min-width="100" />
+ <el-table-column align="center" label="鍒嗙被鍚嶇О" prop="name" min-width="240" />
+ <el-table-column label="鍒嗙被鍥惧浘" min-width="80">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐘舵��" prop="status" min-width="150">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎺掑簭" prop="sort" min-width="150" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:article-category:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:article-category:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ArticleCategoryForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ArticleCategoryApi from '@/api/mall/promotion/articleCategory'
+import ArticleCategoryForm from './ArticleCategoryForm.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+
+defineOptions({ name: 'PromotionArticleCategory' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鍒嗙被鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ArticleCategoryApi.getArticleCategoryPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ArticleCategoryApi.deleteArticleCategory(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/article/index.vue b/src/views/mall/promotion/article/index.vue
new file mode 100644
index 0000000..875168e
--- /dev/null
+++ b/src/views/mall/promotion/article/index.vue
@@ -0,0 +1,230 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戝唴瀹圭鐞�" url="https://doc.iocoder.cn/mall/promotion-content/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="80px"
+ >
+ <el-form-item label="鏂囩珷鍒嗙被" prop="categoryId">
+ <el-select
+ v-model="queryParams.categoryId"
+ class="!w-240px"
+ placeholder="鍏ㄩ儴"
+ @keyup.enter="handleQuery"
+ >
+ <el-option
+ v-for="item in categoryList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏂囩珷鏍囬" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ枃绔犳爣棰�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:article:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="ID" min-width="180" prop="id" />
+ <el-table-column align="center" label="灏侀潰" min-width="80" prop="picUrl">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鏍囬" min-width="180" prop="title" />
+ <el-table-column align="center" label="鍒嗙被" min-width="180" prop="categoryId">
+ <template #default="scope">
+ {{ categoryList.find((item) => item.id === scope.row.categoryId)?.name }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="娴忚閲�" min-width="180" prop="browseCount" />
+ <el-table-column align="center" label="浣滆��" min-width="180" prop="author" />
+ <el-table-column align="center" label="鏂囩珷绠�浠�" min-width="250" prop="introduction" />
+ <el-table-column align="center" label="鎺掑簭" min-width="60" prop="sort" />
+ <el-table-column align="center" label="鐘舵��" min-width="60" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍙戝竷鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="120">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:article:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:article:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ArticleForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ArticleApi from '@/api/mall/promotion/article'
+import ArticleForm from './ArticleForm.vue'
+import * as ArticleCategoryApi from '@/api/mall/promotion/articleCategory'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { createImageViewer } from '@/components/ImageViewer'
+
+defineOptions({ name: 'PromotionArticle' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ categoryId: undefined,
+ title: null,
+ status: undefined,
+ spuId: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+/** 鏂囩珷灏侀潰棰勮 */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ArticleApi.getArticlePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ArticleApi.deleteArticle(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+const categoryList = ref<ArticleCategoryApi.ArticleCategoryVO[]>([])
+const spuList = ref<ProductSpuApi.Spu[]>([])
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇鍒嗙被銆佸晢鍝佸垪琛�
+ categoryList.value =
+ (await ArticleCategoryApi.getSimpleArticleCategoryList()) as ArticleCategoryApi.ArticleCategoryVO[]
+ spuList.value = (await ProductSpuApi.getSpuSimpleList()) as ProductSpuApi.Spu[]
+})
+</script>
diff --git a/src/views/mall/promotion/banner/BannerForm.vue b/src/views/mall/promotion/banner/BannerForm.vue
new file mode 100644
index 0000000..9d1b7fc
--- /dev/null
+++ b/src/views/mall/promotion/banner/BannerForm.vue
@@ -0,0 +1,159 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鏍囬" prop="title">
+ <el-input v-model="formData.title" placeholder="璇疯緭鍏� Banner 鏍囬" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鍥剧墖" prop="picUrl">
+ <UploadImg v-model="formData.picUrl" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璺宠浆鍦板潃" prop="url">
+ <el-input v-model="formData.url" placeholder="璇疯緭鍏ヨ烦杞湴鍧�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="浣嶇疆" prop="position">
+ <el-radio-group v-model="formData.position">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鎻忚堪" prop="memo">
+ <el-input v-model="formData.memo" placeholder="璇疯緭鍏ユ弿杩�" type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as BannerApi from '@/api/mall/market/banner'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ title: undefined,
+ picUrl: undefined,
+ status: 0,
+ position: 1,
+ url: undefined,
+ sort: 0,
+ memo: undefined
+})
+const formRules = reactive({
+ title: [{ required: true, message: 'Banner 鏍囬涓嶈兘涓虹┖', trigger: 'blur' }],
+ picUrl: [{ required: true, message: '鍥剧墖 URL 涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '娲诲姩鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ position: [{ required: true, message: '浣嶇疆涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ url: [{ required: true, message: '璺宠浆鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await BannerApi.getBanner(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as BannerApi.BannerVO
+ if (formType.value === 'create') {
+ await BannerApi.createBanner(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await BannerApi.updateBanner(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ title: undefined,
+ picUrl: undefined,
+ status: 0,
+ position: 1,
+ url: undefined,
+ sort: 0,
+ memo: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/banner/index.vue b/src/views/mall/promotion/banner/index.vue
new file mode 100644
index 0000000..e25431a
--- /dev/null
+++ b/src/views/mall/promotion/banner/index.vue
@@ -0,0 +1,206 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戝唴瀹圭鐞�" url="https://doc.iocoder.cn/mall/promotion-content/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="100px"
+ >
+ <el-form-item label="Banner鏍囬" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏anner鏍囬"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:banner:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="Banner鏍囬" prop="title" />
+ <el-table-column align="center" label="鍥剧墖" min-width="80" prop="picUrl">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹氫綅" prop="position">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_BANNER_POSITION" :value="scope.row.position" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="璺宠浆鍦板潃" prop="url" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎺掑簭" prop="sort" />
+ <el-table-column align="center" label="鎻忚堪" prop="memo" />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:banner:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:banner:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BannerForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as BannerApi from '@/api/mall/market/banner'
+import BannerForm from './BannerForm.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+
+defineOptions({ name: 'Banner' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ title: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏂囩珷灏侀潰棰勮 */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BannerApi.getBannerPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await BannerApi.deleteBanner(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/bargain/activity/BargainActivityForm.vue b/src/views/mall/promotion/bargain/activity/BargainActivityForm.vue
new file mode 100644
index 0000000..d8d1463
--- /dev/null
+++ b/src/views/mall/promotion/bargain/activity/BargainActivityForm.vue
@@ -0,0 +1,233 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <Form
+ ref="formRef"
+ v-loading="formLoading"
+ :is-col="true"
+ :rules="rules"
+ :schema="allSchemas.formSchema"
+ class="mt-10px"
+ >
+ <template #spuId>
+ <el-button @click="spuSelectRef.open()">閫夋嫨鍟嗗搧</el-button>
+ <SpuAndSkuList
+ ref="spuAndSkuListRef"
+ :rule-config="ruleConfig"
+ :spu-list="spuList"
+ :spu-property-list-p="spuPropertyList"
+ >
+ <el-table-column align="center" label="鐮嶄环璧峰浠锋牸(鍏�)" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number
+ v-model="sku.productConfig.bargainFirstPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐮嶄环搴曚环(鍏�)" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number
+ v-model="sku.productConfig.bargainMinPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="娲诲姩搴撳瓨" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number v-model="sku.productConfig.stock" class="w-100%" />
+ </template>
+ </el-table-column>
+ </SpuAndSkuList>
+ </template>
+ </Form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SpuSelect ref="spuSelectRef" :isSelectSku="true" :radio="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
+import { BargainProductVO } from '@/api/mall/promotion/bargain/bargainActivity'
+import { allSchemas, rules } from './bargainActivity.data'
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/components'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { convertToInteger, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'PromotionBargainActivityForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// ================= 鍟嗗搧閫夋嫨鐩稿叧 =================
+
+const spuSelectRef = ref() // 鍟嗗搧鍜屽睘鎬ч�夋嫨 Ref
+const spuAndSkuListRef = ref() // sku 绉掓潃閰嶇疆缁勪欢Ref
+const spuList = ref<BargainActivityApi.SpuExtension[]>([]) // 閫夋嫨鐨� spu
+const spuPropertyList = ref<SpuProperty<BargainActivityApi.SpuExtension>[]>([])
+const ruleConfig: RuleConfig[] = [
+ {
+ name: 'productConfig.bargainFirstPrice',
+ rule: (arg) => arg > 0,
+ message: '鍟嗗搧鐮嶄环璧峰浠锋牸涓嶈兘灏忎簬 0 锛侊紒锛�'
+ },
+ {
+ name: 'productConfig.bargainMinPrice',
+ rule: (arg) => arg >= 0,
+ message: '鍟嗗搧鐮嶄环搴曚环涓嶈兘灏忎簬 0 锛侊紒锛�'
+ },
+ {
+ name: 'productConfig.stock',
+ rule: (arg) => arg >= 1,
+ message: '鍟嗗搧娲诲姩搴撳瓨涓嶈兘灏忎簬 1 锛侊紒锛�'
+ }
+]
+const selectSpu = (spuId: number, skuIds: number[]) => {
+ formRef.value.setValues({ spuId })
+ getSpuDetails(spuId, skuIds)
+}
+/**
+ * 鑾峰彇 SPU 璇︽儏
+ */
+const getSpuDetails = async (
+ spuId: number,
+ skuIds: number[] | undefined,
+ products?: BargainProductVO[]
+) => {
+ const spuProperties: SpuProperty<BargainActivityApi.SpuExtension>[] = []
+ const res = (await ProductSpuApi.getSpuDetailList([spuId])) as BargainActivityApi.SpuExtension[]
+ if (res.length == 0) {
+ return
+ }
+ spuList.value = []
+ // 鍥犱负鍙兘閫夋嫨涓�涓�
+ const spu = res[0]
+ const selectSkus =
+ typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+ selectSkus?.forEach((sku) => {
+ let config: BargainProductVO = {
+ spuId: spu.id!,
+ skuId: sku.id!,
+ bargainFirstPrice: 1,
+ bargainMinPrice: 1,
+ stock: 1
+ }
+ if (typeof products !== 'undefined') {
+ const product = products.find((item) => item.skuId === sku.id)
+ if (product) {
+ product.bargainFirstPrice = formatToFraction(product.bargainFirstPrice)
+ product.bargainMinPrice = formatToFraction(product.bargainMinPrice)
+ }
+ config = product || config
+ }
+ sku.productConfig = config
+ })
+ spu.skus = selectSkus as BargainActivityApi.SkuExtension[]
+ spuProperties.push({
+ spuId: spu.id!,
+ spuDetail: spu,
+ propertyList: getPropertyList(spu)
+ })
+ spuList.value.push(spu)
+ spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ await resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = (await BargainActivityApi.getBargainActivity(
+ id
+ )) as BargainActivityApi.BargainActivityVO
+ // 鐢ㄦ埛姣忔鐮嶄环閲戦鍒嗚浆鍏�, 鍒嗚浆鍏�
+ data.randomMinPrice = formatToFraction(data.randomMinPrice)
+ data.randomMaxPrice = formatToFraction(data.randomMaxPrice)
+ // 瀵归綈娲诲姩鍟嗗搧澶勭悊缁撴瀯
+ await getSpuDetails(
+ data.spuId!,
+ [data.skuId],
+ [
+ {
+ spuId: data.spuId!,
+ skuId: data.skuId,
+ bargainFirstPrice: data.bargainFirstPrice, // 鐮嶄环璧峰浠锋牸锛屽崟浣嶅垎
+ bargainMinPrice: data.bargainMinPrice, // 鐮嶄环搴曚环
+ stock: data.stock // 娲诲姩搴撳瓨
+ }
+ ]
+ )
+ formRef.value.setValues(data)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ spuList.value = []
+ spuPropertyList.value = []
+ await nextTick()
+ formRef.value.getElFormRef().resetFields()
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.getElFormRef().validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = cloneDeep(formRef.value.formModel) as BargainActivityApi.BargainActivityVO
+ const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
+ products.forEach((item: BargainProductVO) => {
+ // 鐮嶄环浠锋牸鍏冭浆鍒�
+ item.bargainFirstPrice = convertToInteger(item.bargainFirstPrice)
+ item.bargainMinPrice = convertToInteger(item.bargainMinPrice)
+ })
+ // 鐢ㄦ埛姣忔鐮嶄环閲戦鍒嗚浆鍏�, 鍏冭浆鍒�
+ data.randomMinPrice = convertToInteger(data.randomMinPrice)
+ data.randomMaxPrice = convertToInteger(data.randomMaxPrice)
+ const formData = { ...data, ...products[0] }
+ if (formType.value === 'create') {
+ await BargainActivityApi.createBargainActivity(formData)
+ message.success(t('common.createSuccess'))
+ } else {
+ await BargainActivityApi.updateBargainActivity(formData)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+</script>
diff --git a/src/views/mall/promotion/bargain/activity/bargainActivity.data.ts b/src/views/mall/promotion/bargain/activity/bargainActivity.data.ts
new file mode 100644
index 0000000..2b124c4
--- /dev/null
+++ b/src/views/mall/promotion/bargain/activity/bargainActivity.data.ts
@@ -0,0 +1,146 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+
+// 琛ㄥ崟鏍¢獙
+export const rules = reactive({
+ name: [required],
+ startTime: [required],
+ endTime: [required],
+ helpMaxCount: [required],
+ bargainCount: [required],
+ singleLimitCount: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+ {
+ label: '鐮嶄环娲诲姩鍚嶇О',
+ field: 'name',
+ isSearch: true,
+ isTable: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ }
+ },
+ {
+ label: '娲诲姩寮�濮嬫椂闂�',
+ field: 'startTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩缁撴潫鏃堕棿',
+ field: 'endTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '鐮嶄环浜烘暟',
+ field: 'helpMaxCount',
+ isSearch: false,
+ form: {
+ component: 'InputNumber',
+ labelMessage: '鍙備笌浜烘暟涓嶈兘灏戜簬涓や汉',
+ value: 2
+ }
+ },
+ {
+ label: '鏈�澶у府鐮嶆鏁�',
+ field: 'bargainCount',
+ isSearch: false,
+ form: {
+ component: 'InputNumber',
+ labelMessage: '鍙備笌浜烘暟涓嶈兘灏戜簬涓や汉',
+ value: 2
+ }
+ },
+ {
+ label: '鎬婚檺璐暟閲�',
+ field: 'totalLimitCount',
+ isSearch: false,
+ form: {
+ component: 'InputNumber',
+ labelMessage: '鐢ㄦ埛鏈�澶ц兘鍙戣捣鐮嶄环鐨勬鏁�',
+ value: 0
+ }
+ },
+ {
+ label: '鐮嶄环鐨勬渶灏忛噾棰�',
+ field: 'randomMinPrice',
+ isSearch: false,
+ isTable: false,
+ form: {
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.1
+ },
+ labelMessage: '鐢ㄦ埛姣忔鐮嶄环鐨勬渶灏忛噾棰�',
+ value: 0
+ }
+ },
+ {
+ label: '鐮嶄环鐨勬渶澶ч噾棰�',
+ field: 'randomMaxPrice',
+ isSearch: false,
+ isTable: false,
+ form: {
+ component: 'InputNumber',
+ componentProps: {
+ min: 0,
+ precision: 2,
+ step: 0.1
+ },
+ labelMessage: '鐢ㄦ埛姣忔鐮嶄环鐨勬渶澶ч噾棰�',
+ value: 0
+ }
+ },
+ {
+ label: '鐮嶄环鍟嗗搧',
+ field: 'spuId',
+ isSearch: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ }
+ }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)
diff --git a/src/views/mall/promotion/bargain/activity/index.vue b/src/views/mall/promotion/bargain/activity/index.vue
new file mode 100644
index 0000000..2e4f1bf
--- /dev/null
+++ b/src/views/mall/promotion/bargain/activity/index.vue
@@ -0,0 +1,233 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戠爫浠锋椿鍔�" url="https://doc.iocoder.cn/mall/promotion-bargain/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:bargain-activity:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="娲诲姩缂栧彿" prop="id" min-width="80" />
+ <el-table-column label="娲诲姩鍚嶇О" prop="name" min-width="140" />
+ <el-table-column label="娲诲姩鏃堕棿" min-width="210">
+ <template #default="scope">
+ {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+ ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍥剧墖" prop="spuName" min-width="80">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ :preview-src-list="[scope.row.picUrl]"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" prop="spuName" min-width="300" />
+ <el-table-column
+ label="璧峰浠锋牸"
+ prop="bargainFirstPrice"
+ min-width="100"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column
+ label="鐮嶄环搴曚环"
+ prop="bargainMinPrice"
+ min-width="100"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鎬荤爫浠蜂汉鏁�" prop="recordUserCount" min-width="100" />
+ <el-table-column label="鎴愬姛鐮嶄环浜烘暟" prop="recordSuccessUserCount" min-width="110" />
+ <el-table-column label="鍔╁姏浜烘暟" prop="helpUserCount" min-width="100" />
+ <el-table-column label="娲诲姩鐘舵��" align="center" prop="status" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" align="center" prop="stock" min-width="80" />
+ <el-table-column label="鎬诲簱瀛�" align="center" prop="totalStock" min-width="80" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="150px" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['promotion:bargain-activity:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleClose(scope.row.id)"
+ v-if="scope.row.status === 0"
+ v-hasPermi="['promotion:bargain-activity:close']"
+ >
+ 鍏抽棴
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-else
+ v-hasPermi="['promotion:bargain-activity:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BargainActivityForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
+import BargainActivityForm from './BargainActivityForm.vue'
+import { formatDate } from '@/utils/formatTime'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+defineOptions({ name: 'PromotionBargainActivity' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BargainActivityApi.getBargainActivityPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+// TODO 鑺嬭壙锛氳繖閲岃鏀逛笅
+/** 鍏抽棴鎸夐挳鎿嶄綔 */
+const handleClose = async (id: number) => {
+ try {
+ // 鍏抽棴鐨勪簩娆$‘璁�
+ await message.confirm('纭鍏抽棴璇ョ爫浠锋椿鍔ㄥ悧锛�')
+ // 鍙戣捣鍏抽棴
+ await BargainActivityApi.closeBargainActivity(id)
+ message.success('鍏抽棴鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await BargainActivityApi.closeBargainActivity(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+})
+</script>
diff --git a/src/views/mall/promotion/bargain/record/BargainRecordListDialog.vue b/src/views/mall/promotion/bargain/record/BargainRecordListDialog.vue
new file mode 100644
index 0000000..9637ac8
--- /dev/null
+++ b/src/views/mall/promotion/bargain/record/BargainRecordListDialog.vue
@@ -0,0 +1,90 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍔╁姏鍒楄〃">
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鐢ㄦ埛缂栧彿" prop="userId" min-width="80px" />
+ <el-table-column label="鐢ㄦ埛澶村儚" prop="avatar" min-width="80px">
+ <template #default="scope">
+ <el-avatar :src="scope.row.avatar" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛鏄电О" prop="nickname" min-width="100px" />
+ <el-table-column
+ label="鐮嶄环閲戦"
+ prop="reducePrice"
+ min-width="100px"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column
+ label="鍔╁姏鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BargainHelpApi from '@/api/mall/promotion/bargain/bargainHelp'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+/** 鍔╁姏鍒楄〃 */
+defineOptions({ name: 'BargainRecordListDialog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ recordId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鎵撳紑寮圭獥 */
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const open = async (recordId: any) => {
+ dialogVisible.value = true
+ queryParams.recordId = recordId
+ resetQuery()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BargainHelpApi.getBargainHelpPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+</script>
diff --git a/src/views/mall/promotion/bargain/record/index.vue b/src/views/mall/promotion/bargain/record/index.vue
new file mode 100644
index 0000000..306d8ea
--- /dev/null
+++ b/src/views/mall/promotion/bargain/record/index.vue
@@ -0,0 +1,197 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戠爫浠锋椿鍔�" url="https://doc.iocoder.cn/mall/promotion-bargain/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐮嶄环鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鐮嶄环鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:bargain-record:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['promotion:bargain-record:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" min-width="50" prop="id" />
+ <el-table-column label="鍙戣捣鐢ㄦ埛" min-width="120">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.avatar"
+ class="h-20px w-20px"
+ :preview-src-list="[scope.row.avatar]"
+ preview-teleported
+ />
+ {{ scope.row.nickname }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戣捣鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鐮嶄环娲诲姩" min-width="150" prop="activity.name" />
+ <el-table-column
+ label="鏈�浣庝环"
+ min-width="100"
+ prop="activity.bargainMinPrice"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column
+ label="褰撳墠浠�"
+ min-width="100"
+ prop="bargainPrice"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鎬荤爫浠锋鏁�" min-width="100" prop="activity.helpMaxCount" />
+ <el-table-column label="鍓╀綑鐮嶄环娆℃暟" min-width="100" prop="helpCount" />
+ <el-table-column label="鐮嶄环鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="缁撴潫鏃堕棿"
+ align="center"
+ prop="endTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="璁㈠崟缂栧彿" align="center" prop="orderId" />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openRecordListDialog(scope.row.id)"
+ v-hasPermi="['promotion:bargain-help:query']"
+ >
+ 鍔╁姏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥 -->
+ <BargainRecordListDialog ref="recordListDialogRef" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as BargainRecordApi from '@/api/mall/promotion/bargain/bargainRecord'
+import { fenToYuanFormat } from '@/utils/formatter'
+import BargainRecordListDialog from './BargainRecordListDialog.vue'
+
+defineOptions({ name: 'PromotionBargainRecord' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BargainRecordApi.getBargainRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎵撳紑[鍔╁姏]寮圭獥 */
+const recordListDialogRef = ref()
+const openRecordListDialog = (id?: number) => {
+ recordListDialogRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/combination/activity/CombinationActivityForm.vue b/src/views/mall/promotion/combination/activity/CombinationActivityForm.vue
new file mode 100644
index 0000000..5b6e582
--- /dev/null
+++ b/src/views/mall/promotion/combination/activity/CombinationActivityForm.vue
@@ -0,0 +1,187 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <Form
+ ref="formRef"
+ v-loading="formLoading"
+ :is-col="true"
+ :rules="rules"
+ :schema="allSchemas.formSchema"
+ class="mt-10px"
+ >
+ <template #spuId>
+ <el-button @click="spuSelectRef.open()">閫夋嫨鍟嗗搧</el-button>
+ <SpuAndSkuList
+ ref="spuAndSkuListRef"
+ :rule-config="ruleConfig"
+ :spu-list="spuList"
+ :spu-property-list-p="spuPropertyList"
+ >
+ <el-table-column align="center" label="鎷煎洟浠锋牸(鍏�)" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number
+ v-model="sku.productConfig.combinationPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ />
+ </template>
+ </el-table-column>
+ </SpuAndSkuList>
+ </template>
+ </Form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { CombinationProductVO } from '@/api/mall/promotion/combination/combinationActivity'
+import { allSchemas, rules } from './combinationActivity.data'
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/components'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { convertToInteger, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'PromotionCombinationActivityForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// ================= 鍟嗗搧閫夋嫨鐩稿叧 =================
+
+const spuSelectRef = ref() // 鍟嗗搧鍜屽睘鎬ч�夋嫨 Ref
+const spuAndSkuListRef = ref() // sku 绉掓潃閰嶇疆缁勪欢Ref
+const spuList = ref<CombinationActivityApi.SpuExtension[]>([]) // 閫夋嫨鐨� spu
+const spuPropertyList = ref<SpuProperty<CombinationActivityApi.SpuExtension>[]>([])
+const ruleConfig: RuleConfig[] = [
+ {
+ name: 'productConfig.combinationPrice',
+ rule: (arg) => arg >= 0.01,
+ message: '鍟嗗搧鎷煎洟浠锋牸涓嶈兘灏忎簬0.01 锛侊紒锛�'
+ }
+]
+const selectSpu = (spuId: number, skuIds: number[]) => {
+ formRef.value.setValues({ spuId })
+ getSpuDetails(spuId, skuIds)
+}
+/**
+ * 鑾峰彇 SPU 璇︽儏
+ */
+const getSpuDetails = async (
+ spuId: number,
+ skuIds: number[] | undefined,
+ products?: CombinationProductVO[]
+) => {
+ const spuProperties: SpuProperty<CombinationActivityApi.SpuExtension>[] = []
+ const res = (await ProductSpuApi.getSpuDetailList([
+ spuId
+ ])) as CombinationActivityApi.SpuExtension[]
+ if (res.length == 0) {
+ return
+ }
+ spuList.value = []
+ // 鍥犱负鍙兘閫夋嫨涓�涓�
+ const spu = res[0]
+ const selectSkus =
+ typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+ selectSkus?.forEach((sku) => {
+ let config: CombinationProductVO = {
+ spuId: spu.id!,
+ skuId: sku.id!,
+ combinationPrice: 0
+ }
+ if (typeof products !== 'undefined') {
+ const product = products.find((item) => item.skuId === sku.id)
+ if (product) {
+ product.combinationPrice = formatToFraction(product.combinationPrice)
+ }
+ config = product || config
+ }
+ sku.productConfig = config
+ })
+ spu.skus = selectSkus as CombinationActivityApi.SkuExtension[]
+ spuProperties.push({
+ spuId: spu.id!,
+ spuDetail: spu,
+ propertyList: getPropertyList(spu)
+ })
+ spuList.value.push(spu)
+ spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ await resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = (await CombinationActivityApi.getCombinationActivity(
+ id
+ )) as CombinationActivityApi.CombinationActivityVO
+ await getSpuDetails(data.spuId!, data.products?.map((sku) => sku.skuId), data.products)
+ formRef.value.setValues(data)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ spuList.value = []
+ spuPropertyList.value = []
+ await nextTick()
+ formRef.value.getElFormRef().resetFields()
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.getElFormRef().validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鑾峰緱鎷煎洟鍟嗗搧閰嶇疆
+ const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
+ products.forEach((item: CombinationActivityApi.CombinationProductVO) => {
+ item.combinationPrice = convertToInteger(item.combinationPrice)
+ })
+ const data = cloneDeep(formRef.value.formModel) as CombinationActivityApi.CombinationActivityVO
+ data.products = products
+ // 鐪熸鎻愪氦
+ if (formType.value === 'create') {
+ await CombinationActivityApi.createCombinationActivity(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await CombinationActivityApi.updateCombinationActivity(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+</script>
diff --git a/src/views/mall/promotion/combination/activity/combinationActivity.data.ts b/src/views/mall/promotion/combination/activity/combinationActivity.data.ts
new file mode 100644
index 0000000..dd3e48f
--- /dev/null
+++ b/src/views/mall/promotion/combination/activity/combinationActivity.data.ts
@@ -0,0 +1,140 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+
+// 琛ㄥ崟鏍¢獙
+export const rules = reactive({
+ name: [required],
+ totalLimitCount: [required],
+ singleLimitCount: [required],
+ startTime: [required],
+ endTime: [required],
+ userSize: [required],
+ limitDuration: [required],
+ virtualGroup: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+ {
+ label: '鎷煎洟鍚嶇О',
+ field: 'name',
+ isSearch: true,
+ isTable: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ }
+ },
+ {
+ label: '娲诲姩寮�濮嬫椂闂�',
+ field: 'startTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩缁撴潫鏃堕棿',
+ field: 'endTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '鍙備笌浜烘暟',
+ field: 'userSize',
+ isSearch: false,
+ form: {
+ component: 'InputNumber',
+ labelMessage: '鍙備笌浜烘暟涓嶈兘灏戜簬涓や汉',
+ value: 2
+ }
+ },
+ {
+ label: '闄愬埗鏃堕暱',
+ field: 'limitDuration',
+ isSearch: false,
+ isTable: false,
+ form: {
+ component: 'InputNumber',
+ labelMessage: '闄愬埗鏃堕暱(灏忔椂)',
+ componentProps: {
+ placeholder: '璇疯緭鍏ラ檺鍒舵椂闀�(灏忔椂)'
+ }
+ }
+ },
+ {
+ label: '鎬婚檺璐暟閲�',
+ field: 'totalLimitCount',
+ isSearch: false,
+ isTable: false,
+ form: {
+ component: 'InputNumber',
+ value: 0
+ }
+ },
+ {
+ label: '鍗曟闄愯喘鏁伴噺',
+ field: 'singleLimitCount',
+ isSearch: false,
+ isTable: false,
+ form: {
+ component: 'InputNumber',
+ value: 0
+ }
+ },
+ {
+ label: '铏氭嫙鎴愬洟',
+ field: 'virtualGroup',
+ dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
+ dictClass: 'boolean',
+ isSearch: true,
+ form: {
+ component: 'Radio',
+ value: false
+ }
+ },
+ {
+ label: '鎷煎洟鍟嗗搧',
+ field: 'spuId',
+ isSearch: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ }
+ }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)
diff --git a/src/views/mall/promotion/combination/activity/index.vue b/src/views/mall/promotion/combination/activity/index.vue
new file mode 100644
index 0000000..a40044d
--- /dev/null
+++ b/src/views/mall/promotion/combination/activity/index.vue
@@ -0,0 +1,240 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戞嫾鍥㈡椿鍔�" url="https://doc.iocoder.cn/mall/promotion-combination/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:combination-activity:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column label="娲诲姩缂栧彿" min-width="80" prop="id" />
+ <el-table-column label="娲诲姩鍚嶇О" min-width="140" prop="name" />
+ <el-table-column label="娲诲姩鏃堕棿" min-width="210">
+ <template #default="scope">
+ {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+ ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍥剧墖" min-width="80" prop="spuName">
+ <template #default="scope">
+ <el-image
+ :preview-src-list="[scope.row.picUrl]"
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" min-width="300" prop="spuName" />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ label="鍘熶环"
+ min-width="100"
+ prop="marketPrice"
+ />
+ <el-table-column label="鎷煎洟浠�" min-width="100" prop="seckillPrice">
+ <template #default="scope">
+ {{ formatCombinationPrice(scope.row.products) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�鍥㈢粍鏁�" min-width="100" prop="groupCount" />
+ <el-table-column label="鎴愬洟缁勬暟" min-width="100" prop="groupSuccessCount" />
+ <el-table-column label="璐拱娆℃暟" min-width="100" prop="recordCount" />
+ <el-table-column align="center" label="娲诲姩鐘舵��" min-width="100" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="150px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:combination-activity:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="scope.row.status === 0"
+ v-hasPermi="['promotion:combination-activity:close']"
+ link
+ type="danger"
+ @click="handleClose(scope.row.id)"
+ >
+ 鍏抽棴
+ </el-button>
+ <el-button
+ v-else
+ v-hasPermi="['promotion:combination-activity:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CombinationActivityForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import CombinationActivityForm from './CombinationActivityForm.vue'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'PromotionBargainActivity' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CombinationActivityApi.getCombinationActivityPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍏抽棴鎸夐挳鎿嶄綔 */
+const handleClose = async (id: number) => {
+ try {
+ // 鍏抽棴鐨勪簩娆$‘璁�
+ await message.confirm('纭鍏抽棴璇ユ嫾鍥㈡椿鍔ㄥ悧锛�')
+ // 鍙戣捣鍏抽棴
+ await CombinationActivityApi.closeCombinationActivity(id)
+ message.success('鍏抽棴鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await CombinationActivityApi.deleteCombinationActivity(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+const formatCombinationPrice = (products) => {
+ const combinationPrice = Math.min(...products.map((item) => item.combinationPrice))
+ return `锟�${fenToYuan(combinationPrice)}`
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+})
+</script>
diff --git a/src/views/mall/promotion/combination/components/CombinationShowcase.vue b/src/views/mall/promotion/combination/components/CombinationShowcase.vue
new file mode 100644
index 0000000..ba88434
--- /dev/null
+++ b/src/views/mall/promotion/combination/components/CombinationShowcase.vue
@@ -0,0 +1,158 @@
+<template>
+ <div class="flex flex-wrap items-center gap-8px">
+ <div
+ v-for="(combinationActivity, index) in Activitys"
+ :key="combinationActivity.id"
+ class="select-box spu-pic"
+ >
+ <el-tooltip :content="combinationActivity.name">
+ <div class="relative h-full w-full">
+ <el-image :src="combinationActivity.picUrl" class="h-full w-full" />
+ <Icon
+ v-show="!disabled"
+ class="del-icon"
+ icon="ep:circle-close-filled"
+ @click="handleRemoveActivity(index)"
+ />
+ </div>
+ </el-tooltip>
+ </div>
+ <el-tooltip content="閫夋嫨娲诲姩" v-if="canAdd">
+ <div class="select-box" @click="openCombinationActivityTableSelect">
+ <Icon icon="ep:plus" />
+ </div>
+ </el-tooltip>
+ </div>
+ <!-- 鎷煎洟娲诲姩閫夋嫨瀵硅瘽妗嗭紙琛ㄦ牸褰㈠紡锛� -->
+ <CombinationTableSelect
+ ref="combinationActivityTableSelectRef"
+ :multiple="limit != 1"
+ @change="handleActivitySelected"
+ />
+</template>
+<script lang="ts" setup>
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { propTypes } from '@/utils/propTypes'
+import { oneOfType } from 'vue-types'
+import { isArray } from '@/utils/is'
+import CombinationTableSelect from '@/views/mall/promotion/combination/components/CombinationTableSelect.vue'
+
+// 娲诲姩姗辩獥锛屼竴鑸敤浜庤淇椂浣跨敤
+// 鎻愪緵鍔熻兘锛氬睍绀烘椿鍔ㄥ垪琛ㄣ�佹坊鍔犳椿鍔ㄣ�佸垹闄ゆ椿鍔�
+defineOptions({ name: 'CombinationShowcase' })
+
+const props = defineProps({
+ modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
+ // 闄愬埗鏁伴噺锛氶粯璁や笉闄愬埗
+ limit: propTypes.number.def(Number.MAX_VALUE),
+ disabled: propTypes.bool.def(false)
+})
+
+// 璁$畻鏄惁鍙互娣诲姞
+const canAdd = computed(() => {
+ // 鎯呭喌涓�锛氱鐢ㄦ椂涓嶅彲浠ユ坊鍔�
+ if (props.disabled) return false
+ // 鎯呭喌浜岋細鏈寚瀹氶檺鍒舵暟閲忔椂锛屽彲浠ユ坊鍔�
+ if (!props.limit) return true
+ // 鎯呭喌涓夛細妫�鏌ュ凡娣诲姞鏁伴噺鏄惁灏忎簬闄愬埗鏁伴噺
+ return Activitys.value.length < props.limit
+})
+
+// 鎷煎洟娲诲姩鍒楄〃
+const Activitys = ref<CombinationActivityApi.CombinationActivityVO[]>([])
+
+watch(
+ () => props.modelValue,
+ async () => {
+ const ids = isArray(props.modelValue)
+ ? // 鎯呭喌涓�锛氬閫�
+ props.modelValue
+ : // 鎯呭喌浜岋細鍗曢��
+ props.modelValue
+ ? [props.modelValue]
+ : []
+ // 涓嶉渶瑕佽繑鏄�
+ if (ids.length === 0) {
+ Activitys.value = []
+ return
+ }
+ // 鍙湁娲诲姩鍙戠敓鍙樺寲涔嬪悗锛屾墠浼氭煡璇㈡椿鍔�
+ if (
+ Activitys.value.length === 0 ||
+ Activitys.value.some((combinationActivity) => !ids.includes(combinationActivity.id!))
+ ) {
+ Activitys.value = await CombinationActivityApi.getCombinationActivityListByIds(ids)
+ }
+ },
+ { immediate: true }
+)
+
+/** 娲诲姩琛ㄦ牸閫夋嫨瀵硅瘽妗� */
+const combinationActivityTableSelectRef = ref()
+// 鎵撳紑瀵硅瘽妗�
+const openCombinationActivityTableSelect = () => {
+ combinationActivityTableSelectRef.value.open(Activitys.value)
+}
+
+/**
+ * 閫夋嫨娲诲姩鍚庤Е鍙�
+ * @param activityVOs 閫変腑鐨勬椿鍔ㄥ垪琛�
+ */
+const handleActivitySelected = (
+ activityVOs:
+ | CombinationActivityApi.CombinationActivityVO
+ | CombinationActivityApi.CombinationActivityVO[]
+) => {
+ Activitys.value = isArray(activityVOs) ? activityVOs : [activityVOs]
+ emitActivityChange()
+}
+
+/**
+ * 鍒犻櫎娲诲姩
+ * @param index 娲诲姩绱㈠紩
+ */
+const handleRemoveActivity = (index: number) => {
+ Activitys.value.splice(index, 1)
+ emitActivityChange()
+}
+const emit = defineEmits(['update:modelValue', 'change'])
+const emitActivityChange = () => {
+ if (props.limit === 1) {
+ const combinationActivity = Activitys.value.length > 0 ? Activitys.value[0] : null
+ emit('update:modelValue', combinationActivity?.id || 0)
+ emit('change', combinationActivity)
+ } else {
+ emit(
+ 'update:modelValue',
+ Activitys.value.map((combinationActivity) => combinationActivity.id)
+ )
+ emit('change', Activitys.value)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.select-box {
+ display: flex;
+ width: 60px;
+ height: 60px;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.spu-pic {
+ position: relative;
+}
+
+.del-icon {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ z-index: 1;
+ width: 20px !important;
+ height: 20px !important;
+}
+</style>
diff --git a/src/views/mall/promotion/combination/components/CombinationTableSelect.vue b/src/views/mall/promotion/combination/components/CombinationTableSelect.vue
new file mode 100644
index 0000000..3639521
--- /dev/null
+++ b/src/views/mall/promotion/combination/components/CombinationTableSelect.vue
@@ -0,0 +1,345 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" title="閫夋嫨娲诲姩" width="70%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+ <!-- 1. 澶氶�夋ā寮忥紙涓嶈兘浣跨敤type="selection"锛孍lement浼氬拷鐣eader鎻掓Ы锛� -->
+ <el-table-column width="55" v-if="multiple">
+ <template #header>
+ <el-checkbox
+ v-model="isCheckAll"
+ :indeterminate="isIndeterminate"
+ @change="handleCheckAll"
+ />
+ </template>
+ <template #default="{ row }">
+ <el-checkbox
+ v-model="checkedStatus[row.id]"
+ @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+ />
+ </template>
+ </el-table-column>
+ <!-- 2. 鍗曢�夋ā寮� -->
+ <el-table-column label="#" width="55" v-else>
+ <template #default="{ row }">
+ <el-radio
+ :value="row.id"
+ v-model="selectedActivityId"
+ @change="handleSingleSelected(row)"
+ >
+ <!-- 绌烘牸涓嶈兘鐪佺暐锛屾槸涓轰簡璁╁崟閫夋涓嶆樉绀簂abel锛屽鏋滀笉鎸囧畾label涓嶄細鏈夐�変腑鐨勬晥鏋� -->
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column label="娲诲姩缂栧彿" prop="id" min-width="80" />
+ <el-table-column label="娲诲姩鍚嶇О" prop="name" min-width="140" />
+ <el-table-column label="娲诲姩鏃堕棿" min-width="210">
+ <template #default="scope">
+ {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+ ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍥剧墖" prop="spuName" min-width="80">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ :preview-src-list="[scope.row.picUrl]"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" prop="spuName" min-width="300" />
+ <el-table-column
+ label="鍘熶环"
+ prop="marketPrice"
+ min-width="100"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鎷煎洟浠�" prop="seckillPrice" min-width="100">
+ <template #default="scope">
+ {{ formatCombinationPrice(scope.row.products) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�鍥㈢粍鏁�" prop="groupCount" min-width="100" />
+ <el-table-column label="鎴愬洟缁勬暟" prop="groupSuccessCount" min-width="100" />
+ <el-table-column label="璐拱娆℃暟" prop="recordCount" min-width="100" />
+ <el-table-column label="娲诲姩鐘舵��" align="center" prop="status" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer v-if="multiple">
+ <el-button type="primary" @click="handleEmitChange">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { handleTree } from '@/utils/tree'
+
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import { propTypes } from '@/utils/propTypes'
+import { CHANGE_EVENT } from 'element-plus'
+import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import { fenToYuan } from '@/utils'
+
+type CombinationActivityVO = Required<CombinationActivityApi.CombinationActivityVO>
+
+/**
+ * 娲诲姩琛ㄦ牸閫夋嫨瀵硅瘽妗�
+ * 1. 鍗曢�夋ā寮忥細
+ * 1.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫崟閫夋鏃讹紝缁撴潫閫夋嫨锛屽苟鍏抽棴瀵硅瘽妗�
+ * 1.2 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ * 2. 澶氶�夋ā寮忥細
+ * 2.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫閫夋鏃讹紝璁板綍閫変腑鐨勬椿鍔�
+ * 2.2 鍒囨崲鍒嗛〉鏃讹紝淇濇寔娲诲姩鐨勯�変腑鐘舵��
+ * 2.3 鐐瑰嚮鍙充笅瑙掔殑纭畾鎸夐挳鏃讹紝缁撴潫閫夋嫨锛屽叧闂璇濇
+ * 2.4 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ */
+defineOptions({ name: 'CombinationTableSelect' })
+
+defineProps({
+ // 澶氶�夋ā寮�
+ multiple: propTypes.bool.def(false)
+})
+
+// 鍒楄〃鐨勬�婚〉鏁�
+const total = ref(0)
+// 鍒楄〃鐨勬暟鎹�
+const list = ref<CombinationActivityVO[]>([])
+// 鍒楄〃鐨勫姞杞戒腑
+const loading = ref(false)
+// 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogVisible = ref(false)
+// 鏌ヨ鍙傛暟
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: undefined
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = (CombinationList?: CombinationActivityVO[]) => {
+ // 閲嶇疆
+ checkedActivitys.value = []
+ checkedStatus.value = {}
+ isCheckAll.value = false
+ isIndeterminate.value = false
+
+ // 澶勭悊宸查�変腑
+ if (CombinationList && CombinationList.length > 0) {
+ checkedActivitys.value = [...CombinationList]
+ checkedStatus.value = Object.fromEntries(
+ CombinationList.map((activityVO) => [activityVO.id, true])
+ )
+ }
+
+ dialogVisible.value = true
+ resetQuery()
+}
+// 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+defineExpose({ open })
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CombinationActivityApi.getCombinationActivityPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ // checkbox缁戝畾undefined浼氭湁闂锛岄渶瑕佺粰涓�涓猙ool鍊�
+ list.value.forEach(
+ (activityVO) =>
+ (checkedStatus.value[activityVO.id] = checkedStatus.value[activityVO.id] || false)
+ )
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ calculateIsCheckAll()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.value = {
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ createTime: []
+ }
+ getList()
+}
+
+/**
+ * 鏍煎紡鍖栨嫾鍥环鏍�
+ * @param products
+ */
+const formatCombinationPrice = (products) => {
+ const combinationPrice = Math.min(...products.map((item) => item.combinationPrice))
+ return `锟�${fenToYuan(combinationPrice)}`
+}
+
+// 鏄惁鍏ㄩ��
+const isCheckAll = ref(false)
+// 鍏ㄩ�夋鏄惁澶勪簬涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+const isIndeterminate = ref(false)
+// 閫変腑鐨勬椿鍔�
+const checkedActivitys = ref<CombinationActivityVO[]>([])
+// 閫変腑鐘舵�侊細key涓烘椿鍔↖D锛寁alue涓烘槸鍚﹂�変腑
+const checkedStatus = ref<Record<string, boolean>>({})
+
+// 閫変腑鐨勬椿鍔� activityId
+const selectedActivityId = ref()
+/** 鍗曢�変腑鏃惰Е鍙� */
+const handleSingleSelected = (combinationActivityVO: CombinationActivityVO) => {
+ emits(CHANGE_EVENT, combinationActivityVO)
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ // 璁颁綇涓婃閫夋嫨鐨処D
+ selectedActivityId.value = combinationActivityVO.id
+}
+
+/** 澶氶�夊畬鎴� */
+const handleEmitChange = () => {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ emits(CHANGE_EVENT, [...checkedActivitys.value])
+}
+
+/** 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢 */
+const emits = defineEmits<{
+ change: [CombinationActivityApi: CombinationActivityVO | CombinationActivityVO[] | any]
+}>()
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckAll = (checked: boolean) => {
+ isCheckAll.value = checked
+ isIndeterminate.value = false
+
+ list.value.forEach((combinationActivity) => handleCheckOne(checked, combinationActivity, false))
+}
+
+/**
+ * 閫変腑涓�琛�
+ * @param checked 鏄惁閫変腑
+ * @param combinationActivity 娲诲姩
+ * @param isCalcCheckAll 鏄惁璁$畻鍏ㄩ��
+ */
+const handleCheckOne = (
+ checked: boolean,
+ combinationActivity: CombinationActivityVO,
+ isCalcCheckAll: boolean
+) => {
+ if (checked) {
+ checkedActivitys.value.push(combinationActivity)
+ checkedStatus.value[combinationActivity.id] = true
+ } else {
+ const index = findCheckedIndex(combinationActivity)
+ if (index > -1) {
+ checkedActivitys.value.splice(index, 1)
+ checkedStatus.value[combinationActivity.id] = false
+ isCheckAll.value = false
+ }
+ }
+
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ if (isCalcCheckAll) {
+ calculateIsCheckAll()
+ }
+}
+
+// 鏌ユ壘娲诲姩鍦ㄥ凡閫変腑娲诲姩鍒楄〃涓殑绱㈠紩
+const findCheckedIndex = (activityVO: CombinationActivityVO) =>
+ checkedActivitys.value.findIndex((item) => item.id === activityVO.id)
+
+// 璁$畻鍏ㄩ�夋鐘舵��
+const calculateIsCheckAll = () => {
+ isCheckAll.value = list.value.every((activityVO) => checkedStatus.value[activityVO.id])
+ // 璁$畻涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+ isIndeterminate.value =
+ !isCheckAll.value && list.value.some((activityVO) => checkedStatus.value[activityVO.id])
+}
+
+// 鍒嗙被鍒楄〃
+const categoryList = ref()
+// 鍒嗙被鏍�
+const categoryTreeList = ref()
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱鍒嗙被鏍�
+ categoryList.value = await ProductCategoryApi.getCategoryList({})
+ categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId')
+})
+</script>
diff --git a/src/views/mall/promotion/combination/record/CombinationRecordListDialog.vue b/src/views/mall/promotion/combination/record/CombinationRecordListDialog.vue
new file mode 100644
index 0000000..13e04a1
--- /dev/null
+++ b/src/views/mall/promotion/combination/record/CombinationRecordListDialog.vue
@@ -0,0 +1,89 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鎷煎洟鍒楄〃" width="950">
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="缂栧彿" prop="id" min-width="50" />
+ <el-table-column align="center" label="澶村儚" prop="avatar" min-width="80">
+ <template #default="scope">
+ <el-avatar :src="scope.row.avatar" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鏄电О" prop="nickname" min-width="100" />
+ <el-table-column align="center" label="寮�鍥㈠洟闀�" prop="headId" min-width="100">
+ <template #default="{ row }: { row: CombinationRecordApi.CombinationRecordVO }">
+ <el-tag> {{ row.headId === 0 ? '鍥㈤暱' : '鍥㈠憳' }} </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍙傚洟鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎷煎洟鐘舵��" prop="status" min-width="150">
+ <template #default="scope">
+ <dict-tag
+ :type="DICT_TYPE.PROMOTION_COMBINATION_RECORD_STATUS"
+ :value="scope.row.status"
+ />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as CombinationRecordApi from '@/api/mall/promotion/combination/combinationRecord'
+import { DICT_TYPE } from '@/utils/dict'
+
+/** 鍔╁姏鍒楄〃 */
+defineOptions({ name: 'CombinationRecordListDialog' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ headId: undefined
+})
+
+/** 鎵撳紑寮圭獥 */
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const open = async (headId: any) => {
+ dialogVisible.value = true
+ queryParams.headId = headId
+ await getList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CombinationRecordApi.getCombinationRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+</script>
diff --git a/src/views/mall/promotion/combination/record/index.vue b/src/views/mall/promotion/combination/record/index.vue
new file mode 100644
index 0000000..b9b5ff7
--- /dev/null
+++ b/src/views/mall/promotion/combination/record/index.vue
@@ -0,0 +1,276 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戞嫾鍥㈡椿鍔�" url="https://doc.iocoder.cn/mall/promotion-combination/" />
+
+ <!-- 缁熻淇℃伅灞曠ず -->
+ <el-row :gutter="12">
+ <el-col :span="6">
+ <ContentWrap class="h-[110px] pb-0!">
+ <div class="flex items-center">
+ <div
+ class="h-[50px] w-[50px] flex items-center justify-center"
+ style="color: rgb(24 144 255); background-color: rgb(24 144 255 / 10%)"
+ >
+ <Icon :size="23" icon="fa:user-times" />
+ </div>
+ <div class="ml-[20px]">
+ <div class="mb-8px text-14px text-gray-400">鍙備笌浜烘暟(涓�)</div>
+ <CountTo
+ :duration="2600"
+ :end-val="recordSummary.userCount"
+ :start-val="0"
+ class="text-20px"
+ />
+ </div>
+ </div>
+ </ContentWrap>
+ </el-col>
+ <el-col :span="6">
+ <ContentWrap class="h-[110px]">
+ <div class="flex items-center">
+ <div
+ class="h-[50px] w-[50px] flex items-center justify-center"
+ style="color: rgb(162 119 255); background-color: rgb(162 119 255 / 10%)"
+ >
+ <Icon :size="23" icon="fa:user-plus" />
+ </div>
+ <div class="ml-[20px]">
+ <div class="mb-8px text-14px text-gray-400">鎴愬洟鏁伴噺(涓�)</div>
+ <CountTo
+ :duration="2600"
+ :end-val="recordSummary.successCount"
+ :start-val="0"
+ class="text-20px"
+ />
+ </div>
+ </div>
+ </ContentWrap>
+ </el-col>
+ <el-col :span="6">
+ <ContentWrap class="h-[110px]">
+ <div class="flex items-center">
+ <div
+ class="h-[50px] w-[50px] flex items-center justify-center"
+ style="color: rgb(162 119 255); background-color: rgb(162 119 255 / 10%)"
+ >
+ <Icon :size="23" icon="fa:user-plus" />
+ </div>
+ <div class="ml-[20px]">
+ <div class="mb-8px text-14px text-gray-400">铏氭嫙鎴愬洟(涓�)</div>
+ <CountTo
+ :duration="2600"
+ :end-val="recordSummary.virtualGroupCount"
+ :start-val="0"
+ class="text-20px"
+ />
+ </div>
+ </div>
+ </ContentWrap>
+ </el-col>
+ </el-row>
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :shortcuts="defaultShortcuts"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="鎷煎洟鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="(dict, index) in getIntDictOptions(
+ DICT_TYPE.PROMOTION_COMBINATION_RECORD_STATUS
+ )"
+ :key="index"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒嗛〉鍒楄〃鏁版嵁灞曠ず -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="pageList">
+ <el-table-column align="center" label="缂栧彿" prop="id" min-width="50" />
+ <el-table-column align="center" label="澶村儚" prop="avatar" min-width="80">
+ <template #default="scope">
+ <el-avatar :src="scope.row.avatar" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鏄电О" prop="nickname" min-width="100" />
+ <el-table-column align="center" label="寮�鍥㈠洟闀�" prop="headId" min-width="100">
+ <template #default="{ row }: { row: CombinationRecordApi.CombinationRecordVO }">
+ {{
+ row.headId ? pageList.find((item) => item.id === row.headId)?.nickname : row.nickname
+ }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="寮�鍥㈡椂闂�"
+ prop="startTime"
+ width="180"
+ />
+ <el-table-column
+ align="center"
+ label="鎷煎洟鍟嗗搧"
+ prop="type"
+ show-overflow-tooltip
+ min-width="300"
+ >
+ <template #default="{ row }">
+ <el-image
+ :src="row.picUrl"
+ class="mr-5px h-30px w-30px align-middle"
+ @click="imagePreview(row.picUrl)"
+ />
+ <span class="align-middle">{{ row.spuName }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍑犱汉鍥�" prop="userSize" min-width="100" />
+ <el-table-column align="center" label="鍙備笌浜烘暟" prop="userCount" min-width="100" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍙傚洟鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="缁撴潫鏃堕棿"
+ prop="endTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎷煎洟鐘舵��" prop="status" min-width="150">
+ <template #default="scope">
+ <dict-tag
+ :type="DICT_TYPE.PROMOTION_COMBINATION_RECORD_STATUS"
+ :value="scope.row.status"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:combination-record:query']"
+ link
+ type="primary"
+ @click="openRecordListDialog(scope.row)"
+ >
+ 鏌ョ湅鎷煎洟
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥 -->
+ <CombinationRecordListDialog ref="combinationRecordListRef" />
+</template>
+<script lang="ts" setup>
+import CombinationRecordListDialog from './CombinationRecordListDialog.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, defaultShortcuts } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import * as CombinationRecordApi from '@/api/mall/promotion/combination/combinationRecord'
+
+defineOptions({ name: 'PromotionCombinationRecord' })
+
+const queryParams = ref({
+ status: undefined, // 鎷煎洟鐘舵��
+ createTime: undefined, // 鍒涘缓鏃堕棿
+ pageSize: 10,
+ pageNo: 1
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const combinationRecordListRef = ref() // 鏌ヨ琛ㄥ崟 Ref
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鎬昏褰曟暟
+const pageList = ref<CombinationRecordApi.CombinationRecordVO[]>([]) // 鍒嗛〉鏁版嵁
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CombinationRecordApi.getCombinationRecordPage(queryParams.value)
+ pageList.value = data.list as CombinationRecordApi.CombinationRecordVO[]
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+// 鎷煎洟缁熻鏁版嵁
+const recordSummary = ref({
+ successCount: 0,
+ userCount: 0,
+ virtualGroupCount: 0
+})
+/** 鑾峰緱鎷煎洟璁板綍缁熻淇℃伅 */
+const getSummary = async () => {
+ recordSummary.value = await CombinationRecordApi.getCombinationRecordSummary()
+}
+
+/** 鏌ョ湅鎷煎洟璇︽儏 */
+const openRecordListDialog = (row: CombinationRecordApi.CombinationRecordVO) => {
+ combinationRecordListRef.value?.open(row.headId || row.id) // 澶氳〃杈惧紡鐨勫師鍥狅紝鍥㈤暱鐨� headId 涓虹┖锛屽氨鏄嚜韬殑鎯呭喌
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getSummary()
+ await getList()
+})
+</script>
diff --git a/src/views/mall/promotion/components/SpuAndSkuList.vue b/src/views/mall/promotion/components/SpuAndSkuList.vue
new file mode 100644
index 0000000..1a0598c
--- /dev/null
+++ b/src/views/mall/promotion/components/SpuAndSkuList.vue
@@ -0,0 +1,138 @@
+<template>
+ <el-table :data="spuData" :expand-row-keys="expandRowKeys" row-key="id">
+ <el-table-column type="expand" width="30">
+ <template #default="{ row }">
+ <SkuList
+ ref="skuListRef"
+ :is-activity-component="true"
+ :prop-form-data="spuPropertyList.find((item) => item.spuId === row.id)?.spuDetail"
+ :property-list="spuPropertyList.find((item) => item.spuId === row.id)?.propertyList"
+ :rule-config="ruleConfig"
+ >
+ <template #extension>
+ <slot></slot>
+ </template>
+ </SkuList>
+ </template>
+ </el-table-column>
+ <el-table-column key="id" align="center" label="鍟嗗搧缂栧彿" prop="id" />
+ <el-table-column label="鍟嗗搧鍥�" min-width="80">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <el-table-column :show-overflow-tooltip="true" label="鍟嗗搧鍚嶇О" min-width="300" prop="name" />
+ <el-table-column align="center" label="鍟嗗搧鍞环" min-width="90" prop="price">
+ <template #default="{ row }">
+ {{ formatToFraction(row.price) }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�閲�" min-width="90" prop="salesCount" />
+ <el-table-column align="center" label="搴撳瓨" min-width="90" prop="stock" />
+ <el-table-column
+ v-if="spuData.length > 1 && deletable"
+ align="center"
+ label="鎿嶄綔"
+ min-width="90"
+ >
+ <template #default="scope">
+ <el-button link type="primary" @click="deleteSpu(scope.row.id)"> 鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+<script generic="T extends Spu" lang="ts" setup>
+import { formatToFraction } from '@/utils'
+import { createImageViewer } from '@/components/ImageViewer'
+import { Spu } from '@/api/mall/product/spu'
+import { RuleConfig, SkuList } from '@/views/mall/product/spu/components'
+import { SpuProperty } from '@/views/mall/promotion/components/index'
+
+defineOptions({ name: 'PromotionSpuAndSkuList' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps<{
+ spuList: T[]
+ ruleConfig: RuleConfig[]
+ spuPropertyListP: SpuProperty<T>[]
+ deletable?: boolean // SPU 鏄惁鍙垹闄わ紱
+}>()
+
+const spuData = ref<Spu[]>([]) // spu 璇︽儏鏁版嵁鍒楄〃
+const skuListRef = ref() // 鍟嗗搧灞炴�у垪琛≧ef
+const spuPropertyList = ref<SpuProperty<T>[]>([]) // spuId 瀵瑰簲鐨� sku 鐨勫睘鎬у垪琛�
+const expandRowKeys = ref<string[]>([]) // 鎺у埗灞曞紑琛岄渶瑕佽缃� row-key 灞炴�ф墠鑳戒娇鐢紝璇ュ睘鎬т负灞曞紑琛岀殑 keys 鏁扮粍銆�
+
+/**
+ * 鑾峰彇鎵�鏈� sku 娲诲姩閰嶇疆
+ *
+ * @param extendedAttribute 鍦� sku 涓婃墿灞曠殑灞炴�э紝渚嬶細绉掓潃娲诲姩 sku 鎵╁睍灞炴�� productConfig 璇峰弬鑰� seckillActivity.ts
+ */
+const getSkuConfigs = (extendedAttribute: string) => {
+ skuListRef.value.validateSku()
+ const seckillProducts: any[] = []
+ spuPropertyList.value.forEach((item) => {
+ item.spuDetail.skus?.forEach((sku: any) => {
+ seckillProducts.push(sku[extendedAttribute] as any)
+ })
+ })
+ return seckillProducts
+}
+// 鏆撮湶鍑虹粰琛ㄥ崟鎻愪氦鏃朵娇鐢�
+defineExpose({ getSkuConfigs })
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ zIndex: 99999999,
+ urlList: [imgUrl]
+ })
+}
+
+// 鍒犻櫎鏃剁殑瑙﹀彂浜嬩欢
+const emits = defineEmits<{
+ (e: 'delete', spuId: number): void
+}>()
+
+/** 澶氶�夋椂鍙互鍒犻櫎 SPU **/
+const deleteSpu = async (spuId: number) => {
+ await message.confirm('鏄惁鍒犻櫎鍟嗗搧缂栧彿涓�' + spuId + '鐨勬暟鎹紵')
+ const index = spuData.value.findIndex((item) => item.id == spuId)
+ spuData.value.splice(index, 1)
+ emits('delete', spuId)
+}
+
+/**
+ * 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 skuList
+ */
+watch(
+ () => props.spuList,
+ (data) => {
+ if (!data) return
+ spuData.value = data as Spu[]
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+/**
+ * 灏嗕紶杩涙潵鐨勫�艰祴鍊肩粰 skuList
+ */
+watch(
+ () => props.spuPropertyListP,
+ (data) => {
+ if (!data) return
+ spuPropertyList.value = data as SpuProperty<T>[] as any
+ // 瑙e喅濡傛灉涔嬪墠閫夋嫨鐨勬槸鍗曡鏍� spu 鐨勮瘽鍚庨潰閫夋嫨澶氳鏍� sku 澶氳鏍煎睘鎬т俊鎭笉灞曠ず鐨勯棶棰樸�傝В鍐虫柟娉曪細璁� SkuList 缁勪欢閲嶆柊娓叉煋锛堣鎶樺彔浼氬共鎺夊寘鍚殑缁勪欢灞曞紑鏃朵細閲嶆柊鍔犺浇锛�
+ setTimeout(() => {
+ expandRowKeys.value = data.map((item) => item.spuId + '')
+ }, 200)
+ },
+ {
+ deep: true,
+ immediate: true
+ }
+)
+</script>
diff --git a/src/views/mall/promotion/components/SpuSelect.vue b/src/views/mall/promotion/components/SpuSelect.vue
new file mode 100644
index 0000000..648a863
--- /dev/null
+++ b/src/views/mall/promotion/components/SpuSelect.vue
@@ -0,0 +1,324 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" :title="dialogTitle" width="70%">
+ <ContentWrap>
+ <el-row :gutter="20" class="mb-10px">
+ <el-col :span="6">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ晢鍝佸悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-col>
+ <el-col :span="6">
+ <el-tree-select
+ v-model="queryParams.categoryId"
+ :data="categoryList"
+ :props="defaultProps"
+ check-strictly
+ class="w-1/1"
+ node-key="id"
+ placeholder="璇烽�夋嫨鍟嗗搧鍒嗙被"
+ />
+ </el-col>
+ <el-col :span="6">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-col>
+ <el-col :span="6">
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-col>
+ </el-row>
+ <el-table
+ ref="spuListRef"
+ v-loading="loading"
+ :data="list"
+ :expand-row-keys="expandRowKeys"
+ row-key="id"
+ @expand-change="expandChange"
+ @selection-change="selectSpu"
+ >
+ <el-table-column v-if="isSelectSku" type="expand" width="30">
+ <template #default>
+ <SkuList
+ v-if="isExpand"
+ ref="skuListRef"
+ :isComponent="true"
+ :isDetail="true"
+ :prop-form-data="spuData"
+ :property-list="propertyList"
+ @selection-change="selectSku"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column type="selection" width="55" />
+ <el-table-column key="id" align="center" label="鍟嗗搧缂栧彿" prop="id" />
+ <el-table-column label="鍟嗗搧鍥�" min-width="80">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :show-overflow-tooltip="true"
+ label="鍟嗗搧鍚嶇О"
+ min-width="300"
+ prop="name"
+ />
+ <el-table-column align="center" label="鍟嗗搧鍞环" min-width="90" prop="price">
+ <template #default="{ row }">
+ {{ formatToFraction(row.price) }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�閲�" min-width="90" prop="salesCount" />
+ <el-table-column align="center" label="搴撳瓨" min-width="90" prop="stock" />
+ <el-table-column align="center" label="鎺掑簭" min-width="70" prop="sort" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button type="primary" @click="confirm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { getPropertyList, PropertyAndValues, SkuList } from '@/views/mall/product/spu/components'
+import { ElTable } from 'element-plus'
+import { dateFormatter } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import { floatToFixed2, formatToFraction } from '@/utils'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'PromotionSpuSelect' })
+
+const props = defineProps({
+ // 榛樿涓嶉渶瑕侊紙涓嶉渶瑕佺殑鎯呭喌涓嬪彧杩斿洖 spu锛岄渶瑕佺殑鎯呭喌涓嬭繑鍥� 閫変腑鐨� spu 鍜� sku 鍒楄〃锛�
+ // 鍏跺畠娲诲姩闇�瑕侀�夋嫨鍟嗗搧鍜屽晢鍝佸睘鎬у鍏ユ缁勪欢鍗冲彲锛岄渶娣诲姞缁勪欢灞炴�� :isSelectSku='true'
+ isSelectSku: propTypes.bool.def(false), // 鏄惁闇�瑕侀�夋嫨 sku 灞炴��
+ radio: propTypes.bool.def(false) // 鏄惁鍗曢�� sku
+})
+
+const message = useMessage() // 娑堟伅寮圭獥
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ tabType: 0, // 榛樿鑾峰彇涓婃灦鐨勫晢鍝�
+ name: '',
+ categoryId: null,
+ createTime: []
+}) // 鏌ヨ鍙傛暟
+const propertyList = ref<PropertyAndValues[]>([]) // 鍟嗗搧灞炴�у垪琛�
+const spuListRef = ref<InstanceType<typeof ElTable>>()
+const skuListRef = ref<InstanceType<typeof SkuList>>() // 鍟嗗搧灞炴�ч�夋嫨 Ref
+const spuData = ref<ProductSpuApi.Spu>() // 鍟嗗搧璇︽儏
+const isExpand = ref(false) // 鎺у埗 SKU 鍒楄〃鏄剧ず
+const expandRowKeys = ref<number[]>() // 鎺у埗灞曞紑琛岄渶瑕佽缃� row-key 灞炴�ф墠鑳戒娇鐢紝璇ュ睘鎬т负灞曞紑琛岀殑 keys 鏁扮粍銆�
+
+//============ 鍟嗗搧閫夋嫨鐩稿叧 ============
+const selectedSpuId = ref<number>(0) // 閫変腑鐨勫晢鍝� spuId
+const selectedSkuIds = ref<number[]>([]) // 閫変腑鐨勫晢鍝� skuIds
+const selectSku = (val: ProductSpuApi.Sku[]) => {
+ const skuTable = skuListRef.value?.getSkuTableRef()
+ if (selectedSpuId.value === 0) {
+ message.warning('璇峰厛閫夋嫨鍟嗗搧鍐嶉�夋嫨鐩稿簲鐨勮鏍硷紒锛侊紒')
+ skuTable?.clearSelection()
+ return
+ }
+ if (val.length === 0) {
+ selectedSkuIds.value = []
+ return
+ }
+ if (props.radio) {
+ // 鍙�夋嫨涓�涓�
+ selectedSkuIds.value = [val.map((sku) => sku.id!)[0]]
+ // 濡傛灉澶т簬1涓�
+ if (val.length > 1) {
+ // 娓呯┖閫夋嫨
+ skuTable?.clearSelection()
+ // 鍙樻洿涓烘渶鍚庝竴娆¢�夋嫨鐨�
+ skuTable?.toggleRowSelection(val.pop(), true)
+ return
+ }
+ } else {
+ selectedSkuIds.value = val.map((sku) => sku.id!)
+ }
+}
+const selectSpu = (val: ProductSpuApi.Spu[]) => {
+ if (val.length === 0) {
+ selectedSpuId.value = 0
+ return
+ }
+ // 鍙�夋嫨涓�涓�
+ selectedSpuId.value = val.map((spu) => spu.id!)[0]
+ // 鍒囨崲閫夋嫨 spu 濡傛灉鏈夐�夋嫨鐨� sku 鍒欐竻绌�,纭繚閫夋嫨鐨� sku 鏄搴旂殑 spu 涓嬮潰鐨�
+ if (selectedSkuIds.value.length > 0) {
+ selectedSkuIds.value = []
+ }
+ // 濡傛灉澶т簬1涓�
+ if (val.length > 1) {
+ // 娓呯┖閫夋嫨
+ spuListRef.value?.clearSelection()
+ // 鍙樻洿涓烘渶鍚庝竴娆¢�夋嫨鐨�
+ spuListRef.value?.toggleRowSelection(val.pop(), true)
+ return
+ }
+ expandChange(val[0], val)
+}
+
+// 璁$畻鍟嗗搧灞炴��
+const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi.Spu[]) => {
+ // 鍒ゆ柇闇�瑕佸睍寮�鐨� spuId === 閫夋嫨鐨� spuId銆傚鏋滈�夋嫨浜� A 灏卞睍寮� A 鐨� skuList銆傚鏋滈�夋嫨浜� A 鎵嬪姩灞曞紑 B 鍒欓樆鏂�
+ // 鐩殑闃叉璇�� sku
+ if (selectedSpuId.value !== 0) {
+ if (row.id !== selectedSpuId.value) {
+ message.warning('浣犲凡閫夋嫨鍟嗗搧璇峰厛鍙栨秷')
+ expandRowKeys.value = [selectedSpuId.value]
+ return
+ }
+ // 濡傛灉宸插睍寮� skuList 鍒欓�夋嫨姝ゅ搴旂殑 spu 涓嶉渶瑕侀噸鏂拌幏鍙栨覆鏌� skuList
+ if (isExpand.value && spuData.value?.id === row.id) {
+ return
+ }
+ }
+ spuData.value = {}
+ propertyList.value = []
+ isExpand.value = false
+ if (expandedRows?.length === 0) {
+ // 濡傛灉灞曞紑涓暟涓� 0
+ expandRowKeys.value = []
+ return
+ }
+ // 鑾峰彇 SPU 璇︽儏
+ const res = (await ProductSpuApi.getSpu(row.id as number)) as ProductSpuApi.Spu
+ res.skus?.forEach((item) => {
+ item.price = floatToFixed2(item.price)
+ item.marketPrice = floatToFixed2(item.marketPrice)
+ item.costPrice = floatToFixed2(item.costPrice)
+ item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
+ item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
+ })
+ propertyList.value = getPropertyList(res)
+ spuData.value = res
+ isExpand.value = true
+ expandRowKeys.value = [row.id!]
+}
+
+// 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢
+const emits = defineEmits<{
+ (e: 'confirm', spuId: number, skuIds?: number[]): void
+}>()
+/**
+ * 纭閫夋嫨杩斿洖閫変腑鐨� spu 鍜� sku (濡傛灉闇�瑕侀�夋嫨sku鐨勮瘽)
+ */
+const confirm = () => {
+ if (selectedSpuId.value === 0) {
+ message.warning('娌℃湁閫夋嫨浠讳綍鍟嗗搧')
+ return
+ }
+ if (props.isSelectSku && selectedSkuIds.value.length === 0) {
+ message.warning('娌℃湁閫夋嫨浠讳綍鍟嗗搧灞炴��')
+ return
+ }
+ // 杩斿洖鍚勮嚜 id 鍒楄〃
+ props.isSelectSku
+ ? emits('confirm', selectedSpuId.value, selectedSkuIds.value)
+ : emits('confirm', selectedSpuId.value)
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ selectedSpuId.value = 0
+ selectedSkuIds.value = []
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = () => {
+ dialogTitle.value = '鍟嗗搧閫夋嫨'
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductSpuApi.getSpuPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.value = {
+ pageNo: 1,
+ pageSize: 10,
+ tabType: 0, // 榛樿鑾峰彇涓婃灦鐨勫晢鍝�
+ name: '',
+ categoryId: null,
+ createTime: []
+ }
+ getList()
+}
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ zIndex: 99999999,
+ urlList: [imgUrl]
+ })
+}
+
+const categoryList = ref() // 鍒嗙被鏍�
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱鍒嗙被鏍�
+ const data = await ProductCategoryApi.getCategoryList({})
+ categoryList.value = handleTree(data, 'id', 'parentId')
+})
+</script>
diff --git a/src/views/mall/promotion/components/index.ts b/src/views/mall/promotion/components/index.ts
new file mode 100644
index 0000000..b42c8ce
--- /dev/null
+++ b/src/views/mall/promotion/components/index.ts
@@ -0,0 +1,14 @@
+import SpuSelect from './SpuSelect.vue'
+import SpuAndSkuList from './SpuAndSkuList.vue'
+import { PropertyAndValues } from '@/views/mall/product/spu/components'
+
+type SpuProperty<T> = {
+ spuId: number
+ spuDetail: T
+ propertyList: PropertyAndValues[]
+}
+
+/**
+ * 鎻愪緵鍟嗗搧娲诲姩鍟嗗搧閫夋嫨閫氱敤缁勪欢
+ */
+export { SpuSelect, SpuAndSkuList, SpuProperty }
diff --git a/src/views/mall/promotion/coupon/components/CouponSelect.vue b/src/views/mall/promotion/coupon/components/CouponSelect.vue
new file mode 100644
index 0000000..66151eb
--- /dev/null
+++ b/src/views/mall/promotion/coupon/components/CouponSelect.vue
@@ -0,0 +1,192 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="82px"
+ >
+ <el-form-item label="浼樻儬鍒稿悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ紭鎯犲姷鍚�"
+ @keyup="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="浼樻儬绫诲瀷" prop="discountType">
+ <el-select
+ v-model="queryParams.discountType"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨浼樻儬鍒哥被鍨�"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="浼樻儬鍒稿悕绉�" min-width="140" prop="name" />
+ <el-table-column label="绫诲瀷" min-width="80" prop="productScope">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浼樻儬" min-width="100" prop="discount">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+ {{ discountFormat(scope.row) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="棰嗗彇鏂瑰紡" min-width="100" prop="takeType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="validityTypeFormat"
+ align="center"
+ label="浣跨敤鏃堕棿"
+ prop="validityType"
+ width="185"
+ />
+ <el-table-column align="center" label="鍙戞斁鏁伴噺" prop="totalCount" />
+ <el-table-column
+ :formatter="remainedCountFormat"
+ align="center"
+ label="鍓╀綑鏁伴噺"
+ prop="totalCount"
+ />
+ <el-table-column
+ :formatter="takeLimitCountFormat"
+ align="center"
+ label="棰嗗彇涓婇檺"
+ prop="takeLimitCount"
+ />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import {
+ discountFormat,
+ remainedCountFormat,
+ takeLimitCountFormat,
+ validityTypeFormat
+} from '@/views/mall/promotion/coupon/formatter'
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import { CouponTemplateTakeTypeEnum } from '@/utils/constants'
+
+defineOptions({ name: 'CouponSelect' })
+
+const props = defineProps<{
+ multipleSelection?: CouponTemplateApi.CouponTemplateVO[]
+ takeType: number // 棰嗗彇鏂瑰紡
+}>()
+const emit = defineEmits<{
+ (e: 'update:multipleSelection', v: CouponTemplateApi.CouponTemplateVO[]): void
+ (e: 'change', v: CouponTemplateApi.CouponTemplateVO[]): void
+}>()
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('閫夋嫨浼樻儬鍔�') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 瀛楀吀琛ㄦ牸鏁版嵁
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ discountType: null,
+ canTakeTypes: [CouponTemplateTakeTypeEnum.USER.type] // 鍙幏寰楃洿鎺ラ鍙栫殑鍒�
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const selectedCouponList = ref<CouponTemplateApi.CouponTemplateVO[]>([]) // 閫夋嫨鐨勬暟鎹�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 鎵ц鏌ヨ
+ queryParams.canTakeTypes = [props.takeType] as any
+ const data = await CouponTemplateApi.getCouponTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef?.value?.resetFields()
+ handleQuery()
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ resetQuery()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+const handleSelectionChange = (val: CouponTemplateApi.CouponTemplateVO[]) => {
+ if (props.multipleSelection) {
+ emit('update:multipleSelection', val)
+ return
+ }
+ selectedCouponList.value = val
+}
+
+const submitForm = () => {
+ dialogVisible.value = false
+ emit('change', selectedCouponList.value)
+}
+</script>
diff --git a/src/views/mall/promotion/coupon/components/CouponSendForm.vue b/src/views/mall/promotion/coupon/components/CouponSendForm.vue
new file mode 100644
index 0000000..4b9edb7
--- /dev/null
+++ b/src/views/mall/promotion/coupon/components/CouponSendForm.vue
@@ -0,0 +1,162 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" title="鍙戦�佷紭鎯犲埜" width="70%">
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="82px"
+ >
+ <el-form-item label="浼樻儬鍒稿悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ placeholder="璇疯緭鍏ヤ紭鎯犲姷鍚�"
+ clearable
+ @keyup="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+ <el-table-column align="center" label="浼樻儬鍒稿悕绉�" prop="name" min-width="60" />
+ <el-table-column
+ label="浼樻儬閲戦 / 鎶樻墸"
+ align="center"
+ prop="discount"
+ :formatter="discountFormat"
+ min-width="60"
+ />
+ <el-table-column
+ align="center"
+ label="鏈�浣庢秷璐�"
+ prop="usePrice"
+ min-width="60"
+ :formatter="usePriceFormat"
+ />
+ <el-table-column
+ align="center"
+ label="鏈夋晥鏈熼檺"
+ prop="validityType"
+ min-width="140"
+ :formatter="validityTypeFormat"
+ />
+ <el-table-column
+ align="center"
+ label="鍓╀綑鏁伴噺"
+ min-width="60"
+ :formatter="remainedCountFormat"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="60px" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ :disabled="sendLoading"
+ :loading="sendLoading"
+ @click="handleSendCoupon(scope.row.id)"
+ v-hasPermi="['promotion:coupon:send']"
+ >
+ 鍙戦��
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ <div class="clear-both"></div>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import * as CouponApi from '@/api/mall/promotion/coupon/coupon'
+import {
+ discountFormat,
+ remainedCountFormat,
+ usePriceFormat,
+ validityTypeFormat
+} from '@/views/mall/promotion/coupon/formatter'
+import { CouponTemplateTakeTypeEnum } from '@/utils/constants'
+
+defineOptions({ name: 'PromotionCouponSendForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const sendLoading = ref(false) // 鍙戦�佹寜閽殑鍔犺浇涓�
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ canTakeTypes: [CouponTemplateTakeTypeEnum.ADMIN.type]
+}) // 鏌ヨ鍙傛暟
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+// 棰嗗彇浜虹殑缂栧彿鍒楄〃
+let userIds: number[] = []
+
+/** 鎵撳紑寮圭獥 */
+const open = (ids: number[]) => {
+ userIds = ids
+ // 鎵撳紑鏃堕噸缃煡璇紝闃叉鍙戦�佸垪琛ㄥ墿浣欐暟閲忔湭鏇存柊鐨勯棶棰�
+ resetQuery()
+
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await CouponTemplateApi.getCouponTemplatePage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef?.value?.resetFields()
+ handleQuery()
+}
+
+/** 鍙戦�佹搷浣� **/
+const handleSendCoupon = async (templateId: number) => {
+ try {
+ sendLoading.value = true
+ await CouponApi.sendCoupon({ templateId, userIds })
+ // 鎻愮ず
+ message.success('鍙戦�佹垚鍔�')
+ dialogVisible.value = false
+ } finally {
+ sendLoading.value = false
+ }
+}
+</script>
diff --git a/src/views/mall/promotion/coupon/components/index.ts b/src/views/mall/promotion/coupon/components/index.ts
new file mode 100644
index 0000000..6a0e56f
--- /dev/null
+++ b/src/views/mall/promotion/coupon/components/index.ts
@@ -0,0 +1,4 @@
+import CouponSendForm from './CouponSendForm.vue'
+import CouponSelect from './CouponSelect.vue'
+
+export { CouponSendForm, CouponSelect }
diff --git a/src/views/mall/promotion/coupon/formatter.ts b/src/views/mall/promotion/coupon/formatter.ts
new file mode 100644
index 0000000..8005d0c
--- /dev/null
+++ b/src/views/mall/promotion/coupon/formatter.ts
@@ -0,0 +1,59 @@
+import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import { CouponTemplateVO } from '@/api/mall/promotion/coupon/couponTemplate'
+import { floatToFixed2 } from '@/utils'
+
+// 鏍煎紡鍖栥�愪紭鎯犻噾棰�/鎶樻墸銆�
+export const discountFormat = (row: CouponTemplateVO) => {
+ if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) {
+ return `锟�${floatToFixed2(row.discountPrice)}`
+ }
+ if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
+ return `${row.discountPercent}%`
+ }
+ return '鏈煡銆�' + row.discountType + '銆�'
+}
+
+// 鏍煎紡鍖栥�愰鍙栦笂闄愩��
+export const takeLimitCountFormat = (row: CouponTemplateVO) => {
+ if (row.takeLimitCount) {
+ if (row.takeLimitCount === -1) {
+ return '鏃犻鍙栭檺鍒�'
+ }
+ return `${row.takeLimitCount} 寮�/浜篳
+ } else {
+ return ' '
+ }
+}
+
+// 鏍煎紡鍖栥�愭湁鏁堟湡闄愩��
+export const validityTypeFormat = (row: CouponTemplateVO) => {
+ if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
+ return `${formatDate(row.validStartTime)} 鑷� ${formatDate(row.validEndTime)}`
+ }
+ if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
+ return `棰嗗彇鍚庣 ${row.fixedStartTerm} - ${row.fixedEndTerm} 澶╁唴鍙敤`
+ }
+ return '鏈煡銆�' + row.validityType + '銆�'
+}
+
+// 鏍煎紡鍖栥�恡otalCount銆�
+export const totalCountFormat = (row: CouponTemplateVO) => {
+ if (row.totalCount === -1) {
+ return '涓嶉檺鍒�'
+ }
+ return row.totalCount
+}
+
+// 鏍煎紡鍖栥�愬墿浣欐暟閲忋��
+export const remainedCountFormat = (row: CouponTemplateVO) => {
+ if (row.totalCount === -1) {
+ return '涓嶉檺鍒�'
+ }
+ return row.totalCount - row.takeCount
+}
+
+// 鏍煎紡鍖栥�愭渶浣庢秷璐广��
+export const usePriceFormat = (row: CouponTemplateVO) => {
+ return `锟�${floatToFixed2(row.usePrice)}`
+}
diff --git a/src/views/mall/promotion/coupon/index.vue b/src/views/mall/promotion/coupon/index.vue
new file mode 100644
index 0000000..25d2e94
--- /dev/null
+++ b/src/views/mall/promotion/coupon/index.vue
@@ -0,0 +1,201 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戜紭鎯犲姷" url="https://doc.iocoder.cn/mall/promotion-coupon/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="浼氬憳鏄电О" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ class="!w-240px"
+ placeholder="璇疯緭鍏ヤ細鍛樻樀绉�"
+ clearable
+ @keyup="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="棰嗗彇鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />鎼滅储 </el-button>
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />閲嶇疆 </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <!-- Tab 閫夐」锛氱湡姝g殑鍐呭鍦� Lab -->
+ <el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
+ <el-tab-pane
+ v-for="tab in statusTabs"
+ :key="tab.value"
+ :label="tab.label"
+ :name="tab.value"
+ />
+ </el-tabs>
+
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="浼氬憳鏄电О" align="center" min-width="100" prop="nickname" />
+ <el-table-column label="浼樻儬鍒稿悕绉�" align="center" min-width="140" prop="name" />
+ <el-table-column label="绫诲瀷" align="center" prop="discountType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浼樻儬" min-width="100" prop="discount">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+ {{ discountFormat(scope.row) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="棰嗗彇鏂瑰紡" align="center" prop="takeType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="棰嗗彇鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column
+ label="浣跨敤鏃堕棿"
+ align="center"
+ prop="useTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:coupon:delete']"
+ type="danger"
+ link
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍥炴敹
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts" name="PromotionCoupon">
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { discountFormat } from '@/views/mall/promotion/coupon/formatter'
+
+defineOptions({ name: 'PromotionCoupon' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 瀛楀吀琛ㄦ牸鏁版嵁
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ createTime: [],
+ status: undefined,
+ nickname: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+const activeTab = ref('all') // Tab 绛涢��
+const statusTabs = reactive([
+ {
+ label: '鍏ㄩ儴',
+ value: 'all'
+ }
+])
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ // 鎵ц鏌ヨ
+ try {
+ const data = await getCouponPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 浜屾纭
+ await message.confirm(
+ '鍥炴敹灏嗕細鏀跺洖浼氬憳棰嗗彇鐨勫緟浣跨敤鐨勪紭鎯犲埜锛屽凡浣跨敤鐨勫皢鏃犳硶鍥炴敹锛岀‘瀹氳鍥炴敹鎵�閫変紭鎯犲埜鍚楋紵'
+ )
+ // 鍙戣捣鍒犻櫎
+ await deleteCoupon(id)
+ message.notifySuccess('鍥炴敹鎴愬姛')
+ // 閲嶆柊鍔犺浇鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** tab 鍒囨崲 */
+const onTabChange = (tabName) => {
+ queryParams.status = tabName === 'all' ? undefined : tabName
+ getList()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+ // 璁剧疆 statuses 杩囨护
+ for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
+ statusTabs.push({
+ label: dict.label,
+ value: dict.value as string
+ })
+ }
+})
+</script>
diff --git a/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
new file mode 100644
index 0000000..b076dea
--- /dev/null
+++ b/src/views/mall/promotion/coupon/template/CouponTemplateForm.vue
@@ -0,0 +1,404 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ >
+ <el-form-item label="浼樻儬鍒稿悕绉�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヤ紭鎯犲埜鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="浼樻儬鍒告弿杩�" prop="description">
+ <el-input
+ v-model="formData.description"
+ :autosize="{ minRows: 2, maxRows: 2 }"
+ :clearable="true"
+ :show-word-limit="true"
+ class="w-1/1!"
+ maxlength="512"
+ placeholder="璇疯緭鍏ヤ紭鎯犲埜鎻忚堪"
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="浼樻儬鍔电被鍨�" prop="productScope">
+ <el-radio-group v-model="formData.productScope">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope"
+ label="鍟嗗搧"
+ prop="productSpuIds"
+ >
+ <SpuShowcase v-model="formData.productSpuIds" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.productScope === PromotionProductScopeEnum.CATEGORY.scope"
+ label="鍒嗙被"
+ prop="productCategoryIds"
+ >
+ <ProductCategorySelect v-model="formData.productCategoryIds" />
+ </el-form-item>
+ <el-form-item label="浼樻儬绫诲瀷" prop="discountType">
+ <el-radio-group v-model="formData.discountType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="formData.discountType === PromotionDiscountTypeEnum.PRICE.type"
+ label="浼樻儬鍒搁潰棰�"
+ prop="discountPrice"
+ >
+ <el-input-number
+ v-model="formData.discountPrice"
+ :min="0"
+ :precision="2"
+ class="mr-2 !w-400px"
+ placeholder="璇疯緭鍏ヤ紭鎯犻噾棰濓紝鍗曚綅锛氬厓"
+ />
+ 鍏�
+ </el-form-item>
+ <el-form-item
+ v-if="formData.discountType === PromotionDiscountTypeEnum.PERCENT.type"
+ label="浼樻儬鍒告姌鎵�"
+ prop="discountPercent"
+ >
+ <el-input-number
+ v-model="formData.discountPercent"
+ :max="9.9"
+ :min="1"
+ :precision="1"
+ class="mr-2 !w-400px"
+ placeholder="浼樻儬鍒告姌鎵d笉鑳藉皬浜� 1 鎶橈紝涓斾笉鍙ぇ浜� 9.9 鎶�"
+ />
+ 鎶�
+ </el-form-item>
+ <el-form-item
+ v-if="formData.discountType === PromotionDiscountTypeEnum.PERCENT.type"
+ label="鏈�澶氫紭鎯�"
+ prop="discountLimitPrice"
+ >
+ <el-input-number
+ v-model="formData.discountLimitPrice"
+ :min="0"
+ :precision="2"
+ class="mr-2 !w-400px"
+ placeholder="璇疯緭鍏ユ渶澶氫紭鎯�"
+ />
+ 鍏�
+ </el-form-item>
+ <el-form-item label="婊″灏戝厓鍙互浣跨敤" prop="usePrice">
+ <el-input-number
+ v-model="formData.usePrice"
+ :min="0"
+ :precision="2"
+ class="mr-2 !w-400px"
+ placeholder="鏃犻棬妲涜璁句负 0"
+ />
+ 鍏�
+ </el-form-item>
+ <el-form-item label="棰嗗彇鏂瑰紡" prop="takeType">
+ <el-radio-group v-model="formData.takeType">
+ <el-radio :key="1" :value="1">鐩存帴棰嗗彇</el-radio>
+ <el-radio :key="2" :value="2">鎸囧畾鍙戞斁</el-radio>
+ <el-radio :key="2" :value="3">鏂颁汉鍔�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="formData.takeType === 1" label="鍙戞斁鏁伴噺" prop="totalCount">
+ <el-input-number
+ v-model="formData.totalCount"
+ :min="-1"
+ :precision="0"
+ class="mr-2 !w-400px"
+ placeholder="鍙戞斁鏁伴噺锛屾病鏈変箣鍚庝笉鑳介鍙栨垨鍙戞斁锛�-1 涓轰笉闄愬埗"
+ />
+ 寮�
+ </el-form-item>
+ <el-form-item v-if="formData.takeType === 1" label="姣忎汉闄愰涓暟" prop="takeLimitCount">
+ <el-input-number
+ v-model="formData.takeLimitCount"
+ :min="-1"
+ :precision="0"
+ class="mr-2 !w-400px"
+ placeholder="璁剧疆涓� -1 鏃讹紝鍙棤闄愰鍙�"
+ />
+ 寮�
+ </el-form-item>
+ <el-form-item label="鏈夋晥鏈熺被鍨�" prop="validityType">
+ <el-radio-group v-model="formData.validityType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="formData.validityType === CouponTemplateValidityTypeEnum.DATE.type"
+ label="鍥哄畾鏃ユ湡"
+ prop="validTimes"
+ >
+ <el-date-picker
+ v-model="formData.validTimes"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 2, 1, 23, 59, 59)]"
+ type="datetimerange"
+ value-format="x"
+ />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.validityType === CouponTemplateValidityTypeEnum.TERM.type"
+ label="棰嗗彇鏃ユ湡"
+ prop="fixedStartTerm"
+ >
+ 绗�
+ <el-input-number
+ v-model="formData.fixedStartTerm"
+ :min="0"
+ :precision="0"
+ class="mx-2"
+ placeholder="0 涓轰粖澶╃敓鏁�"
+ />
+ 鑷�
+ <el-input-number
+ v-model="formData.fixedEndTerm"
+ :min="0"
+ :precision="0"
+ class="mx-2"
+ placeholder="璇疯緭鍏ョ粨鏉熷ぉ鏁�"
+ />
+ 澶╂湁鏁�
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import {
+ CouponTemplateValidityTypeEnum,
+ PromotionDiscountTypeEnum,
+ PromotionProductScopeEnum
+} from '@/utils/constants'
+import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
+import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
+import { convertToInteger, formatToFraction } from '@/utils'
+
+defineOptions({ name: 'CouponTemplateForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ discountType: PromotionDiscountTypeEnum.PRICE.type,
+ discountPrice: undefined,
+ discountPercent: undefined,
+ discountLimitPrice: undefined,
+ usePrice: undefined,
+ takeType: 1,
+ totalCount: undefined,
+ takeLimitCount: undefined,
+ validityType: CouponTemplateValidityTypeEnum.DATE.type,
+ validTimes: [],
+ validStartTime: undefined,
+ validEndTime: undefined,
+ fixedStartTerm: undefined,
+ fixedEndTerm: undefined,
+ productScope: PromotionProductScopeEnum.ALL.scope,
+ description: undefined,
+ productScopeValues: [], // 鍟嗗搧鑼冨洿锛氬�间负 鍝佺被缂栧彿鍒楄〃 鎴� 鍟嗗搧缂栧彿鍒楄〃 锛岀敤浜庢彁浜�
+ productCategoryIds: [], // 浠呯敤浜庤〃鍗曪紝涓嶆彁浜�
+ productSpuIds: [] // 浠呯敤浜庤〃鍗曪紝涓嶆彁浜�
+})
+const formRules = reactive({
+ name: [{ required: true, message: '浼樻儬鍒稿悕绉颁笉鑳戒负绌�', trigger: 'blur' }],
+ discountType: [{ required: true, message: '浼樻儬鍒哥被鍨嬩笉鑳戒负绌�', trigger: 'change' }],
+ discountPrice: [{ required: true, message: '浼樻儬鍒搁潰棰濅笉鑳戒负绌�', trigger: 'blur' }],
+ discountPercent: [{ required: true, message: '浼樻儬鍒告姌鎵d笉鑳戒负绌�', trigger: 'blur' }],
+ discountLimitPrice: [{ required: true, message: '鏈�澶氫紭鎯犱笉鑳戒负绌�', trigger: 'blur' }],
+ usePrice: [{ required: true, message: '婊″灏戝厓鍙互浣跨敤涓嶈兘涓虹┖', trigger: 'blur' }],
+ takeType: [{ required: true, message: '棰嗗彇鏂瑰紡涓嶈兘涓虹┖', trigger: 'change' }],
+ totalCount: [{ required: true, message: '鍙戞斁鏁伴噺涓嶈兘涓虹┖', trigger: 'blur' }],
+ takeLimitCount: [{ required: true, message: '姣忎汉闄愰涓暟涓嶈兘涓虹┖', trigger: 'blur' }],
+ validityType: [{ required: true, message: '鏈夋晥鏈熺被鍨嬩笉鑳戒负绌�', trigger: 'change' }],
+ validTimes: [{ required: true, message: '鍥哄畾鏃ユ湡涓嶈兘涓虹┖', trigger: 'change' }],
+ fixedStartTerm: [{ required: true, message: '寮�濮嬮鍙栧ぉ鏁颁笉鑳戒负绌�', trigger: 'blur' }],
+ fixedEndTerm: [{ required: true, message: '寮�濮嬮鍙栧ぉ鏁颁笉鑳戒负绌�', trigger: 'blur' }],
+ productScope: [{ required: true, message: '鍟嗗搧鑼冨洿涓嶈兘涓虹┖', trigger: 'blur' }],
+ productSpuIds: [{ required: true, message: '鍟嗗搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ productCategoryIds: [{ required: true, message: '鍒嗙被涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = await CouponTemplateApi.getCouponTemplate(id)
+ formData.value = {
+ ...data,
+ discountPrice: formatToFraction(data.discountPrice),
+ discountPercent:
+ data.discountPercent !== undefined ? data.discountPercent / 10.0 : undefined,
+ discountLimitPrice: formatToFraction(data.discountLimitPrice),
+ usePrice: formatToFraction(data.usePrice),
+ validTimes: [data.validStartTime, data.validEndTime]
+ }
+ // 鑾峰緱鍟嗗搧鑼冨洿
+ await getProductScope()
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = {
+ ...formData.value,
+ discountPrice: convertToInteger(formData.value.discountPrice),
+ discountPercent:
+ formData.value.discountPercent !== undefined
+ ? formData.value.discountPercent * 10
+ : undefined,
+ discountLimitPrice: convertToInteger(formData.value.discountLimitPrice),
+ usePrice: convertToInteger(formData.value.usePrice),
+ validStartTime:
+ formData.value.validTimes && formData.value.validTimes.length === 2
+ ? formData.value.validTimes[0]
+ : undefined,
+ validEndTime:
+ formData.value.validTimes && formData.value.validTimes.length === 2
+ ? formData.value.validTimes[1]
+ : undefined,
+ totalCount: formData.value.takeType === 1 ? formData.value.totalCount : -1,
+ takeLimitCount: formData.value.takeType === 1 ? formData.value.takeLimitCount : -1
+ } as unknown as CouponTemplateApi.CouponTemplateVO
+
+ // 璁剧疆鍟嗗搧鑼冨洿
+ setProductScopeValues(data)
+
+ if (formType.value === 'create') {
+ await CouponTemplateApi.createCouponTemplate(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await CouponTemplateApi.updateCouponTemplate(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ description: undefined,
+ discountType: PromotionDiscountTypeEnum.PRICE.type,
+ discountPrice: undefined,
+ discountPercent: undefined,
+ discountLimitPrice: undefined,
+ usePrice: undefined,
+ takeType: 1,
+ totalCount: undefined,
+ takeLimitCount: undefined,
+ validityType: CouponTemplateValidityTypeEnum.DATE.type,
+ validTimes: [],
+ validStartTime: undefined,
+ validEndTime: undefined,
+ fixedStartTerm: undefined,
+ fixedEndTerm: undefined,
+ productScope: PromotionProductScopeEnum.ALL.scope,
+ productScopeValues: [],
+ productSpuIds: [],
+ productCategoryIds: []
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鑾峰緱鍟嗗搧鑼冨洿 */
+const getProductScope = async () => {
+ switch (formData.value.productScope) {
+ case PromotionProductScopeEnum.SPU.scope:
+ // 璁剧疆鍟嗗搧缂栧彿
+ formData.value.productSpuIds = formData.value.productScopeValues
+ break
+ case PromotionProductScopeEnum.CATEGORY.scope:
+ await nextTick(() => {
+ let productCategoryIds = formData.value.productScopeValues
+ if (Array.isArray(productCategoryIds) && productCategoryIds.length > 0) {
+ // 鍗曢�夋椂浣跨敤鏁扮粍涓嶈兘鍙嶆樉
+ productCategoryIds = productCategoryIds[0]
+ }
+ // 璁剧疆鍝佺被缂栧彿
+ formData.value.productCategoryIds = productCategoryIds
+ })
+ break
+ default:
+ break
+ }
+}
+
+/** 璁剧疆鍟嗗搧鑼冨洿 */
+function setProductScopeValues(data: CouponTemplateApi.CouponTemplateVO) {
+ switch (formData.value.productScope) {
+ case PromotionProductScopeEnum.SPU.scope:
+ data.productScopeValues = formData.value.productSpuIds
+ break
+ case PromotionProductScopeEnum.CATEGORY.scope:
+ data.productScopeValues = Array.isArray(formData.value.productCategoryIds)
+ ? formData.value.productCategoryIds
+ : [formData.value.productCategoryIds]
+ break
+ default:
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/mall/promotion/coupon/template/index.vue b/src/views/mall/promotion/coupon/template/index.vue
new file mode 100644
index 0000000..bc121ea
--- /dev/null
+++ b/src/views/mall/promotion/coupon/template/index.vue
@@ -0,0 +1,284 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戜紭鎯犲姷" url="https://doc.iocoder.cn/mall/promotion-coupon/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="82px"
+ >
+ <el-form-item label="浼樻儬鍒稿悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヤ紭鎯犲姷鍚�"
+ @keyup="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="浼樻儬绫诲瀷" prop="discountType">
+ <el-select
+ v-model="queryParams.discountType"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨浼樻儬鍒哥被鍨�"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浼樻儬鍒哥姸鎬�" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨浼樻儬鍒哥姸鎬�"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:coupon-template:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="浼樻儬鍒稿悕绉�" min-width="140" prop="name" />
+ <el-table-column label="绫诲瀷" min-width="130" prop="productScope">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浼樻儬" min-width="110" prop="discount">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+ <div>{{ discountFormat(scope.row) }}</div>
+ </template>
+ </el-table-column>
+ <el-table-column label="棰嗗彇鏂瑰紡" min-width="100" prop="takeType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="validityTypeFormat"
+ align="center"
+ label="浣跨敤鏃堕棿"
+ prop="validityType"
+ width="185"
+ />
+ <el-table-column
+ :formatter="totalCountFormat"
+ align="center"
+ label="鍙戞斁鏁伴噺"
+ prop="totalCount"
+ />
+ <el-table-column
+ :formatter="remainedCountFormat"
+ align="center"
+ label="鍓╀綑鏁伴噺"
+ prop="totalCount"
+ />
+ <el-table-column
+ :formatter="takeLimitCountFormat"
+ align="center"
+ label="棰嗗彇涓婇檺"
+ prop="takeLimitCount"
+ />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column
+ align="center"
+ class-name="small-padding fixed-width"
+ fixed="right"
+ label="鎿嶄綔"
+ width="120"
+ >
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:coupon-template:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:coupon-template:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <CouponTemplateForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import CouponTemplateForm from './CouponTemplateForm.vue'
+import {
+ discountFormat,
+ remainedCountFormat,
+ takeLimitCountFormat,
+ totalCountFormat,
+ validityTypeFormat
+} from '@/views/mall/promotion/coupon/formatter'
+
+defineOptions({ name: 'PromotionCouponTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 瀛楀吀琛ㄦ牸鏁版嵁
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null,
+ discountType: null,
+ type: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 鎵ц鏌ヨ
+ const data = await CouponTemplateApi.getCouponTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef?.value?.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 浼樻儬鍔垫ā鏉跨姸鎬佷慨鏀� */
+const handleStatusChange = async (row: any) => {
+ // 姝ゆ椂锛宺ow 宸茬粡鍙樻垚鐩爣鐘舵�佷簡锛屾墍浠ュ彲浠ョ洿鎺ユ彁浜よ姹傚拰鎻愮ず
+ let text = row.status === CommonStatusEnum.ENABLE ? '鍚敤' : '鍋滅敤'
+
+ try {
+ await message.confirm('纭瑕�"' + text + '""' + row.name + '"浼樻儬鍔靛悧?')
+ await CouponTemplateApi.updateCouponTemplateStatus(row.id, row.status)
+ message.success(text + '鎴愬姛')
+ } catch {
+ // 寮傚父鏃讹紝闇�瑕佸皢 row.status 鐘舵�侀噸缃洖涔嬪墠鐨�
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.confirm('鏄惁纭鍒犻櫎浼樻儬鍔电紪鍙蜂负"' + id + '"鐨勬暟鎹」?')
+ // 鍙戣捣鍒犻櫎
+ await CouponTemplateApi.deleteCouponTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/discountActivity/DiscountActivityForm.vue b/src/views/mall/promotion/discountActivity/DiscountActivityForm.vue
new file mode 100644
index 0000000..6c2469f
--- /dev/null
+++ b/src/views/mall/promotion/discountActivity/DiscountActivityForm.vue
@@ -0,0 +1,265 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <Form
+ ref="formRef"
+ v-loading="formLoading"
+ :isCol="true"
+ :rules="rules"
+ :schema="allSchemas.formSchema"
+ >
+ <!-- 鍏堥�夋嫨 -->
+ <template #spuId>
+ <el-button @click="spuSelectRef.open()">閫夋嫨鍟嗗搧</el-button>
+ <SpuAndSkuList
+ ref="spuAndSkuListRef"
+ :deletable="true"
+ :rule-config="ruleConfig"
+ :spu-list="spuList"
+ :spu-property-list-p="spuPropertyList"
+ @delete="deleteSpu"
+ >
+ <el-table-column align="center" label="浼樻儬閲戦" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.productConfig.discountPrice"
+ :max="parseFloat(fenToYuan(row.price))"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ @change="handleSkuDiscountPriceChange(row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎶樻墸鐧惧垎姣�(%)" min-width="168">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.productConfig.discountPercent"
+ :max="100"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ @change="handleSkuDiscountPercentChange(row)"
+ />
+ </template>
+ </el-table-column>
+ </SpuAndSkuList>
+ </template>
+ </Form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '../components'
+import { allSchemas, rules } from './discountActivity.data'
+import { cloneDeep, debounce } from 'lodash-es'
+import * as DiscountActivityApi from '@/api/mall/promotion/discount/discountActivity'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import { convertToInteger, erpCalculatePercentage, fenToYuan, yuanToFen } from '@/utils'
+import { PromotionDiscountTypeEnum } from '@/utils/constants'
+
+defineOptions({ name: 'PromotionDiscountActivityForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formRef = ref() // 琛ㄥ崟 Ref
+// ================= 鍟嗗搧閫夋嫨鐩稿叧 =================
+
+const spuSelectRef = ref() // 鍟嗗搧鍜屽睘鎬ч�夋嫨 Ref
+const spuAndSkuListRef = ref() // sku 闄愭椂鎶樻墸 閰嶇疆缁勪欢Ref
+const ruleConfig: RuleConfig[] = [
+ {
+ name: 'productConfig.discountPrice',
+ rule: (arg) => arg > 0,
+ message: '鍟嗗搧浼樻儬閲戦涓嶈兘涓� 0 锛侊紒锛�'
+ }
+]
+const spuList = ref<DiscountActivityApi.SpuExtension[]>([]) // 閫夋嫨鐨� spu
+const spuPropertyList = ref<SpuProperty<DiscountActivityApi.SpuExtension>[]>([])
+const spuIds = ref<number[]>([])
+const selectSpu = (spuId: number, skuIds: number[]) => {
+ getSpuDetails(spuId, skuIds)
+}
+/**
+ * 鑾峰彇 SPU 璇︽儏
+ */
+const getSpuDetails = async (
+ spuId: number,
+ skuIds: number[] | undefined,
+ products?: DiscountActivityApi.DiscountProductVO[],
+ type?: string
+) => {
+ // 濡傛灉宸茬粡鍖呭惈 SPU 鍒欒烦杩�
+ if (spuIds.value.includes(spuId)) {
+ if (type !== 'load') {
+ message.error('鏁版嵁閲嶅閫夋嫨锛�')
+ }
+ return
+ }
+ spuIds.value.push(spuId)
+ const res = (await ProductSpuApi.getSpuDetailList([spuId])) as DiscountActivityApi.SpuExtension[]
+ if (res.length == 0) {
+ return
+ }
+ //spuList.value = []
+ // 鍥犱负鍙兘閫夋嫨涓�涓�
+ const spu = res[0]
+ const selectSkus =
+ typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+ selectSkus?.forEach((sku) => {
+ let config: DiscountActivityApi.DiscountProductVO = {
+ skuId: sku.id!,
+ spuId: spu.id!,
+ discountType: 1,
+ discountPercent: 0,
+ discountPrice: 0
+ }
+ if (typeof products !== 'undefined') {
+ const product = products.find((item) => item.skuId === sku.id)
+ if (product) {
+ product.discountPercent = fenToYuan(product.discountPercent) as any
+ product.discountPrice = fenToYuan(product.discountPrice) as any
+ }
+ config = product || config
+ }
+ sku.productConfig = config
+ })
+ spu.skus = selectSkus as DiscountActivityApi.SkuExtension[]
+ spuPropertyList.value.push({
+ spuId: spu.id!,
+ spuDetail: spu,
+ propertyList: getPropertyList(spu)
+ })
+ spuList.value.push(spu)
+}
+
+// ================= end =================
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ await resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = (await DiscountActivityApi.getDiscountActivity(
+ id
+ )) as DiscountActivityApi.DiscountActivityVO
+ for (let productsKey in data.products) {
+ const supId = data.products[productsKey].spuId
+ await getSpuDetails(
+ supId!,
+ data.products?.map((sku) => sku.skuId),
+ data.products,
+ 'load'
+ )
+ }
+ formRef.value.setValues(data)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.getElFormRef().validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鑾峰彇鎶樻墸鍟嗗搧閰嶇疆
+ const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
+ products.forEach((item: DiscountActivityApi.DiscountProductVO) => {
+ item.discountPercent = convertToInteger(item.discountPercent)
+ item.discountPrice = convertToInteger(item.discountPrice)
+ })
+ const data = cloneDeep(formRef.value.formModel) as DiscountActivityApi.DiscountActivityVO
+ data.products = products
+ // 鐪熸鎻愪氦
+ if (formType.value === 'create') {
+ await DiscountActivityApi.createDiscountActivity(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DiscountActivityApi.updateDiscountActivity(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊 sku 浼樻儬閲戦鍙樺姩 */
+const handleSkuDiscountPriceChange = debounce((row: any) => {
+ // 鏍¢獙杈圭晫
+ if (row.productConfig.discountPrice <= 0) {
+ return
+ }
+
+ // 璁剧疆浼樻儬绫诲瀷锛氭弧鍑�
+ row.productConfig.discountType = PromotionDiscountTypeEnum.PRICE.type
+ // 璁剧疆鎶樻墸
+ row.productConfig.discountPercent = erpCalculatePercentage(
+ row.price - yuanToFen(row.productConfig.discountPrice),
+ row.price
+ )
+}, 200)
+/** 澶勭悊 sku 浼樻儬鎶樻墸鍙樺姩 */
+const handleSkuDiscountPercentChange = debounce((row: any) => {
+ // 鏍¢獙杈圭晫
+ if (row.productConfig.discountPercent <= 0 || row.productConfig.discountPercent >= 100) {
+ return
+ }
+
+ // 璁剧疆浼樻儬绫诲瀷锛氭姌鎵�
+ row.productConfig.discountType = PromotionDiscountTypeEnum.PERCENT.type
+ // 璁剧疆婊″噺閲戦
+ row.productConfig.discountPrice = fenToYuan(
+ row.price - row.price * (row.productConfig.discountPercent / 100.0 || 0)
+ )
+}, 200)
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ spuList.value = []
+ spuPropertyList.value = []
+ spuIds.value = []
+ await nextTick()
+ formRef.value.getElFormRef().resetFields()
+}
+
+/**
+ * 鍒犻櫎 SPU
+ */
+const deleteSpu = (spuId: number) => {
+ spuIds.value.splice(
+ spuIds.value.findIndex((item) => item == spuId),
+ 1
+ )
+ spuPropertyList.value.splice(
+ spuPropertyList.value.findIndex((item) => item.spuId == spuId),
+ 1
+ )
+}
+</script>
diff --git a/src/views/mall/promotion/discountActivity/discountActivity.data.ts b/src/views/mall/promotion/discountActivity/discountActivity.data.ts
new file mode 100644
index 0000000..81540b0
--- /dev/null
+++ b/src/views/mall/promotion/discountActivity/discountActivity.data.ts
@@ -0,0 +1,106 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+
+// 琛ㄥ崟鏍¢獙
+export const rules = reactive({
+ name: [required],
+ startTime: [required],
+ endTime: [required],
+ discountType: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+ {
+ label: '娲诲姩鍚嶇О',
+ field: 'name',
+ isSearch: true,
+ form: {
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩寮�濮嬫椂闂�',
+ field: 'startTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩缁撴潫鏃堕棿',
+ field: 'endTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩鍟嗗搧',
+ field: 'spuId',
+ isTable: true,
+ isSearch: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 300
+ }
+ },
+ {
+ label: '澶囨敞',
+ field: 'remark',
+ isSearch: false,
+ form: {
+ component: 'Input',
+ componentProps: {
+ type: 'textarea',
+ rows: 4
+ },
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 300
+ }
+ }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)
diff --git a/src/views/mall/promotion/discountActivity/index.vue b/src/views/mall/promotion/discountActivity/index.vue
new file mode 100644
index 0000000..1841603
--- /dev/null
+++ b/src/views/mall/promotion/discountActivity/index.vue
@@ -0,0 +1,239 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戦檺鏃舵姌鎵�" url="https://doc.iocoder.cn/mall/promotion-discount/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娲诲姩鏃堕棿" prop="activeTime">
+ <el-date-picker
+ v-model="queryParams.activeTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:discount-activity:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板娲诲姩
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="娲诲姩缂栧彿" prop="id" min-width="80" />
+ <el-table-column label="娲诲姩鍚嶇О" prop="name" min-width="140" />
+ <el-table-column label="娲诲姩鏃堕棿" min-width="210">
+ <template #default="scope">
+ {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+ ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+ </template>
+ </el-table-column>
+<!-- <el-table-column label="鍟嗗搧鍥剧墖" prop="spuName" min-width="80">-->
+<!-- <template #default="scope">-->
+<!-- <el-image-->
+<!-- :src="scope.row.picUrl"-->
+<!-- class="h-40px w-40px"-->
+<!-- :preview-src-list="[scope.row.picUrl]"-->
+<!-- preview-teleported-->
+<!-- />-->
+<!-- </template>-->
+<!-- </el-table-column>-->
+<!-- <el-table-column label="鍟嗗搧鏍囬" prop="spuName" min-width="300" />-->
+ <el-table-column label="娲诲姩鐘舵��" align="center" prop="status" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="150px" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['promotion:discount-activity:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleClose(scope.row.id)"
+ v-if="scope.row.status === 0"
+ v-hasPermi="['promotion:discount-activity:close']"
+ >
+ 鍏抽棴
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-else
+ v-hasPermi="['promotion:discount-activity:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DiscountActivityForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as DiscountActivity from '@/api/mall/promotion/discount/discountActivity'
+import DiscountActivityForm from './DiscountActivityForm.vue'
+import { formatDate } from '@/utils/formatTime'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'DiscountActivity' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ activeTime: null,
+ name: null,
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DiscountActivity.getDiscountActivityPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍏抽棴鎸夐挳鎿嶄綔 */
+const handleClose = async (id: number) => {
+ try {
+ // 鍏抽棴鐨勪簩娆$‘璁�
+ await message.confirm('纭鍏抽棴璇ラ檺鏃舵姌鎵f椿鍔ㄥ悧锛�')
+ // 鍙戣捣鍏抽棴
+ await DiscountActivity.closeDiscountActivity(id)
+ message.success('鍏抽棴鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DiscountActivity.deleteDiscountActivity(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+const configList = ref([]) // 鏃舵閰嶇疆绮剧畝鍒楄〃
+// const formatConfigNames = (configId) => {
+// const config = configList.value.find((item) => item.id === configId)
+// return config != null ? `${config.name}[${config.startTime} ~ ${config.endTime}]` : ''
+// }
+
+const formatSeckillPrice = (products) => {
+ // const seckillPrice = Math.min(...products.map((item) => item.seckillPrice))
+ console.log(products)
+ const seckillPrice = 200
+ return `锟�${fenToYuan(seckillPrice)}`
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+})
+</script>
diff --git a/src/views/mall/promotion/diy/page/DiyPageForm.vue b/src/views/mall/promotion/diy/page/DiyPageForm.vue
new file mode 100644
index 0000000..4c47187
--- /dev/null
+++ b/src/views/mall/promotion/diy/page/DiyPageForm.vue
@@ -0,0 +1,104 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="椤甸潰鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ〉闈㈠悕绉�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ <el-form-item label="棰勮鍥�" prop="previewPicUrls">
+ <UploadImgs v-model="formData.previewPicUrls" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DiyPageApi from '@/api/mall/promotion/diy/page'
+
+/** 瑁呬慨椤甸潰琛ㄥ崟 */
+defineOptions({ name: 'DiyPageForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ remark: undefined,
+ previewPicUrls: []
+})
+const formRules = reactive({
+ name: [{ required: true, message: '椤甸潰鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DiyPageApi.getDiyPage(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as DiyPageApi.DiyPageVO
+ if (formType.value === 'create') {
+ await DiyPageApi.createDiyPage(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DiyPageApi.updateDiyPage(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ remark: undefined,
+ previewPicUrls: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/diy/page/decorate.vue b/src/views/mall/promotion/diy/page/decorate.vue
new file mode 100644
index 0000000..fa20c3e
--- /dev/null
+++ b/src/views/mall/promotion/diy/page/decorate.vue
@@ -0,0 +1,74 @@
+<template>
+ <DiyEditor
+ v-if="formData && !formLoading"
+ v-model="formData.property"
+ :title="formData.name"
+ :libs="PAGE_LIBS"
+ @save="submitForm"
+ />
+</template>
+<script setup lang="ts">
+import * as DiyPageApi from '@/api/mall/promotion/diy/page'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { PAGE_LIBS } from '@/components/DiyEditor/util'
+
+/** 瑁呬慨椤甸潰琛ㄥ崟 */
+defineOptions({ name: 'DiyPageDecorate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref<DiyPageApi.DiyPageVO>()
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// 鑾峰彇璇︽儏
+const getPageDetail = async (id: any) => {
+ formLoading.value = true
+ try {
+ formData.value = await DiyPageApi.getDiyPageProperty(id)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await DiyPageApi.updateDiyPageProperty(unref(formData)!)
+ message.success('淇濆瓨鎴愬姛')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ templateId: undefined,
+ name: '',
+ remark: '',
+ previewPicUrls: [],
+ property: ''
+ } as DiyPageApi.DiyPageVO
+ formRef.value?.resetFields()
+}
+
+/** 鍒濆鍖� **/
+const { currentRoute } = useRouter() // 璺敱
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const route = useRoute()
+onMounted(() => {
+ resetForm()
+ if (!route.params.id) {
+ message.warning('鍙傛暟閿欒锛岄〉闈㈢紪鍙蜂笉鑳戒负绌猴紒')
+ delView(unref(currentRoute))
+ return
+ }
+ getPageDetail(route.params.id)
+})
+</script>
diff --git a/src/views/mall/promotion/diy/page/index.vue b/src/views/mall/promotion/diy/page/index.vue
new file mode 100644
index 0000000..f225332
--- /dev/null
+++ b/src/views/mall/promotion/diy/page/index.vue
@@ -0,0 +1,191 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戝晢鍩庤淇�" url="https://doc.iocoder.cn/mall/diy/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="椤甸潰鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ラ〉闈㈠悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:diy-page:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="棰勮鍥�" align="center" prop="previewPicUrls">
+ <template #default="scope">
+ <el-image
+ class="h-40px max-w-40px"
+ v-for="(url, index) in scope.row.previewPicUrls"
+ :key="index"
+ :src="url"
+ :preview-src-list="scope.row.previewPicUrls"
+ :initial-index="index"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="椤甸潰鍚嶇О" align="center" prop="name" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="handleDecorate(scope.row.id)"
+ v-hasPermi="['promotion:diy-page:update']"
+ >
+ 瑁呬慨
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['promotion:diy-page:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['promotion:diy-page:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DiyPageForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as DiyPageApi from '@/api/mall/promotion/diy/page'
+import DiyPageForm from './DiyPageForm.vue'
+
+/** 瑁呬慨椤甸潰 */
+defineOptions({ name: 'DiyPage' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DiyPageApi.getDiyPagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DiyPageApi.deleteDiyPage(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵撳紑瑁呬慨椤甸潰 */
+const { push } = useRouter()
+const handleDecorate = (id: number) => {
+ push({ name: 'DiyPageDecorate', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/diy/template/DiyTemplateForm.vue b/src/views/mall/promotion/diy/template/DiyTemplateForm.vue
new file mode 100644
index 0000000..f430d35
--- /dev/null
+++ b/src/views/mall/promotion/diy/template/DiyTemplateForm.vue
@@ -0,0 +1,104 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユā鏉垮悕绉�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" type="textarea" />
+ </el-form-item>
+ <el-form-item label="棰勮鍥�" prop="previewPicUrls">
+ <UploadImgs v-model="formData.previewPicUrls" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
+
+/** 瑁呬慨妯℃澘琛ㄥ崟 */
+defineOptions({ name: 'DiyTemplateForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ remark: undefined,
+ previewPicUrls: []
+})
+const formRules = reactive({
+ name: [{ required: true, message: '妯℃澘鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DiyTemplateApi.getDiyTemplate(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as DiyTemplateApi.DiyTemplateVO
+ if (formType.value === 'create') {
+ await DiyTemplateApi.createDiyTemplate(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DiyTemplateApi.updateDiyTemplate(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ remark: undefined,
+ previewPicUrls: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/diy/template/decorate.vue b/src/views/mall/promotion/diy/template/decorate.vue
new file mode 100644
index 0000000..63f3931
--- /dev/null
+++ b/src/views/mall/promotion/diy/template/decorate.vue
@@ -0,0 +1,214 @@
+<template>
+ <DiyEditor
+ v-if="formData && !formLoading"
+ v-model="currentFormData!.property"
+ :libs="libs"
+ :preview-url="previewUrl"
+ :show-navigation-bar="selectedTemplateItem !== 0"
+ :show-page-config="selectedTemplateItem !== 0"
+ :show-tab-bar="selectedTemplateItem === 0"
+ :title="templateItems[selectedTemplateItem].name"
+ @reset="handleEditorReset"
+ @save="submitForm"
+ >
+ <template #toolBarLeft>
+ <el-radio-group
+ :model-value="selectedTemplateItem"
+ class="h-full!"
+ @change="handleTemplateItemChange"
+ >
+ <el-tooltip v-for="(item, index) in templateItems" :key="index" :content="item.name">
+ <el-radio-button :value="index">
+ <Icon :icon="item.icon" :size="24" />
+ </el-radio-button>
+ </el-tooltip>
+ </el-radio-group>
+ </template>
+ </DiyEditor>
+</template>
+<script lang="ts" setup>
+// TODO @鐤媯锛氳涓嶈寤轰釜 decorate 鐩綍锛岀劧鍚庢尓杩涘幓锛屾敼鎴� index.vue锛岃繖鏍峰彲浠ユ洿鏄庣‘鐪嬪埌鏄釜鐙珛鐣岄潰鍝堬紝鏇村ソ鎵�
+import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
+import * as DiyPageApi from '@/api/mall/promotion/diy/page'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util' // 鍟嗗煄鐨� DIY 缁勪欢锛屽湪 DiyEditor 鐩綍涓�
+import { toNumber } from 'lodash-es'
+import { isEmpty } from '@/utils/is'
+import { getTenantId } from '@/utils/auth'
+
+/** 瑁呬慨妯℃澘琛ㄥ崟 */
+defineOptions({ name: 'DiyTemplateDecorate' })
+
+// 宸︿笂瑙掑伐鍏锋爮鎿嶄綔鎸夐挳
+const selectedTemplateItem = ref(0)
+const templateItems = reactive([
+ { name: '鍩虹璁剧疆', icon: 'ep:iphone' },
+ { name: '棣栭〉', icon: 'ep:home-filled' },
+ { name: '鎴戠殑', icon: 'ep:user-filled' }
+])
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>()
+const formRef = ref() // 琛ㄥ崟 Ref
+// 褰撳墠缂栬緫鐨勫睘鎬�
+const currentFormData = ref<DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>({
+ property: ''
+} as DiyPageApi.DiyPageVO)
+// templateItem 瀵瑰簲鐨勭紦瀛�
+const currentFormDataMap = ref<
+ Map<string, DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>
+>(new Map())
+// 鍟嗗煄 H5 棰勮鍦板潃
+const previewUrl = ref('')
+
+// 鑾峰彇璇︽儏
+const getPageDetail = async (id: any) => {
+ formLoading.value = true
+ try {
+ formData.value = await DiyTemplateApi.getDiyTemplateProperty(id)
+ // 鎷兼帴鎵嬫満棰勮閾炬帴
+ const domain = import.meta.env.VITE_MALL_H5_DOMAIN
+ previewUrl.value = `${domain}?templateId=${formData.value.id}&tenantId=${getTenantId()}`
+ } finally {
+ formLoading.value = false
+ }
+}
+
+// 妯℃澘缁勪欢搴�
+const templateLibs = [] as DiyComponentLibrary[]
+// 褰撳墠缁勪欢搴�
+const libs = ref<DiyComponentLibrary[]>(templateLibs)
+// 妯℃澘閫夐」鍒囨崲
+const handleTemplateItemChange = (val: number) => {
+ // 缂撳瓨妯$増缂栬緫鏁版嵁
+ currentFormDataMap.value.set(
+ templateItems[selectedTemplateItem.value].name,
+ currentFormData.value!
+ )
+ // 璇诲彇妯$増缂撳瓨
+ const data = currentFormDataMap.value.get(templateItems[val].name)
+
+ // 鍒囨崲妯$増
+ selectedTemplateItem.value = val
+ // 缂栬緫妯℃澘
+ if (val === 0) {
+ libs.value = templateLibs
+ currentFormData.value = (isEmpty(data) ? formData.value : data) as
+ | DiyTemplateApi.DiyTemplatePropertyVO
+ | DiyPageApi.DiyPageVO
+ return
+ }
+
+ // 缂栬緫椤甸潰
+ libs.value = PAGE_LIBS
+ currentFormData.value = (
+ isEmpty(data)
+ ? formData.value!.pages.find(
+ (page: DiyPageApi.DiyPageVO) => page.name === templateItems[val].name
+ )
+ : data
+ ) as DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 瀵规墍鏈夌殑 templateItems 閮借繘琛屼繚瀛橈紝鏈夌紦瀛樺垯淇濆瓨缂撳瓨锛岃В鍐抽兘鏈変慨鏀规椂鍙繚瀛樹簡褰撳墠鎵�缂栬緫鐨� templateItem锛屽鑷磋淇晥鏋滃瓨鍦ㄥ樊寮�
+ for (let i = 0; i < templateItems.length; i++) {
+ const data = currentFormDataMap.value.get(templateItems[i].name) as any
+ // 鎯呭喌涓�锛氬熀纭�璁剧疆
+ if (i === 0) {
+ // 鎻愪氦妯℃澘灞炴��
+ await DiyTemplateApi.updateDiyTemplateProperty(isEmpty(data) ? unref(formData)! : data)
+ continue
+ }
+ // 鎻愪氦椤甸潰灞炴��
+ // 鎯呭喌浜岋細鎻愪氦褰撳墠姝e湪缂栬緫鐨勯〉闈�
+ if (currentFormData.value?.name.includes(templateItems[i].name)) {
+ await DiyPageApi.updateDiyPageProperty(unref(currentFormData)!)
+ continue
+ }
+ // 鎯呭喌涓夛細鎻愪氦椤甸潰缂栬緫缂撳瓨
+ if (!isEmpty(data)) {
+ await DiyPageApi.updateDiyPageProperty(data!)
+ }
+ }
+ message.success('淇濆瓨鎴愬姛')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ used: false,
+ usedTime: undefined,
+ remark: '',
+ previewPicUrls: [],
+ property: '',
+ pages: []
+ } as DiyTemplateApi.DiyTemplatePropertyVO
+ formRef.value?.resetFields()
+}
+
+// 閲嶇疆鏃惰褰曞綋鍓嶇紪杈戠殑椤甸潰
+const handleEditorReset = () => storePageIndex()
+
+//#region 鏃犳劅鍒锋柊
+// 璁板綍鏍囪瘑
+const DIY_PAGE_INDEX_KEY = 'diy_page_index'
+
+// 1. 璁板綍
+function storePageIndex() {
+ debugger
+ return sessionStorage.setItem(DIY_PAGE_INDEX_KEY, `${selectedTemplateItem.value}`)
+}
+// 2. 鎭㈠
+const recoverPageIndex = () => {
+ debugger
+ // 鎭㈠閲嶇疆鍓嶇殑椤甸潰锛岄粯璁ゆ槸绗竴涓〉闈�
+ const pageIndex = toNumber(sessionStorage.getItem(DIY_PAGE_INDEX_KEY)) || 0
+ // 绉婚櫎鏍囪
+ sessionStorage.removeItem(DIY_PAGE_INDEX_KEY)
+
+ // 閲嶆柊鍒濆鍖栨暟鎹�
+ currentFormData.value = formData.value as
+ | DiyTemplateApi.DiyTemplatePropertyVO
+ | DiyPageApi.DiyPageVO
+ currentFormDataMap.value = new Map<
+ string,
+ DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO
+ >()
+ // 鍒囨崲椤甸潰
+ if (pageIndex !== selectedTemplateItem.value) {
+ handleTemplateItemChange(pageIndex)
+ }
+}
+//#endregion
+
+/** 鍒濆鍖� **/
+const { currentRoute } = useRouter() // 璺敱
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+onMounted(async () => {
+ resetForm()
+ if (!currentRoute.value.params.id) {
+ message.warning('鍙傛暟閿欒锛岄〉闈㈢紪鍙蜂笉鑳戒负绌猴紒')
+ delView(unref(currentRoute))
+ return
+ }
+
+ // 鏌ヨ璇︽儏
+ await getPageDetail(currentRoute.value.params.id)
+ // 鎭㈠閲嶇疆鍓嶇殑椤甸潰
+ recoverPageIndex()
+})
+</script>
diff --git a/src/views/mall/promotion/diy/template/index.vue b/src/views/mall/promotion/diy/template/index.vue
new file mode 100644
index 0000000..1eddddc
--- /dev/null
+++ b/src/views/mall/promotion/diy/template/index.vue
@@ -0,0 +1,220 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戝晢鍩庤淇�" url="https://doc.iocoder.cn/mall/diy/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:diy-template:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="棰勮鍥�" align="center" prop="previewPicUrls">
+ <template #default="scope">
+ <el-image
+ class="h-40px max-w-40px"
+ v-for="(url, index) in scope.row.previewPicUrls"
+ :key="index"
+ :src="url"
+ :preview-src-list="scope.row.previewPicUrls"
+ :initial-index="index"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯℃澘鍚嶇О" align="center" prop="name" min-width="180" />
+ <el-table-column label="鏄惁浣跨敤" align="center" prop="used">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.used" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" min-width="180" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="200">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="handleDecorate(scope.row.id)"
+ v-hasPermi="['promotion:diy-template:update']"
+ >
+ 瑁呬慨
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['promotion:diy-template:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <template v-if="!scope.row.used">
+ <el-button
+ link
+ type="primary"
+ @click="handleUse(scope.row)"
+ v-hasPermi="['promotion:diy-template:use']"
+ >
+ 浣跨敤
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['promotion:diy-template:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DiyTemplateForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
+import DiyTemplateForm from './DiyTemplateForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+/** 瑁呬慨妯℃澘 */
+defineOptions({ name: 'DiyTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DiyTemplateApi.getDiyTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DiyTemplateApi.deleteDiyTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 浣跨敤妯℃澘 */
+const handleUse = async (row: DiyTemplateApi.DiyTemplateVO) => {
+ try {
+ // 浣跨敤妯℃澘鐨勪簩娆$‘璁�
+ await message.confirm(`鏄惁浣跨敤妯℃澘鈥�${row.name}鈥�?`)
+ // 鍙戣捣鍒犻櫎
+ await DiyTemplateApi.useDiyTemplate(row.id!)
+ message.success('浣跨敤鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵撳紑瑁呬慨椤甸潰 */
+const { push } = useRouter()
+const handleDecorate = (id: number) => {
+ push({ name: 'DiyTemplateDecorate', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/kefu/components/KeFuConversationList.vue b/src/views/mall/promotion/kefu/components/KeFuConversationList.vue
new file mode 100644
index 0000000..318e27d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/KeFuConversationList.vue
@@ -0,0 +1,254 @@
+<template>
+ <el-aside class="kefu pt-5px h-100%" width="260px">
+ <div class="color-[#999] font-bold my-10px">
+ 浼氳瘽璁板綍({{ kefuStore.getConversationList.length }})
+ </div>
+ <div
+ v-for="item in kefuStore.getConversationList"
+ :key="item.id"
+ :class="{ active: item.id === activeConversationId, pinned: item.adminPinned }"
+ class="kefu-conversation px-10px flex items-center"
+ @click="openRightMessage(item)"
+ @contextmenu.prevent="rightClick($event as PointerEvent, item)"
+ >
+ <div class="flex justify-center items-center w-100%">
+ <div class="flex justify-center items-center w-50px h-50px">
+ <!-- 澶村儚 + 鏈 -->
+ <el-badge
+ :hidden="item.adminUnreadMessageCount === 0"
+ :max="99"
+ :value="item.adminUnreadMessageCount"
+ >
+ <el-avatar :src="item.userAvatar" alt="avatar" />
+ </el-badge>
+ </div>
+ <div class="ml-10px w-100%">
+ <div class="flex justify-between items-center w-100%">
+ <span class="username">{{ item.userNickname }}</span>
+ <span class="color-[#999]" style="font-size: 13px">
+ {{ lastMessageTimeMap.get(item.id) ?? '璁$畻涓�' }}
+ </span>
+ </div>
+ <!-- 鏈�鍚庤亰澶╁唴瀹� -->
+ <div
+ v-dompurify-html="
+ getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
+ "
+ class="last-message flex items-center color-[#999]"
+ >
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍙抽敭锛岃繘琛屾搷浣滐紙绫讳技寰俊锛� -->
+ <ul v-show="showRightMenu" :style="rightMenuStyle" class="right-menu-ul">
+ <li
+ v-show="!rightClickConversation.adminPinned"
+ class="flex items-center"
+ @click.stop="updateConversationPinned(true)"
+ >
+ <Icon class="mr-5px" icon="ep:top" />
+ 缃《浼氳瘽
+ </li>
+ <li
+ v-show="rightClickConversation.adminPinned"
+ class="flex items-center"
+ @click.stop="updateConversationPinned(false)"
+ >
+ <Icon class="mr-5px" icon="ep:bottom" />
+ 鍙栨秷缃《
+ </li>
+ <li class="flex items-center" @click.stop="deleteConversation">
+ <Icon class="mr-5px" color="red" icon="ep:delete" />
+ 鍒犻櫎浼氳瘽
+ </li>
+ <li class="flex items-center" @click.stop="closeRightMenu">
+ <Icon class="mr-5px" color="red" icon="ep:close" />
+ 鍙栨秷
+ </li>
+ </ul>
+ </el-aside>
+</template>
+
+<script lang="ts" setup>
+import { KeFuConversationApi, KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { useEmoji } from './tools/emoji'
+import { formatPast } from '@/utils/formatTime'
+import { KeFuMessageContentTypeEnum } from './tools/constants'
+import { useAppStore } from '@/store/modules/app'
+import { useMallKefuStore } from '@/store/modules/mall/kefu'
+import { jsonParse } from '@/utils'
+
+defineOptions({ name: 'KeFuConversationList' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const appStore = useAppStore()
+const kefuStore = useMallKefuStore() // 瀹㈡湇缂撳瓨
+const { replaceEmoji } = useEmoji()
+const activeConversationId = ref(-1) // 閫変腑鐨勪細璇�
+const collapse = computed(() => appStore.getCollapse) // 鎶樺彔鑿滃崟
+
+/** 璁$畻娑堟伅鏈�鍚庡彂閫佹椂闂磋窛绂荤幇鍦ㄨ繃鍘讳簡澶氫箙 */
+const lastMessageTimeMap = ref<Map<number, string>>(new Map<number, string>())
+const calculationLastMessageTime = () => {
+ kefuStore.getConversationList?.forEach((item) => {
+ lastMessageTimeMap.value.set(item.id, formatPast(item.lastMessageTime, 'YYYY-MM-DD'))
+ })
+}
+defineExpose({ calculationLastMessageTime })
+
+/** 鎵撳紑鍙充晶鐨勬秷鎭垪琛� */
+const emits = defineEmits<{
+ (e: 'change', v: KeFuConversationRespVO): void
+}>()
+const openRightMessage = (item: KeFuConversationRespVO) => {
+ // 鍚屼竴涓細璇濆垯涓嶅鐞�
+ if (activeConversationId.value === item.id) {
+ return
+ }
+ activeConversationId.value = item.id
+ emits('change', item)
+}
+
+/** 鑾峰緱娑堟伅绫诲瀷 */
+const getConversationDisplayText = computed(
+ () => (lastMessageContentType: number, lastMessageContent: string) => {
+ switch (lastMessageContentType) {
+ case KeFuMessageContentTypeEnum.SYSTEM:
+ return '[绯荤粺娑堟伅]'
+ case KeFuMessageContentTypeEnum.VIDEO:
+ return '[瑙嗛娑堟伅]'
+ case KeFuMessageContentTypeEnum.IMAGE:
+ return '[鍥剧墖娑堟伅]'
+ case KeFuMessageContentTypeEnum.PRODUCT:
+ return '[鍟嗗搧娑堟伅]'
+ case KeFuMessageContentTypeEnum.ORDER:
+ return '[璁㈠崟娑堟伅]'
+ case KeFuMessageContentTypeEnum.VOICE:
+ return '[璇煶娑堟伅]'
+ case KeFuMessageContentTypeEnum.TEXT:
+ return replaceEmoji(jsonParse(lastMessageContent).text || lastMessageContent)
+ default:
+ return ''
+ }
+ }
+)
+
+//======================= 鍙抽敭鑿滃崟 =======================
+const showRightMenu = ref(false) // 鏄剧ず鍙抽敭鑿滃崟
+const rightMenuStyle = ref<any>({}) // 鍙抽敭鑿滃崟 Style
+const rightClickConversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 鍙抽敭閫変腑鐨勪細璇濆璞�
+
+/** 鎵撳紑鍙抽敭鑿滃崟 */
+const rightClick = (mouseEvent: PointerEvent, item: KeFuConversationRespVO) => {
+ rightClickConversation.value = item
+ // 鏄剧ず鍙抽敭鑿滃崟
+ showRightMenu.value = true
+ rightMenuStyle.value = {
+ top: mouseEvent.clientY - 110 + 'px',
+ left: collapse.value ? mouseEvent.clientX - 80 + 'px' : mouseEvent.clientX - 210 + 'px'
+ }
+}
+/** 鍏抽棴鍙抽敭鑿滃崟 */
+const closeRightMenu = () => {
+ showRightMenu.value = false
+}
+
+/** 缃《浼氳瘽 */
+const updateConversationPinned = async (adminPinned: boolean) => {
+ // 1. 浼氳瘽缃《/鍙栨秷缃《
+ await KeFuConversationApi.updateConversationPinned({
+ id: rightClickConversation.value.id,
+ adminPinned
+ })
+ message.notifySuccess(adminPinned ? '缃《鎴愬姛' : '鍙栨秷缃《鎴愬姛')
+ // 2. 鍏抽棴鍙抽敭鑿滃崟锛屾洿鏂颁細璇濆垪琛�
+ closeRightMenu()
+ await kefuStore.updateConversation(rightClickConversation.value.id)
+}
+
+/** 鍒犻櫎浼氳瘽 */
+const deleteConversation = async () => {
+ // 1. 鍒犻櫎浼氳瘽
+ await message.confirm('鎮ㄧ‘瀹氳鍒犻櫎璇ヤ細璇濆悧锛�')
+ await KeFuConversationApi.deleteConversation(rightClickConversation.value.id)
+ // 2. 鍏抽棴鍙抽敭鑿滃崟锛屾洿鏂颁細璇濆垪琛�
+ closeRightMenu()
+ kefuStore.deleteConversation(rightClickConversation.value.id)
+}
+
+/** 鐩戝惉鍙抽敭鑿滃崟鐨勬樉绀虹姸鎬侊紝娣诲姞鐐瑰嚮浜嬩欢鐩戝惉鍣� */
+watch(showRightMenu, (val) => {
+ if (val) {
+ document.body.addEventListener('click', closeRightMenu)
+ } else {
+ document.body.removeEventListener('click', closeRightMenu)
+ }
+})
+
+const timer = ref<any>()
+/** 鍒濆鍖� */
+onMounted(() => {
+ timer.value = setInterval(calculationLastMessageTime, 1000 * 10) // 鍗佺璁$畻涓�娆�
+})
+/** 缁勪欢鍗歌浇鍓� */
+onBeforeUnmount(() => {
+ clearInterval(timer.value)
+})
+</script>
+
+<style lang="scss" scoped>
+.kefu {
+ background-color: var(--app-content-bg-color);
+
+ &-conversation {
+ height: 60px;
+ //background-color: #fff;
+ //transition: border-left 0.05s ease-in-out; /* 璁剧疆杩囨浮鏁堟灉 */
+
+ .username {
+ min-width: 0;
+ max-width: 60%;
+ }
+
+ .last-message {
+ font-size: 13px;
+ }
+
+ .last-message,
+ .username {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+ }
+ }
+
+ .active {
+ background-color: rgba(128, 128, 128, 0.5); // 閫忔槑鑹诧紝鏆楅粦妯″紡涓嬩篃鑳戒綋鐜�
+ }
+
+ .right-menu-ul {
+ position: absolute;
+ background-color: var(--app-content-bg-color);
+ padding: 5px;
+ margin: 0;
+ list-style-type: none; /* 绉婚櫎榛樿鐨勯」鐩鍙� */
+ border-radius: 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 闃村奖鏁堟灉 */
+ width: 130px;
+
+ li {
+ padding: 8px 16px;
+ cursor: pointer;
+ border-radius: 12px;
+ transition: background-color 0.3s; /* 骞虫粦杩囨浮 */
+ &:hover {
+ background-color: var(--left-menu-bg-active-color); /* 鎮仠鏃剁殑鑳屾櫙棰滆壊 */
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/mall/promotion/kefu/components/KeFuMessageList.vue b/src/views/mall/promotion/kefu/components/KeFuMessageList.vue
new file mode 100644
index 0000000..a293735
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/KeFuMessageList.vue
@@ -0,0 +1,526 @@
+<template>
+ <el-container v-if="showKeFuMessageList" class="kefu">
+ <el-header class="kefu-header">
+ <div class="kefu-title">{{ conversation.userNickname }}</div>
+ </el-header>
+ <el-main class="kefu-content overflow-visible">
+ <el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
+ <div v-if="refreshContent" ref="innerRef" class="w-[100%] px-10px">
+ <!-- 娑堟伅鍒楄〃 -->
+ <div v-for="(item, index) in getMessageList0" :key="item.id" class="w-[100%]">
+ <div class="flex justify-center items-center mb-20px">
+ <!-- 鏃ユ湡 -->
+ <div
+ v-if="
+ item.contentType !== KeFuMessageContentTypeEnum.SYSTEM && showTime(item, index)
+ "
+ class="date-message"
+ >
+ {{ formatDate(item.createTime) }}
+ </div>
+ <!-- 绯荤粺娑堟伅 -->
+ <div
+ v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
+ class="system-message"
+ >
+ {{ item.content }}
+ </div>
+ </div>
+ <div
+ :class="[
+ item.senderType === UserTypeEnum.MEMBER
+ ? `ss-row-left`
+ : item.senderType === UserTypeEnum.ADMIN
+ ? `ss-row-right`
+ : ''
+ ]"
+ class="flex mb-20px w-[100%]"
+ >
+ <el-avatar
+ v-if="item.senderType === UserTypeEnum.MEMBER"
+ :src="conversation.userAvatar"
+ alt="avatar"
+ class="w-60px h-60px"
+ />
+ <div
+ :class="{
+ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType
+ }"
+ >
+ <!-- 鏂囨湰娑堟伅 -->
+ <MessageItem :message="item">
+ <template v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType">
+ <div
+ v-dompurify-html="replaceEmoji(getMessageContent(item).text || item.content)"
+ class="line-height-normal text-justify h-1/1 w-full"
+ ></div>
+ </template>
+ </MessageItem>
+ <!-- 鍥剧墖娑堟伅 -->
+ <MessageItem :message="item">
+ <el-image
+ v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
+ :initial-index="0"
+ :preview-src-list="[getMessageContent(item).picUrl || item.content]"
+ :src="getMessageContent(item).picUrl || item.content"
+ class="w-200px mx-10px"
+ fit="contain"
+ preview-teleported
+ />
+ </MessageItem>
+ <!-- 鍟嗗搧娑堟伅 -->
+ <MessageItem :message="item">
+ <ProductItem
+ v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
+ :picUrl="getMessageContent(item).picUrl"
+ :price="getMessageContent(item).price"
+ :sales-count="getMessageContent(item).salesCount"
+ :spuId="getMessageContent(item).spuId"
+ :stock="getMessageContent(item).stock"
+ :title="getMessageContent(item).spuName"
+ class="max-w-300px mx-10px"
+ />
+ </MessageItem>
+ <!-- 璁㈠崟娑堟伅 -->
+ <MessageItem :message="item">
+ <OrderItem
+ v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
+ :message="item"
+ class="max-w-100% mx-10px"
+ />
+ </MessageItem>
+ </div>
+ <el-avatar
+ v-if="item.senderType === UserTypeEnum.ADMIN"
+ :src="item.senderAvatar"
+ alt="avatar"
+ />
+ </div>
+ </div>
+ </div>
+ </el-scrollbar>
+ <div
+ v-show="showNewMessageTip"
+ class="newMessageTip flex items-center cursor-pointer"
+ @click="handleToNewMessage"
+ >
+ <span>鏈夋柊娑堟伅</span>
+ <Icon class="ml-5px" icon="ep:bottom" />
+ </div>
+ </el-main>
+ <el-footer class="kefu-footer">
+ <div class="chat-tools flex items-center">
+ <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
+ <PictureSelectUpload
+ class="ml-15px mt-3px cursor-pointer"
+ @send-picture="handleSendPicture"
+ />
+ </div>
+ <el-input
+ v-model="message"
+ :rows="6"
+ placeholder="杈撳叆娑堟伅锛孍nter鍙戦�侊紝Shift+Enter鎹㈣"
+ style="border-style: none"
+ type="textarea"
+ @keyup.enter.prevent="handleSendMessage"
+ />
+ </el-footer>
+ </el-container>
+ <el-container v-else class="kefu">
+ <el-main>
+ <el-empty description="璇烽�夋嫨宸︿晶鐨勪竴涓細璇濆悗寮�濮�" />
+ </el-main>
+ </el-container>
+</template>
+
+<script lang="ts" setup>
+import { ElScrollbar as ElScrollbarType } from 'element-plus'
+import { KeFuMessageApi, KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import EmojiSelectPopover from './tools/EmojiSelectPopover.vue'
+import PictureSelectUpload from './tools/PictureSelectUpload.vue'
+import ProductItem from './message/ProductItem.vue'
+import OrderItem from './message/OrderItem.vue'
+import { Emoji, useEmoji } from './tools/emoji'
+import { KeFuMessageContentTypeEnum } from './tools/constants'
+import { isEmpty } from '@/utils/is'
+import { UserTypeEnum } from '@/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import { debounce } from 'lodash-es'
+import { jsonParse } from '@/utils'
+import { useMallKefuStore } from '@/store/modules/mall/kefu'
+
+dayjs.extend(relativeTime)
+
+defineOptions({ name: 'KeFuMessageList' })
+
+const message = ref('') // 娑堟伅寮圭獥
+const { replaceEmoji } = useEmoji()
+const messageTool = useMessage()
+const messageList = ref<KeFuMessageRespVO[]>([]) // 娑堟伅鍒楄〃
+const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 鐢ㄦ埛浼氳瘽
+const showNewMessageTip = ref(false) // 鏄剧ず鏈夋柊娑堟伅鎻愮ず
+const queryParams = reactive({
+ conversationId: 0,
+ createTime: undefined
+})
+const total = ref(0) // 娑堟伅鎬绘潯鏁�
+const refreshContent = ref(false) // 鍐呭鍒锋柊,涓昏瑙e喅浼氳瘽娑堟伅椤甸潰楂樺害涓嶄竴鑷村鑷寸殑婊氬姩鍔熻兘绮惧害澶辨晥
+const kefuStore = useMallKefuStore() // 瀹㈡湇缂撳瓨
+
+/** 鑾锋倝娑堟伅鍐呭 */
+const getMessageContent = computed(() => (item: any) => jsonParse(item.content))
+/** 鑾峰緱娑堟伅鍒楄〃 */
+const getMessageList = async () => {
+ const res = await KeFuMessageApi.getKeFuMessageList(queryParams)
+ if (isEmpty(res)) {
+ // 褰撹繑鍥炵殑鏄┖鍒楄〃璇存槑娌℃湁娑堟伅鎴栬�呭凡缁忔煡璇㈠畬浜嗗巻鍙叉秷鎭�
+ skipGetMessageList.value = true
+ return
+ }
+ queryParams.createTime = formatDate(res.at(-1).createTime) as any
+
+ // 鎯呭喌涓�锛氬姞杞芥渶鏂版秷鎭�
+ if (!queryParams.createTime) {
+ messageList.value = res
+ } else {
+ // 鎯呭喌浜岋細鍔犺浇鍘嗗彶娑堟伅
+ for (const item of res) {
+ pushMessage(item)
+ }
+ }
+ refreshContent.value = true
+}
+
+/** 娣诲姞娑堟伅 */
+const pushMessage = (message: any) => {
+ if (messageList.value.some((val) => val.id === message.id)) {
+ return
+ }
+ messageList.value.push(message)
+}
+
+/** 鎸夌収鏃堕棿鍊掑簭锛岃幏鍙栨秷鎭垪琛� */
+const getMessageList0 = computed(() => {
+ messageList.value.sort((a: any, b: any) => a.createTime - b.createTime)
+ return messageList.value
+})
+
+/** 鍒锋柊娑堟伅鍒楄〃 */
+const refreshMessageList = async (message?: any) => {
+ if (!conversation.value) {
+ return
+ }
+
+ if (typeof message !== 'undefined') {
+ // 褰撳墠鏌ヨ浼氳瘽涓庢秷鎭墍灞炰細璇濅笉涓�鑷村垯涓嶅仛澶勭悊
+ if (message.conversationId !== conversation.value.id) {
+ return
+ }
+ pushMessage(message)
+ } else {
+ queryParams.createTime = undefined
+ await getMessageList()
+ }
+
+ if (loadHistory.value) {
+ // 鍙充笅瑙掓樉绀烘湁鏂版秷鎭彁绀�
+ showNewMessageTip.value = true
+ } else {
+ // 婊氬姩鍒版渶鏂版秷鎭
+ await handleToNewMessage()
+ }
+}
+
+/** 鑾峰緱鏂颁細璇濈殑娑堟伅鍒楄〃, 鐐瑰嚮鍒囨崲鏃讹紝璇诲彇缂撳瓨锛涚劧鍚庡紓姝ヨ幏鍙栨柊娑堟伅锛宮erge 涓嬶紱 */
+const getNewMessageList = async (val: KeFuConversationRespVO) => {
+ // 1. 缂撳瓨褰撳墠浼氳瘽娑堟伅鍒楄〃
+ kefuStore.saveMessageList(conversation.value.id, messageList.value)
+ // 2.1 浼氳瘽鍒囨崲,閲嶇疆鐩稿叧鍙傛暟
+ messageList.value = kefuStore.getConversationMessageList(val.id) || []
+ total.value = messageList.value.length || 0
+ loadHistory.value = false
+ refreshContent.value = false
+ skipGetMessageList.value = false
+ // 2.2 璁剧疆浼氳瘽鐩稿叧灞炴��
+ conversation.value = val
+ queryParams.conversationId = val.id
+ queryParams.createTime = undefined
+ // 3. 鑾峰彇娑堟伅
+ await refreshMessageList()
+}
+defineExpose({ getNewMessageList, refreshMessageList })
+
+const showKeFuMessageList = computed(() => !isEmpty(conversation.value)) // 鏄惁鏄剧ず鑱婂ぉ鍖哄煙
+const skipGetMessageList = ref(false) // 璺宠繃娑堟伅鑾峰彇
+
+/** 澶勭悊琛ㄦ儏閫夋嫨 */
+const handleEmojiSelect = (item: Emoji) => {
+ message.value += item.name
+}
+
+/** 澶勭悊鍥剧墖鍙戦�� */
+const handleSendPicture = async (picUrl: string) => {
+ // 缁勭粐鍙戦�佹秷鎭�
+ const msg = {
+ conversationId: conversation.value.id,
+ contentType: KeFuMessageContentTypeEnum.IMAGE,
+ content: JSON.stringify({ picUrl })
+ }
+ await sendMessage(msg)
+}
+
+/** 鍙戦�佹枃鏈秷鎭� */
+const handleSendMessage = async (event: any) => {
+ // shift 涓嶅彂閫�
+ if (event.shiftKey) {
+ return
+ }
+ // 1. 鏍¢獙娑堟伅鏄惁涓虹┖
+ if (isEmpty(unref(message.value)?.trim())) {
+ messageTool.notifyWarning('璇疯緭鍏ユ秷鎭悗鍐嶅彂閫佸摝锛�')
+ message.value = ''
+ return
+ }
+ // 2. 缁勭粐鍙戦�佹秷鎭�
+ const msg = {
+ conversationId: conversation.value.id,
+ contentType: KeFuMessageContentTypeEnum.TEXT,
+ content: JSON.stringify({ text: message.value })
+ }
+ await sendMessage(msg)
+}
+
+/** 鐪熸鍙戦�佹秷鎭� 銆愬叡鐢ㄣ��*/
+const sendMessage = async (msg: any) => {
+ // 鍙戦�佹秷鎭�
+ await KeFuMessageApi.sendKeFuMessage(msg)
+ message.value = ''
+ // 鍔犺浇娑堟伅鍒楄〃
+ await refreshMessageList()
+ // 鏇存柊浼氳瘽缂撳瓨
+ await kefuStore.updateConversation(conversation.value.id)
+}
+
+/** 婊氬姩鍒板簳閮� */
+const innerRef = ref<HTMLDivElement>()
+const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
+const scrollToBottom = async () => {
+ // 1. 棣栨鍔犺浇鏃舵粴鍔ㄥ埌鏈�鏂版秷鎭紝濡傛灉鍔犺浇鐨勬槸鍘嗗彶娑堟伅鍒欎笉婊氬姩
+ if (loadHistory.value) {
+ return
+ }
+ // 2.1 婊氬姩鍒版渶鏂版秷鎭紝鍏抽棴鏂版秷鎭彁绀�
+ await nextTick()
+ scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight)
+ showNewMessageTip.value = false
+ // 2.2 娑堟伅宸茶
+ await KeFuMessageApi.updateKeFuMessageReadStatus(conversation.value.id)
+}
+
+/** 鏌ョ湅鏂版秷鎭� */
+const handleToNewMessage = async () => {
+ loadHistory.value = false
+ await scrollToBottom()
+}
+
+const loadHistory = ref(false) // 鍔犺浇鍘嗗彶娑堟伅
+/** 澶勭悊娑堟伅鍒楄〃婊氬姩浜嬩欢(debounce 闄愭祦) */
+const handleScroll = debounce(({ scrollTop }) => {
+ if (skipGetMessageList.value) {
+ return
+ }
+ // 瑙﹂《鑷姩鍔犺浇涓嬩竴椤垫暟鎹�
+ if (Math.floor(scrollTop) === 0) {
+ handleOldMessage()
+ }
+ const wrap = scrollbarRef.value?.wrapRef
+ // 瑙﹀簳閲嶇疆
+ if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
+ loadHistory.value = false
+ refreshMessageList()
+ }
+}, 200)
+/** 鍔犺浇鍘嗗彶娑堟伅 */
+const handleOldMessage = async () => {
+ // 璁板綍宸叉湁椤甸潰楂樺害
+ const oldPageHeight = innerRef.value?.clientHeight
+ if (!oldPageHeight) {
+ return
+ }
+ loadHistory.value = true
+ await getMessageList()
+ // 绛夐〉闈㈠姞杞藉畬鍚庯紝鑾峰緱涓婁竴椤垫渶鍚庝竴鏉℃秷鎭殑浣嶇疆锛屾帶鍒舵粴鍔ㄥ埌瀹冩墍鍦ㄤ綅缃�
+ scrollbarRef.value!.setScrollTop(innerRef.value!.clientHeight - oldPageHeight)
+}
+
+/**
+ * 鏄惁鏄剧ず鏃堕棿
+ *
+ * @param {*} item - 鏁版嵁
+ * @param {*} index - 绱㈠紩
+ */
+const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
+ if (unref(messageList.value)[index + 1]) {
+ let dateString = dayjs(unref(messageList.value)[index + 1].createTime).fromNow()
+ return dateString !== dayjs(unref(item).createTime).fromNow()
+ }
+ return false
+})
+</script>
+
+<style lang="scss" scoped>
+.kefu {
+ background-color: var(--app-content-bg-color);
+ position: relative;
+ width: calc(100% - 300px - 260px);
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px; /* 瀹為檯瀹藉害 */
+ height: 100%;
+ background-color: var(--el-border-color);
+ transform: scaleX(0.3); /* 缂╁皬瀹藉害 */
+ }
+
+ .kefu-header {
+ background-color: var(--app-content-bg-color);
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &::before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 1px; /* 鍒濆瀹藉害 */
+ background-color: var(--el-border-color);
+ transform: scaleY(0.3); /* 缂╁皬瑙嗚楂樺害 */
+ }
+
+ &-title {
+ font-size: 18px;
+ font-weight: bold;
+ }
+ }
+
+ &-content {
+ margin: 0;
+ padding: 10px;
+ position: relative;
+ height: 100%;
+ width: 100%;
+
+ .newMessageTip {
+ position: absolute;
+ bottom: 35px;
+ right: 35px;
+ background-color: var(--app-content-bg-color);
+ padding: 10px;
+ border-radius: 30px;
+ font-size: 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 闃村奖鏁堟灉 */
+ }
+
+ .ss-row-left {
+ justify-content: flex-start;
+
+ .kefu-message {
+ background-color: #fff;
+ margin-left: 10px;
+ margin-top: 3px;
+ border-top-right-radius: 10px;
+ border-bottom-right-radius: 10px;
+ border-bottom-left-radius: 10px;
+ }
+ }
+
+ .ss-row-right {
+ justify-content: flex-end;
+
+ .kefu-message {
+ background-color: rgb(206, 223, 255);
+ margin-right: 10px;
+ margin-top: 3px;
+ border-top-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+ border-bottom-left-radius: 10px;
+ }
+ }
+
+ // 娑堟伅姘旀场
+ .kefu-message {
+ color: #414141;
+ font-weight: 500;
+ padding: 5px 10px;
+ width: auto;
+ max-width: 50%;
+ //text-align: left;
+ //display: inline-block !important;
+ //word-break: break-all;
+ transition: all 0.2s;
+
+ &:hover {
+ transform: scale(1.03);
+ }
+ }
+
+ .date-message,
+ .system-message {
+ width: fit-content;
+ background-color: rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ padding: 0 5px;
+ color: #fff;
+ font-size: 10px;
+ }
+ }
+
+ .kefu-footer {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: auto;
+ margin: 0;
+ padding: 0;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 1px; /* 鍒濆瀹藉害 */
+ background-color: var(--el-border-color);
+ transform: scaleY(0.3); /* 缂╁皬瑙嗚楂樺害 */
+ }
+
+ .chat-tools {
+ width: 100%;
+ height: 44px;
+ }
+ }
+
+ ::v-deep(textarea) {
+ resize: none;
+ background-color: var(--app-content-bg-color);
+ }
+
+ :deep(.el-input__wrapper) {
+ box-shadow: none !important;
+ border-radius: 0;
+ }
+
+ ::v-deep(.el-textarea__inner) {
+ box-shadow: none !important;
+ }
+}
+</style>
diff --git a/src/views/mall/promotion/kefu/components/asserts/a.png b/src/views/mall/promotion/kefu/components/asserts/a.png
new file mode 100644
index 0000000..3293900
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/a.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/aini.png b/src/views/mall/promotion/kefu/components/asserts/aini.png
new file mode 100644
index 0000000..02cf5c4
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/aini.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/aixin.png b/src/views/mall/promotion/kefu/components/asserts/aixin.png
new file mode 100644
index 0000000..25e6422
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/aixin.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/baiyan.png b/src/views/mall/promotion/kefu/components/asserts/baiyan.png
new file mode 100644
index 0000000..d16260a
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/baiyan.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/bizui.png b/src/views/mall/promotion/kefu/components/asserts/bizui.png
new file mode 100644
index 0000000..a3b1800
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/bizui.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/buhaoyisi.png b/src/views/mall/promotion/kefu/components/asserts/buhaoyisi.png
new file mode 100644
index 0000000..54c4b3f
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/buhaoyisi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/bukesiyi.png b/src/views/mall/promotion/kefu/components/asserts/bukesiyi.png
new file mode 100644
index 0000000..5f272e3
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/bukesiyi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/dajing.png b/src/views/mall/promotion/kefu/components/asserts/dajing.png
new file mode 100644
index 0000000..8649727
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/dajing.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/danao.png b/src/views/mall/promotion/kefu/components/asserts/danao.png
new file mode 100644
index 0000000..aa85a29
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/danao.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/daxiao.png b/src/views/mall/promotion/kefu/components/asserts/daxiao.png
new file mode 100644
index 0000000..26206bc
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/daxiao.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/dianzan.png b/src/views/mall/promotion/kefu/components/asserts/dianzan.png
new file mode 100644
index 0000000..2e7f00e
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/dianzan.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/emo.png b/src/views/mall/promotion/kefu/components/asserts/emo.png
new file mode 100644
index 0000000..9c84551
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/emo.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/esi.png b/src/views/mall/promotion/kefu/components/asserts/esi.png
new file mode 100644
index 0000000..84e9726
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/esi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/fadai.png b/src/views/mall/promotion/kefu/components/asserts/fadai.png
new file mode 100644
index 0000000..0772de2
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/fadai.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/fankun.png b/src/views/mall/promotion/kefu/components/asserts/fankun.png
new file mode 100644
index 0000000..6e18dac
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/fankun.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/feiwen.png b/src/views/mall/promotion/kefu/components/asserts/feiwen.png
new file mode 100644
index 0000000..be97616
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/feiwen.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/fennu.png b/src/views/mall/promotion/kefu/components/asserts/fennu.png
new file mode 100644
index 0000000..20c5733
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/fennu.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/ganga.png b/src/views/mall/promotion/kefu/components/asserts/ganga.png
new file mode 100644
index 0000000..30ec329
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/ganga.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/ganmao.png b/src/views/mall/promotion/kefu/components/asserts/ganmao.png
new file mode 100644
index 0000000..35bbb89
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/ganmao.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/hanyan.png b/src/views/mall/promotion/kefu/components/asserts/hanyan.png
new file mode 100644
index 0000000..a0bc838
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/hanyan.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/haochi.png b/src/views/mall/promotion/kefu/components/asserts/haochi.png
new file mode 100644
index 0000000..2e52b6b
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/haochi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/hongxin.png b/src/views/mall/promotion/kefu/components/asserts/hongxin.png
new file mode 100644
index 0000000..65b5de8
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/hongxin.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/huaixiao.png b/src/views/mall/promotion/kefu/components/asserts/huaixiao.png
new file mode 100644
index 0000000..bc0e76c
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/huaixiao.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/jingkong.png b/src/views/mall/promotion/kefu/components/asserts/jingkong.png
new file mode 100644
index 0000000..7aa6584
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/jingkong.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/jingshu.png b/src/views/mall/promotion/kefu/components/asserts/jingshu.png
new file mode 100644
index 0000000..0e984d6
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/jingshu.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/jingya.png b/src/views/mall/promotion/kefu/components/asserts/jingya.png
new file mode 100644
index 0000000..9ba6bab
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/jingya.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/kaixin.png b/src/views/mall/promotion/kefu/components/asserts/kaixin.png
new file mode 100644
index 0000000..29c9f5d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/kaixin.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/keai.png b/src/views/mall/promotion/kefu/components/asserts/keai.png
new file mode 100644
index 0000000..d3b582c
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/keai.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/keshui.png b/src/views/mall/promotion/kefu/components/asserts/keshui.png
new file mode 100644
index 0000000..cef489e
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/keshui.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/kun.png b/src/views/mall/promotion/kefu/components/asserts/kun.png
new file mode 100644
index 0000000..1ddc388
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/kun.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/lengku.png b/src/views/mall/promotion/kefu/components/asserts/lengku.png
new file mode 100644
index 0000000..c5c6fee
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/lengku.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/liuhan.png b/src/views/mall/promotion/kefu/components/asserts/liuhan.png
new file mode 100644
index 0000000..e6ddc6f
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/liuhan.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/liukoushui.png b/src/views/mall/promotion/kefu/components/asserts/liukoushui.png
new file mode 100644
index 0000000..3e2fba6
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/liukoushui.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/liulei.png b/src/views/mall/promotion/kefu/components/asserts/liulei.png
new file mode 100644
index 0000000..dbf8204
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/liulei.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/mengbi.png b/src/views/mall/promotion/kefu/components/asserts/mengbi.png
new file mode 100644
index 0000000..a4206ee
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/mengbi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/mianwubiaoqing.png b/src/views/mall/promotion/kefu/components/asserts/mianwubiaoqing.png
new file mode 100644
index 0000000..6f315b9
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/mianwubiaoqing.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/nanguo.png b/src/views/mall/promotion/kefu/components/asserts/nanguo.png
new file mode 100644
index 0000000..19b9fb9
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/nanguo.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/outu.png b/src/views/mall/promotion/kefu/components/asserts/outu.png
new file mode 100644
index 0000000..2f9a06d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/outu.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/picture.svg b/src/views/mall/promotion/kefu/components/asserts/picture.svg
new file mode 100644
index 0000000..8811d49
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/picture.svg
@@ -0,0 +1,10 @@
+<?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="1720063872285" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6895"
+ width="200" height="200">
+ <path d="M782.16 880.98c-179.31 23.91-361 23.91-540.32 0C138.89 867.25 62 779.43 62 675.57V348.43c0-103.86 76.89-191.69 179.84-205.41 179.31-23.91 361-23.91 540.31 0C885.11 156.75 962 244.57 962 348.43v327.13c0 103.87-76.89 191.69-179.84 205.42z"
+ fill="#FF554D" p-id="6896"></path>
+ <path d="M226.11 596.86c-9.74 47.83 17.26 95.6 63.48 111.3C333.49 723.08 394.55 737 469.53 737c59.25 0 105.46-8.69 140.23-19.7 51.59-16.34 79.94-71.16 63.37-122.68-24.47-76.11-65.57-180.7-106.68-180.7-64.62 0-64.62 96.92-64.62 96.92S437.22 317 372.61 317c-82.11 0-117.85 139.12-146.5 279.86z"
+ fill="#FFFFFF" p-id="6897"></path>
+ <path d="M782 347m-60 0a60 60 0 1 0 120 0 60 60 0 1 0-120 0Z" fill="#FFBC55" p-id="6898"></path>
+</svg>
diff --git a/src/views/mall/promotion/kefu/components/asserts/shengqi.png b/src/views/mall/promotion/kefu/components/asserts/shengqi.png
new file mode 100644
index 0000000..7dce41d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/shengqi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/shuizhuo.png b/src/views/mall/promotion/kefu/components/asserts/shuizhuo.png
new file mode 100644
index 0000000..97d0f0a
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/shuizhuo.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/tianshi.png b/src/views/mall/promotion/kefu/components/asserts/tianshi.png
new file mode 100644
index 0000000..eb922dd
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/tianshi.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/xiaodiaoya.png b/src/views/mall/promotion/kefu/components/asserts/xiaodiaoya.png
new file mode 100644
index 0000000..29fbc0e
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/xiaodiaoya.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/xiaoku.png b/src/views/mall/promotion/kefu/components/asserts/xiaoku.png
new file mode 100644
index 0000000..88a169d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/xiaoku.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/xinsui.png b/src/views/mall/promotion/kefu/components/asserts/xinsui.png
new file mode 100644
index 0000000..a0f572a
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/xinsui.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/xiong.png b/src/views/mall/promotion/kefu/components/asserts/xiong.png
new file mode 100644
index 0000000..43dfd70
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/xiong.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/yiwen.png b/src/views/mall/promotion/kefu/components/asserts/yiwen.png
new file mode 100644
index 0000000..4c0da70
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/yiwen.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/yun.png b/src/views/mall/promotion/kefu/components/asserts/yun.png
new file mode 100644
index 0000000..56e5d02
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/yun.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/asserts/ziya.png b/src/views/mall/promotion/kefu/components/asserts/ziya.png
new file mode 100644
index 0000000..593ef5e
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/asserts/ziya.png
Binary files differ
diff --git a/src/views/mall/promotion/kefu/components/index.ts b/src/views/mall/promotion/kefu/components/index.ts
new file mode 100644
index 0000000..0f60a6e
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/index.ts
@@ -0,0 +1,5 @@
+import KeFuConversationList from './KeFuConversationList.vue'
+import KeFuMessageList from './KeFuMessageList.vue'
+import MemberInfo from './member/MemberInfo.vue'
+
+export { KeFuConversationList, KeFuMessageList, MemberInfo }
diff --git a/src/views/mall/promotion/kefu/components/member/MemberInfo.vue b/src/views/mall/promotion/kefu/components/member/MemberInfo.vue
new file mode 100644
index 0000000..3acfece
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/member/MemberInfo.vue
@@ -0,0 +1,255 @@
+<!-- 鍙充晶淇℃伅锛氫細鍛樹俊鎭� + 鏈�杩戞祻瑙� + 浜ゆ槗璁㈠崟 -->
+<template>
+ <el-container class="kefu">
+ <el-header class="kefu-header">
+ <div
+ :class="{ 'kefu-header-item-activation': tabActivation('浼氬憳淇℃伅') }"
+ class="kefu-header-item cursor-pointer flex items-center justify-center"
+ @click="handleClick('浼氬憳淇℃伅')"
+ >
+ 浼氬憳淇℃伅
+ </div>
+ <div
+ :class="{ 'kefu-header-item-activation': tabActivation('鏈�杩戞祻瑙�') }"
+ class="kefu-header-item cursor-pointer flex items-center justify-center"
+ @click="handleClick('鏈�杩戞祻瑙�')"
+ >
+ 鏈�杩戞祻瑙�
+ </div>
+ <div
+ :class="{ 'kefu-header-item-activation': tabActivation('浜ゆ槗璁㈠崟') }"
+ class="kefu-header-item cursor-pointer flex items-center justify-center"
+ @click="handleClick('浜ゆ槗璁㈠崟')"
+ >
+ 浜ゆ槗璁㈠崟
+ </div>
+ </el-header>
+ <el-main class="kefu-content p-10px!">
+ <div v-if="!isEmpty(conversation)" v-loading="loading">
+ <!-- 鍩烘湰淇℃伅 -->
+ <UserBasicInfo v-if="activeTab === '浼氬憳淇℃伅'" :user="user" mode="kefu">
+ <template #header>
+ <CardTitle title="鍩烘湰淇℃伅" />
+ </template>
+ </UserBasicInfo>
+ <!-- 璐︽埛淇℃伅 -->
+ <el-card v-if="activeTab === '浼氬憳淇℃伅'" class="h-full mt-10px" shadow="never">
+ <template #header>
+ <CardTitle title="璐︽埛淇℃伅" />
+ </template>
+ <UserAccountInfo :column="1" :user="user" :wallet="wallet" />
+ </el-card>
+ </div>
+ <div v-show="!isEmpty(conversation)">
+ <el-scrollbar ref="scrollbarRef" always @scroll="handleScroll">
+ <!-- 鏈�杩戞祻瑙� -->
+ <ProductBrowsingHistory v-if="activeTab === '鏈�杩戞祻瑙�'" ref="productBrowsingHistoryRef" />
+ <!-- 浜ゆ槗璁㈠崟 -->
+ <OrderBrowsingHistory v-if="activeTab === '浜ゆ槗璁㈠崟'" ref="orderBrowsingHistoryRef" />
+ </el-scrollbar>
+ </div>
+ <el-empty v-show="isEmpty(conversation)" description="璇烽�夋嫨宸︿晶鐨勪竴涓細璇濆悗寮�濮�" />
+ </el-main>
+ </el-container>
+</template>
+
+<script lang="ts" setup>
+import ProductBrowsingHistory from './ProductBrowsingHistory.vue'
+import OrderBrowsingHistory from './OrderBrowsingHistory.vue'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { isEmpty } from '@/utils/is'
+import { debounce } from 'lodash-es'
+import { ElScrollbar as ElScrollbarType } from 'element-plus/es/components/scrollbar/index'
+import { CardTitle } from '@/components/Card'
+import UserBasicInfo from '@/views/member/user/detail/UserBasicInfo.vue'
+import UserAccountInfo from '@/views/member/user/detail/UserAccountInfo.vue'
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
+
+defineOptions({ name: 'MemberBrowsingHistory' })
+
+const activeTab = ref('浼氬憳淇℃伅')
+const tabActivation = computed(() => (tab: string) => activeTab.value === tab)
+
+/** tab 鍒囨崲 */
+const productBrowsingHistoryRef = ref<InstanceType<typeof ProductBrowsingHistory>>()
+const orderBrowsingHistoryRef = ref<InstanceType<typeof OrderBrowsingHistory>>()
+const handleClick = async (tab: string) => {
+ if (isEmpty(conversation)) {
+ return
+ }
+ activeTab.value = tab
+ await nextTick()
+ await getHistoryList()
+}
+
+/** 鑾峰緱鍘嗗彶鏁版嵁 */
+const getHistoryList = async () => {
+ switch (activeTab.value) {
+ case '浼氬憳淇℃伅':
+ await getUserData()
+ await getUserWallet()
+ break
+ case '鏈�杩戞祻瑙�':
+ await productBrowsingHistoryRef.value?.getHistoryList(conversation.value)
+ break
+ case '浜ゆ槗璁㈠崟':
+ await orderBrowsingHistoryRef.value?.getHistoryList(conversation.value)
+ break
+ default:
+ break
+ }
+}
+
+/** 鍔犺浇涓嬩竴椤垫暟鎹� */
+const loadMore = async () => {
+ switch (activeTab.value) {
+ case '浼氬憳淇℃伅':
+ break
+ case '鏈�杩戞祻瑙�':
+ await productBrowsingHistoryRef.value?.loadMore()
+ break
+ case '浜ゆ槗璁㈠崟':
+ await orderBrowsingHistoryRef.value?.loadMore()
+ break
+ default:
+ break
+ }
+}
+
+/** 娴忚鍘嗗彶鍒濆鍖� */
+const conversation = ref<KeFuConversationRespVO>({} as KeFuConversationRespVO) // 鐢ㄦ埛浼氳瘽
+const initHistory = async (val: KeFuConversationRespVO) => {
+ activeTab.value = '浼氬憳淇℃伅'
+ conversation.value = val
+ await nextTick()
+ await getHistoryList()
+}
+defineExpose({ initHistory })
+
+/** 澶勭悊娑堟伅鍒楄〃婊氬姩浜嬩欢(debounce 闄愭祦) */
+const scrollbarRef = ref<InstanceType<typeof ElScrollbarType>>()
+const handleScroll = debounce(() => {
+ const wrap = scrollbarRef.value?.wrapRef
+ // 瑙﹀簳閲嶇疆
+ if (Math.abs(wrap!.scrollHeight - wrap!.clientHeight - wrap!.scrollTop) < 1) {
+ loadMore()
+ }
+}, 200)
+
+/** 鏌ヨ鐢ㄦ埛閽卞寘淇℃伅 */
+const WALLET_INIT_DATA = {
+ balance: 0,
+ totalExpense: 0,
+ totalRecharge: 0
+} as WalletApi.WalletVO // 閽卞寘鍒濆鍖栨暟鎹�
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 閽卞寘淇℃伅
+const getUserWallet = async () => {
+ if (!conversation.value.userId) {
+ wallet.value = WALLET_INIT_DATA
+ return
+ }
+ wallet.value =
+ (await WalletApi.getWallet({ userId: conversation.value.userId })) || WALLET_INIT_DATA
+}
+
+/** 鑾峰緱鐢ㄦ埛 */
+const loading = ref(true) // 鍔犺浇涓�
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
+const getUserData = async () => {
+ loading.value = true
+ try {
+ user.value = await UserApi.getUser(conversation.value.userId)
+ } finally {
+ loading.value = false
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.kefu {
+ position: relative;
+ width: 300px !important;
+ background-color: var(--app-content-bg-color);
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 1px; /* 瀹為檯瀹藉害 */
+ height: 100%;
+ background-color: var(--el-border-color);
+ transform: scaleX(0.3); /* 缂╁皬瀹藉害 */
+ }
+
+ &-header {
+ background-color: var(--app-content-bg-color);
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+
+ &::before {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 1px; /* 鍒濆瀹藉害 */
+ background-color: var(--el-border-color);
+ transform: scaleY(0.3); /* 缂╁皬瑙嗚楂樺害 */
+ }
+
+ &-title {
+ font-size: 18px;
+ font-weight: bold;
+ }
+
+ &-item {
+ height: 100%;
+ width: 100%;
+ position: relative;
+
+ &-activation::before {
+ content: '';
+ position: absolute; /* 缁濆瀹氫綅 */
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0; /* 瑕嗙洊鏁翠釜鍏冪礌 */
+ border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 杈规鏍峰紡 */
+ pointer-events: none; /* 纭繚鐐瑰嚮浜嬩欢涓嶄細琚吉鍏冪礌鎷︽埅 */
+ }
+
+ &:hover::before {
+ content: '';
+ position: absolute; /* 缁濆瀹氫綅 */
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0; /* 瑕嗙洊鏁翠釜鍏冪礌 */
+ border-bottom: 2px solid rgba(128, 128, 128, 0.5); /* 杈规鏍峰紡 */
+ pointer-events: none; /* 纭繚鐐瑰嚮浜嬩欢涓嶄細琚吉鍏冪礌鎷︽埅 */
+ }
+ }
+ }
+
+ &-content {
+ margin: 0;
+ padding: 0;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ }
+
+ &-tabs {
+ height: 100%;
+ width: 100%;
+ }
+}
+
+.header-title {
+ border-bottom: #e4e0e0 solid 1px;
+}
+</style>
diff --git a/src/views/mall/promotion/kefu/components/member/OrderBrowsingHistory.vue b/src/views/mall/promotion/kefu/components/member/OrderBrowsingHistory.vue
new file mode 100644
index 0000000..8fb8891
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/member/OrderBrowsingHistory.vue
@@ -0,0 +1,44 @@
+<template>
+ <OrderItem v-for="item in list" :key="item.id" :order="item" class="mb-10px" />
+</template>
+
+<script lang="ts" setup>
+import OrderItem from '@/views/mall/promotion/kefu/components/message/OrderItem.vue'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { getOrderPage } from '@/api/mall/trade/order'
+import { concat } from 'lodash-es'
+
+defineOptions({ name: 'OrderBrowsingHistory' })
+
+const list = ref<any>([]) // 鍒楄〃
+const total = ref(0) // 鎬绘暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: 0
+})
+const skipGetMessageList = computed(() => {
+ // 宸插姞杞藉埌鏈�鍚庝竴椤电殑璇濆垯涓嶈Е鍙戞柊鐨勬秷鎭幏鍙�
+ return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
+}) // 璺宠繃娑堟伅鑾峰彇
+
+/** 鑾峰緱娴忚璁板綍 */
+const getHistoryList = async (val: KeFuConversationRespVO) => {
+ queryParams.userId = val.userId
+ const res = await getOrderPage(queryParams)
+ total.value = res.total
+ list.value = res.list
+}
+
+/** 鍔犺浇涓嬩竴椤垫暟鎹� */
+const loadMore = async () => {
+ if (skipGetMessageList.value) {
+ return
+ }
+ queryParams.pageNo += 1
+ const res = await getOrderPage(queryParams)
+ total.value = res.total
+ concat(list.value, res.list)
+}
+defineExpose({ getHistoryList, loadMore })
+</script>
diff --git a/src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue b/src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue
new file mode 100644
index 0000000..a9b3856
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue
@@ -0,0 +1,57 @@
+<template>
+ <ProductItem
+ v-for="item in list"
+ :key="item.id"
+ :picUrl="item.picUrl"
+ :price="item.price"
+ :sales-count="item.salesCount"
+ :spu-id="item.spuId"
+ :stock="item.stock"
+ :title="item.spuName"
+ class="mb-10px"
+ />
+</template>
+
+<script lang="ts" setup>
+import { getBrowseHistoryPage } from '@/api/mall/product/history'
+import ProductItem from '@/views/mall/promotion/kefu/components/message/ProductItem.vue'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { concat } from 'lodash-es'
+
+defineOptions({ name: 'ProductBrowsingHistory' })
+
+const list = ref<any>([]) // 鍒楄〃
+const total = ref(0) // 鎬绘暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: 0,
+ userDeleted: false
+})
+const skipGetMessageList = computed(() => {
+ // 宸插姞杞藉埌鏈�鍚庝竴椤电殑璇濆垯涓嶈Е鍙戞柊鐨勬秷鎭幏鍙�
+ return total.value > 0 && Math.ceil(total.value / queryParams.pageSize) === queryParams.pageNo
+}) // 璺宠繃娑堟伅鑾峰彇
+
+/** 鑾峰緱娴忚璁板綍 */
+const getHistoryList = async (val: KeFuConversationRespVO) => {
+ queryParams.userId = val.userId
+ const res = await getBrowseHistoryPage(queryParams)
+ total.value = res.total
+ list.value = res.list
+}
+
+/** 鍔犺浇涓嬩竴椤垫暟鎹� */
+const loadMore = async () => {
+ if (skipGetMessageList.value) {
+ return
+ }
+ queryParams.pageNo += 1
+ const res = await getBrowseHistoryPage(queryParams)
+ total.value = res.total
+ concat(list.value, res.list)
+}
+defineExpose({ getHistoryList, loadMore })
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/mall/promotion/kefu/components/message/MessageItem.vue b/src/views/mall/promotion/kefu/components/message/MessageItem.vue
new file mode 100644
index 0000000..325a363
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/message/MessageItem.vue
@@ -0,0 +1,24 @@
+<template>
+ <!-- 娑堟伅缁勪欢 -->
+ <div
+ :class="[
+ message.senderType === UserTypeEnum.MEMBER
+ ? `ml-10px`
+ : message.senderType === UserTypeEnum.ADMIN
+ ? `mr-10px`
+ : ''
+ ]"
+ >
+ <slot></slot>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { UserTypeEnum } from '@/utils/constants'
+import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+
+defineOptions({ name: 'MessageItem' })
+defineProps<{
+ message: KeFuMessageRespVO
+}>()
+</script>
diff --git a/src/views/mall/promotion/kefu/components/message/OrderItem.vue b/src/views/mall/promotion/kefu/components/message/OrderItem.vue
new file mode 100644
index 0000000..4ebbb7f
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/message/OrderItem.vue
@@ -0,0 +1,181 @@
+<template>
+ <div v-if="isObject(getMessageContent)">
+ <div :key="getMessageContent.id" class="order-list-card-box mt-14px">
+ <div class="order-card-header flex items-center justify-between p-x-5px">
+ <div class="order-no">
+ 璁㈠崟鍙凤細
+ <span style="cursor: pointer" @click="openDetail(getMessageContent.id)">
+ {{ getMessageContent.no }}
+ </span>
+ </div>
+ <div :class="formatOrderColor(getMessageContent)" class="order-state font-16">
+ {{ formatOrderStatus(getMessageContent) }}
+ </div>
+ </div>
+ <div v-for="item in getMessageContent.items" :key="item.id" class="border-bottom">
+ <ProductItem
+ :num="item.count"
+ :picUrl="item.picUrl"
+ :price="item.price"
+ :skuText="item.properties.map((property: any) => property.valueName).join(' ')"
+ :spu-id="item.spuId"
+ :title="item.spuName"
+ />
+ </div>
+ <div class="pay-box flex justify-end pr-5px">
+ <div class="flex items-center">
+ <div class="discounts-title pay-color"
+ >鍏� {{ getMessageContent?.productCount }} 浠跺晢鍝�,鎬婚噾棰�:
+ </div>
+ <div class="discounts-money pay-color">
+ 锟{ fenToYuan(getMessageContent?.payPrice) }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { fenToYuan, jsonParse } from '@/utils'
+import { KeFuMessageRespVO } from '@/api/mall/promotion/kefu/message'
+import { isObject } from '@/utils/is'
+import ProductItem from '@/views/mall/promotion/kefu/components/message/ProductItem.vue'
+
+const { push } = useRouter()
+
+defineOptions({ name: 'OrderItem' })
+const props = defineProps<{
+ message?: KeFuMessageRespVO
+ order?: any
+}>()
+
+const getMessageContent = computed(() =>
+ typeof props.message !== 'undefined' ? jsonParse(props!.message!.content) : props.order
+)
+
+/** 鏌ョ湅璁㈠崟璇︽儏 */
+const openDetail = (id: number) => {
+ console.log(getMessageContent)
+ push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/**
+ * 鏍煎紡鍖栬鍗曠姸鎬佺殑棰滆壊
+ *
+ * @param order 璁㈠崟
+ * @return {string} 棰滆壊鐨� class 鍚嶇О
+ */
+function formatOrderColor(order: any) {
+ if (order.status === 0) {
+ return 'info-color'
+ }
+ if (order.status === 10 || order.status === 20 || (order.status === 30 && !order.commentStatus)) {
+ return 'warning-color'
+ }
+ if (order.status === 30 && order.commentStatus) {
+ return 'success-color'
+ }
+ return 'danger-color'
+}
+
+/**
+ * 鏍煎紡鍖栬鍗曠姸鎬�
+ *
+ * @param order 璁㈠崟
+ */
+function formatOrderStatus(order: any) {
+ if (order.status === 0) {
+ return '寰呬粯娆�'
+ }
+ if (order.status === 10 && order.deliveryType === 1) {
+ return '寰呭彂璐�'
+ }
+ if (order.status === 10 && order.deliveryType === 2) {
+ return '寰呮牳閿�'
+ }
+ if (order.status === 20) {
+ return '寰呮敹璐�'
+ }
+ if (order.status === 30 && !order.commentStatus) {
+ return '寰呰瘎浠�'
+ }
+ if (order.status === 30 && order.commentStatus) {
+ return '宸插畬鎴�'
+ }
+ return '宸插叧闂�'
+}
+</script>
+
+<style lang="scss" scoped>
+.order-list-card-box {
+ border-radius: 10px;
+ padding: 10px;
+ border: 1px var(--el-border-color) solid;
+ background-color: rgba(128, 128, 128, 0.3); // 閫忔槑鑹诧紝鏆楅粦妯″紡涓嬩篃鑳戒綋鐜�
+
+ .order-card-header {
+ height: 28px;
+ font-weight: bold;
+
+ .order-no {
+ font-size: 13px;
+
+ span {
+ &:hover {
+ text-decoration: underline;
+ color: var(--left-menu-bg-active-color);
+ }
+ }
+ }
+
+ .order-state {
+ font-size: 13px;
+ }
+ }
+
+ .pay-box {
+ padding-top: 10px;
+ font-weight: bold;
+
+ .discounts-title {
+ font-size: 16px;
+ line-height: normal;
+ }
+
+ .discounts-money {
+ font-size: 16px;
+ line-height: normal;
+ font-family: OPPOSANS;
+ }
+
+ .pay-color {
+ font-size: 13px;
+ }
+ }
+}
+
+.warning-color {
+ color: #faad14;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.danger-color {
+ color: #ff3000;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.success-color {
+ color: #52c41a;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.info-color {
+ color: #999999;
+ font-size: 11px;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/mall/promotion/kefu/components/message/ProductItem.vue b/src/views/mall/promotion/kefu/components/message/ProductItem.vue
new file mode 100644
index 0000000..1ec313e
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/message/ProductItem.vue
@@ -0,0 +1,116 @@
+<template>
+ <div class="product-warp" style="cursor: pointer" @click.stop="openDetail(spuId)">
+ <!-- 宸︿晶鍟嗗搧鍥剧墖-->
+ <div class="product-warp-left mr-24px">
+ <el-image
+ :initial-index="0"
+ :preview-src-list="[picUrl]"
+ :src="picUrl"
+ class="product-warp-left-img"
+ fit="contain"
+ preview-teleported
+ @click.stop
+ />
+ </div>
+ <!-- 鍙充晶鍟嗗搧淇℃伅 -->
+ <div class="product-warp-right">
+ <div class="description">{{ title }}</div>
+ <div class="my-5px">
+ <span class="mr-20px">搴撳瓨: {{ stock || 0 }}</span>
+ <span>閿�閲�: {{ salesCount || 0 }}</span>
+ </div>
+ <div class="flex justify-between items-center">
+ <span class="price">锟{ fenToYuan(price) }}</span>
+ <el-button size="small" text type="primary">璇︽儏</el-button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { fenToYuan } from '@/utils'
+
+const { push } = useRouter()
+
+defineOptions({ name: 'ProductItem' })
+defineProps({
+ spuId: {
+ type: Number,
+ default: 0
+ },
+ picUrl: {
+ type: String,
+ default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto'
+ },
+ title: {
+ type: String,
+ default: ''
+ },
+ price: {
+ type: [String, Number],
+ default: ''
+ },
+ salesCount: {
+ type: [String, Number],
+ default: ''
+ },
+ stock: {
+ type: [String, Number],
+ default: ''
+ }
+})
+
+/** 鏌ョ湅鍟嗗搧璇︽儏 */
+const openDetail = (spuId: number) => {
+ push({ name: 'ProductSpuDetail', params: { id: spuId } })
+}
+</script>
+
+<style lang="scss" scoped>
+.button {
+ background-color: #007bff;
+ color: white;
+ border: none;
+ padding: 5px 10px;
+ cursor: pointer;
+}
+
+.product-warp {
+ width: 100%;
+ background-color: rgba(128, 128, 128, 0.3);
+ border: 1px solid var(--el-border-color);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ padding: 10px;
+
+ &-left {
+ width: 70px;
+
+ &-img {
+ width: 100%;
+ height: 100%;
+ border-radius: 8px;
+ }
+ }
+
+ &-right {
+ flex: 1;
+
+ .description {
+ width: 100%;
+ font-size: 16px;
+ font-weight: bold;
+ display: -webkit-box;
+ -webkit-line-clamp: 1; /* 鏄剧ず涓�琛� */
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .price {
+ color: #ff3000;
+ }
+ }
+}
+</style>
diff --git a/src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue b/src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue
new file mode 100644
index 0000000..43c096d
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/tools/EmojiSelectPopover.vue
@@ -0,0 +1,42 @@
+<!-- emoji 琛ㄦ儏閫夋嫨缁勪欢 -->
+<template>
+ <el-popover :width="500" placement="top" trigger="click">
+ <template #reference>
+ <Icon :size="30" class="ml-10px cursor-pointer" icon="twemoji:grinning-face" />
+ </template>
+ <ElScrollbar height="300px">
+ <ul class="ml-2 flex flex-wrap px-2">
+ <li
+ v-for="(item, index) in emojiList"
+ :key="index"
+ :style="{
+ borderColor: 'var(--el-color-primary)',
+ color: 'var(--el-color-primary)'
+ }"
+ :title="item.name"
+ class="icon-item mr-2 mt-1 w-1/10 flex cursor-pointer items-center justify-center border border-solid p-2"
+ @click="handleSelect(item)"
+ >
+ <img :src="item.url" class="w-24px h-24px" />
+ </li>
+ </ul>
+ </ElScrollbar>
+ </el-popover>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'EmojiSelectPopover' })
+import { Emoji, useEmoji } from './emoji'
+
+const { getEmojiList } = useEmoji()
+const emojiList = computed(() => getEmojiList())
+
+/** 閫夋嫨 emoji 琛ㄦ儏 */
+const emits = defineEmits<{
+ (e: 'select-emoji', v: Emoji)
+}>()
+const handleSelect = (item: Emoji) => {
+ // 鏁翠釜 emoji 鏁版嵁浼犻�掑嚭鍘伙紝鏂逛究浠ュ悗杈撳叆妗嗙洿鎺ユ樉绀鸿〃鎯�
+ emits('select-emoji', item)
+}
+</script>
diff --git a/src/views/mall/promotion/kefu/components/tools/PictureSelectUpload.vue b/src/views/mall/promotion/kefu/components/tools/PictureSelectUpload.vue
new file mode 100644
index 0000000..3f6850b
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/tools/PictureSelectUpload.vue
@@ -0,0 +1,93 @@
+<!-- 鍥剧墖閫夋嫨 -->
+<template>
+ <div>
+ <img :src="Picture" class="w-35px h-35px" @click="selectAndUpload" />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import Picture from '@/views/mall/promotion/kefu/components/asserts/picture.svg'
+import * as FileApi from '@/api/infra/file'
+
+defineOptions({ name: 'PictureSelectUpload' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+/** 閫夋嫨骞朵笂浼犳枃浠� */
+const emits = defineEmits<{
+ (e: 'send-picture',; v: string): void
+}>()
+const selectAndUpload = async () => {
+ const files: any = await getFiles()
+ message.success('鍥剧墖鍙戦�佷腑璇风◢绛夈�傘�傘��')
+ const res = await FileApi.updateFile({ file: files[0].file })
+ emits('send-picture', res.data)files
+}
+
+/**
+ * 鍞よ捣鏂囦欢閫夋嫨绐楀彛锛屽苟鑾峰彇閫夋嫨鐨勬枃浠�
+ *
+ * @param {Object} options - 閰嶇疆閫夐」
+ * @param {boolean} [options.multiple=true] - 鏄惁鏀寔澶氶��
+ * @param {string} [options.accept=''] - 鏂囦欢涓婁紶鏍煎紡闄愬埗
+ * @param {number} [options.limit=1] - 鍗曟涓婁紶鏈�澶ф枃浠舵暟
+ * @param {number} [options.fileSize=500] - 鍗曚釜鏂囦欢澶у皬闄愬埗锛堝崟浣嶏細MB锛�
+ * @returns {Promise<Array>} 閫夋嫨鐨勬枃浠跺垪琛紝姣忎釜鏂囦欢甯︽湁涓�涓猽id
+ */
+async function getFiles(options = {}) {
+ const { multiple, accept, limit, fileSize } = {
+ multiple: true,;
+ accept: 'image/jpeg, image/png, image/gif', // 榛樿閫夋嫨鍥剧墖;
+ limit: 1,;
+ fileSize: 500,
+ ...options
+ }
+
+ // 鍒涘缓鏂囦欢閫夋嫨鍏冪礌
+ const input = document.createElement('input')
+ input.type = 'file'
+ input.style.display = 'none'
+ if (multiple) input.multiple = true
+ if (accept) input.accept = accept
+
+ // 灏嗘枃浠堕�夋嫨鍏冪礌娣诲姞鍒版枃妗d腑
+ document.body.appendChild(input)
+
+ // 瑙﹀彂鏂囦欢閫夋嫨鍏冪礌鐨勭偣鍑讳簨浠�
+ input.click()
+
+ // 绛夊緟鏂囦欢閫夋嫨鍏冪礌鐨� change 浜嬩欢
+ try {
+ return await new Promise((resolve, reject) => {
+ input.addEventListener('change', (event: any) => {
+ const filesArray = Array.from(event?.target?.files || [])
+
+ // 浠庢枃妗d腑绉婚櫎鏂囦欢閫夋嫨鍏冪礌
+ document.body.removeChild(input)
+
+ // 鍒ゆ柇鏄惁瓒呭嚭涓婁紶鏁伴噺闄愬埗
+ if (filesArray.length > limit) {
+ reject({ errorType: 'limit', files: filesArray })
+ return
+ }
+
+ // 鍒ゆ柇鏄惁瓒呭嚭涓婁紶鏂囦欢澶у皬闄愬埗
+ const overSizedFiles = filesArray.filter((file: File) => file.size / 1024 ** 2 > fileSize)
+ if (overSizedFiles.length > 0) {
+ reject({ errorType: 'fileSize', files: overSizedFiles })
+ return
+ }
+
+ // 鐢熸垚鏂囦欢鍒楄〃锛屽苟娣诲姞 uid
+ const fileList = filesArray.map((file, index) => ({ file, uid: Date.now() + index }))
+ resolve(fileList)
+ })
+ })
+ } catch (error) {
+ console.error('閫夋嫨鏂囦欢鍑洪敊:', error)
+ throw error
+ }
+}
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/mall/promotion/kefu/components/tools/constants.ts b/src/views/mall/promotion/kefu/components/tools/constants.ts
new file mode 100644
index 0000000..750e7f5
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/tools/constants.ts
@@ -0,0 +1,17 @@
+// 瀹㈡湇娑堟伅绫诲瀷鏋氫妇绫�
+export const KeFuMessageContentTypeEnum = {
+ TEXT: 1, // 鏂囨湰娑堟伅
+ IMAGE: 2, // 鍥剧墖娑堟伅
+ VOICE: 3, // 璇煶娑堟伅
+ VIDEO: 4, // 瑙嗛娑堟伅
+ SYSTEM: 5, // 绯荤粺娑堟伅
+ // ========== 鍟嗗煄鐗规畩娑堟伅 ==========
+ PRODUCT: 10, // 鍟嗗搧娑堟伅
+ ORDER: 11 // 璁㈠崟娑堟伅"
+}
+
+// Promotion 鐨� WebSocket 娑堟伅绫诲瀷鏋氫妇绫�
+export const WebSocketMessageTypeConstants = {
+ KEFU_MESSAGE_TYPE: 'kefu_message_type', // 瀹㈡湇娑堟伅绫诲瀷
+ KEFU_MESSAGE_ADMIN_READ: 'kefu_message_read_status_change' // 瀹㈡湇娑堟伅绠$悊鍛樺凡璇�
+}
diff --git a/src/views/mall/promotion/kefu/components/tools/emoji.ts b/src/views/mall/promotion/kefu/components/tools/emoji.ts
new file mode 100644
index 0000000..3755e38
--- /dev/null
+++ b/src/views/mall/promotion/kefu/components/tools/emoji.ts
@@ -0,0 +1,129 @@
+import { isEmpty } from '@/utils/is'
+
+const emojiList = [
+ { name: '[绗戞帀鐗橾', file: 'xiaodiaoya.png' },
+ { name: '[鍙埍]', file: 'keai.png' },
+ { name: '[鍐烽叿]', file: 'lengku.png' },
+ { name: '[闂槾]', file: 'bizui.png' },
+ { name: '[鐢熸皵]', file: 'shengqi.png' },
+ { name: '[鎯婃亹]', file: 'jingkong.png' },
+ { name: '[鐬岀潯]', file: 'keshui.png' },
+ { name: '[澶х瑧]', file: 'daxiao.png' },
+ { name: '[鐖卞績]', file: 'aixin.png' },
+ { name: '[鍧忕瑧]', file: 'huaixiao.png' },
+ { name: '[椋炲惢]', file: 'feiwen.png' },
+ { name: '[鐤戦棶]', file: 'yiwen.png' },
+ { name: '[寮�蹇僝', file: 'kaixin.png' },
+ { name: '[鍙戝憜]', file: 'fadai.png' },
+ { name: '[娴佹唱]', file: 'liulei.png' },
+ { name: '[姹楅]', file: 'hanyan.png' },
+ { name: '[鎯婃倸]', file: 'jingshu.png' },
+ { name: '[鍥皛]', file: 'kun.png' },
+ { name: '[蹇冪]', file: 'xinsui.png' },
+ { name: '[澶╀娇]', file: 'tianshi.png' },
+ { name: '[鏅昡', file: 'yun.png' },
+ { name: '[鍟奭', file: 'a.png' },
+ { name: '[鎰ゆ�抅', file: 'fennu.png' },
+ { name: '[鐫$潃]', file: 'shuizhuo.png' },
+ { name: '[闈㈡棤琛ㄦ儏]', file: 'mianwubiaoqing.png' },
+ { name: '[闅捐繃]', file: 'nanguo.png' },
+ { name: '[鐘洶]', file: 'fankun.png' },
+ { name: '[濂藉悆]', file: 'haochi.png' },
+ { name: '[鍛曞悙]', file: 'outu.png' },
+ { name: '[榫囩墮]', file: 'ziya.png' },
+ { name: '[鎳垫瘮]', file: 'mengbi.png' },
+ { name: '[鐧界溂]', file: 'baiyan.png' },
+ { name: '[楗挎]', file: 'esi.png' },
+ { name: '[鍑禲', file: 'xiong.png' },
+ { name: '[鎰熷啋]', file: 'ganmao.png' },
+ { name: '[娴佹睏]', file: 'liuhan.png' },
+ { name: '[绗戝摥]', file: 'xiaoku.png' },
+ { name: '[娴佸彛姘碷', file: 'liukoushui.png' },
+ { name: '[灏村艾]', file: 'ganga.png' },
+ { name: '[鎯婅]', file: 'jingya.png' },
+ { name: '[澶ф儕]', file: 'dajing.png' },
+ { name: '[涓嶅ソ鎰忔�漖', file: 'buhaoyisi.png' },
+ { name: '[澶ч椆]', file: 'danao.png' },
+ { name: '[涓嶅彲鎬濊]', file: 'bukesiyi.png' },
+ { name: '[鐖变綘]', file: 'aini.png' },
+ { name: '[绾㈠績]', file: 'hongxin.png' },
+ { name: '[鐐硅禐]', file: 'dianzan.png' },
+ { name: '[鎭堕瓟]', file: 'emo.png' }
+]
+
+export interface Emoji {
+ name: string
+ url: string
+}
+
+export const useEmoji = () => {
+ const emojiPathList = ref<any[]>([])
+
+ /** 鍔犺浇鏈湴鍥剧墖 */
+ const initStaticEmoji = async () => {
+ const pathList = import.meta.glob(
+ '@/views/mall/promotion/kefu/components/asserts/*.{png,jpg,jpeg,svg}'
+ )
+ for (const path in pathList) {
+ const imageModule: any = await pathList[path]()
+ emojiPathList.value.push({ path: path, src: imageModule.default })
+ }
+ }
+
+ /** 鍒濆鍖� */
+ onMounted(async () => {
+ if (isEmpty(emojiPathList.value)) {
+ await initStaticEmoji()
+ }
+ })
+
+ /**
+ * 灏嗘枃鏈腑鐨勮〃鎯呮浛鎹㈡垚鍥剧墖
+ *
+ * @return 鏇挎崲鍚庣殑鏂囨湰
+ * @param content 娑堟伅鍐呭
+ */
+ const replaceEmoji = (content: string) => {
+ let newData = content
+ if (typeof newData !== 'object') {
+ const reg = /\[(.+?)]/g // [] 涓嫭鍙�
+ const zhEmojiName = newData.match(reg)
+ if (zhEmojiName) {
+ zhEmojiName.forEach((item) => {
+ const emojiFile = getEmojiFileByName(item)
+ newData = newData.replace(
+ item,
+ `<img style="width: 20px;height: 20px;margin:0 1px 3px 1px;vertical-align: middle;" src="${emojiFile}" alt=""/>`
+ )
+ })
+ }
+ }
+ return newData
+ }
+
+ /**
+ * 鑾峰緱鎵�鏈夎〃鎯�
+ *
+ * @return 琛ㄦ儏鍒楄〃
+ */
+ function getEmojiList(): Emoji[] {
+ return emojiList.map((item) => ({
+ url: getEmojiFileByName(item.name),
+ name: item.name
+ })) as Emoji[]
+ }
+
+ function getEmojiFileByName(name: string) {
+ for (const emoji of emojiList) {
+ if (emoji.name === name) {
+ const emojiPath = emojiPathList.value.find(
+ (item: { path: string; src: string }) => item.path.indexOf(emoji.file) > -1
+ )
+ return emojiPath ? emojiPath.src : undefined
+ }
+ }
+ return false
+ }
+
+ return { replaceEmoji, getEmojiList }
+}
diff --git a/src/views/mall/promotion/kefu/index.vue b/src/views/mall/promotion/kefu/index.vue
new file mode 100644
index 0000000..64b8aba
--- /dev/null
+++ b/src/views/mall/promotion/kefu/index.vue
@@ -0,0 +1,136 @@
+<template>
+ <el-container class="kefu-layout">
+ <!-- 浼氳瘽鍒楄〃 -->
+ <KeFuConversationList ref="keFuConversationRef" @change="handleChange" />
+ <!-- 浼氳瘽璇︽儏锛堥�変腑浼氳瘽鐨勬秷鎭垪琛級 -->
+ <KeFuMessageList ref="keFuChatBoxRef" />
+ <!-- 浼氬憳淇℃伅锛堥�変腑浼氳瘽鐨勪細鍛樹俊鎭級 -->
+ <MemberInfo ref="memberInfoRef" />
+ </el-container>
+</template>
+
+<script lang="ts" setup>
+import { KeFuConversationList, KeFuMessageList, MemberInfo } from './components'
+import { WebSocketMessageTypeConstants } from './components/tools/constants'
+import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
+import { getRefreshToken } from '@/utils/auth'
+import { useWebSocket } from '@vueuse/core'
+import { useMallKefuStore } from '@/store/modules/mall/kefu'
+
+defineOptions({ name: 'KeFu' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const kefuStore = useMallKefuStore() // 瀹㈡湇缂撳瓨
+
+// ======================= WebSocket start =======================
+const server = ref(
+ (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
+ '?token=' +
+ getRefreshToken() // 浣跨敤 getRefreshToken() 鏂规硶锛岃�屼笉浣跨敤 getAccessToken() 鏂规硶鐨勫師鍥狅細WebSocket 鏃犳硶鏂逛究鐨勫埛鏂拌闂护鐗�
+) // WebSocket 鏈嶅姟鍦板潃
+
+/** 鍙戣捣 WebSocket 杩炴帴 */
+const { data, close, open } = useWebSocket(server.value, {
+ autoReconnect: true,
+ heartbeat: true
+})
+
+/** 鐩戝惉 WebSocket 鏁版嵁 */
+watch(
+ () => data.value,
+ (newData) => {
+ if (!newData) return
+ try {
+ // 1. 鏀跺埌蹇冭烦
+ if (newData === 'pong') return
+
+ // 2.1 瑙f瀽 type 娑堟伅绫诲瀷
+ const jsonMessage = JSON.parse(newData)
+ const type = jsonMessage.type
+ if (!type) {
+ message.error('鏈煡鐨勬秷鎭被鍨嬶細' + newData)
+ return
+ }
+
+ // 2.2 娑堟伅绫诲瀷锛欿EFU_MESSAGE_TYPE
+ if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_TYPE) {
+ const message = JSON.parse(jsonMessage.content)
+ // 鍒锋柊浼氳瘽鍒楄〃
+ kefuStore.updateConversation(message.conversationId)
+ // 鍒锋柊娑堟伅鍒楄〃
+ keFuChatBoxRef.value?.refreshMessageList(message)
+ return
+ }
+
+ // 2.3 娑堟伅绫诲瀷锛欿EFU_MESSAGE_ADMIN_READ
+ if (type === WebSocketMessageTypeConstants.KEFU_MESSAGE_ADMIN_READ) {
+ // 鏇存柊浼氳瘽宸茶
+ const message = JSON.parse(jsonMessage.content)
+ kefuStore.updateConversationStatus(message.conversationId)
+ }
+ } catch (error) {
+ console.error(error)
+ }
+ },
+ {
+ immediate: false // 涓嶇珛鍗虫墽琛�
+ }
+)
+// ======================= WebSocket end =======================
+
+/** 鍔犺浇鎸囧畾浼氳瘽鐨勬秷鎭垪琛� */
+const keFuChatBoxRef = ref<InstanceType<typeof KeFuMessageList>>()
+const memberInfoRef = ref<InstanceType<typeof MemberInfo>>()
+const handleChange = (conversation: KeFuConversationRespVO) => {
+ keFuChatBoxRef.value?.getNewMessageList(conversation)
+ memberInfoRef.value?.initHistory(conversation)
+}
+
+const keFuConversationRef = ref<InstanceType<typeof KeFuConversationList>>()
+/** 鍒濆鍖� */
+onMounted(() => {
+ /** 鍔犺浇浼氳瘽鍒楄〃 */
+ kefuStore.setConversationList().then(() => {
+ keFuConversationRef.value?.calculationLastMessageTime()
+ })
+ // 鎵撳紑 websocket 杩炴帴
+ open()
+})
+
+/** 閿�姣� */
+onBeforeUnmount(() => {
+ // 鍏抽棴 websocket 杩炴帴
+ close()
+})
+</script>
+
+<style lang="scss">
+.kefu-layout {
+ position: absolute;
+ flex: 1;
+ top: 0;
+ left: 0;
+ height: 100%;
+ width: 100%;
+}
+
+/* 瀹氫箟婊氬姩鏉℃牱寮� */
+::-webkit-scrollbar {
+ width: 10px;
+ height: 6px;
+}
+
+/* 瀹氫箟婊氬姩鏉¤建閬� 鍐呴槾褰�+鍦嗚 */
+::-webkit-scrollbar-track {
+ box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
+ border-radius: 10px;
+ background-color: #fff;
+}
+
+/* 瀹氫箟婊戝潡 鍐呴槾褰�+鍦嗚 */
+::-webkit-scrollbar-thumb {
+ border-radius: 10px;
+ box-shadow: inset 0 0 0 rgba(240, 240, 240, 0.5);
+ background-color: rgba(240, 240, 240, 0.5);
+}
+</style>
diff --git a/src/views/mall/promotion/point/activity/PointActivityForm.vue b/src/views/mall/promotion/point/activity/PointActivityForm.vue
new file mode 100644
index 0000000..a09565c
--- /dev/null
+++ b/src/views/mall/promotion/point/activity/PointActivityForm.vue
@@ -0,0 +1,227 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <Form
+ ref="formRef"
+ v-loading="formLoading"
+ :isCol="true"
+ :rules="rules"
+ :schema="allSchemas.formSchema"
+ >
+ <!-- 鍏堥�夋嫨 -->
+ <template #spuId>
+ <el-button v-if="!isFormUpdate" @click="spuSelectRef.open()">閫夋嫨鍟嗗搧</el-button>
+ <SpuAndSkuList
+ ref="spuAndSkuListRef"
+ :rule-config="ruleConfig"
+ :spu-list="spuList"
+ :spu-property-list-p="spuPropertyList"
+ >
+ <el-table-column align="center" label="鍙厬鎹㈠簱瀛�" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number
+ v-model="sku.productConfig.stock"
+ :max="sku.stock"
+ :min="0"
+ class="w-100%"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍙厬鎹㈡鏁�" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number v-model="sku.productConfig.count" :min="0" class="w-100%" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵�闇�绉垎" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number v-model="sku.productConfig.point" :min="0" class="w-100%" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵�闇�閲戦(鍏�)" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number
+ v-model="sku.productConfig.price"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ />
+ </template>
+ </el-table-column>
+ </SpuAndSkuList>
+ </template>
+ </Form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
+import { allSchemas, rules } from './pointActivity.data'
+import { cloneDeep } from 'lodash-es'
+import {
+ PointActivityApi,
+ PointActivityVO,
+ PointProductVO,
+ SkuExtension,
+ SpuExtension
+} from '@/api/mall/promotion/point'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import { convertToInteger, formatToFraction } from '@/utils'
+
+defineOptions({ name: 'PromotionSeckillActivityForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formRef = ref() // 琛ㄥ崟 Ref
+const isFormUpdate = ref(false) // 鏄惁鏇存柊琛ㄥ崟
+
+// ================= 鍟嗗搧閫夋嫨鐩稿叧 =================
+
+const spuSelectRef = ref() // 鍟嗗搧鍜屽睘鎬ч�夋嫨 Ref
+const spuAndSkuListRef = ref() // sku 绉垎鍟嗗煄鍟嗗搧閰嶇疆缁勪欢Ref
+const ruleConfig: RuleConfig[] = [
+ {
+ name: 'productConfig.stock',
+ rule: (arg) => arg >= 1,
+ message: '鍟嗗搧鍙厬鎹㈠簱瀛樺繀椤诲ぇ浜庣瓑浜� 1 锛侊紒锛�'
+ },
+ {
+ name: 'productConfig.point',
+ rule: (arg) => arg >= 1,
+ message: '鍟嗗搧鎵�闇�鍏戞崲绉垎蹇呴』澶т簬绛変簬 1 锛侊紒锛�'
+ },
+ {
+ name: 'productConfig.count',
+ rule: (arg) => arg >= 1,
+ message: '鍟嗗搧鍙厬鎹㈡鏁板繀椤诲ぇ浜庣瓑浜� 1 锛侊紒锛�'
+ }
+]
+const spuList = ref<SpuExtension[]>([]) // 閫夋嫨鐨� spu
+const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([])
+const selectSpu = (spuId: number, skuIds: number[]) => {
+ formRef.value.setValues({ spuId })
+ getSpuDetails(spuId, skuIds)
+}
+/**
+ * 鑾峰彇 SPU 璇︽儏
+ */
+const getSpuDetails = async (
+ spuId: number,
+ skuIds: number[] | undefined,
+ products?: PointProductVO[]
+) => {
+ const spuProperties: SpuProperty<SpuExtension>[] = []
+ const res = (await ProductSpuApi.getSpuDetailList([spuId])) as SpuExtension[]
+ if (res.length == 0) {
+ return
+ }
+ spuList.value = []
+ // 鍥犱负鍙兘閫夋嫨涓�涓�
+ const spu = res[0]
+ const selectSkus =
+ typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+ selectSkus?.forEach((sku) => {
+ let config: PointProductVO = {
+ skuId: sku.id!,
+ stock: 0,
+ price: 0,
+ point: 0,
+ count: 0
+ }
+ if (typeof products !== 'undefined') {
+ const product = products.find((item) => item.skuId === sku.id)
+ if (product) {
+ product.price = formatToFraction(product.price) as any
+ }
+ config = product || config
+ }
+ sku.productConfig = config
+ })
+ spu.skus = selectSkus as SkuExtension[]
+ spuProperties.push({
+ spuId: spu.id!,
+ spuDetail: spu,
+ propertyList: getPropertyList(spu)
+ })
+ spuList.value.push(spu)
+ spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ await resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = (await PointActivityApi.getPointActivity(id)) as PointActivityVO
+ isFormUpdate.value = true
+ await getSpuDetails(
+ data.spuId!,
+ data.products?.map((sku) => sku.skuId),
+ data.products
+ )
+ formRef.value.setValues(data)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.getElFormRef().validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鑾峰彇绉掓潃鍟嗗搧閰嶇疆
+ const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
+ products.forEach((item: PointProductVO) => {
+ item.price = convertToInteger(item.price)
+ })
+ const data = formRef.value.formModel as PointActivityVO
+ data.products = products
+ // 鐪熸鎻愪氦
+ if (formType.value === 'create') {
+ await PointActivityApi.createPointActivity(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PointActivityApi.updatePointActivity(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ spuList.value = []
+ spuPropertyList.value = []
+ isFormUpdate.value = false
+ await nextTick()
+ formRef.value.getElFormRef().resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/point/activity/index.vue b/src/views/mall/promotion/point/activity/index.vue
new file mode 100644
index 0000000..e663240
--- /dev/null
+++ b/src/views/mall/promotion/point/activity/index.vue
@@ -0,0 +1,218 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戠Н鍒嗗晢鍩庢椿鍔�" url="https://doc.iocoder.cn/mall/promotion-point/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:point-activity:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column label="娲诲姩缂栧彿" min-width="80" prop="id" />
+ <el-table-column label="鍟嗗搧鍥剧墖" min-width="80" prop="spuName">
+ <template #default="scope">
+ <el-image
+ :preview-src-list="[scope.row.picUrl]"
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" min-width="300" prop="spuName" />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ label="鍘熶环"
+ min-width="100"
+ prop="marketPrice"
+ />
+ <el-table-column align="center" label="娲诲姩鐘舵��" min-width="100" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="搴撳瓨" min-width="80" prop="stock" />
+ <el-table-column align="center" label="鎬诲簱瀛�" min-width="80" prop="totalStock" />
+ <el-table-column align="center" label="宸插厬鎹㈡暟閲�" min-width="100" prop="redeemedQuantity">
+ <template #default="{ row }">
+ {{ getRedeemedQuantity(row) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="150px">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:point-activity:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="scope.row.status === 0"
+ v-hasPermi="['promotion:point-activity:close']"
+ link
+ type="danger"
+ @click="handleClose(scope.row.id)"
+ >
+ 鍏抽棴
+ </el-button>
+ <el-button
+ v-else
+ v-hasPermi="['promotion:point-activity:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <PointActivityForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import PointActivityForm from './PointActivityForm.vue'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { PointActivityApi } from '@/api/mall/promotion/point'
+
+defineOptions({ name: 'PointActivity' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const getRedeemedQuantity = computed(() => (row: any) => (row.totalStock || 0) - (row.stock || 0)) // 鑾峰緱鍟嗗搧宸插厬鎹㈡暟閲�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PointActivityApi.getPointActivityPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍏抽棴鎸夐挳鎿嶄綔 */
+const handleClose = async (id: number) => {
+ try {
+ // 鍏抽棴鐨勪簩娆$‘璁�
+ await message.confirm('纭鍏抽棴璇ョН鍒嗗晢鍩庢椿鍔ㄥ悧锛�')
+ // 鍙戣捣鍏抽棴
+ await PointActivityApi.closePointActivity(id)
+ message.success('鍏抽棴鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PointActivityApi.deletePointActivity(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+})
+</script>
diff --git a/src/views/mall/promotion/point/activity/pointActivity.data.ts b/src/views/mall/promotion/point/activity/pointActivity.data.ts
new file mode 100644
index 0000000..7d925c2
--- /dev/null
+++ b/src/views/mall/promotion/point/activity/pointActivity.data.ts
@@ -0,0 +1,55 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+
+// 琛ㄥ崟鏍¢獙
+export const rules = reactive({
+ spuId: [required],
+ sort: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+ {
+ label: '鎺掑簭',
+ field: 'sort',
+ form: {
+ component: 'InputNumber',
+ value: 0
+ },
+ table: {
+ width: 80
+ }
+ },
+ {
+ label: '娲诲姩鍟嗗搧',
+ field: 'spuId',
+ isTable: true,
+ isSearch: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 300
+ }
+ },
+ {
+ label: '澶囨敞',
+ field: 'remark',
+ isSearch: false,
+ form: {
+ component: 'Input',
+ componentProps: {
+ type: 'textarea',
+ rows: 4
+ },
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 300
+ }
+ }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)
diff --git a/src/views/mall/promotion/point/components/PointShowcase.vue b/src/views/mall/promotion/point/components/PointShowcase.vue
new file mode 100644
index 0000000..82e490c
--- /dev/null
+++ b/src/views/mall/promotion/point/components/PointShowcase.vue
@@ -0,0 +1,154 @@
+<template>
+ <div class="flex flex-wrap items-center gap-8px">
+ <div
+ v-for="(pointActivity, index) in pointActivityList"
+ :key="pointActivity.id"
+ class="select-box spu-pic"
+ >
+ <el-tooltip :content="pointActivity.name">
+ <div class="relative h-full w-full">
+ <el-image :src="pointActivity.picUrl" class="h-full w-full" />
+ <Icon
+ v-show="!disabled"
+ class="del-icon"
+ icon="ep:circle-close-filled"
+ @click="handleRemoveActivity(index)"
+ />
+ </div>
+ </el-tooltip>
+ </div>
+ <el-tooltip v-if="canAdd" content="閫夋嫨娲诲姩">
+ <div class="select-box" @click="openSeckillActivityTableSelect">
+ <Icon icon="ep:plus" />
+ </div>
+ </el-tooltip>
+ </div>
+ <!-- 鎷煎洟娲诲姩閫夋嫨瀵硅瘽妗嗭紙琛ㄦ牸褰㈠紡锛� -->
+ <PointTableSelect
+ ref="pointActivityTableSelectRef"
+ :multiple="limit != 1"
+ @change="handleActivitySelected"
+ />
+</template>
+<script lang="ts" setup>
+import PointTableSelect from './PointTableSelect.vue'
+import { PointActivityApi, PointActivityVO } from '@/api/mall/promotion/point'
+import { propTypes } from '@/utils/propTypes'
+import { oneOfType } from 'vue-types'
+import { isArray } from '@/utils/is'
+
+// 娲诲姩姗辩獥锛屼竴鑸敤浜庤淇椂浣跨敤
+// 鎻愪緵鍔熻兘锛氬睍绀烘椿鍔ㄥ垪琛ㄣ�佹坊鍔犳椿鍔ㄣ�佸垹闄ゆ椿鍔�
+defineOptions({ name: 'PointShowcase' })
+
+const props = defineProps({
+ modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
+ // 闄愬埗鏁伴噺锛氶粯璁や笉闄愬埗
+ limit: propTypes.number.def(Number.MAX_VALUE),
+ disabled: propTypes.bool.def(false)
+})
+
+// 璁$畻鏄惁鍙互娣诲姞
+const canAdd = computed(() => {
+ // 鎯呭喌涓�锛氱鐢ㄦ椂涓嶅彲浠ユ坊鍔�
+ if (props.disabled) return false
+ // 鎯呭喌浜岋細鏈寚瀹氶檺鍒舵暟閲忔椂锛屽彲浠ユ坊鍔�
+ if (!props.limit) return true
+ // 鎯呭喌涓夛細妫�鏌ュ凡娣诲姞鏁伴噺鏄惁灏忎簬闄愬埗鏁伴噺
+ return pointActivityList.value.length < props.limit
+})
+
+// 鎷煎洟娲诲姩鍒楄〃
+const pointActivityList = ref<PointActivityVO[]>([])
+
+watch(
+ () => props.modelValue,
+ async () => {
+ const ids = isArray(props.modelValue)
+ ? // 鎯呭喌涓�锛氬閫�
+ props.modelValue
+ : // 鎯呭喌浜岋細鍗曢��
+ props.modelValue
+ ? [props.modelValue]
+ : []
+ // 涓嶉渶瑕佽繑鏄�
+ if (ids.length === 0) {
+ pointActivityList.value = []
+ return
+ }
+ // 鍙湁娲诲姩鍙戠敓鍙樺寲涔嬪悗锛屾墠浼氭煡璇㈡椿鍔�
+ if (
+ pointActivityList.value.length === 0 ||
+ pointActivityList.value.some((pointActivity) => !ids.includes(pointActivity.id!))
+ ) {
+ pointActivityList.value = await PointActivityApi.getPointActivityListByIds(ids)
+ }
+ },
+ { immediate: true }
+)
+
+/** 娲诲姩琛ㄦ牸閫夋嫨瀵硅瘽妗� */
+const pointActivityTableSelectRef = ref()
+// 鎵撳紑瀵硅瘽妗�
+const openSeckillActivityTableSelect = () => {
+ pointActivityTableSelectRef.value.open(pointActivityList.value)
+}
+
+/**
+ * 閫夋嫨娲诲姩鍚庤Е鍙�
+ * @param activityList 閫変腑鐨勬椿鍔ㄥ垪琛�
+ */
+const handleActivitySelected = (activityList: PointActivityVO | PointActivityVO[]) => {
+ pointActivityList.value = isArray(activityList) ? activityList : [activityList]
+ emitActivityChange()
+}
+
+/**
+ * 鍒犻櫎娲诲姩
+ * @param index 娲诲姩绱㈠紩
+ */
+const handleRemoveActivity = (index: number) => {
+ pointActivityList.value.splice(index, 1)
+ emitActivityChange()
+}
+const emit = defineEmits(['update:modelValue', 'change'])
+const emitActivityChange = () => {
+ if (props.limit === 1) {
+ const pointActivity = pointActivityList.value.length > 0 ? pointActivityList.value[0] : null
+ emit('update:modelValue', pointActivity?.id || 0)
+ emit('change', pointActivity)
+ } else {
+ emit(
+ 'update:modelValue',
+ pointActivityList.value.map((pointActivity) => pointActivity.id)
+ )
+ emit('change', pointActivityList.value)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.select-box {
+ display: flex;
+ width: 60px;
+ height: 60px;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.spu-pic {
+ position: relative;
+}
+
+.del-icon {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ z-index: 1;
+ width: 20px !important;
+ height: 20px !important;
+}
+</style>
diff --git a/src/views/mall/promotion/point/components/PointTableSelect.vue b/src/views/mall/promotion/point/components/PointTableSelect.vue
new file mode 100644
index 0000000..d68b5f1
--- /dev/null
+++ b/src/views/mall/promotion/point/components/PointTableSelect.vue
@@ -0,0 +1,300 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" title="閫夋嫨娲诲姩" width="70%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+ <!-- 1. 澶氶�夋ā寮忥紙涓嶈兘浣跨敤type="selection"锛孍lement浼氬拷鐣eader鎻掓Ы锛� -->
+ <el-table-column v-if="multiple" width="55">
+ <template #header>
+ <el-checkbox
+ v-model="isCheckAll"
+ :indeterminate="isIndeterminate"
+ @change="handleCheckAll"
+ />
+ </template>
+ <template #default="{ row }">
+ <el-checkbox
+ v-model="checkedStatus[row.id]"
+ @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+ />
+ </template>
+ </el-table-column>
+ <!-- 2. 鍗曢�夋ā寮� -->
+ <el-table-column v-else label="#" width="55">
+ <template #default="{ row }">
+ <el-radio
+ v-model="selectedActivityId"
+ :value="row.id"
+ @change="handleSingleSelected(row)"
+ >
+ <!-- 绌烘牸涓嶈兘鐪佺暐锛屾槸涓轰簡璁╁崟閫夋涓嶆樉绀簂abel锛屽鏋滀笉鎸囧畾label涓嶄細鏈夐�変腑鐨勬晥鏋� -->
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column label="娲诲姩缂栧彿" min-width="80" prop="id" />
+ <el-table-column label="鍟嗗搧鍥剧墖" min-width="80" prop="spuName">
+ <template #default="scope">
+ <el-image
+ :preview-src-list="[scope.row.picUrl]"
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" min-width="300" prop="spuName" />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ label="鍘熶环"
+ min-width="100"
+ prop="marketPrice"
+ />
+ <el-table-column label="鍘熶环" min-width="100" prop="marketPrice" />
+ <el-table-column align="center" label="娲诲姩鐘舵��" min-width="100" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="搴撳瓨" min-width="80" prop="stock" />
+ <el-table-column align="center" label="鎬诲簱瀛�" min-width="80" prop="totalStock" />
+ <el-table-column align="center" label="宸插厬鎹㈡暟閲�" min-width="100" prop="redeemedQuantity">
+ <template #default="{ row }">
+ {{ getRedeemedQuantity(row) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template v-if="multiple" #footer>
+ <el-button type="primary" @click="handleEmitChange">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { CHANGE_EVENT } from 'element-plus'
+import { PointActivityApi, PointActivityVO } from '@/api/mall/promotion/point'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+/**
+ * 娲诲姩琛ㄦ牸閫夋嫨瀵硅瘽妗�
+ * 1. 鍗曢�夋ā寮忥細
+ * 1.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫崟閫夋鏃讹紝缁撴潫閫夋嫨锛屽苟鍏抽棴瀵硅瘽妗�
+ * 1.2 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ * 2. 澶氶�夋ā寮忥細
+ * 2.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫閫夋鏃讹紝璁板綍閫変腑鐨勬椿鍔�
+ * 2.2 鍒囨崲鍒嗛〉鏃讹紝淇濇寔娲诲姩鐨勯�変腑鐘舵��
+ * 2.3 鐐瑰嚮鍙充笅瑙掔殑纭畾鎸夐挳鏃讹紝缁撴潫閫夋嫨锛屽叧闂璇濇
+ * 2.4 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ */
+defineOptions({ name: 'PointTableSelect' })
+
+defineProps({
+ // 澶氶�夋ā寮�
+ multiple: propTypes.bool.def(false)
+})
+
+// 鍒楄〃鐨勬�婚〉鏁�
+const total = ref(0)
+// 鍒楄〃鐨勬暟鎹�
+const list = ref<PointActivityVO[]>([])
+// 鍒楄〃鐨勫姞杞戒腑
+const loading = ref(false)
+// 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogVisible = ref(false)
+// 鏌ヨ鍙傛暟
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: undefined
+})
+const getRedeemedQuantity = computed(() => (row: any) => (row.totalStock || 0) - (row.stock || 0)) // 鑾峰緱鍟嗗搧宸插厬鎹㈡暟閲�
+/** 鎵撳紑寮圭獥 */
+const open = (pointList?: PointActivityVO[]) => {
+ // 閲嶇疆
+ checkedActivities.value = []
+ checkedStatus.value = {}
+ isCheckAll.value = false
+ isIndeterminate.value = false
+
+ // 澶勭悊宸查�変腑
+ if (pointList && pointList.length > 0) {
+ checkedActivities.value = [...pointList]
+ checkedStatus.value = Object.fromEntries(pointList.map((activityVO) => [activityVO.id, true]))
+ }
+
+ dialogVisible.value = true
+ resetQuery()
+}
+// 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+defineExpose({ open })
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PointActivityApi.getPointActivityPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ // checkbox缁戝畾undefined浼氭湁闂锛岄渶瑕佺粰涓�涓猙ool鍊�
+ list.value.forEach(
+ (activityVO) =>
+ (checkedStatus.value[activityVO.id] = checkedStatus.value[activityVO.id] || false)
+ )
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ calculateIsCheckAll()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.value = {
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: undefined
+ }
+ getList()
+}
+
+// 鏄惁鍏ㄩ��
+const isCheckAll = ref(false)
+// 鍏ㄩ�夋鏄惁澶勪簬涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+const isIndeterminate = ref(false)
+// 閫変腑鐨勬椿鍔�
+const checkedActivities = ref<PointActivityVO[]>([])
+// 閫変腑鐘舵�侊細key涓烘椿鍔↖D锛寁alue涓烘槸鍚﹂�変腑
+const checkedStatus = ref<Record<string, boolean>>({})
+
+// 閫変腑鐨勬椿鍔� activityId
+const selectedActivityId = ref()
+/** 鍗曢�変腑鏃惰Е鍙� */
+const handleSingleSelected = (pointActivityVO: PointActivityVO) => {
+ emits(CHANGE_EVENT, pointActivityVO)
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ // 璁颁綇涓婃閫夋嫨鐨処D
+ selectedActivityId.value = pointActivityVO.id
+}
+
+/** 澶氶�夊畬鎴� */
+const handleEmitChange = () => {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ emits(CHANGE_EVENT, [...checkedActivities.value])
+}
+
+/** 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢 */
+const emits = defineEmits<{
+ (e: CHANGE_EVENT, v: PointActivityVO | PointActivityVO[] | any): void
+}>()
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckAll = (checked: boolean) => {
+ isCheckAll.value = checked
+ isIndeterminate.value = false
+
+ list.value.forEach((pointActivity) => handleCheckOne(checked, pointActivity, false))
+}
+
+/**
+ * 閫変腑涓�琛�
+ * @param checked 鏄惁閫変腑
+ * @param pointActivity 娲诲姩
+ * @param isCalcCheckAll 鏄惁璁$畻鍏ㄩ��
+ */
+const handleCheckOne = (
+ checked: boolean,
+ pointActivity: PointActivityVO,
+ isCalcCheckAll: boolean
+) => {
+ if (checked) {
+ checkedActivities.value.push(pointActivity)
+ checkedStatus.value[pointActivity.id] = true
+ } else {
+ const index = findCheckedIndex(pointActivity)
+ if (index > -1) {
+ checkedActivities.value.splice(index, 1)
+ checkedStatus.value[pointActivity.id] = false
+ isCheckAll.value = false
+ }
+ }
+
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ if (isCalcCheckAll) {
+ calculateIsCheckAll()
+ }
+}
+
+// 鏌ユ壘娲诲姩鍦ㄥ凡閫変腑娲诲姩鍒楄〃涓殑绱㈠紩
+const findCheckedIndex = (activityVO: PointActivityVO) =>
+ checkedActivities.value.findIndex((item) => item.id === activityVO.id)
+
+// 璁$畻鍏ㄩ�夋鐘舵��
+const calculateIsCheckAll = () => {
+ isCheckAll.value = list.value.every((activityVO) => checkedStatus.value[activityVO.id])
+ // 璁$畻涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+ isIndeterminate.value =
+ !isCheckAll.value && list.value.some((activityVO) => checkedStatus.value[activityVO.id])
+}
+</script>
diff --git a/src/views/mall/promotion/rewardActivity/RewardForm.vue b/src/views/mall/promotion/rewardActivity/RewardForm.vue
new file mode 100644
index 0000000..64a2dd4
--- /dev/null
+++ b/src/views/mall/promotion/rewardActivity/RewardForm.vue
@@ -0,0 +1,224 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�" />
+ </el-form-item>
+ <el-form-item label="娲诲姩鏃堕棿" prop="startAndEndTime">
+ <el-date-picker
+ v-model="formData.startAndEndTime"
+ :end-placeholder="t('common.endTimeText')"
+ :start-placeholder="t('common.startTimeText')"
+ range-separator="-"
+ type="datetimerange"
+ value-format="x"
+ />
+ </el-form-item>
+ <el-form-item label="鏉′欢绫诲瀷" prop="conditionType">
+ <el-radio-group v-model="formData.conditionType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="浼樻儬璁剧疆">
+ <RewardRule ref="rewardRuleRef" v-model="formData" />
+ </el-form-item>
+ <el-form-item label="娲诲姩鑼冨洿" prop="productScope">
+ <el-radio-group v-model="formData.productScope">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
+ :key="dict.value"
+ :label="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item
+ v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope"
+ prop="productSpuIds"
+ >
+ <SpuShowcase v-model="formData.productSpuIds" />
+ </el-form-item>
+ <el-form-item
+ v-if="formData.productScope === PromotionProductScopeEnum.CATEGORY.scope"
+ label="鍒嗙被"
+ prop="productCategoryIds"
+ >
+ <ProductCategorySelect v-model="formData.productCategoryIds" :multiple="true" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import RewardRule from './components/RewardRule.vue'
+import SpuShowcase from '@/views/mall/product/spu/components/SpuShowcase.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'
+import { PromotionConditionTypeEnum, PromotionProductScopeEnum } from '@/utils/constants'
+import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
+import { cloneDeep } from 'lodash-es'
+import { fenToYuan, yuanToFen } from '@/utils'
+
+defineOptions({ name: 'ProductBrandForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<RewardActivityApi.RewardActivityVO>({
+ conditionType: PromotionConditionTypeEnum.PRICE.type,
+ productScope: PromotionProductScopeEnum.ALL.scope,
+ rules: []
+} as RewardActivityApi.RewardActivityVO)
+const formRules = reactive({
+ name: [{ required: true, message: '娲诲姩鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ startAndEndTime: [{ required: true, message: '娲诲姩鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }],
+ conditionType: [{ required: true, message: '鏉′欢绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ productScope: [{ required: true, message: '鍟嗗搧鑼冨洿涓嶈兘涓虹┖', trigger: 'blur' }],
+ productSpuIds: [{ required: true, message: '鍟嗗搧涓嶈兘涓虹┖', trigger: 'blur' }],
+ productCategoryIds: [{ required: true, message: '鍟嗗搧鍒嗙被涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const rewardRuleRef = ref<InstanceType<typeof RewardRule>>() // 娲诲姩瑙勫垯 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = await RewardActivityApi.getReward(id)
+ // 杞尯娈垫椂闂�
+ data.startAndEndTime = [data.startTime, data.endTime]
+ // 瑙勫垯鍒嗚浆鍏�
+ data.rules?.forEach((item: any) => {
+ item.discountPrice = fenToYuan(item.discountPrice || 0)
+ if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
+ item.limit = fenToYuan(item.limit || 0)
+ }
+ })
+ formData.value = data
+ // 鑾峰緱鍟嗗搧鑼冨洿
+ await getProductScope()
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef.value) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 璁剧疆娲诲姩瑙勫垯浼樻儬鍒�
+ rewardRuleRef.value?.setRuleCoupon()
+ const data = cloneDeep(formData.value)
+ // 鏃堕棿娈佃浆鎹�
+ data.startTime = data.startAndEndTime![0]
+ data.endTime = data.startAndEndTime![1]
+ delete data.startAndEndTime
+ // 瑙勫垯鍏冭浆鍒�
+ data.rules.forEach((item) => {
+ item.discountPrice = yuanToFen(item.discountPrice || 0)
+ if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
+ item.limit = yuanToFen(item.limit || 0)
+ }
+ })
+ // 璁剧疆鍟嗗搧鑼冨洿
+ setProductScopeValues(data)
+ if (formType.value === 'create') {
+ await RewardActivityApi.createRewardActivity(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await RewardActivityApi.updateRewardActivity(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ conditionType: PromotionConditionTypeEnum.PRICE.type,
+ productScope: PromotionProductScopeEnum.ALL.scope,
+ rules: []
+ } as RewardActivityApi.RewardActivityVO
+}
+
+/** 鑾峰緱鍟嗗搧鑼冨洿 */
+const getProductScope = async () => {
+ switch (formData.value.productScope) {
+ case PromotionProductScopeEnum.SPU.scope:
+ // 璁剧疆鍟嗗搧缂栧彿
+ formData.value.productSpuIds = formData.value.productScopeValues
+ break
+ case PromotionProductScopeEnum.CATEGORY.scope:
+ await nextTick()
+ let productCategoryIds = formData.value.productScopeValues as any
+ if (Array.isArray(productCategoryIds) && productCategoryIds.length === 1) {
+ // 鍗曢�夋椂浣跨敤鏁扮粍涓嶈兘鍙嶆樉
+ productCategoryIds = productCategoryIds[0]
+ }
+ // 璁剧疆鍝佺被缂栧彿
+ formData.value.productCategoryIds = productCategoryIds
+ break
+ default:
+ break
+ }
+}
+
+/** 璁剧疆鍟嗗搧鑼冨洿 */
+function setProductScopeValues(data: any) {
+ switch (formData.value.productScope) {
+ case PromotionProductScopeEnum.SPU.scope:
+ data.productScopeValues = formData.value.productSpuIds
+ break
+ case PromotionProductScopeEnum.CATEGORY.scope:
+ data.productScopeValues = Array.isArray(formData.value.productCategoryIds)
+ ? formData.value.productCategoryIds
+ : [formData.value.productCategoryIds]
+ break
+ default:
+ break
+ }
+}
+</script>
diff --git a/src/views/mall/promotion/rewardActivity/components/RewardRule.vue b/src/views/mall/promotion/rewardActivity/components/RewardRule.vue
new file mode 100644
index 0000000..2c63a42
--- /dev/null
+++ b/src/views/mall/promotion/rewardActivity/components/RewardRule.vue
@@ -0,0 +1,138 @@
+<template>
+ <!-- 婊″噺閫佹椿鍔ㄨ鍒欑粍浠� -->
+ <el-row>
+ <template v-if="formData.rules">
+ <el-col v-for="(rule, index) in formData.rules" :key="index" :span="24">
+ <span class="font-bold">娲诲姩灞傜骇{{ index + 1 }}</span>
+ <el-button v-if="index !== 0" link type="danger" @click="deleteRule(index)">
+ 鍒犻櫎
+ </el-button>
+ <el-form ref="formRef" :model="rule">
+ <el-form-item label="浼樻儬闂ㄦ:" label-width="100px" prop="limit">
+ 婊�
+ <el-input-number
+ v-if="PromotionConditionTypeEnum.PRICE.type === formData.conditionType"
+ v-model="rule.limit"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-150px! p-x-20px!"
+ placeholder=""
+ type="number"
+ controls-position="right"
+ />
+ <el-input
+ v-else
+ v-model="rule.limit"
+ :min="0"
+ class="w-150px! p-x-20px!"
+ placeholder=""
+ type="number"
+ />
+ {{ PromotionConditionTypeEnum.PRICE.type === formData.conditionType ? '鍏�' : '浠�' }}
+ </el-form-item>
+ <el-form-item label="浼樻儬鍐呭:" label-width="100px">
+ <el-col :span="24">
+ 璁㈠崟閲戦浼樻儬
+ <el-form-item>
+ 鍑�
+ <el-input-number
+ v-model="rule.discountPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-150px! p-x-20px!"
+ controls-position="right"
+ />
+ 鍏�
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <span>鍖呴偖锛�</span>
+ <el-switch
+ v-model="rule.freeDelivery"
+ active-text="鏄�"
+ inactive-text="鍚�"
+ inline-prompt
+ />
+ </el-col>
+ <el-col :span="24">
+ <span>閫佺Н鍒嗭細</span>
+ <el-form-item>
+ 閫�
+ <el-input
+ v-model="rule.point"
+ class="w-150px! p-x-20px!"
+ placeholder=""
+ type="number"
+ />
+ 绉垎
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <span>閫佷紭鎯犲埜锛�</span>
+ <RewardRuleCouponSelect ref="rewardRuleCouponSelectRef" v-model="rule!" />
+ </el-col>
+ </el-form-item>
+ </el-form>
+ </el-col>
+ </template>
+ <el-col :span="24" class="mt-10px">
+ <el-button type="primary" @click="addRule">娣诲姞浼樻儬瑙勫垯</el-button>
+ </el-col>
+ <el-col :span="24">
+ <el-tag type="warning"> 璧犻�佺Н鍒嗕负 0 鏃朵笉璧犻�併�傛湭閫変紭鎯犲埜鏃朵笉璧犻�併��</el-tag>
+ </el-col>
+ </el-row>
+</template>
+
+<script lang="ts" setup>
+import RewardRuleCouponSelect from './RewardRuleCouponSelect.vue'
+import { RewardActivityVO } from '@/api/mall/promotion/reward/rewardActivity'
+import { PromotionConditionTypeEnum } from '@/utils/constants'
+import { useVModel } from '@vueuse/core'
+import { isEmpty } from '@/utils/is'
+
+defineOptions({ name: 'RewardRule' })
+
+const props = defineProps<{
+ modelValue: RewardActivityVO
+}>()
+
+const emits = defineEmits<{
+ (e: 'update:modelValue', v: any): void
+ (e: 'deleteRule', v: number): void
+}>()
+
+const formData = useVModel(props, 'modelValue', emits) // 娲诲姩鏁版嵁
+const rewardRuleCouponSelectRef = ref<InstanceType<typeof RewardRuleCouponSelect>[]>() // 娲诲姩瑙勫垯浼樻儬鍒� Ref
+
+/** 鍒犻櫎浼樻儬瑙勫垯 */
+const deleteRule = (ruleIndex: number) => {
+ formData.value.rules.splice(ruleIndex, 1)
+}
+
+/** 娣诲姞浼樻儬瑙勫垯 */
+const addRule = () => {
+ if (isEmpty(formData.value.rules)) {
+ formData.value.rules = []
+ }
+ formData.value.rules.push({
+ limit: 0,
+ discountPrice: 0,
+ freeDelivery: false,
+ point: 0
+ })
+}
+
+/** 璁剧疆瑙勫垯浼樻儬鍒�-鎻愪氦鏃� */
+const setRuleCoupon = () => {
+ if (isEmpty(rewardRuleCouponSelectRef.value)) {
+ return
+ }
+
+ rewardRuleCouponSelectRef.value?.forEach((item) => item.setGiveCouponList())
+}
+
+defineExpose({ setRuleCoupon })
+</script>
diff --git a/src/views/mall/promotion/rewardActivity/components/RewardRuleCouponSelect.vue b/src/views/mall/promotion/rewardActivity/components/RewardRuleCouponSelect.vue
new file mode 100644
index 0000000..41b1f2b
--- /dev/null
+++ b/src/views/mall/promotion/rewardActivity/components/RewardRuleCouponSelect.vue
@@ -0,0 +1,133 @@
+<template>
+ <el-button class="ml-10px" type="text" @click="selectCoupon">娣诲姞浼樻儬鍔�</el-button>
+
+ <div
+ v-for="(item, index) in list"
+ :key="item.id"
+ class="coupon-list-item p-x-10px mb-10px flex justify-between"
+ >
+ <div class="coupon-list-item-left flex items-center flex-wrap">
+ <div class="mr-10px"> 浼樻儬鍒稿悕绉帮細{{ item.name }}</div>
+ <div class="mr-10px">
+ 鑼冨洿锛�
+ <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="item.productScope" />
+ </div>
+ <div class="flex items-center">
+ 浼樻儬锛�
+ <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="item.discountType" />
+ {{ discountFormat(item) }}
+ </div>
+ </div>
+ <div class="coupon-list-item-right">
+ 閫�
+ <el-input v-model="item.giveCount" class="w-150px! p-x-20px!" placeholder="" type="number" />
+ 寮�
+ <el-button class="ml-20px" link type="danger" @click="deleteCoupon(index)">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <!-- 浼樻儬鍒搁�夋嫨 -->
+ <CouponSelect
+ ref="couponSelectRef"
+ :take-type="CouponTemplateTakeTypeEnum.ADMIN.type"
+ @change="handleCouponChange"
+ />
+</template>
+
+<script lang="ts" setup>
+import { CouponSelect } from '@/views/mall/promotion/coupon/components'
+import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
+import { RewardRule } from '@/api/mall/promotion/reward/rewardActivity'
+import { DICT_TYPE } from '@/utils/dict'
+import { CouponTemplateTakeTypeEnum } from '@/utils/constants'
+import { discountFormat } from '@/views/mall/promotion/coupon/formatter'
+import { isEmpty } from '@/utils/is'
+import { useVModel } from '@vueuse/core'
+
+defineOptions({ name: 'RewardRuleCouponSelect' })
+
+const props = defineProps<{
+ modelValue: RewardRule
+}>()
+
+const emits = defineEmits<{
+ (e: 'update:modelValue', v: any): void
+}>()
+
+const rewardRule = useVModel(props, 'modelValue', emits) // 璧犻�佽鍒�
+const list = ref<GiveCouponVO[]>([]) // 閫夋嫨鐨勪紭鎯犲埜鍒楄〃
+
+/** 閫夋嫨璧犻�佺殑浼樻儬绫诲瀷鎷撳睍 */
+interface GiveCouponVO extends CouponTemplateApi.CouponTemplateVO {
+ giveCount?: number
+}
+
+/** 閫夋嫨浼樻儬鍒� */
+const couponSelectRef = ref<InstanceType<typeof CouponSelect>>() // 浼樻儬鍒搁�夋嫨
+const selectCoupon = () => {
+ couponSelectRef.value?.open()
+}
+
+/** 閫夋嫨浼樻儬鍒稿悗鐨勫洖璋� */
+const handleCouponChange = (val: CouponTemplateApi.CouponTemplateVO[]) => {
+ for (const item of val) {
+ if (list.value.some((v) => v.id === item.id)) {
+ continue
+ }
+ list.value.push(item)
+ }
+}
+
+/** 鍒犻櫎浼樻儬鍒� */
+const deleteCoupon = (index: number) => {
+ list.value.splice(index, 1)
+}
+
+/** 鍒濆鍖栬禒閫佺殑浼樻儬鍒稿垪琛� */
+const initGiveCouponList = async () => {
+ // 鏍¢獙浼樻儬鍒稿瓨鍦�
+ if (isEmpty(rewardRule.value) || isEmpty(rewardRule.value.giveCouponTemplateCounts)) {
+ return
+ }
+ const tempLateIds = Object.keys(rewardRule.value.giveCouponTemplateCounts!)
+ const data = await CouponTemplateApi.getCouponTemplateList(tempLateIds)
+ if (!data) {
+ return
+ }
+ // 鍥炴樉
+ data.forEach((coupon) => {
+ list.value.push({
+ ...coupon,
+ giveCount: rewardRule.value.giveCouponTemplateCounts![coupon.id]
+ })
+ })
+}
+
+/** 璁剧疆璧犻�佺殑浼樻儬鍒� */
+const setGiveCouponList = () => {
+ if (isEmpty(rewardRule.value)) {
+ return
+ }
+ // 鏍稿績锛氭竻绌� rewardRule.value.giveCouponTemplateCounts锛岃В鍐冲垹闄や笉鐢熸晥鐨勯棶棰�
+ rewardRule.value.giveCouponTemplateCounts = {}
+
+ // 璁剧疆浼樻儬鍒稿拰鍏舵暟閲忕殑瀵瑰簲
+ list.value.forEach((rule) => {
+ rewardRule.value.giveCouponTemplateCounts![rule.id] = rule.giveCount!
+ })
+}
+defineExpose({ setGiveCouponList })
+
+/** 缁勪欢鍒濆鍖� */
+onMounted(async () => {
+ await nextTick()
+ await initGiveCouponList()
+})
+</script>
+
+<style lang="scss" scoped>
+.coupon-list-item {
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+}
+</style>
diff --git a/src/views/mall/promotion/rewardActivity/index.vue b/src/views/mall/promotion/rewardActivity/index.vue
new file mode 100644
index 0000000..544420e
--- /dev/null
+++ b/src/views/mall/promotion/rewardActivity/index.vue
@@ -0,0 +1,227 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戞弧鍑忛��" url="https://doc.iocoder.cn/mall/promotion-record/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="娲诲姩鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="娲诲姩缁撴潫鏃ユ湡"
+ start-placeholder="娲诲姩寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:reward-activity:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" default-expand-all row-key="id">
+ <el-table-column label="娲诲姩鍚嶇О" prop="name" />
+ <el-table-column label="娲诲姩鑼冨洿" prop="productScope" >
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="娲诲姩寮�濮嬫椂闂�"
+ prop="startTime"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="娲诲姩缁撴潫鏃堕棿"
+ prop="endTime"
+ />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:reward-activity:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="scope.row.status === 0"
+ v-hasPermi="['promotion:reward-activity:close']"
+ link
+ type="danger"
+ @click="handleClose(scope.row.id)"
+ >
+ 鍏抽棴
+ </el-button>
+ <el-button
+ v-hasPermi="['promotion:reward-activity:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <RewardForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'
+import RewardForm from './RewardForm.vue'
+
+defineOptions({ name: 'PromotionRewardActivity' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await RewardActivityApi.getRewardActivityPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref<InstanceType<typeof RewardForm>>()
+const openForm = (type: string, id?: number) => {
+ formRef.value?.open(type, id)
+}
+
+/** 鍏抽棴鎸夐挳鎿嶄綔 */
+const handleClose = async (id: number) => {
+ try {
+ // 鍏抽棴鐨勪簩娆$‘璁�
+ await message.confirm('纭鍏抽棴璇ユ弧鍑忔椿鍔ㄥ悧锛�')
+ // 鍙戣捣鍏抽棴
+ await RewardActivityApi.closeRewardActivity(id)
+ message.success('鍏抽棴鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await RewardActivityApi.deleteRewardActivity(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue b/src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue
new file mode 100644
index 0000000..486b71d
--- /dev/null
+++ b/src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue
@@ -0,0 +1,196 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
+ <Form
+ ref="formRef"
+ v-loading="formLoading"
+ :isCol="true"
+ :rules="rules"
+ :schema="allSchemas.formSchema"
+ >
+ <!-- 鍏堥�夋嫨 -->
+ <template #spuId>
+ <el-button @click="spuSelectRef.open()">閫夋嫨鍟嗗搧</el-button>
+ <SpuAndSkuList
+ ref="spuAndSkuListRef"
+ :rule-config="ruleConfig"
+ :spu-list="spuList"
+ :spu-property-list-p="spuPropertyList"
+ >
+ <el-table-column align="center" label="绉掓潃搴撳瓨" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number v-model="sku.productConfig.stock" :min="0" class="w-100%" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="绉掓潃浠锋牸(鍏�)" min-width="168">
+ <template #default="{ row: sku }">
+ <el-input-number
+ v-model="sku.productConfig.seckillPrice"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="w-100%"
+ />
+ </template>
+ </el-table-column>
+ </SpuAndSkuList>
+ </template>
+ </Form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+</template>
+<script lang="ts" setup>
+import { SpuAndSkuList, SpuProperty, SpuSelect } from '../../components'
+import { allSchemas, rules } from './seckillActivity.data'
+import { cloneDeep } from 'lodash-es'
+
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { SeckillProductVO } from '@/api/mall/promotion/seckill/seckillActivity'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
+import { convertToInteger, formatToFraction } from '@/utils'
+
+defineOptions({ name: 'PromotionSeckillActivityForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// ================= 鍟嗗搧閫夋嫨鐩稿叧 =================
+
+const spuSelectRef = ref() // 鍟嗗搧鍜屽睘鎬ч�夋嫨 Ref
+const spuAndSkuListRef = ref() // sku 绉掓潃閰嶇疆缁勪欢Ref
+const ruleConfig: RuleConfig[] = [
+ {
+ name: 'productConfig.stock',
+ rule: (arg) => arg >= 1,
+ message: '鍟嗗搧绉掓潃搴撳瓨蹇呴』澶т簬绛変簬 1 锛侊紒锛�'
+ },
+ {
+ name: 'productConfig.seckillPrice',
+ rule: (arg) => arg >= 0.01,
+ message: '鍟嗗搧绉掓潃浠锋牸蹇呴』澶т簬绛変簬 0.01 锛侊紒锛�'
+ }
+]
+const spuList = ref<SeckillActivityApi.SpuExtension[]>([]) // 閫夋嫨鐨� spu
+const spuPropertyList = ref<SpuProperty<SeckillActivityApi.SpuExtension>[]>([])
+const selectSpu = (spuId: number, skuIds: number[]) => {
+ formRef.value.setValues({ spuId })
+ getSpuDetails(spuId, skuIds)
+}
+/**
+ * 鑾峰彇 SPU 璇︽儏
+ */
+const getSpuDetails = async (
+ spuId: number,
+ skuIds: number[] | undefined,
+ products?: SeckillProductVO[]
+) => {
+ const spuProperties: SpuProperty<SeckillActivityApi.SpuExtension>[] = []
+ const res = (await ProductSpuApi.getSpuDetailList([spuId])) as SeckillActivityApi.SpuExtension[]
+ if (res.length == 0) {
+ return
+ }
+ spuList.value = []
+ // 鍥犱负鍙兘閫夋嫨涓�涓�
+ const spu = res[0]
+ const selectSkus =
+ typeof skuIds === 'undefined' ? spu?.skus : spu?.skus?.filter((sku) => skuIds.includes(sku.id!))
+ selectSkus?.forEach((sku) => {
+ let config: SeckillActivityApi.SeckillProductVO = {
+ skuId: sku.id!,
+ stock: 0,
+ seckillPrice: 0
+ }
+ if (typeof products !== 'undefined') {
+ const product = products.find((item) => item.skuId === sku.id)
+ if (product) {
+ product.seckillPrice = formatToFraction(product.seckillPrice)
+ }
+ config = product || config
+ }
+ sku.productConfig = config
+ })
+ spu.skus = selectSkus as SeckillActivityApi.SkuExtension[]
+ spuProperties.push({
+ spuId: spu.id!,
+ spuDetail: spu,
+ propertyList: getPropertyList(spu)
+ })
+ spuList.value.push(spu)
+ spuPropertyList.value = spuProperties
+}
+
+// ================= end =================
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ await resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const data = (await SeckillActivityApi.getSeckillActivity(
+ id
+ )) as SeckillActivityApi.SeckillActivityVO
+ await getSpuDetails(data.spuId!, data.products?.map((sku) => sku.skuId), data.products)
+ formRef.value.setValues(data)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.getElFormRef().validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鑾峰彇绉掓潃鍟嗗搧閰嶇疆
+ const products = cloneDeep(spuAndSkuListRef.value.getSkuConfigs('productConfig'))
+ products.forEach((item: SeckillProductVO) => {
+ item.seckillPrice = convertToInteger(item.seckillPrice)
+ })
+ const data = formRef.value.formModel as SeckillActivityApi.SeckillActivityVO
+ data.products = products
+ // 鐪熸鎻愪氦
+ if (formType.value === 'create') {
+ await SeckillActivityApi.createSeckillActivity(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SeckillActivityApi.updateSeckillActivity(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async () => {
+ spuList.value = []
+ spuPropertyList.value = []
+ await nextTick()
+ formRef.value.getElFormRef().resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/seckill/activity/index.vue b/src/views/mall/promotion/seckill/activity/index.vue
new file mode 100644
index 0000000..bffe265
--- /dev/null
+++ b/src/views/mall/promotion/seckill/activity/index.vue
@@ -0,0 +1,256 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戠鏉�娲诲姩" url="https://doc.iocoder.cn/mall/promotion-seckill/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:seckill-activity:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="娲诲姩缂栧彿" prop="id" min-width="80" />
+ <el-table-column label="娲诲姩鍚嶇О" prop="name" min-width="140" />
+ <el-table-column
+ label="绉掓潃鏃舵"
+ prop="configIds"
+ width="220px"
+ :show-overflow-tooltip="false"
+ >
+ <template #default="scope">
+ <el-tag v-for="(configId, index) in scope.row.configIds" :key="index" class="mr-5px">
+ {{ formatConfigNames(configId) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="娲诲姩鏃堕棿" min-width="210">
+ <template #default="scope">
+ {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+ ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍥剧墖" prop="spuName" min-width="80">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ :preview-src-list="[scope.row.picUrl]"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" prop="spuName" min-width="300" />
+ <el-table-column
+ label="鍘熶环"
+ prop="marketPrice"
+ min-width="100"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鍘熶环" prop="marketPrice" min-width="100" />
+ <el-table-column label="绉掓潃浠�" prop="seckillPrice" min-width="100">
+ <template #default="scope">
+ {{ formatSeckillPrice(scope.row.products) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="娲诲姩鐘舵��" align="center" prop="status" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴撳瓨" align="center" prop="stock" min-width="80" />
+ <el-table-column label="鎬诲簱瀛�" align="center" prop="totalStock" min-width="80" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="150px" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['promotion:seckill-activity:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleClose(scope.row.id)"
+ v-if="scope.row.status === 0"
+ v-hasPermi="['promotion:seckill-activity:close']"
+ >
+ 鍏抽棴
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-else
+ v-hasPermi="['promotion:seckill-activity:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SeckillActivityForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { SeckillConfigApi } from '@/api/mall/promotion/seckill/seckillConfig'
+import SeckillActivityForm from './SeckillActivityForm.vue'
+import { formatDate } from '@/utils/formatTime'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'SeckillActivity' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SeckillActivityApi.getSeckillActivityPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍏抽棴鎸夐挳鎿嶄綔 */
+const handleClose = async (id: number) => {
+ try {
+ // 鍏抽棴鐨勪簩娆$‘璁�
+ await message.confirm('纭鍏抽棴璇ョ鏉�娲诲姩鍚楋紵')
+ // 鍙戣捣鍏抽棴
+ await SeckillActivityApi.closeSeckillActivity(id)
+ message.success('鍏抽棴鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SeckillActivityApi.deleteSeckillActivity(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+const configList = ref([]) // 鏃舵閰嶇疆绮剧畝鍒楄〃
+const formatConfigNames = (configId) => {
+ const config = configList.value.find((item) => item.id === configId)
+ return config != null ? `${config.name}[${config.startTime} ~ ${config.endTime}]` : ''
+}
+
+const formatSeckillPrice = (products) => {
+ const seckillPrice = Math.min(...products.map((item) => item.seckillPrice))
+ return `锟�${fenToYuan(seckillPrice)}`
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱绉掓潃鏃堕棿娈�
+ configList.value = await SeckillConfigApi.getSimpleSeckillConfigList()
+})
+</script>
diff --git a/src/views/mall/promotion/seckill/activity/seckillActivity.data.ts b/src/views/mall/promotion/seckill/activity/seckillActivity.data.ts
new file mode 100644
index 0000000..b6e6422
--- /dev/null
+++ b/src/views/mall/promotion/seckill/activity/seckillActivity.data.ts
@@ -0,0 +1,163 @@
+import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
+import { dateFormatter2 } from '@/utils/formatTime'
+import { SeckillConfigApi } from '@/api/mall/promotion/seckill/seckillConfig'
+
+// 琛ㄥ崟鏍¢獙
+export const rules = reactive({
+ spuId: [required],
+ name: [required],
+ startTime: [required],
+ endTime: [required],
+ sort: [required],
+ configIds: [required],
+ totalLimitCount: [required],
+ singleLimitCount: [required],
+ totalStock: [required]
+})
+
+// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
+const crudSchemas = reactive<CrudSchema[]>([
+ {
+ label: '绉掓潃娲诲姩鍚嶇О',
+ field: 'name',
+ isSearch: true,
+ form: {
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩寮�濮嬫椂闂�',
+ field: 'startTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '娲诲姩缁撴潫鏃堕棿',
+ field: 'endTime',
+ formatter: dateFormatter2,
+ isSearch: true,
+ search: {
+ component: 'DatePicker',
+ componentProps: {
+ valueFormat: 'YYYY-MM-DD',
+ type: 'daterange'
+ }
+ },
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'date',
+ valueFormat: 'x'
+ }
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '绉掓潃鏃舵',
+ field: 'configIds',
+ form: {
+ component: 'Select',
+ componentProps: {
+ multiple: true,
+ optionsAlias: {
+ labelField: 'name',
+ valueField: 'id'
+ }
+ },
+ api: SeckillConfigApi.getSimpleSeckillConfigList
+ },
+ table: {
+ width: 300
+ }
+ },
+ {
+ label: '鎬婚檺璐暟閲�',
+ field: 'totalLimitCount',
+ form: {
+ component: 'InputNumber',
+ value: 0
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '鍗曟闄愬鏁伴噺',
+ field: 'singleLimitCount',
+ form: {
+ component: 'InputNumber',
+ value: 0
+ },
+ table: {
+ width: 120
+ }
+ },
+ {
+ label: '鎺掑簭',
+ field: 'sort',
+ form: {
+ component: 'InputNumber',
+ value: 0
+ },
+ table: {
+ width: 80
+ }
+ },
+ {
+ label: '绉掓潃娲诲姩鍟嗗搧',
+ field: 'spuId',
+ isTable: true,
+ isSearch: false,
+ form: {
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 300
+ }
+ },
+ {
+ label: '澶囨敞',
+ field: 'remark',
+ isSearch: false,
+ form: {
+ component: 'Input',
+ componentProps: {
+ type: 'textarea',
+ rows: 4
+ },
+ colProps: {
+ span: 24
+ }
+ },
+ table: {
+ width: 300
+ }
+ }
+])
+export const { allSchemas } = useCrudSchemas(crudSchemas)
diff --git a/src/views/mall/promotion/seckill/components/SeckillShowcase.vue b/src/views/mall/promotion/seckill/components/SeckillShowcase.vue
new file mode 100644
index 0000000..a924e8c
--- /dev/null
+++ b/src/views/mall/promotion/seckill/components/SeckillShowcase.vue
@@ -0,0 +1,156 @@
+<template>
+ <div class="flex flex-wrap items-center gap-8px">
+ <div
+ v-for="(seckillActivity, index) in Activitys"
+ :key="seckillActivity.id"
+ class="select-box spu-pic"
+ >
+ <el-tooltip :content="seckillActivity.name">
+ <div class="relative h-full w-full">
+ <el-image :src="seckillActivity.picUrl" class="h-full w-full" />
+ <Icon
+ v-show="!disabled"
+ class="del-icon"
+ icon="ep:circle-close-filled"
+ @click="handleRemoveActivity(index)"
+ />
+ </div>
+ </el-tooltip>
+ </div>
+ <el-tooltip content="閫夋嫨娲诲姩" v-if="canAdd">
+ <div class="select-box" @click="openSeckillActivityTableSelect">
+ <Icon icon="ep:plus" />
+ </div>
+ </el-tooltip>
+ </div>
+ <!-- 鎷煎洟娲诲姩閫夋嫨瀵硅瘽妗嗭紙琛ㄦ牸褰㈠紡锛� -->
+ <SeckillTableSelect
+ ref="seckillActivityTableSelectRef"
+ :multiple="limit != 1"
+ @change="handleActivitySelected"
+ />
+</template>
+<script lang="ts" setup>
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { propTypes } from '@/utils/propTypes'
+import { oneOfType } from 'vue-types'
+import { isArray } from '@/utils/is'
+import SeckillTableSelect from '@/views/mall/promotion/seckill/components/SeckillTableSelect.vue'
+
+// 娲诲姩姗辩獥锛屼竴鑸敤浜庤淇椂浣跨敤
+// 鎻愪緵鍔熻兘锛氬睍绀烘椿鍔ㄥ垪琛ㄣ�佹坊鍔犳椿鍔ㄣ�佸垹闄ゆ椿鍔�
+defineOptions({ name: 'SeckillShowcase' })
+
+const props = defineProps({
+ modelValue: oneOfType<number | Array<number>>([Number, Array]).isRequired,
+ // 闄愬埗鏁伴噺锛氶粯璁や笉闄愬埗
+ limit: propTypes.number.def(Number.MAX_VALUE),
+ disabled: propTypes.bool.def(false)
+})
+
+// 璁$畻鏄惁鍙互娣诲姞
+const canAdd = computed(() => {
+ // 鎯呭喌涓�锛氱鐢ㄦ椂涓嶅彲浠ユ坊鍔�
+ if (props.disabled) return false
+ // 鎯呭喌浜岋細鏈寚瀹氶檺鍒舵暟閲忔椂锛屽彲浠ユ坊鍔�
+ if (!props.limit) return true
+ // 鎯呭喌涓夛細妫�鏌ュ凡娣诲姞鏁伴噺鏄惁灏忎簬闄愬埗鏁伴噺
+ return Activitys.value.length < props.limit
+})
+
+// 鎷煎洟娲诲姩鍒楄〃
+const Activitys = ref<SeckillActivityApi.SeckillActivityVO[]>([])
+
+watch(
+ () => props.modelValue,
+ async () => {
+ const ids = isArray(props.modelValue)
+ ? // 鎯呭喌涓�锛氬閫�
+ props.modelValue
+ : // 鎯呭喌浜岋細鍗曢��
+ props.modelValue
+ ? [props.modelValue]
+ : []
+ // 涓嶉渶瑕佽繑鏄�
+ if (ids.length === 0) {
+ Activitys.value = []
+ return
+ }
+ // 鍙湁娲诲姩鍙戠敓鍙樺寲涔嬪悗锛屾墠浼氭煡璇㈡椿鍔�
+ if (
+ Activitys.value.length === 0 ||
+ Activitys.value.some((seckillActivity) => !ids.includes(seckillActivity.id!))
+ ) {
+ Activitys.value = await SeckillActivityApi.getSeckillActivityListByIds(ids)
+ }
+ },
+ { immediate: true }
+)
+
+/** 娲诲姩琛ㄦ牸閫夋嫨瀵硅瘽妗� */
+const seckillActivityTableSelectRef = ref()
+// 鎵撳紑瀵硅瘽妗�
+const openSeckillActivityTableSelect = () => {
+ seckillActivityTableSelectRef.value.open(Activitys.value)
+}
+
+/**
+ * 閫夋嫨娲诲姩鍚庤Е鍙�
+ * @param activityVOs 閫変腑鐨勬椿鍔ㄥ垪琛�
+ */
+const handleActivitySelected = (
+ activityVOs: SeckillActivityApi.SeckillActivityVO | SeckillActivityApi.SeckillActivityVO[]
+) => {
+ Activitys.value = isArray(activityVOs) ? activityVOs : [activityVOs]
+ emitActivityChange()
+}
+
+/**
+ * 鍒犻櫎娲诲姩
+ * @param index 娲诲姩绱㈠紩
+ */
+const handleRemoveActivity = (index: number) => {
+ Activitys.value.splice(index, 1)
+ emitActivityChange()
+}
+const emit = defineEmits(['update:modelValue', 'change'])
+const emitActivityChange = () => {
+ if (props.limit === 1) {
+ const seckillActivity = Activitys.value.length > 0 ? Activitys.value[0] : null
+ emit('update:modelValue', seckillActivity?.id || 0)
+ emit('change', seckillActivity)
+ } else {
+ emit(
+ 'update:modelValue',
+ Activitys.value.map((seckillActivity) => seckillActivity.id)
+ )
+ emit('change', Activitys.value)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.select-box {
+ display: flex;
+ width: 60px;
+ height: 60px;
+ border: 1px dashed var(--el-border-color-darker);
+ border-radius: 8px;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+}
+
+.spu-pic {
+ position: relative;
+}
+
+.del-icon {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ z-index: 1;
+ width: 20px !important;
+ height: 20px !important;
+}
+</style>
diff --git a/src/views/mall/promotion/seckill/components/SeckillTableSelect.vue b/src/views/mall/promotion/seckill/components/SeckillTableSelect.vue
new file mode 100644
index 0000000..3e4e67e
--- /dev/null
+++ b/src/views/mall/promotion/seckill/components/SeckillTableSelect.vue
@@ -0,0 +1,343 @@
+<template>
+ <Dialog v-model="dialogVisible" :appendToBody="true" title="閫夋嫨娲诲姩" width="70%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="娲诲姩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ椿鍔ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+ <!-- 1. 澶氶�夋ā寮忥紙涓嶈兘浣跨敤type="selection"锛孍lement浼氬拷鐣eader鎻掓Ы锛� -->
+ <el-table-column width="55" v-if="multiple">
+ <template #header>
+ <el-checkbox
+ v-model="isCheckAll"
+ :indeterminate="isIndeterminate"
+ @change="handleCheckAll"
+ />
+ </template>
+ <template #default="{ row }">
+ <el-checkbox
+ v-model="checkedStatus[row.id]"
+ @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+ />
+ </template>
+ </el-table-column>
+ <!-- 2. 鍗曢�夋ā寮� -->
+ <el-table-column label="#" width="55" v-else>
+ <template #default="{ row }">
+ <el-radio
+ :value="row.id"
+ v-model="selectedActivityId"
+ @change="handleSingleSelected(row)"
+ >
+ <!-- 绌烘牸涓嶈兘鐪佺暐锛屾槸涓轰簡璁╁崟閫夋涓嶆樉绀簂abel锛屽鏋滀笉鎸囧畾label涓嶄細鏈夐�変腑鐨勬晥鏋� -->
+
+ </el-radio>
+ </template>
+ </el-table-column>
+ <el-table-column label="娲诲姩缂栧彿" prop="id" min-width="80" />
+ <el-table-column label="娲诲姩鍚嶇О" prop="name" min-width="140" />
+ <el-table-column label="娲诲姩鏃堕棿" min-width="210">
+ <template #default="scope">
+ {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+ ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍥剧墖" prop="spuName" min-width="80">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.picUrl"
+ class="h-40px w-40px"
+ :preview-src-list="[scope.row.picUrl]"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鏍囬" prop="spuName" min-width="300" />
+ <el-table-column
+ label="鍘熶环"
+ prop="marketPrice"
+ min-width="100"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鎷煎洟浠�" prop="seckillPrice" min-width="100">
+ <template #default="scope">
+ {{ formatSeckillPrice(scope.row.products) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�鍥㈢粍鏁�" prop="groupCount" min-width="100" />
+ <el-table-column label="鎴愬洟缁勬暟" prop="groupSuccessCount" min-width="100" />
+ <el-table-column label="璐拱娆℃暟" prop="recordCount" min-width="100" />
+ <el-table-column label="娲诲姩鐘舵��" align="center" prop="status" min-width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <template #footer v-if="multiple">
+ <el-button type="primary" @click="handleEmitChange">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { handleTree } from '@/utils/tree'
+
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import { propTypes } from '@/utils/propTypes'
+import { CHANGE_EVENT } from 'element-plus'
+import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import { fenToYuan } from '@/utils'
+
+type SeckillActivityVO = Required<SeckillActivityApi.SeckillActivityVO>
+
+/**
+ * 娲诲姩琛ㄦ牸閫夋嫨瀵硅瘽妗�
+ * 1. 鍗曢�夋ā寮忥細
+ * 1.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫崟閫夋鏃讹紝缁撴潫閫夋嫨锛屽苟鍏抽棴瀵硅瘽妗�
+ * 1.2 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ * 2. 澶氶�夋ā寮忥細
+ * 2.1 鐐瑰嚮琛ㄦ牸宸︿晶鐨勫閫夋鏃讹紝璁板綍閫変腑鐨勬椿鍔�
+ * 2.2 鍒囨崲鍒嗛〉鏃讹紝淇濇寔娲诲姩鐨勯�変腑鐘舵��
+ * 2.3 鐐瑰嚮鍙充笅瑙掔殑纭畾鎸夐挳鏃讹紝缁撴潫閫夋嫨锛屽叧闂璇濇
+ * 2.4 鍐嶆鎵撳紑鏃讹紝淇濇寔閫変腑鐘舵��
+ */
+defineOptions({ name: 'SeckillTableSelect' })
+
+defineProps({
+ // 澶氶�夋ā寮�
+ multiple: propTypes.bool.def(false)
+})
+
+// 鍒楄〃鐨勬�婚〉鏁�
+const total = ref(0)
+// 鍒楄〃鐨勬暟鎹�
+const list = ref<SeckillActivityVO[]>([])
+// 鍒楄〃鐨勫姞杞戒腑
+const loading = ref(false)
+// 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogVisible = ref(false)
+// 鏌ヨ鍙傛暟
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: undefined
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = (SeckillList?: SeckillActivityVO[]) => {
+ // 閲嶇疆
+ checkedActivitys.value = []
+ checkedStatus.value = {}
+ isCheckAll.value = false
+ isIndeterminate.value = false
+
+ // 澶勭悊宸查�変腑
+ if (SeckillList && SeckillList.length > 0) {
+ checkedActivitys.value = [...SeckillList]
+ checkedStatus.value = Object.fromEntries(SeckillList.map((activityVO) => [activityVO.id, true]))
+ }
+
+ dialogVisible.value = true
+ resetQuery()
+}
+// 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+defineExpose({ open })
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SeckillActivityApi.getSeckillActivityPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ // checkbox缁戝畾undefined浼氭湁闂锛岄渶瑕佺粰涓�涓猙ool鍊�
+ list.value.forEach(
+ (activityVO) =>
+ (checkedStatus.value[activityVO.id] = checkedStatus.value[activityVO.id] || false)
+ )
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ calculateIsCheckAll()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.value = {
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ createTime: []
+ }
+ getList()
+}
+
+/**
+ * 鏍煎紡鍖栨嫾鍥环鏍�
+ * @param products
+ */
+const formatSeckillPrice = (products) => {
+ const seckillPrice = Math.min(...products.map((item) => item.seckillPrice))
+ return `锟�${fenToYuan(seckillPrice)}`
+}
+
+// 鏄惁鍏ㄩ��
+const isCheckAll = ref(false)
+// 鍏ㄩ�夋鏄惁澶勪簬涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+const isIndeterminate = ref(false)
+// 閫変腑鐨勬椿鍔�
+const checkedActivitys = ref<SeckillActivityVO[]>([])
+// 閫変腑鐘舵�侊細key涓烘椿鍔↖D锛寁alue涓烘槸鍚﹂�変腑
+const checkedStatus = ref<Record<string, boolean>>({})
+
+// 閫変腑鐨勬椿鍔� activityId
+const selectedActivityId = ref()
+/** 鍗曢�変腑鏃惰Е鍙� */
+const handleSingleSelected = (seckillActivityVO: SeckillActivityVO) => {
+ emits(CHANGE_EVENT, seckillActivityVO)
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ // 璁颁綇涓婃閫夋嫨鐨処D
+ selectedActivityId.value = seckillActivityVO.id
+}
+
+/** 澶氶�夊畬鎴� */
+const handleEmitChange = () => {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ emits(CHANGE_EVENT, [...checkedActivitys.value])
+}
+
+/** 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢 */
+const emits = defineEmits<{
+ change: [SeckillActivityApi: SeckillActivityVO | SeckillActivityVO[] | any]
+}>()
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckAll = (checked: boolean) => {
+ isCheckAll.value = checked
+ isIndeterminate.value = false
+
+ list.value.forEach((seckillActivity) => handleCheckOne(checked, seckillActivity, false))
+}
+
+/**
+ * 閫変腑涓�琛�
+ * @param checked 鏄惁閫変腑
+ * @param seckillActivity 娲诲姩
+ * @param isCalcCheckAll 鏄惁璁$畻鍏ㄩ��
+ */
+const handleCheckOne = (
+ checked: boolean,
+ seckillActivity: SeckillActivityVO,
+ isCalcCheckAll: boolean
+) => {
+ if (checked) {
+ checkedActivitys.value.push(seckillActivity)
+ checkedStatus.value[seckillActivity.id] = true
+ } else {
+ const index = findCheckedIndex(seckillActivity)
+ if (index > -1) {
+ checkedActivitys.value.splice(index, 1)
+ checkedStatus.value[seckillActivity.id] = false
+ isCheckAll.value = false
+ }
+ }
+
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ if (isCalcCheckAll) {
+ calculateIsCheckAll()
+ }
+}
+
+// 鏌ユ壘娲诲姩鍦ㄥ凡閫変腑娲诲姩鍒楄〃涓殑绱㈠紩
+const findCheckedIndex = (activityVO: SeckillActivityVO) =>
+ checkedActivitys.value.findIndex((item) => item.id === activityVO.id)
+
+// 璁$畻鍏ㄩ�夋鐘舵��
+const calculateIsCheckAll = () => {
+ isCheckAll.value = list.value.every((activityVO) => checkedStatus.value[activityVO.id])
+ // 璁$畻涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+ isIndeterminate.value =
+ !isCheckAll.value && list.value.some((activityVO) => checkedStatus.value[activityVO.id])
+}
+
+// 鍒嗙被鍒楄〃
+const categoryList = ref()
+// 鍒嗙被鏍�
+const categoryTreeList = ref()
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱鍒嗙被鏍�
+ categoryList.value = await ProductCategoryApi.getCategoryList({})
+ categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId')
+})
+</script>
diff --git a/src/views/mall/promotion/seckill/config/SeckillConfigForm.vue b/src/views/mall/promotion/seckill/config/SeckillConfigForm.vue
new file mode 100644
index 0000000..185b256
--- /dev/null
+++ b/src/views/mall/promotion/seckill/config/SeckillConfigForm.vue
@@ -0,0 +1,133 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="绉掓潃鏃舵鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ鏉�鏃舵鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="寮�濮嬫椂闂寸偣" prop="startTime">
+ <el-time-picker
+ v-model="formData.startTime"
+ value-format="HH:mm:ss"
+ placeholder="閫夋嫨寮�濮嬫椂闂寸偣"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴潫鏃堕棿鐐�" prop="endTime">
+ <el-time-picker
+ v-model="formData.endTime"
+ value-format="HH:mm:ss"
+ placeholder="閫夋嫨缁撴潫鏃堕棿鐐�"
+ />
+ </el-form-item>
+ <el-form-item label="绉掓潃杞挱鍥�" prop="sliderPicUrls">
+ <UploadImgs v-model="formData.sliderPicUrls" placeholder="璇疯緭鍏ョ鏉�杞挱鍥�" />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { SeckillConfigApi, SeckillConfigVO } from '@/api/mall/promotion/seckill/seckillConfig.ts'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** 绉掓潃鏃舵 琛ㄥ崟 */
+defineOptions({ name: 'SeckillConfigForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ startTime: undefined,
+ endTime: undefined,
+ sliderPicUrls: undefined,
+ status: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '绉掓潃鏃舵鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ startTime: [{ required: true, message: '寮�濮嬫椂闂寸偣涓嶈兘涓虹┖', trigger: 'blur' }],
+ endTime: [{ required: true, message: '缁撴潫鏃堕棿鐐逛笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '娲诲姩鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SeckillConfigApi.getSeckillConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ await formRef.value.validate()
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SeckillConfigVO
+ if (formType.value === 'create') {
+ await SeckillConfigApi.createSeckillConfig(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SeckillConfigApi.updateSeckillConfig(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ startTime: undefined,
+ endTime: undefined,
+ sliderPicUrls: [],
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/promotion/seckill/config/index.vue b/src/views/mall/promotion/seckill/config/index.vue
new file mode 100644
index 0000000..9fa2c1e
--- /dev/null
+++ b/src/views/mall/promotion/seckill/config/index.vue
@@ -0,0 +1,211 @@
+<template>
+ <doc-alert title="銆愯惀閿�銆戠鏉�娲诲姩" url="https://doc.iocoder.cn/mall/promotion-seckill/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="108px"
+ >
+ <el-form-item label="绉掓潃鏃舵鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ョ鏉�鏃舵鍚嶇О"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娲诲姩鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨娲诲姩鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['promotion:seckill-config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="绉掓潃鏃舵鍚嶇О" align="center" prop="name" />
+ <el-table-column label="寮�濮嬫椂闂寸偣" align="center" prop="startTime" />
+ <el-table-column label="缁撴潫鏃堕棿鐐�" align="center" prop="endTime" />
+ <el-table-column label="绉掓潃杞挱鍥�" align="center" prop="sliderPicUrls">
+ <template #default="scope">
+ <el-image
+ class="h-40px max-w-40px"
+ v-for="(url, index) in scope?.row.sliderPicUrls"
+ :key="index"
+ :src="url"
+ :preview-src-list="scope?.row.sliderPicUrls"
+ :initial-index="index"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="娲诲姩鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['promotion:seckill-config:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['promotion:seckill-config:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SeckillConfigForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { SeckillConfigApi, SeckillConfigVO } from '@/api/mall/promotion/seckill/seckillConfig.ts'
+import SeckillConfigForm from './SeckillConfigForm.vue'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** 绉掓潃鏃舵 鍒楄〃 */
+defineOptions({ name: 'SeckillConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<SeckillConfigVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SeckillConfigApi.getSeckillConfigPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SeckillConfigApi.deleteSeckillConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 淇敼鐢ㄦ埛鐘舵�� */
+const handleStatusChange = async (row: SeckillConfigVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.status === CommonStatusEnum.ENABLE ? '鍚敤' : '鍋滅敤'
+ await message.confirm('纭瑕�' + text + '"' + row.name + '"娲诲姩鍚�?')
+ // 鍙戣捣淇敼鐘舵��
+ await SeckillConfigApi.updateSeckillConfigStatus(row.id, row.status)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/statistics/member/components/MemberFunnelCard.vue b/src/views/mall/statistics/member/components/MemberFunnelCard.vue
new file mode 100644
index 0000000..609c679
--- /dev/null
+++ b/src/views/mall/statistics/member/components/MemberFunnelCard.vue
@@ -0,0 +1,121 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <div class="my--1.5 flex flex-row items-center justify-between">
+ <CardTitle title="浼氬憳姒傝" />
+ <!-- 鏌ヨ鏉′欢 -->
+ <ShortcutDateRangePicker @change="handleTimeRangeChange" />
+ </div>
+ </template>
+ <div class="min-w-225 py-1.75" v-loading="loading">
+ <div class="relative h-24 flex">
+ <div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%">
+ <div class="ml-15 h-full flex flex-col justify-center">
+ <div class="font-bold">
+ 娉ㄥ唽鐢ㄦ埛鏁伴噺锛歿{ analyseData?.comparison?.value?.registerUserCount || 0 }}
+ </div>
+ <div class="mt-2 text-3.5">
+ 鐜瘮澧為暱鐜囷細{{
+ calculateRelativeRate(
+ analyseData?.comparison?.value?.registerUserCount,
+ analyseData?.comparison?.reference?.registerUserCount
+ )
+ }}%
+ </div>
+ </div>
+ </div>
+ <div
+ class="trapezoid1 ml--38.5 mt-1.5 h-full w-77 flex flex-col items-center justify-center bg-blue-5 text-3.5 text-white"
+ >
+ <span class="text-6 font-bold">{{ analyseData?.visitUserCount || 0 }}</span>
+ <span>璁垮</span>
+ </div>
+ </div>
+ <div class="relative h-24 flex">
+ <div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%">
+ <div class="ml-15 h-full flex flex-col justify-center">
+ <div class="font-bold">
+ 娲昏穬鐢ㄦ埛鏁伴噺锛歿{ analyseData?.comparison?.value?.visitUserCount || 0 }}
+ </div>
+ <div class="mt-2 text-3.5">
+ 鐜瘮澧為暱鐜囷細{{
+ calculateRelativeRate(
+ analyseData?.comparison?.value?.visitUserCount,
+ analyseData?.comparison?.reference?.visitUserCount
+ )
+ }}%
+ </div>
+ </div>
+ </div>
+ <div
+ class="trapezoid2 ml--28 mt-1.7 h-25 w-56 flex flex-col items-center justify-center bg-cyan-5 text-3.5 text-white"
+ >
+ <span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span>
+ <span>涓嬪崟</span>
+ </div>
+ </div>
+ <div class="relative h-24 flex">
+ <div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%">
+ <div class="ml-15 h-full flex flex-row gap-x-16">
+ <div class="flex flex-col justify-center">
+ <div class="font-bold">
+ 鍏呭�肩敤鎴锋暟閲忥細{{ analyseData?.comparison?.value?.rechargeUserCount || 0 }}
+ </div>
+ <div class="mt-2 text-3.5">
+ 鐜瘮澧為暱鐜囷細{{
+ calculateRelativeRate(
+ analyseData?.comparison?.value?.rechargeUserCount,
+ analyseData?.comparison?.reference?.rechargeUserCount
+ )
+ }}%
+ </div>
+ </div>
+ <div class="flex flex-col justify-center">
+ <div class="font-bold">瀹㈠崟浠凤細{{ fenToYuan(analyseData?.atv || 0) }}</div>
+ </div>
+ </div>
+ </div>
+ <div
+ class="trapezoid3 ml--18 mt-3.25 h-23 w-36 flex flex-col items-center justify-center bg-slate-5 text-3.5 text-white"
+ >
+ <span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span>
+ <span>鎴愪氦鐢ㄦ埛</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import dayjs from 'dayjs'
+import { calculateRelativeRate, fenToYuan } from '@/utils'
+import { MemberAnalyseRespVO } from '@/api/mall/statistics/member'
+import { CardTitle } from '@/components/Card'
+
+/** 浼氬憳姒傝鍗$墖 */
+defineOptions({ name: 'MemberFunnelCard' })
+
+const loading = ref(true) // 鍔犺浇涓�
+const analyseData = ref<MemberAnalyseRespVO>() // 浼氬憳鍒嗘瀽鏁版嵁
+
+/** 鏌ヨ浼氬憳姒傝鏁版嵁鍒楄〃 */
+const handleTimeRangeChange = async (times: [dayjs.ConfigType, dayjs.ConfigType]) => {
+ loading.value = true
+ // 鏌ヨ鏁版嵁
+ analyseData.value = await MemberStatisticsApi.getMemberAnalyse({ times })
+ loading.value = false
+}
+</script>
+<style lang="scss" scoped>
+.trapezoid1 {
+ transform: perspective(5em) rotateX(-11deg);
+}
+
+.trapezoid2 {
+ transform: perspective(7em) rotateX(-20deg);
+}
+
+.trapezoid3 {
+ transform: perspective(3em) rotateX(-13deg);
+}
+</style>
diff --git a/src/views/mall/statistics/member/components/MemberTerminalCard.vue b/src/views/mall/statistics/member/components/MemberTerminalCard.vue
new file mode 100644
index 0000000..5451f49
--- /dev/null
+++ b/src/views/mall/statistics/member/components/MemberTerminalCard.vue
@@ -0,0 +1,68 @@
+<template>
+ <el-card shadow="never" v-loading="loading">
+ <template #header>
+ <CardTitle title="浼氬憳缁堢" />
+ </template>
+ <Echart :height="300" :options="terminalChartOptions" />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import { EChartsOption } from 'echarts'
+import { MemberTerminalStatisticsRespVO } from '@/api/mall/statistics/member'
+import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
+import { CardTitle } from '@/components/Card'
+
+/** 浼氬憳缁堢鍗$墖 */
+defineOptions({ name: 'MemberTerminalCard' })
+
+const loading = ref(true) // 鍔犺浇涓�
+
+/** 浼氬憳缁堢缁熻鍥鹃厤缃� */
+const terminalChartOptions = reactive<EChartsOption>({
+ tooltip: {
+ trigger: 'item',
+ confine: true,
+ formatter: '{a} <br/>{b} : {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'right'
+ },
+ series: [
+ {
+ name: '浼氬憳缁堢',
+ type: 'pie',
+ label: {
+ show: false
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鎸夌収缁堢锛屾煡璇細鍛樼粺璁″垪琛� */
+const getMemberTerminalStatisticsList = async () => {
+ loading.value = true
+ const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
+ const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
+ terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+ const userCount = list.find(
+ (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
+ )?.userCount
+ return {
+ name: dictData.label,
+ value: userCount || 0
+ }
+ })
+ loading.value = false
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getMemberTerminalStatisticsList()
+})
+</script>
diff --git a/src/views/mall/statistics/member/index.vue b/src/views/mall/statistics/member/index.vue
new file mode 100644
index 0000000..0e1bbaf
--- /dev/null
+++ b/src/views/mall/statistics/member/index.vue
@@ -0,0 +1,313 @@
+<template>
+ <doc-alert title="銆愮粺璁°�戜細鍛樸�佸晢鍝併�佷氦鏄撶粺璁�" url="https://doc.iocoder.cn/mall/statistics/" />
+
+ <div class="flex flex-col">
+ <el-row :gutter="16" class="summary">
+ <el-col v-loading="loading" :sm="6" :xs="12">
+ <SummaryCard
+ :value="summary?.userCount || 0"
+ icon="fa-solid:users"
+ icon-bg-color="text-blue-500"
+ icon-color="bg-blue-100"
+ title="绱浼氬憳鏁�"
+ />
+ </el-col>
+ <el-col v-loading="loading" :sm="6" :xs="12">
+ <SummaryCard
+ :value="summary?.rechargeUserCount || 0"
+ icon="fa-solid:user"
+ icon-bg-color="text-purple-500"
+ icon-color="bg-purple-100"
+ title="绱鍏呭�间汉鏁�"
+ />
+ </el-col>
+ <el-col v-loading="loading" :sm="6" :xs="12">
+ <SummaryCard
+ :decimals="2"
+ :value="fenToYuan(summary?.rechargePrice || 0)"
+ icon="fa-solid:money-check-alt"
+ icon-bg-color="text-yellow-500"
+ icon-color="bg-yellow-100"
+ prefix="锟�"
+ title="绱鍏呭�奸噾棰�"
+ />
+ </el-col>
+ <el-col v-loading="loading" :sm="6" :xs="12">
+ <SummaryCard
+ :decimals="2"
+ :value="fenToYuan(summary?.expensePrice || 0)"
+ icon="fa-solid:yen-sign"
+ icon-bg-color="text-green-500"
+ icon-color="bg-green-100"
+ prefix="锟�"
+ title="绱娑堣垂閲戦"
+ />
+ </el-col>
+ </el-row>
+ <el-row :gutter="16" class="mb-4">
+ <el-col :md="18" :sm="24">
+ <!-- 浼氬憳姒傝 -->
+ <MemberFunnelCard />
+ </el-col>
+ <el-col :md="6" :sm="24">
+ <!-- 浼氬憳缁堢 -->
+ <MemberTerminalCard />
+ </el-col>
+ </el-row>
+ <el-row :gutter="16">
+ <el-col :md="18" :sm="24">
+ <el-card shadow="never">
+ <template #header>
+ <CardTitle title="浼氬憳鍦板煙鍒嗗竷" />
+ </template>
+ <el-row v-loading="loading">
+ <el-col :span="10">
+ <Echart :height="300" :options="areaChartOptions" />
+ </el-col>
+ <el-col :span="14">
+ <el-table :data="areaStatisticsList" :height="300">
+ <el-table-column
+ :sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
+ align="center"
+ label="鐪佷唤"
+ min-width="80"
+ prop="areaName"
+ show-overflow-tooltip
+ sortable
+ />
+ <el-table-column
+ align="center"
+ label="浼氬憳鏁伴噺"
+ min-width="105"
+ prop="userCount"
+ sortable
+ />
+ <el-table-column
+ align="center"
+ label="璁㈠崟鍒涘缓鏁伴噺"
+ min-width="135"
+ prop="orderCreateUserCount"
+ sortable
+ />
+ <el-table-column
+ align="center"
+ label="璁㈠崟鏀粯鏁伴噺"
+ min-width="135"
+ prop="orderPayUserCount"
+ sortable
+ />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ align="center"
+ label="璁㈠崟鏀粯閲戦"
+ min-width="135"
+ prop="orderPayPrice"
+ sortable
+ />
+ </el-table>
+ </el-col>
+ </el-row>
+ </el-card>
+ </el-col>
+ <el-col :md="6" :sm="24">
+ <el-card v-loading="loading" shadow="never">
+ <template #header>
+ <CardTitle title="浼氬憳鎬у埆姣斾緥" />
+ </template>
+ <Echart :height="300" :options="sexChartOptions" />
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+<script lang="ts" setup>
+import * as MemberStatisticsApi from '@/api/mall/statistics/member'
+import {
+ MemberAreaStatisticsRespVO,
+ MemberSexStatisticsRespVO,
+ MemberSummaryRespVO,
+ MemberTerminalStatisticsRespVO
+} from '@/api/mall/statistics/member'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import china from '@/assets/map/json/china.json'
+import { areaReplace, fenToYuan } from '@/utils'
+import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
+import echarts from '@/plugins/echarts'
+import { fenToYuanFormat } from '@/utils/formatter'
+import MemberFunnelCard from './components/MemberFunnelCard.vue'
+import MemberTerminalCard from './components/MemberTerminalCard.vue'
+import { CardTitle } from '@/components/Card'
+
+/** 浼氬憳缁熻 */
+defineOptions({ name: 'MemberStatistics' })
+
+const loading = ref(true) // 鍔犺浇涓�
+const summary = ref<MemberSummaryRespVO>() // 浼氬憳缁熻鏁版嵁
+const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 鐪佷唤浼氬憳缁熻
+
+// 娉ㄥ唽鍦板浘
+echarts?.registerMap('china', china as any)
+
+/** 浼氬憳缁堢缁熻鍥鹃厤缃� */
+const terminalChartOptions = reactive<EChartsOption>({
+ tooltip: {
+ trigger: 'item',
+ confine: true,
+ formatter: '{a} <br/>{b} : {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'right'
+ },
+ roseType: 'area',
+ series: [
+ {
+ name: '浼氬憳缁堢',
+ type: 'pie',
+ label: {
+ show: false
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 浼氬憳鎬у埆缁熻鍥鹃厤缃� */
+const sexChartOptions = reactive<EChartsOption>({
+ tooltip: {
+ trigger: 'item',
+ confine: true,
+ formatter: '{a} <br/>{b} : {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'right'
+ },
+ roseType: 'area',
+ series: [
+ {
+ name: '浼氬憳鎬у埆',
+ type: 'pie',
+ label: {
+ show: false
+ },
+ labelLine: {
+ show: false
+ },
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+const areaChartOptions = reactive<EChartsOption>({
+ tooltip: {
+ trigger: 'item',
+ formatter: (params: any) => {
+ return `${params?.data?.areaName || params?.name}<br/>
+浼氬憳鏁伴噺锛�${params?.data?.userCount || 0}<br/>
+璁㈠崟鍒涘缓鏁伴噺锛�${params?.data?.orderCreateUserCount || 0}<br/>
+璁㈠崟鏀粯鏁伴噺锛�${params?.data?.orderPayUserCount || 0}<br/>
+璁㈠崟鏀粯閲戦锛�${fenToYuan(params?.data?.orderPayPrice || 0)}`
+ }
+ },
+ visualMap: {
+ text: ['楂�', '浣�'],
+ realtime: false,
+ calculable: true,
+ top: 'middle',
+ inRange: {
+ color: ['#fff', '#3b82f6']
+ }
+ },
+ series: [
+ {
+ name: '浼氬憳鍦板煙鍒嗗竷',
+ type: 'map',
+ map: 'china',
+ roam: false,
+ selectedMode: false,
+ data: []
+ }
+ ]
+}) as EChartsOption
+
+/** 鏌ヨ浼氬憳缁熻 */
+const getMemberSummary = async () => {
+ summary.value = await MemberStatisticsApi.getMemberSummary()
+}
+
+/** 鎸夌収鐪佷唤锛屾煡璇細鍛樼粺璁″垪琛� */
+const getMemberAreaStatisticsList = async () => {
+ const list = await MemberStatisticsApi.getMemberAreaStatisticsList()
+ areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
+ return {
+ ...item,
+ areaName: areaReplace(item.areaName)
+ }
+ })
+ let min = 0
+ let max = 0
+ areaChartOptions.series![0].data = areaStatisticsList.value.map((item) => {
+ min = Math.min(min, item.orderPayUserCount || 0)
+ max = Math.max(max, item.orderPayUserCount || 0)
+ return { ...item, name: item.areaName, value: item.orderPayUserCount || 0 }
+ })
+ areaChartOptions.visualMap!['min'] = min
+ areaChartOptions.visualMap!['max'] = max
+}
+
+/** 鎸夌収鎬у埆锛屾煡璇細鍛樼粺璁″垪琛� */
+const getMemberSexStatisticsList = async () => {
+ const list = await MemberStatisticsApi.getMemberSexStatisticsList()
+ const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
+ dictDataList.push({ label: '鏈煡', value: null } as any)
+ sexChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+ const userCount = list.find(
+ (item: MemberSexStatisticsRespVO) => item.sex === dictData.value
+ )?.userCount
+ return {
+ name: dictData.label,
+ value: userCount || 0
+ }
+ })
+}
+
+/** 鎸夌収缁堢锛屾煡璇細鍛樼粺璁″垪琛� */
+const getMemberTerminalStatisticsList = async () => {
+ const list = await MemberStatisticsApi.getMemberTerminalStatisticsList()
+ const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
+ dictDataList.push({ label: '鏈煡', value: null } as any)
+ terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+ const userCount = list.find(
+ (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
+ )?.userCount
+ return {
+ name: dictData.label,
+ value: userCount || 0
+ }
+ })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ loading.value = true
+ await Promise.all([
+ getMemberSummary(),
+ getMemberTerminalStatisticsList(),
+ getMemberAreaStatisticsList(),
+ getMemberSexStatisticsList()
+ ])
+ loading.value = false
+})
+</script>
+<style lang="scss" scoped>
+.summary {
+ .el-col {
+ margin-bottom: 1rem;
+ }
+}
+</style>
diff --git a/src/views/mall/statistics/product/components/ProductRank.vue b/src/views/mall/statistics/product/components/ProductRank.vue
new file mode 100644
index 0000000..d86ecfc
--- /dev/null
+++ b/src/views/mall/statistics/product/components/ProductRank.vue
@@ -0,0 +1,108 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <!-- 鏍囬 -->
+ <div class="flex flex-row items-center justify-between">
+ <CardTitle title="鍟嗗搧鎺掕" />
+ <!-- 鏌ヨ鏉′欢 -->
+ <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="handleDateRangeChange" />
+ </div>
+ </template>
+ <!-- 鎺掕鍒楄〃 -->
+ <el-table v-loading="loading" :data="list" @sort-change="handleSortChange">
+ <el-table-column label="鍟嗗搧 ID" prop="spuId" min-width="70" />
+ <el-table-column label="鍟嗗搧鍥剧墖" align="center" prop="picUrl" width="80">
+ <template #default="{ row }">
+ <el-image
+ :src="row.picUrl"
+ :preview-src-list="[row.picUrl]"
+ class="h-30px w-30px"
+ preview-teleported
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍚嶇О" prop="name" min-width="200" :show-overflow-tooltip="true" />
+ <el-table-column label="娴忚閲�" prop="browseCount" min-width="90" sortable="custom" />
+ <el-table-column label="璁垮鏁�" prop="browseUserCount" min-width="90" sortable="custom" />
+ <el-table-column label="鍔犺喘浠舵暟" prop="cartCount" min-width="105" sortable="custom" />
+ <el-table-column label="涓嬪崟浠舵暟" prop="orderCount" min-width="105" sortable="custom" />
+ <el-table-column label="鏀粯浠舵暟" prop="orderPayCount" min-width="105" sortable="custom" />
+ <el-table-column
+ label="鏀粯閲戦"
+ prop="orderPayPrice"
+ min-width="105"
+ sortable="custom"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鏀惰棌鏁�" prop="favoriteCount" min-width="90" sortable="custom" />
+ <el-table-column
+ label="璁垮-鏀粯杞寲鐜�(%)"
+ prop="browseConvertPercent"
+ min-width="180"
+ sortable="custom"
+ :formatter="formatConvertRate"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getSpuList"
+ />
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
+import { CardTitle } from '@/components/Card'
+import { buildSortingField } from '@/utils'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+/** 鍟嗗搧鎺掕 */
+defineOptions({ name: 'ProductRank' })
+
+// 鏍煎紡鍖栵細璁垮-鏀粯杞寲鐜�
+const formatConvertRate = (row: ProductStatisticsVO) => {
+ return `${row.browseConvertPercent}%`
+}
+
+const handleSortChange = (params: any) => {
+ queryParams.sortingFields = [buildSortingField(params)]
+ getSpuList()
+}
+
+const handleDateRangeChange = (times: any[]) => {
+ queryParams.times = times as []
+ getSpuList()
+}
+
+const shortcutDateRangePicker = ref()
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ times: [],
+ sortingFields: {}
+})
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<ProductStatisticsVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌ヨ鍟嗗搧鍒楄〃 */
+const getSpuList = async () => {
+ loading.value = true
+ try {
+ const data = await ProductStatisticsApi.getProductStatisticsRankPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getSpuList()
+})
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/mall/statistics/product/components/ProductSummary.vue b/src/views/mall/statistics/product/components/ProductSummary.vue
new file mode 100644
index 0000000..0669223
--- /dev/null
+++ b/src/views/mall/statistics/product/components/ProductSummary.vue
@@ -0,0 +1,304 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <!-- 鏍囬 -->
+ <div class="flex flex-row items-center justify-between">
+ <CardTitle title="鍟嗗搧姒傚喌" />
+ <!-- 鏌ヨ鏉′欢 -->
+ <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getProductTrendData">
+ <el-button
+ class="ml-4"
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['statistics:product:export']"
+ >
+ <Icon icon="ep:download" class="mr-1" />瀵煎嚭
+ </el-button>
+ </ShortcutDateRangePicker>
+ </div>
+ </template>
+ <!-- 缁熻鍊� -->
+ <el-row :gutter="16">
+ <el-col :xl="4" :md="8" :sm="24">
+ <SummaryCard
+ title="鍟嗗搧娴忚閲�"
+ tooltip="鍦ㄩ�夊畾鏉′欢涓嬶紝鎵�鏈夊晢鍝佽鎯呴〉琚闂殑娆℃暟锛屼竴涓汉鍦ㄧ粺璁℃椂闂村唴璁块棶澶氭璁颁负澶氭"
+ icon="ep:view"
+ icon-color="bg-blue-100"
+ icon-bg-color="text-blue-500"
+ prefix=""
+ :decimals="0"
+ :value="trendSummary?.value?.browseCount || 0"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.browseCount,
+ trendSummary?.reference?.browseCount
+ )
+ "
+ />
+ </el-col>
+ <el-col :xl="4" :md="8" :sm="24">
+ <SummaryCard
+ title="鍟嗗搧璁垮鏁�"
+ tooltip="鍦ㄩ�夊畾鏉′欢涓嬶紝璁块棶浠讳綍鍟嗗搧璇︽儏椤电殑浜烘暟锛屼竴涓汉鍦ㄧ粺璁℃椂闂磋寖鍥村唴璁块棶澶氭鍙涓轰竴涓�"
+ icon="ep:user-filled"
+ icon-color="bg-purple-100"
+ icon-bg-color="text-purple-500"
+ prefix=""
+ :decimals="0"
+ :value="trendSummary?.value?.browseUserCount || 0"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.browseUserCount,
+ trendSummary?.reference?.browseUserCount
+ )
+ "
+ />
+ </el-col>
+ <el-col :xl="4" :md="8" :sm="24">
+ <SummaryCard
+ title="鏀粯浠舵暟"
+ tooltip="鍦ㄩ�夊畾鏉′欢涓嬶紝鎴愬姛浠樻璁㈠崟鐨勫晢鍝佷欢鏁颁箣鍜�"
+ icon="fa-solid:money-check-alt"
+ icon-color="bg-yellow-100"
+ icon-bg-color="text-yellow-500"
+ prefix=""
+ :decimals="0"
+ :value="trendSummary?.value?.orderPayCount || 0"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.orderPayCount,
+ trendSummary?.reference?.orderPayCount
+ )
+ "
+ />
+ </el-col>
+ <el-col :xl="4" :md="8" :sm="24">
+ <SummaryCard
+ title="鏀粯閲戦"
+ tooltip="鍦ㄩ�夊畾鏉′欢涓嬶紝鎴愬姛浠樻璁㈠崟鐨勫晢鍝侀噾棰濅箣鍜�"
+ icon="ep:warning-filled"
+ icon-color="bg-green-100"
+ icon-bg-color="text-green-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.orderPayPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.orderPayPrice,
+ trendSummary?.reference?.orderPayPrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :xl="4" :md="8" :sm="24">
+ <SummaryCard
+ title="閫�娆句欢鏁�"
+ tooltip="鍦ㄩ�夊畾鏉′欢涓嬶紝鎴愬姛閫�娆剧殑鍟嗗搧浠舵暟涔嬪拰"
+ icon="fa-solid:wallet"
+ icon-color="bg-cyan-100"
+ icon-bg-color="text-cyan-500"
+ prefix=""
+ :decimals="0"
+ :value="trendSummary?.value?.afterSaleCount || 0"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.afterSaleCount,
+ trendSummary?.reference?.afterSaleCount
+ )
+ "
+ />
+ </el-col>
+ <el-col :xl="4" :md="8" :sm="24">
+ <SummaryCard
+ title="閫�娆鹃噾棰�"
+ tooltip="鍦ㄩ�夊畾鏉′欢涓嬶紝鎴愬姛閫�娆剧殑鍟嗗搧閲戦涔嬪拰"
+ icon="fa-solid:award"
+ icon-color="bg-yellow-100"
+ icon-bg-color="text-yellow-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.afterSaleRefundPrice,
+ trendSummary?.reference?.afterSaleRefundPrice
+ )
+ "
+ />
+ </el-col>
+ </el-row>
+ <!-- 鎶樼嚎鍥� -->
+ <el-skeleton :loading="trendLoading" animated>
+ <Echart :height="500" :options="lineChartOptions" />
+ </el-skeleton>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { ProductStatisticsApi, ProductStatisticsVO } from '@/api/mall/statistics/product'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+import { calculateRelativeRate, fenToYuan } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 鍟嗗搧姒傚喌 */
+defineOptions({ name: 'ProductSummary' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const trendLoading = ref(true) // 鍟嗗搧鐘舵�佸姞杞戒腑
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const trendSummary = ref<DataComparisonRespVO<ProductStatisticsVO>>() // 鍟嗗搧鐘跺喌缁熻鏁版嵁
+const shortcutDateRangePicker = ref()
+
+/** 鎶樼嚎鍥鹃厤缃� */
+const lineChartOptions = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['time', 'browseCount', 'browseUserCount', 'orderPayPrice', 'afterSaleRefundPrice'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ { name: '鍟嗗搧娴忚閲�', type: 'line', smooth: true, itemStyle: { color: '#B37FEB' } },
+ { name: '鍟嗗搧璁垮鏁�', type: 'line', smooth: true, itemStyle: { color: '#FFAB2B' } },
+ { name: '鏀粯閲戦', type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#1890FF' } },
+ { name: '閫�娆鹃噾棰�', type: 'bar', smooth: true, yAxisIndex: 1, itemStyle: { color: '#00C050' } }
+ ],
+ toolbox: {
+ feature: {
+ // 鏁版嵁鍖哄煙缂╂斁
+ dataZoom: {
+ yAxisIndex: false // Y杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '鍟嗗搧鐘跺喌' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ },
+ padding: [5, 10]
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: true,
+ axisTick: {
+ show: false
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '閲戦',
+ axisLine: {
+ show: false
+ },
+ axisTick: {
+ show: false
+ },
+ axisLabel: {
+ textStyle: {
+ color: '#7F8B9C'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#F5F7F9'
+ }
+ }
+ },
+ {
+ type: 'value',
+ name: '鏁伴噺',
+ axisLine: {
+ show: false
+ },
+ axisTick: {
+ show: false
+ },
+ axisLabel: {
+ textStyle: {
+ color: '#7F8B9C'
+ }
+ },
+ splitLine: {
+ show: true,
+ lineStyle: {
+ color: '#F5F7F9'
+ }
+ }
+ }
+ ]
+}) as EChartsOption
+
+/** 澶勭悊鍟嗗搧鐘跺喌鏌ヨ */
+const getProductTrendData = async () => {
+ trendLoading.value = true
+ // 1. 澶勭悊鏃堕棿: 寮�濮嬩笌鎴鍦ㄥ悓涓�澶╃殑, 鎶樼嚎鍥惧嚭涓嶆潵, 闇�瑕佸欢闀夸竴澶�
+ const times = shortcutDateRangePicker.value.times
+ if (DateUtil.isSameDay(times[0], times[1])) {
+ // 鍓嶅ぉ
+ times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+ }
+ // 鏌ヨ鏁版嵁
+ await Promise.all([getProductTrendSummary(), getProductStatisticsList()])
+ trendLoading.value = false
+}
+
+/** 鏌ヨ鍟嗗搧鐘跺喌鏁版嵁缁熻 */
+const getProductTrendSummary = async () => {
+ const times = shortcutDateRangePicker.value.times
+ trendSummary.value = await ProductStatisticsApi.getProductStatisticsAnalyse({ times })
+}
+
+/** 鏌ヨ鍟嗗搧鐘跺喌鏁版嵁鍒楄〃 */
+const getProductStatisticsList = async () => {
+ // 鏌ヨ鏁版嵁
+ const times = shortcutDateRangePicker.value.times
+ const list: ProductStatisticsVO[] = await ProductStatisticsApi.getProductStatisticsList({ times })
+ // 澶勭悊鏁版嵁
+ for (let item of list) {
+ item.orderPayPrice = fenToYuan(item.orderPayPrice)
+ item.afterSaleRefundPrice = fenToYuan(item.afterSaleRefundPrice)
+ }
+ // 鏇存柊 Echarts 鏁版嵁
+ if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+ lineChartOptions.dataset['source'] = list
+ }
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const times = shortcutDateRangePicker.value.times
+ const data = await ProductStatisticsApi.exportProductStatisticsExcel({ times })
+ download.excel(data, '鍟嗗搧鐘跺喌.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/mall/statistics/product/index.vue b/src/views/mall/statistics/product/index.vue
new file mode 100644
index 0000000..d1bcba6
--- /dev/null
+++ b/src/views/mall/statistics/product/index.vue
@@ -0,0 +1,16 @@
+<template>
+ <doc-alert title="銆愮粺璁°�戜細鍛樸�佸晢鍝併�佷氦鏄撶粺璁�" url="https://doc.iocoder.cn/mall/statistics/" />
+
+ <!-- 鍟嗗搧姒傝 -->
+ <ProductSummary />
+ <!-- 鍟嗗搧鎺掕 -->
+ <ProductRank class="mt-16px" />
+</template>
+<script lang="ts" setup>
+import ProductSummary from './components/ProductSummary.vue'
+import ProductRank from './components/ProductRank.vue'
+
+/** 鍟嗗搧缁熻 */
+defineOptions({ name: 'ProductStatistics' })
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/mall/statistics/trade/components/TradeStatisticValue.vue b/src/views/mall/statistics/trade/components/TradeStatisticValue.vue
new file mode 100644
index 0000000..77b8822
--- /dev/null
+++ b/src/views/mall/statistics/trade/components/TradeStatisticValue.vue
@@ -0,0 +1,36 @@
+<template>
+ <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+ <div class="flex items-center justify-between text-gray-500">
+ <span>{{ title }}</span>
+ <el-tooltip :content="tooltip" placement="top-start" v-if="tooltip">
+ <Icon icon="ep:warning" />
+ </el-tooltip>
+ </div>
+ <div class="mb-4 text-3xl">
+ <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" />
+ </div>
+ <div class="flex flex-row gap-1 text-sm">
+ <span class="text-gray-500">鐜瘮</span>
+ <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
+ {{ Math.abs(toNumber(percent)) }}%
+ <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
+ </span>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+
+/** 浜ゆ槗缁熻鍊肩粍浠� */
+defineOptions({ name: 'TradeStatisticValue' })
+
+defineProps({
+ tooltip: propTypes.string.def(''),
+ title: propTypes.string.def(''),
+ prefix: propTypes.string.def(''),
+ value: propTypes.number.def(0),
+ decimals: propTypes.number.def(0),
+ percent: propTypes.oneOfType([Number, String]).def(0)
+})
+</script>
diff --git a/src/views/mall/statistics/trade/index.vue b/src/views/mall/statistics/trade/index.vue
new file mode 100644
index 0000000..0a25fd7
--- /dev/null
+++ b/src/views/mall/statistics/trade/index.vue
@@ -0,0 +1,363 @@
+<template>
+ <doc-alert title="銆愮粺璁°�戜細鍛樸�佸晢鍝併�佷氦鏄撶粺璁�" url="https://doc.iocoder.cn/mall/statistics/" />
+
+ <div class="flex flex-col">
+ <el-row :gutter="16" class="summary">
+ <el-col :sm="6" :xs="12">
+ <TradeStatisticValue
+ tooltip="鏄ㄦ棩璁㈠崟鏁伴噺"
+ title="鏄ㄦ棩璁㈠崟鏁伴噺"
+ :value="summary?.value?.yesterdayOrderCount || 0"
+ :percent="
+ calculateRelativeRate(
+ summary?.value?.yesterdayOrderCount,
+ summary?.reference?.yesterdayOrderCount
+ )
+ "
+ />
+ </el-col>
+ <el-col :sm="6" :xs="12">
+ <TradeStatisticValue
+ tooltip="鏈湀璁㈠崟鏁伴噺"
+ title="鏈湀璁㈠崟鏁伴噺"
+ :value="summary?.value?.monthOrderCount || 0"
+ :percent="
+ calculateRelativeRate(
+ summary?.value?.monthOrderCount,
+ summary?.reference?.monthOrderCount
+ )
+ "
+ />
+ </el-col>
+ <el-col :sm="6" :xs="12">
+ <TradeStatisticValue
+ tooltip="鏄ㄦ棩鏀粯閲戦"
+ title="鏄ㄦ棩鏀粯閲戦"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(summary?.value?.yesterdayPayPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ summary?.value?.yesterdayPayPrice,
+ summary?.reference?.yesterdayPayPrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :sm="6" :xs="12">
+ <TradeStatisticValue
+ tooltip="鏈湀鏀粯閲戦"
+ title="鏈湀鏀粯閲戦"
+ prefix="锟�"
+ ::decimals="2"
+ :value="fenToYuan(summary?.value?.monthPayPrice || 0)"
+ :percent="
+ calculateRelativeRate(summary?.value?.monthPayPrice, summary?.reference?.monthPayPrice)
+ "
+ />
+ </el-col>
+ </el-row>
+ <el-card shadow="never">
+ <template #header>
+ <!-- 鏍囬 -->
+ <div class="flex flex-row items-center justify-between">
+ <CardTitle title="浜ゆ槗鐘跺喌" />
+ <!-- 鏌ヨ鏉′欢 -->
+ <ShortcutDateRangePicker ref="shortcutDateRangePicker" @change="getTradeTrendData">
+ <el-button
+ class="ml-4"
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['statistics:trade:export']"
+ >
+ <Icon icon="ep:download" class="mr-1" />瀵煎嚭
+ </el-button>
+ </ShortcutDateRangePicker>
+ </div>
+ </template>
+ <!-- 缁熻鍊� -->
+ <el-row :gutter="16">
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="钀ヤ笟棰�"
+ tooltip="鍟嗗搧鏀粯閲戦銆佸厖鍊奸噾棰�"
+ icon="fa-solid:yen-sign"
+ icon-color="bg-blue-100"
+ icon-bg-color="text-blue-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.turnoverPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.turnoverPrice,
+ trendSummary?.reference?.turnoverPrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="鍟嗗搧鏀粯閲戦"
+ tooltip="鐢ㄦ埛璐拱鍟嗗搧鐨勫疄闄呮敮浠橀噾棰濓紝鍖呮嫭寰俊鏀粯銆佷綑棰濇敮浠樸�佹敮浠樺疂鏀粯銆佺嚎涓嬫敮浠橀噾棰濓紙鎷煎洟鍟嗗搧鍦ㄦ垚鍥箣鍚庤鍏ワ紝绾夸笅鏀粯璁㈠崟鍦ㄥ悗鍙扮‘璁ゆ敮浠樺悗璁″叆锛�"
+ icon="fa-solid:shopping-cart"
+ icon-color="bg-purple-100"
+ icon-bg-color="text-purple-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.orderPayPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.orderPayPrice,
+ trendSummary?.reference?.orderPayPrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="鍏呭�奸噾棰�"
+ tooltip="鐢ㄦ埛鎴愬姛鍏呭�肩殑閲戦"
+ icon="fa-solid:money-check-alt"
+ icon-color="bg-yellow-100"
+ icon-bg-color="text-yellow-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.rechargePrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.rechargePrice,
+ trendSummary?.reference?.rechargePrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="鏀嚭閲戦"
+ tooltip="浣欓鏀粯閲戦銆佹敮浠樹剑閲戦噾棰濄�佸晢鍝侀��娆鹃噾棰�"
+ icon="ep:warning-filled"
+ icon-color="bg-green-100"
+ icon-bg-color="text-green-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.expensePrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.expensePrice,
+ trendSummary?.reference?.expensePrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="浣欓鏀粯閲戦"
+ tooltip="鐢ㄦ埛涓嬪崟鏃朵娇鐢ㄤ綑棰濆疄闄呮敮浠樼殑閲戦"
+ icon="fa-solid:wallet"
+ icon-color="bg-cyan-100"
+ icon-bg-color="text-cyan-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.walletPayPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.walletPayPrice,
+ trendSummary?.reference?.walletPayPrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="鏀粯浣i噾閲戦"
+ tooltip="鍚庡彴缁欐帹骞垮憳鏀粯鐨勬帹骞夸剑閲戯紝浠ュ疄闄呮敮浠樹负鍑�"
+ icon="fa-solid:award"
+ icon-color="bg-yellow-100"
+ icon-bg-color="text-yellow-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.brokerageSettlementPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.brokerageSettlementPrice,
+ trendSummary?.reference?.brokerageSettlementPrice
+ )
+ "
+ />
+ </el-col>
+ <el-col :md="6" :sm="12" :xs="24">
+ <SummaryCard
+ title="鍟嗗搧閫�娆鹃噾棰�"
+ tooltip="鐢ㄦ埛鎴愬姛閫�娆剧殑鍟嗗搧閲戦"
+ icon="fa-solid:times-circle"
+ icon-color="bg-blue-100"
+ icon-bg-color="text-blue-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(trendSummary?.value?.afterSaleRefundPrice || 0)"
+ :percent="
+ calculateRelativeRate(
+ trendSummary?.value?.afterSaleRefundPrice,
+ trendSummary?.reference?.afterSaleRefundPrice
+ )
+ "
+ />
+ </el-col>
+ </el-row>
+ <!-- 鎶樼嚎鍥� -->
+ <el-skeleton :loading="trendLoading" animated>
+ <Echart :height="500" :options="lineChartOptions" />
+ </el-skeleton>
+ </el-card>
+ </div>
+</template>
+<script lang="ts" setup>
+import * as TradeStatisticsApi from '@/api/mall/statistics/trade'
+import TradeStatisticValue from './components/TradeStatisticValue.vue'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { EChartsOption } from 'echarts'
+import { DataComparisonRespVO } from '@/api/mall/statistics/common'
+import { TradeSummaryRespVO, TradeTrendSummaryRespVO } from '@/api/mall/statistics/trade'
+import { calculateRelativeRate, fenToYuan } from '@/utils'
+import download from '@/utils/download'
+import { CardTitle } from '@/components/Card'
+import * as DateUtil from '@/utils/formatTime'
+import dayjs from 'dayjs'
+
+/** 浜ゆ槗缁熻 */
+defineOptions({ name: 'TradeStatistics' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const trendLoading = ref(true) // 浜ゆ槗鐘舵�佸姞杞戒腑
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const summary = ref<DataComparisonRespVO<TradeSummaryRespVO>>() // 浜ゆ槗缁熻鏁版嵁
+const trendSummary = ref<DataComparisonRespVO<TradeTrendSummaryRespVO>>() // 浜ゆ槗鐘跺喌缁熻鏁版嵁
+const shortcutDateRangePicker = ref()
+
+/** 鎶樼嚎鍥鹃厤缃� */
+const lineChartOptions = reactive<EChartsOption>({
+ dataset: {
+ dimensions: ['date', 'turnoverPrice', 'orderPayPrice', 'rechargePrice', 'expensePrice'],
+ source: []
+ },
+ grid: {
+ left: 20,
+ right: 20,
+ bottom: 20,
+ top: 80,
+ containLabel: true
+ },
+ legend: {
+ top: 50
+ },
+ series: [
+ { name: '钀ヤ笟棰�', type: 'line', smooth: true },
+ { name: '鍟嗗搧鏀粯閲戦', type: 'line', smooth: true },
+ { name: '鍏呭�奸噾棰�', type: 'line', smooth: true },
+ { name: '鏀嚭閲戦', type: 'line', smooth: true }
+ ],
+ toolbox: {
+ feature: {
+ // 鏁版嵁鍖哄煙缂╂斁
+ dataZoom: {
+ yAxisIndex: false // Y杞翠笉缂╂斁
+ },
+ brush: {
+ type: ['lineX', 'clear'] // 鍖哄煙缂╂斁鎸夐挳銆佽繕鍘熸寜閽�
+ },
+ saveAsImage: { show: true, name: '浜ゆ槗鐘跺喌' } // 淇濆瓨涓哄浘鐗�
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ },
+ padding: [5, 10]
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ axisTick: {
+ show: false
+ }
+ },
+ yAxis: {
+ axisTick: {
+ show: false
+ }
+ }
+}) as EChartsOption
+
+/** 澶勭悊浜ゆ槗鐘跺喌鏌ヨ */
+const getTradeTrendData = async () => {
+ trendLoading.value = true
+ // 1. 澶勭悊鏃堕棿: 寮�濮嬩笌鎴鍦ㄥ悓涓�澶╃殑, 鎶樼嚎鍥惧嚭涓嶆潵, 闇�瑕佸欢闀夸竴澶�
+ const times = shortcutDateRangePicker.value.times
+ if (DateUtil.isSameDay(times[0], times[1])) {
+ // 鍓嶅ぉ
+ times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+ }
+ // 鏌ヨ鏁版嵁
+ await Promise.all([getTradeStatisticsAnalyse(), getTradeStatisticsList()])
+ trendLoading.value = false
+}
+
+/** 鏌ヨ浜ゆ槗缁熻 */
+const getTradeStatisticsSummary = async () => {
+ summary.value = await TradeStatisticsApi.getTradeStatisticsSummary()
+}
+
+/** 鏌ヨ浜ゆ槗鐘跺喌鏁版嵁缁熻 */
+const getTradeStatisticsAnalyse = async () => {
+ const times = shortcutDateRangePicker.value.times
+ trendSummary.value = await TradeStatisticsApi.getTradeStatisticsAnalyse({ times })
+}
+
+/** 鏌ヨ浜ゆ槗鐘跺喌鏁版嵁鍒楄〃 */
+const getTradeStatisticsList = async () => {
+ // 鏌ヨ鏁版嵁
+ const times = shortcutDateRangePicker.value.times
+ const list = await TradeStatisticsApi.getTradeStatisticsList({ times })
+ // 澶勭悊鏁版嵁
+ for (let item of list) {
+ item.turnoverPrice = fenToYuan(item.turnoverPrice)
+ item.orderPayPrice = fenToYuan(item.orderPayPrice)
+ item.rechargePrice = fenToYuan(item.rechargePrice)
+ item.expensePrice = fenToYuan(item.expensePrice)
+ }
+ // 鏇存柊 Echarts 鏁版嵁
+ if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+ lineChartOptions.dataset['source'] = list
+ }
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const times = shortcutDateRangePicker.value.times
+ const data = await TradeStatisticsApi.exportTradeStatisticsExcel({ times })
+ download.excel(data, '浜ゆ槗鐘跺喌.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getTradeStatisticsSummary()
+})
+</script>
+<style lang="scss" scoped>
+.summary {
+ .el-col {
+ margin-bottom: 1rem;
+ }
+}
+</style>
diff --git a/src/views/mall/trade/afterSale/detail/index.vue b/src/views/mall/trade/afterSale/detail/index.vue
new file mode 100644
index 0000000..4835ee6
--- /dev/null
+++ b/src/views/mall/trade/afterSale/detail/index.vue
@@ -0,0 +1,358 @@
+<template>
+ <ContentWrap>
+ <!-- 璁㈠崟淇℃伅 -->
+ <el-descriptions title="璁㈠崟淇℃伅">
+ <el-descriptions-item label="璁㈠崟鍙�: ">{{ formData.orderNo }}</el-descriptions-item>
+ <el-descriptions-item label="閰嶉�佹柟寮�: ">
+ <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.order.deliveryType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟绫诲瀷: ">
+ <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.order.type" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀惰揣浜�: ">
+ {{ formData.order.receiverName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涔板鐣欒█: ">
+ {{ formData.order.userRemark }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟鏉ユ簮: ">
+ <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.order.terminal" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鐢佃瘽: ">
+ {{ formData.order.receiverMobile }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗗澶囨敞: ">{{ formData.order.remark }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯鍗曞彿: ">
+ {{ formData.order.payOrderId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡: ">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.order.payChannelCode" />
+ </el-descriptions-item>
+ <el-descriptions-item label="涔板: ">{{ formData?.user?.nickname }}</el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鍞悗淇℃伅 -->
+ <el-descriptions title="鍞悗淇℃伅">
+ <el-descriptions-item label="閫�娆剧紪鍙�: ">{{ formData.no }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿: ">
+ {{ formatDate(formData.auditTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍞悗绫诲瀷: ">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_TYPE" :value="formData.type" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍞悗鏂瑰紡: ">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="formData.way" />
+ </el-descriptions-item>
+ <el-descriptions-item label="閫�娆鹃噾棰�: ">
+ {{ fenToYuan(formData.refundPrice) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閫�娆惧師鍥�: ">{{ formData.applyReason }}</el-descriptions-item>
+ <el-descriptions-item label="琛ュ厖鎻忚堪: ">
+ {{ formData.applyDescription }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍑瘉鍥剧墖: ">
+ <el-image
+ v-for="(item, index) in formData.applyPicUrls"
+ :key="index"
+ :src="item.url"
+ class="mr-10px h-60px w-60px"
+ @click="imagePreview(formData.applyPicUrls)"
+ />
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 閫�娆剧姸鎬� -->
+ <el-descriptions :column="1" title="閫�娆剧姸鎬�">
+ <el-descriptions-item label="閫�娆剧姸鎬�: ">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="formData.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label-class-name="no-colon">
+ <el-button v-if="formData.status === 10" type="primary" @click="agree">鍚屾剰鍞悗</el-button>
+ <el-button v-if="formData.status === 10" type="primary" @click="disagree">
+ 鎷掔粷鍞悗
+ </el-button>
+ <el-button v-if="formData.status === 30" type="primary" @click="receive">
+ 纭鏀惰揣
+ </el-button>
+ <el-button v-if="formData.status === 30" type="primary" @click="refuse">鎷掔粷鏀惰揣</el-button>
+ <el-button v-if="formData.status === 40" type="primary" @click="refund">纭閫�娆�</el-button>
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label><span style="color: red">鎻愰啋: </span></template>
+ 濡傛灉鏈彂璐э紝璇风偣鍑诲悓鎰忛��娆剧粰涔板銆�<br />
+ 濡傛灉瀹為檯宸插彂璐э紝璇蜂富鍔ㄤ笌涔板鑱旂郴銆�<br />
+ 濡傛灉璁㈠崟鏁翠綋閫�娆惧悗锛屼紭鎯犲埜鍜屼綑棰濅細閫�杩樼粰涔板.
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鍟嗗搧淇℃伅 -->
+ <el-descriptions title="鍟嗗搧淇℃伅">
+ <el-descriptions-item labelClassName="no-colon">
+ <el-row :gutter="20">
+ <el-col :span="15">
+ <el-table v-if="formData.orderItem" :data="[formData.orderItem]" border>
+ <el-table-column label="鍟嗗搧" prop="spuName" width="auto">
+ <template #default="{ row }">
+ {{ row.spuName }}
+ <el-tag
+ v-for="property in row.properties"
+ :key="property.propertyId"
+ class="mr-10px"
+ >
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍘熶环" prop="price" width="150">
+ <template #default="{ row }">{{ fenToYuan(row.price) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" width="100" />
+ <el-table-column label="鍚堣" prop="payPrice" width="150">
+ <template #default="{ row }">{{ fenToYuan(row.payPrice) }} 鍏�</template>
+ </el-table-column>
+ </el-table>
+ </el-col>
+ <el-col :span="10" />
+ </el-row>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鎿嶄綔鏃ュ織 -->
+ <el-descriptions title="鍞悗鏃ュ織">
+ <el-descriptions-item labelClassName="no-colon">
+ <el-timeline>
+ <el-timeline-item
+ v-for="saleLog in formData.logs"
+ :key="saleLog.id"
+ :timestamp="formatDate(saleLog.createTime)"
+ placement="top"
+ >
+ <div class="el-timeline-right-content">
+ <span>{{ saleLog.content }}</span>
+ </div>
+ <template #dot>
+ <span
+ :style="{ backgroundColor: getUserTypeColor(saleLog.userType) }"
+ class="dot-node-style"
+ >
+ {{ getDictLabel(DICT_TYPE.USER_TYPE, saleLog.userType)[0] || '绯�' }}
+ </span>
+ </template>
+ </el-timeline-item>
+ </el-timeline>
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+
+ <!-- 鍚勭鎿嶄綔鐨勫脊绐� -->
+ <UpdateAuditReasonForm ref="updateAuditReasonFormRef" @success="getDetail" />
+</template>
+<script lang="ts" setup>
+import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
+import { fenToYuan } from '@/utils'
+import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import UpdateAuditReasonForm from '@/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+import { isArray } from '@/utils/is'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+defineOptions({ name: 'TradeAfterSaleDetail' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const { params } = useRoute() // 鏌ヨ鍙傛暟
+const { push, currentRoute } = useRouter() // 璺敱
+const formData = ref({
+ order: {},
+ logs: []
+})
+const updateAuditReasonFormRef = ref() // 鎷掔粷鍞悗琛ㄥ崟 Ref
+
+/** 鑾峰緱 userType 棰滆壊 */
+const getUserTypeColor = (type: number) => {
+ const dict = getDictObj(DICT_TYPE.USER_TYPE, type)
+ switch (dict?.colorType) {
+ case 'success':
+ return '#67C23A'
+ case 'info':
+ return '#909399'
+ case 'warning':
+ return '#E6A23C'
+ case 'danger':
+ return '#F56C6C'
+ }
+ return '#409EFF'
+}
+
+/** 鑾峰緱璇︽儏 */
+const getDetail = async () => {
+ const id = params.id as unknown as number
+ if (id) {
+ const res = await AfterSaleApi.getAfterSale(id)
+ // 娌℃湁琛ㄥ崟淇℃伅鍒欏叧闂〉闈㈣繑鍥�
+ if (res == null) {
+ message.notifyError('鍞悗璁㈠崟涓嶅瓨鍦�')
+ close()
+ }
+ formData.value = res
+ }
+}
+
+/** 鍚屾剰鍞悗 */
+const agree = async () => {
+ try {
+ // 浜屾纭
+ await message.confirm('鏄惁鍚屾剰鍞悗锛�')
+ await AfterSaleApi.agree(formData.value.id)
+ // 鎻愮ず鎴愬姛
+ message.success(t('common.success'))
+ await getDetail()
+ } catch {}
+}
+
+/** 鎷掔粷鍞悗 */
+const disagree = async () => {
+ updateAuditReasonFormRef.value?.open(formData.value)
+}
+
+/** 纭鏀惰揣 */
+const receive = async () => {
+ try {
+ // 浜屾纭
+ await message.confirm('鏄惁纭鏀惰揣锛�')
+ await AfterSaleApi.receive(formData.value.id)
+ // 鎻愮ず鎴愬姛
+ message.success(t('common.success'))
+ await getDetail()
+ } catch {}
+}
+
+/** 鎷掔粷鏀惰揣 */
+const refuse = async () => {
+ try {
+ // 浜屾纭
+ await message.confirm('鏄惁鎷掔粷鏀惰揣锛�')
+ await AfterSaleApi.refuse(formData.value.id)
+ // 鎻愮ず鎴愬姛
+ message.success(t('common.success'))
+ await getDetail()
+ } catch {}
+}
+
+/** 纭閫�娆� */
+const refund = async () => {
+ try {
+ // 浜屾纭
+ await message.confirm('鏄惁纭閫�娆撅紵')
+ await AfterSaleApi.refund(formData.value.id)
+ // 鎻愮ず鎴愬姛
+ message.success(t('common.success'))
+ await getDetail()
+ } catch {}
+}
+
+/** 鍥剧墖棰勮 */
+const imagePreview = (args) => {
+ const urlList = []
+ if (isArray(args)) {
+ args.forEach((item) => {
+ urlList.push(item.url)
+ })
+ } else {
+ urlList.push(args)
+ }
+ createImageViewer({
+ urlList
+ })
+}
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+/** 鍏抽棴 tag */
+const close = () => {
+ delView(unref(currentRoute))
+ push({ name: 'TradeAfterSale' })
+}
+onMounted(async () => {
+ await getDetail()
+})
+</script>
+<style lang="scss" scoped>
+:deep(.el-descriptions) {
+ &:not(:nth-child(1)) {
+ margin-top: 20px;
+ }
+
+ .el-descriptions__title {
+ display: flex;
+ align-items: center;
+
+ &::before {
+ display: inline-block;
+ width: 3px;
+ height: 20px;
+ margin-right: 10px;
+ background-color: #409eff;
+ content: '';
+ }
+ }
+
+ .el-descriptions-item__container {
+ margin: 0 10px;
+
+ .no-colon {
+ margin: 0;
+
+ &::after {
+ content: '';
+ }
+ }
+ }
+}
+
+// 鏃堕棿绾挎牱寮忚皟鏁�
+:deep(.el-timeline) {
+ margin: 10px 0 0 160px;
+
+ .el-timeline-item__wrapper {
+ position: relative;
+ top: -20px;
+
+ .el-timeline-item__timestamp {
+ position: absolute !important;
+ top: 10px;
+ left: -150px;
+ }
+ }
+
+ .el-timeline-right-content {
+ display: flex;
+ align-items: center;
+ min-height: 30px;
+ padding: 10px;
+ background-color: var(--app-content-bg-color);
+
+ &::before {
+ position: absolute;
+ top: 10px;
+ left: 13px;
+ border-color: transparent var(--app-content-bg-color) transparent transparent; /* 灏栬棰滆壊锛屽乏渚ф湞鍚� */
+ border-style: solid;
+ border-width: 8px; /* 璋冩暣灏栬澶у皬 */
+ content: '';
+ }
+ }
+
+ .dot-node-style {
+ position: absolute;
+ left: -5px;
+ display: flex;
+ width: 20px;
+ height: 20px;
+ font-size: 10px;
+ color: #fff;
+ border-radius: 50%;
+ justify-content: center;
+ align-items: center;
+ }
+}
+</style>
diff --git a/src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue b/src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue
new file mode 100644
index 0000000..af3ab35
--- /dev/null
+++ b/src/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue
@@ -0,0 +1,70 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鎷掔粷鍞悗" width="45%">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="瀹℃壒澶囨敞">
+ <el-input
+ v-model="formData.auditReason"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ鎵瑰娉�"
+ type="textarea"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
+
+defineOptions({ name: 'AfterSaleDisagreeForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined, // 鍞悗璁㈠崟缂栧彿
+ auditReason: '' // 瀹℃壒澶囨敞
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: AfterSaleApi.TradeAfterSaleVO) => {
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formData.value.id = row.id
+ formData.value.auditReason = row.auditReason
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = unref(formData)
+ await AfterSaleApi.disagree(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined, // 鍞悗璁㈠崟缂栧彿
+ auditReason: '' // 瀹℃壒澶囨敞
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/trade/afterSale/index.vue b/src/views/mall/trade/afterSale/index.vue
new file mode 100644
index 0000000..0c4d0b8
--- /dev/null
+++ b/src/views/mall/trade/afterSale/index.vue
@@ -0,0 +1,273 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝敭鍚庨��娆�" url="https://doc.iocoder.cn/mall/trade-aftersale/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="spuName">
+ <el-input
+ v-model="queryParams.spuName"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ュ晢鍝� SPU 鍚嶇О"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="閫�娆剧紪鍙�" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ラ��娆剧紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="璁㈠崟缂栧彿" prop="orderNo">
+ <el-input
+ v-model="queryParams.orderNo"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ヨ鍗曠紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍞悗鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-280px"
+ clearable
+ placeholder="璇烽�夋嫨鍞悗鐘舵��"
+ >
+ <el-option label="鍏ㄩ儴" value="0" />
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍞悗鏂瑰紡" prop="way">
+ <el-select
+ v-model="queryParams.way"
+ class="!w-280px"
+ clearable
+ placeholder="璇烽�夋嫨鍞悗鏂瑰紡"
+ >
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍞悗绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ class="!w-280px"
+ clearable
+ placeholder="璇烽�夋嫨鍞悗绫诲瀷"
+ >
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-280px"
+ end-placeholder="鑷畾涔夋椂闂�"
+ start-placeholder="鑷畾涔夋椂闂�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-tabs v-model="queryParams.status" @tab-click="tabClick">
+ <el-tab-pane
+ v-for="item in statusTabs"
+ :key="item.label"
+ :label="item.label"
+ :name="item.value"
+ />
+ </el-tabs>
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="閫�娆剧紪鍙�" min-width="200" prop="no" />
+ <el-table-column align="center" label="璁㈠崟缂栧彿" min-width="200" prop="orderNo">
+ <template #default="{ row }">
+ <el-button link type="primary" @click="openOrderDetail(row.orderId)">
+ {{ row.orderNo }}
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧淇℃伅" min-width="600" prop="spuName">
+ <template #default="{ row }">
+ <div class="flex items-center">
+ <el-image
+ :src="row.picUrl"
+ class="mr-10px h-30px w-30px"
+ @click="imagePreview(row.picUrl)"
+ />
+ <span class="mr-10px">{{ row.spuName }}</span>
+ <el-tag v-for="property in row.properties" :key="property.propertyId" class="mr-10px">
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="璁㈠崟閲戦" min-width="120" prop="refundPrice">
+ <template #default="scope">
+ <span>{{ fenToYuan(scope.row.refundPrice) }} 鍏�</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="涔板" prop="user.nickname" />
+ <el-table-column align="center" label="鐢宠鏃堕棿" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ formatDate(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍞悗鐘舵��" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍞悗鏂瑰紡">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="scope.row.way" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="160">
+ <template #default="{ row }">
+ <el-button link type="primary" @click="openAfterSaleDetail(row.id)">澶勭悊閫�娆�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import { TabsPaneContext } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'TradeAfterSale' })
+
+const { push } = useRouter() // 璺敱璺宠浆
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<AfterSaleApi.TradeAfterSaleVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const statusTabs = ref([
+ {
+ label: '鍏ㄩ儴',
+ value: '0'
+ }
+])
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: null,
+ status: '0',
+ orderNo: null,
+ spuName: null,
+ createTime: [],
+ way: null,
+ type: null
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = cloneDeep(queryParams)
+ // 澶勭悊鎺夊叏閮ㄧ殑鐘舵�侊紝涓嶄紶灏辨槸鍏ㄩ儴
+ if (data.status === '0') {
+ delete data.status
+ }
+ // 鎵ц鏌ヨ
+ const res = await AfterSaleApi.getAfterSalePage(data)
+ list.value = res.list as AfterSaleApi.TradeAfterSaleVO[]
+ total.value = res.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = async () => {
+ queryParams.pageNo = 1
+ await getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** tab 鍒囨崲 */
+const tabClick = async (tab: TabsPaneContext) => {
+ queryParams.status = tab.paneName
+ await getList()
+}
+
+/** 澶勭悊閫�娆� */
+const openAfterSaleDetail = (id: number) => {
+ push({ name: 'TradeAfterSaleDetail', params: { id } })
+}
+
+/** 鏌ョ湅璁㈠崟璇︽儏 */
+const openOrderDetail = (id: number) => {
+ push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+onMounted(async () => {
+ await getList()
+ // 璁剧疆 statuses 杩囨护
+ for (const dict of getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)) {
+ statusTabs.value.push({
+ label: dict.label,
+ value: dict.value
+ })
+ }
+})
+</script>
diff --git a/src/views/mall/trade/brokerage/record/index.vue b/src/views/mall/trade/brokerage/record/index.vue
new file mode 100644
index 0000000..8f138ad
--- /dev/null
+++ b/src/views/mall/trade/brokerage/record/index.vue
@@ -0,0 +1,171 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝垎閿�杩斾剑" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="涓氬姟绫诲瀷" prop="bizType">
+ <el-select
+ v-model="queryParams.bizType"
+ placeholder="璇烽�夋嫨涓氬姟绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" min-width="60" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" min-width="80" />
+ <el-table-column label="澶村儚" align="center" prop="userAvatar" width="70px">
+ <template #default="scope">
+ <el-avatar :src="scope.row.userAvatar" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄电О" align="center" prop="userNickname" min-width="80px" />
+ <el-table-column label="涓氬姟绫诲瀷" align="center" prop="bizType" min-width="85">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE" :value="scope.row.bizType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="涓氬姟缂栧彿" align="center" prop="bizId" min-width="80" />
+ <el-table-column label="鏍囬" align="center" prop="title" min-width="110" />
+ <el-table-column
+ label="閲戦"
+ align="center"
+ prop="price"
+ min-width="60"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="璇存槑" align="center" prop="description" min-width="120" />
+ <el-table-column label="鐘舵��" align="center" prop="status" min-width="85">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="瑙e喕鏃堕棿"
+ align="center"
+ prop="unfreezeTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+defineOptions({ name: 'TradeBrokerageRecord' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ bizType: null,
+ price: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BrokerageRecordApi.getBrokerageRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue b/src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue
new file mode 100644
index 0000000..7cff8a1
--- /dev/null
+++ b/src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue
@@ -0,0 +1,160 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鎺ㄥ箍璁㈠崟鍒楄〃" width="75%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="85px"
+ >
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="sourceUserLevel">
+ <el-radio-group v-model="queryParams.sourceUserLevel" @change="handleQuery">
+ <el-radio-button :value="0">鍏ㄩ儴</el-radio-button>
+ <el-radio-button :value="1">涓�绾ф帹骞夸汉</el-radio-button>
+ <el-radio-button :value="2">浜岀骇鎺ㄥ箍浜�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缁戝畾鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="璁㈠崟缂栧彿" min-width="80px" prop="bizId" />
+ <el-table-column align="center" label="鐢ㄦ埛缂栧彿" min-width="80px" prop="sourceUserId" />
+ <el-table-column align="center" label="澶村儚" prop="sourceUserAvatar" width="70px">
+ <template #default="scope">
+ <el-avatar :src="scope.row.sourceUserAvatar" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鏄电О" min-width="80px" prop="sourceUserNickname" />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ align="center"
+ label="浣i噾"
+ min-width="100px"
+ prop="price"
+ />
+ <el-table-column align="center" label="鐘舵��" min-width="85" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BROKERAGE_RECORD_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageRecordApi from '@/api/mall/trade/brokerage/record'
+import { BrokerageRecordBizTypeEnum } from '@/utils/constants'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+/** 鎺ㄥ箍璁㈠崟鍒楄〃 */
+defineOptions({ name: 'BrokerageOrderListDialog' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ bizType: BrokerageRecordBizTypeEnum.ORDER.type,
+ sourceUserLevel: 0,
+ createTime: [],
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鎵撳紑寮圭獥 */
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const open = async (userId: any) => {
+ dialogVisible.value = true
+ queryParams.userId = userId
+ resetQuery()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 澶勭悊鍏ㄩ儴鐨勬儏鍐�
+ const data = await BrokerageRecordApi.getBrokerageRecordPage({
+ ...queryParams,
+ sourceUserLevel: queryParams.sourceUserLevel === 0 ? undefined : queryParams.sourceUserLevel
+ })
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+</script>
diff --git a/src/views/mall/trade/brokerage/user/BrokerageUserCreateForm.vue b/src/views/mall/trade/brokerage/user/BrokerageUserCreateForm.vue
new file mode 100644
index 0000000..9a979f0
--- /dev/null
+++ b/src/views/mall/trade/brokerage/user/BrokerageUserCreateForm.vue
@@ -0,0 +1,161 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍒涘缓鍒嗛攢鍛�" width="800">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="90"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12" :xs="24">
+ <el-form-item label="鍒嗛攢鍛�" prop="userId">
+ <el-input
+ v-model="formData.userId"
+ v-loading="formLoading"
+ placeholder="璇疯緭鍏ュ垎閿�鍛樼紪鍙�"
+ >
+ <template #append>
+ <el-button @click="handleGetUser(formData.userId, '鍒嗛攢鍛�')">
+ <Icon class="mr-5px" icon="ep:search" />
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <!-- 灞曠ず鍒嗛攢鍛樼殑淇℃伅 -->
+ <el-descriptions v-if="userInfo.user" :column="1" border>
+ <el-descriptions-item label="澶村儚">
+ <el-avatar :src="userInfo.user?.avatar" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄电О">{{ userInfo.user?.nickname }}</el-descriptions-item>
+ </el-descriptions>
+ </el-col>
+
+ <el-col :span="12" :xs="24">
+ <el-form-item label="涓婄骇鎺ㄥ箍浜�" prop="bindUserId">
+ <el-input
+ v-model="formData.bindUserId"
+ v-loading="formLoading"
+ placeholder="璇疯緭鍏ユ帹骞垮憳缂栧彿"
+ >
+ <template #append>
+ <el-button @click="handleGetUser(formData.bindUserId, '鎺ㄥ箍鍛�')">
+ <Icon class="mr-5px" icon="ep:search" />
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <!-- 灞曠ず涓婄骇鎺ㄥ箍浜虹殑淇℃伅 -->
+ <el-descriptions v-if="userInfo.bindUser" :column="1" border>
+ <el-descriptions-item label="澶村儚">
+ <el-avatar :src="userInfo.bindUser?.avatar" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄电О"
+ >{{ userInfo.bindUser?.nickname }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎺ㄥ箍璧勬牸">
+ <el-tag v-if="userInfo.bindUser?.brokerageEnabled">鏈�</el-tag>
+ <el-tag v-else type="info">鏃�</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎴愪负鎺ㄥ箍鍛樼殑鏃堕棿">
+ {{ formatDate(userInfo.bindUser?.brokerageTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+import * as UserApi from '@/api/member/user'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'BrokerageUserCreateForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ userId: undefined,
+ bindUserId: undefined
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const formRules = reactive({
+ userId: [{ required: true, message: '鍒嗛攢鍛樹笉鑳戒负绌�', trigger: 'blur' }]
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ resetForm()
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+/** 鍒涘缓鍒嗛攢鍛� */
+const submitForm = async () => {
+ if (formLoading.value) return
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鍙戣捣淇敼
+ await BrokerageUserApi.createBrokerageUser(formData.value)
+ message.success(t('common.createSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formRef.value?.resetFields()
+ formData.value = {
+ userId: undefined,
+ bindUserId: undefined
+ }
+
+ userInfo.bindUser = undefined
+ userInfo.user = undefined
+}
+
+/** 鏌ヨ鎺ㄥ箍鍛樺拰鍒嗛攢鍛� */
+const userInfo = reactive<{
+ bindUser: BrokerageUserApi.BrokerageUserVO | undefined
+ user: BrokerageUserApi.BrokerageUserVO | undefined
+}>({
+ bindUser: undefined,
+ user: undefined
+})
+const handleGetUser = async (id: any, userType: string) => {
+ if (!id) {
+ message.warning(`璇峰厛杈撳叆${userType}缂栧彿鍚庨噸璇曪紒锛侊紒`)
+ return
+ }
+ if (userType === '鎺ㄥ箍鍛�' && formData.value.bindUserId == formData.value.userId) {
+ message.error('涓嶈兘缁戝畾鑷繁涓烘帹骞夸汉')
+ return
+ }
+ const user =
+ userType === '鎺ㄥ箍鍛�' ? await BrokerageUserApi.getBrokerageUser(id) : await UserApi.getUser(id)
+ userType === '鎺ㄥ箍鍛�' ? (userInfo.bindUser = user) : (userInfo.user = user)
+ if (!user) {
+ message.warning(`${userType}涓嶅瓨鍦╜)
+ }
+}
+</script>
diff --git a/src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue b/src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue
new file mode 100644
index 0000000..732f4bb
--- /dev/null
+++ b/src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue
@@ -0,0 +1,137 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鎺ㄥ箍浜哄垪琛�" width="75%">
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="85px"
+ >
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="level">
+ <el-radio-group v-model="queryParams.level" @change="handleQuery">
+ <el-radio-button checked>鍏ㄩ儴</el-radio-button>
+ <el-radio-button value="1">涓�绾ф帹骞夸汉</el-radio-button>
+ <el-radio-button value="2">浜岀骇鎺ㄥ箍浜�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="缁戝畾鏃堕棿" prop="bindUserTime">
+ <el-date-picker
+ v-model="queryParams.bindUserTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="id" min-width="80px" />
+ <el-table-column label="澶村儚" align="center" prop="avatar" width="70px">
+ <template #default="scope">
+ <el-avatar :src="scope.row.avatar" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄电О" align="center" prop="nickname" min-width="80px" />
+ <el-table-column
+ label="鎺ㄥ箍浜烘暟"
+ align="center"
+ prop="brokerageUserCount"
+ min-width="80px"
+ />
+ <el-table-column
+ label="鎺ㄥ箍璁㈠崟鏁伴噺"
+ align="center"
+ prop="brokerageOrderCount"
+ min-width="110px"
+ />
+ <el-table-column label="鎺ㄥ箍璧勬牸" align="center" prop="brokerageEnabled" min-width="80px">
+ <template #default="scope">
+ <el-tag v-if="scope.row.brokerageEnabled">鏈�</el-tag>
+ <el-tag v-else type="info">鏃�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="缁戝畾鏃堕棿"
+ align="center"
+ prop="bindUserTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 鎺ㄥ箍浜哄垪琛� */
+defineOptions({ name: 'BrokerageUserListDialog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ bindUserId: null,
+ level: '',
+ bindUserTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鎵撳紑寮圭獥 */
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const open = async (bindUserId: any) => {
+ dialogVisible.value = true
+ queryParams.bindUserId = bindUserId
+ resetQuery()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+</script>
diff --git a/src/views/mall/trade/brokerage/user/BrokerageUserUpdateForm.vue b/src/views/mall/trade/brokerage/user/BrokerageUserUpdateForm.vue
new file mode 100644
index 0000000..941b715
--- /dev/null
+++ b/src/views/mall/trade/brokerage/user/BrokerageUserUpdateForm.vue
@@ -0,0 +1,127 @@
+<template>
+ <Dialog v-model="dialogVisible" title="淇敼涓婄骇鎺ㄥ箍浜�" width="500">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鎺ㄥ箍浜�" prop="bindUserId">
+ <el-input
+ v-model="formData.bindUserId"
+ placeholder="璇疯緭鍏ユ帹骞垮憳缂栧彿"
+ v-loading="formLoading"
+ >
+ <template #append>
+ <el-button @click="handleGetUser"><Icon icon="ep:search" class="mr-5px" /></el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-form>
+ <!-- 灞曠ず涓婄骇鎺ㄥ箍浜虹殑淇℃伅 -->
+ <el-descriptions v-if="bindUser" :column="1" border>
+ <el-descriptions-item label="澶村儚">
+ <el-avatar :src="bindUser.avatar" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄电О">{{ bindUser.nickname }}</el-descriptions-item>
+ <el-descriptions-item label="鎺ㄥ箍璧勬牸">
+ <el-tag v-if="bindUser.brokerageEnabled">鏈�</el-tag>
+ <el-tag v-else type="info">鏃�</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎴愪负鎺ㄥ箍鍛樼殑鏃堕棿">
+ {{ formatDate(bindUser.brokerageTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+import { formatDate } from '@/utils/formatTime'
+
+/** 淇敼鍒嗛攢鐢ㄦ埛 */
+defineOptions({ name: 'BrokerageUserUpdateForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref()
+const formRef = ref() // 琛ㄥ崟 Ref
+const formRules = reactive({
+ bindUserId: [{ required: true, message: '鎺ㄥ箍鍛樹汉涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: BrokerageUserApi.BrokerageUserVO) => {
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formData.value.id = row.id
+ formData.value.bindUserId = row.bindUserId
+ // 鍙嶆樉涓婄骇鎺ㄥ箍浜�
+ if (row.bindUserId) {
+ await handleGetUser()
+ }
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+/** 淇敼涓婄骇鎺ㄥ箍浜� */
+const submitForm = async () => {
+ if (formLoading.value) return
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鏈煡鎵惧埌鍚堥�傜殑涓婄骇
+ if (!bindUser.value) {
+ message.error('璇峰厛鏌ヨ骞剁‘璁ゆ帹骞夸汉')
+ return
+ }
+
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ // 鍙戣捣淇敼
+ await BrokerageUserApi.updateBindUser(formData.value)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ bindUserId: undefined
+ }
+ formRef.value?.resetFields()
+ bindUser.value = undefined
+}
+
+/** 鏌ヨ鎺ㄥ箍鍛� */
+const bindUser = ref<BrokerageUserApi.BrokerageUserVO>()
+const handleGetUser = async () => {
+ if (formData.value.bindUserId == formData.value.id) {
+ message.error('涓嶈兘缁戝畾鑷繁涓烘帹骞夸汉')
+ return
+ }
+ formLoading.value = true
+ bindUser.value = await BrokerageUserApi.getBrokerageUser(formData.value.bindUserId)
+ if (!bindUser.value) {
+ message.warning('鎺ㄥ箍鍛樹笉瀛樺湪')
+ }
+ formLoading.value = false
+}
+</script>
diff --git a/src/views/mall/trade/brokerage/user/index.vue b/src/views/mall/trade/brokerage/user/index.vue
new file mode 100644
index 0000000..bbb17fb
--- /dev/null
+++ b/src/views/mall/trade/brokerage/user/index.vue
@@ -0,0 +1,331 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝垎閿�杩斾剑" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="85px"
+ >
+ <el-form-item label="鎺ㄥ箍鍛樼紪鍙�" prop="bindUserId">
+ <el-input
+ v-model="queryParams.bindUserId"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ帹骞垮憳缂栧彿"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎺ㄥ箍璧勬牸" prop="brokerageEnabled">
+ <el-select
+ v-model="queryParams.brokerageEnabled"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鎺ㄥ箍璧勬牸"
+ >
+ <el-option :value="true" label="鏈�" />
+ <el-option :value="false" label="鏃�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['trade:brokerage-user:create']"
+ plain
+ type="primary"
+ @click="openCreateUserForm"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="鐢ㄦ埛缂栧彿" min-width="80px" prop="id" />
+ <el-table-column align="center" label="澶村儚" prop="avatar" width="70px">
+ <template #default="scope">
+ <el-avatar :src="scope.row.avatar" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鏄电О" min-width="80px" prop="nickname" />
+ <el-table-column align="center" label="鎺ㄥ箍浜烘暟" prop="brokerageUserCount" width="80px" />
+ <el-table-column
+ align="center"
+ label="鎺ㄥ箍璁㈠崟鏁伴噺"
+ min-width="110px"
+ prop="brokerageOrderCount"
+ />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ align="center"
+ label="鎺ㄥ箍璁㈠崟閲戦"
+ min-width="110px"
+ prop="brokerageOrderPrice"
+ />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ align="center"
+ label="宸叉彁鐜伴噾棰�"
+ min-width="100px"
+ prop="withdrawPrice"
+ />
+ <el-table-column align="center" label="宸叉彁鐜版鏁�" min-width="100px" prop="withdrawCount" />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ align="center"
+ label="鏈彁鐜伴噾棰�"
+ min-width="100px"
+ prop="price"
+ />
+ <el-table-column
+ :formatter="fenToYuanFormat"
+ align="center"
+ label="鍐荤粨涓剑閲�"
+ min-width="100px"
+ prop="frozenPrice"
+ />
+ <el-table-column align="center" label="鎺ㄥ箍璧勬牸" min-width="80px" prop="brokerageEnabled">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.brokerageEnabled"
+ :disabled="!checkPermi(['trade:brokerage-user:update-bind-user'])"
+ active-text="鏈�"
+ inactive-text="鏃�"
+ inline-prompt
+ @change="handleBrokerageEnabledChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鎴愪负鎺ㄥ箍鍛樻椂闂�"
+ prop="brokerageTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="涓婄骇鎺ㄥ箍鍛樼紪鍙�" prop="bindUserId" width="150px" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鎺ㄥ箍鍛樼粦瀹氭椂闂�"
+ prop="bindUserTime"
+ width="180px"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="150px">
+ <template #default="scope">
+ <el-dropdown
+ v-hasPermi="[
+ 'trade:brokerage-user:user-query',
+ 'trade:brokerage-user:order-query',
+ 'trade:brokerage-user:update-bind-user',
+ 'trade:brokerage-user:clear-bind-user'
+ ]"
+ @command="(command) => handleCommand(command, scope.row)"
+ >
+ <el-button link type="primary">
+ <Icon icon="ep:d-arrow-right" />
+ 鏇村
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item
+ v-if="checkPermi(['trade:brokerage-user:user-query'])"
+ command="openBrokerageUserTable"
+ >
+ 鎺ㄥ箍浜�
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-if="checkPermi(['trade:brokerage-user:order-query'])"
+ command="openBrokerageOrderTable"
+ >
+ 鎺ㄥ箍璁㈠崟
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-if="checkPermi(['trade:brokerage-user:update-bind-user'])"
+ command="openUpdateBindUserForm"
+ >
+ 淇敼涓婄骇鎺ㄥ箍浜�
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-if="
+ scope.row.bindUserId && checkPermi(['trade:brokerage-user:clear-bind-user'])
+ "
+ command="handleClearBindUser"
+ >
+ 娓呴櫎涓婄骇鎺ㄥ箍浜�
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ <!-- 淇敼涓婄骇鎺ㄥ箍浜鸿〃鍗� -->
+ <BrokerageUserUpdateForm ref="updateFormRef" @success="getList" />
+ <!-- 鎺ㄥ箍浜哄垪琛� -->
+ <BrokerageUserListDialog ref="listDialogRef" />
+ <!-- 鎺ㄥ箍璁㈠崟鍒楄〃 -->
+ <BrokerageOrderListDialog ref="orderDialogRef" />
+ <!-- 鍒涘缓鍒嗛攢鍛� -->
+ <BrokerageUserCreateForm ref="createFormRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+import { checkPermi } from '@/utils/permission'
+import { fenToYuanFormat } from '@/utils/formatter'
+import BrokerageUserUpdateForm from '@/views/mall/trade/brokerage/user/BrokerageUserUpdateForm.vue'
+import BrokerageUserListDialog from '@/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue'
+import BrokerageOrderListDialog from '@/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue'
+import BrokerageUserCreateForm from '@/views/mall/trade/brokerage/user/BrokerageUserCreateForm.vue'
+
+defineOptions({ name: 'TradeBrokerageUser' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ bindUserId: null,
+ brokerageEnabled: true,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+const handleCommand = (command: string, row: BrokerageUserApi.BrokerageUserVO) => {
+ switch (command) {
+ case 'openBrokerageUserTable':
+ openBrokerageUserTable(row.id)
+ break
+ case 'openBrokerageOrderTable':
+ openBrokerageOrderTable(row.id)
+ break
+ case 'openUpdateBindUserForm':
+ openUpdateBindUserForm(row)
+ break
+ case 'handleClearBindUser':
+ handleClearBindUser(row)
+ break
+ }
+}
+
+/** 鎵撳紑鎺ㄥ箍浜哄垪琛� */
+const listDialogRef = ref()
+const openBrokerageUserTable = (id: number) => {
+ listDialogRef.value.open(id)
+}
+
+/** 鎵撳紑鎺ㄥ箍璁㈠崟鍒楄〃 */
+const orderDialogRef = ref()
+const openBrokerageOrderTable = (id: number) => {
+ orderDialogRef.value.open(id)
+}
+
+/** 鎵撳紑琛ㄥ崟锛氫慨鏀逛笂绾ф帹骞夸汉 */
+const updateFormRef = ref()
+const openUpdateBindUserForm = (row: BrokerageUserApi.BrokerageUserVO) => {
+ updateFormRef.value.open(row)
+}
+
+/** 鍒涘缓鍒嗛攢鍛� */
+const createFormRef = ref<InstanceType<typeof CreateUserForm>>()
+const openCreateUserForm = () => {
+ createFormRef.value?.open()
+}
+
+/** 娓呴櫎涓婄骇鎺ㄥ箍浜� */
+const handleClearBindUser = async (row: BrokerageUserApi.BrokerageUserVO) => {
+ try {
+ // 浜屾纭
+ await message.confirm(`纭瑕佹竻闄�"${row.nickname}"鐨勪笂绾ф帹骞夸汉鍚楋紵`)
+ // 鍙戣捣淇敼
+ await BrokerageUserApi.clearBindUser({ id: row.id })
+ message.success('娓呴櫎鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎺ㄥ箍璧勬牸锛氬紑閫�/鍏抽棴 */
+const handleBrokerageEnabledChange = async (row: BrokerageUserApi.BrokerageUserVO) => {
+ try {
+ // 浜屾纭
+ const text = row.brokerageEnabled ? '寮�閫�' : '鍏抽棴'
+ await message.confirm(`纭瑕�${text}"${row.nickname}"鐨勬帹骞胯祫鏍煎悧锛焋)
+ // 鍙戣捣淇敼
+ await BrokerageUserApi.updateBrokerageEnabled({ id: row.id, enabled: row.brokerageEnabled })
+ message.success(text + '鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ // 寮傚父鏃讹紝闇�瑕侀噸缃洖涔嬪墠鐨勫��
+ row.brokerageEnabled = !row.brokerageEnabled
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue b/src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue
new file mode 100644
index 0000000..2a69b5b
--- /dev/null
+++ b/src/views/mall/trade/brokerage/withdraw/BrokerageWithdrawRejectForm.vue
@@ -0,0 +1,73 @@
+<template>
+ <Dialog title="瀹℃牳" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="椹冲洖鍘熷洜" prop="auditReason">
+ <el-input v-model="formData.auditReason" type="textarea" placeholder="璇疯緭鍏ラ┏鍥炲師鍥�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ auditReason: undefined
+})
+const formRules = reactive({
+ auditReason: [{ required: true, message: '椹冲洖鍘熷洜涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ formData.value.id = id
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as BrokerageWithdrawApi.BrokerageWithdrawVO
+ await BrokerageWithdrawApi.rejectBrokerageWithdraw(data)
+ message.success('椹冲洖鎴愬姛')
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ auditReason: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/trade/brokerage/withdraw/index.vue b/src/views/mall/trade/brokerage/withdraw/index.vue
new file mode 100644
index 0000000..d19c485
--- /dev/null
+++ b/src/views/mall/trade/brokerage/withdraw/index.vue
@@ -0,0 +1,309 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝垎閿�杩斾剑" url="https://doc.iocoder.cn/mall/trade-brokerage/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎻愮幇绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ placeholder="璇烽�夋嫨鎻愮幇绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璐﹀彿" prop="userAccount">
+ <el-input
+ v-model="queryParams.userAccount"
+ placeholder="璇疯緭鍏ヨ处鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐪熷疄鍚嶅瓧" prop="userName">
+ <el-input
+ v-model="queryParams.userName"
+ placeholder="璇疯緭鍏ョ湡瀹炲悕瀛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎻愮幇閾惰" prop="bankName">
+ <el-select
+ v-model="queryParams.bankName"
+ placeholder="璇烽�夋嫨鎻愮幇閾惰"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="left" prop="id" min-width="60px" />
+ <el-table-column label="鐢ㄦ埛淇℃伅" align="left" min-width="120px">
+ <template #default="scope">
+ <div>缂栧彿锛歿{ scope.row.userId }}</div>
+ <div>鏄电О锛歿{ scope.row.userNickname }}</div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮幇閲戦" align="left" prop="price" min-width="80px">
+ <template #default="scope">
+ <div>閲戙��棰濓細锟{ fenToYuan(scope.row.price) }}</div>
+ <div>鎵嬬画璐癸細锟{ fenToYuan(scope.row.feePrice) }}</div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮幇鏂瑰紡" align="left" prop="type" min-width="80px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BROKERAGE_WITHDRAW_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮幇淇℃伅" align="left" min-width="120px">
+ <template #default="scope">
+ <div v-if="scope.row.type === BrokerageWithdrawTypeEnum.WALLET.type">-</div>
+ <div v-else>
+ <div v-if="scope.row.userAccount">璐﹀彿锛歿{ scope.row.userAccount }}</div>
+ <div v-if="scope.row.userName">鐪熷疄濮撳悕锛歿{ scope.row.userName }}</div>
+ </div>
+ <template v-if="scope.row.type === BrokerageWithdrawTypeEnum.BANK.type">
+ <div>
+ 閾惰鍚嶇О锛�
+ <dict-tag :type="DICT_TYPE.BROKERAGE_BANK_NAME" :value="scope.row.bankName" />
+ </div>
+ <div>寮�鎴峰湴鍧�锛歿{ scope.row.bankAddress }}</div>
+ </template>
+ <div v-if="scope.row.qrCodeUrl" class="mt-2">
+ <div>鏀舵鐮侊細</div>
+ <el-image
+ :src="scope.row.qrCodeUrl"
+ class="h-40px w-40px"
+ :preview-src-list="[scope.row.qrCodeUrl]"
+ preview-teleported
+ />
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鐢宠鏃堕棿"
+ align="left"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="澶囨敞" align="left" prop="remark" />
+ <el-table-column label="鐘舵��" align="left" prop="status" min-width="120px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.BROKERAGE_WITHDRAW_STATUS" :value="scope.row.status" />
+ <div v-if="scope.row.auditTime" class="text-xs">
+ 鏃堕棿锛歿{ formatDate(scope.row.auditTime) }}
+ </div>
+ <div v-if="scope.row.auditReason" class="text-xs">
+ 瀹℃牳鍘熷洜锛歿{ scope.row.auditReason }}
+ </div>
+ <!-- 鎻愮幇澶辫触鍘熷洜 -->
+ <div v-if="scope.row.transferErrorMsg" class="text-xs text-red-500">
+ 杞处澶辫触鍘熷洜锛歿{ scope.row.transferErrorMsg }}
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="left" width="110px" fixed="right">
+ <template #default="scope">
+ <template
+ v-if="
+ scope.row.status === BrokerageWithdrawStatusEnum.AUDITING.status &&
+ !scope.row.payTransferId
+ "
+ >
+ <el-button
+ link
+ type="primary"
+ @click="handleApprove(scope.row.id)"
+ v-hasPermi="['trade:brokerage-withdraw:audit']"
+ >
+ 閫氳繃
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="openForm(scope.row.id)"
+ v-hasPermi="['trade:brokerage-withdraw:audit']"
+ >
+ 椹冲洖
+ </el-button>
+ </template>
+ <template v-if="scope.row.status === BrokerageWithdrawStatusEnum.WITHDRAW_FAIL.status">
+ <el-button
+ link
+ type="warning"
+ @click="handleRetryTransfer(scope.row.id)"
+ v-hasPermi="['trade:brokerage-withdraw:audit']"
+ >
+ 閲嶆柊杞处
+ </el-button>
+ </template>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <BrokerageWithdrawRejectForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import * as BrokerageWithdrawApi from '@/api/mall/trade/brokerage/withdraw'
+import BrokerageWithdrawRejectForm from './BrokerageWithdrawRejectForm.vue'
+import { BrokerageWithdrawStatusEnum, BrokerageWithdrawTypeEnum } from '@/utils/constants'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'BrokerageWithdraw' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ type: undefined,
+ userName: null,
+ userAccount: null,
+ bankName: undefined,
+ status: undefined,
+ auditReason: null,
+ auditTime: [],
+ remark: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await BrokerageWithdrawApi.getBrokerageWithdrawPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (id: number) => {
+ formRef.value.open(id)
+}
+
+/** 瀹℃牳閫氳繃 */
+const handleApprove = async (id: number) => {
+ try {
+ loading.value = true
+ await message.confirm('纭畾瑕佸鏍搁�氳繃鍚楋紵')
+ await BrokerageWithdrawApi.approveBrokerageWithdraw(id)
+ await message.success(t('common.success'))
+ await getList()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶆柊杞处 */
+const handleRetryTransfer = async (id: number) => {
+ try {
+ loading.value = true
+ await message.confirm('纭畾瑕侀噸鏂拌浆璐﹀悧锛�')
+ await BrokerageWithdrawApi.approveBrokerageWithdraw(id)
+ await message.success(t('common.success'))
+ await getList()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/trade/config/index.vue b/src/views/mall/trade/config/index.vue
new file mode 100644
index 0000000..6aa4b97
--- /dev/null
+++ b/src/views/mall/trade/config/index.vue
@@ -0,0 +1,291 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戜氦鏄撹鍗�" url="https://doc.iocoder.cn/mall/trade-order/" />
+ <doc-alert title="銆愪氦鏄撱�戣喘鐗╄溅" url="https://doc.iocoder.cn/mall/trade-cart/" />
+
+ <ContentWrap>
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ >
+ <el-form-item v-show="false" label="hideId">
+ <el-input v-model="formData.id" />
+ </el-form-item>
+ <el-tabs>
+ <!-- 鍞悗 -->
+ <el-tab-pane label="鍞悗">
+ <el-form-item label="閫�娆剧悊鐢�" prop="afterSaleRefundReasons">
+ <el-select
+ v-model="formData.afterSaleRefundReasons"
+ allow-create
+ filterable
+ multiple
+ placeholder="璇风洿鎺ヨ緭鍏ラ��娆剧悊鐢�"
+ >
+ <el-option
+ v-for="reason in formData.afterSaleRefundReasons"
+ :key="reason"
+ :label="reason"
+ :value="reason"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐х悊鐢�" prop="afterSaleReturnReasons">
+ <el-select
+ v-model="formData.afterSaleReturnReasons"
+ allow-create
+ filterable
+ multiple
+ placeholder="璇风洿鎺ヨ緭鍏ラ��璐х悊鐢�"
+ >
+ <el-option
+ v-for="reason in formData.afterSaleReturnReasons"
+ :key="reason"
+ :label="reason"
+ :value="reason"
+ />
+ </el-select>
+ </el-form-item>
+ </el-tab-pane>
+ <!-- 閰嶉�� -->
+ <el-tab-pane label="閰嶉��">
+ <el-form-item label="鍚敤鍖呴偖" prop="deliveryExpressFreeEnabled">
+ <el-switch v-model="formData.deliveryExpressFreeEnabled" style="user-select: none" />
+ <el-text class="w-full" size="small" type="info"> 鍟嗗煄鏄惁鍚敤鍏ㄥ満鍖呴偖</el-text>
+ </el-form-item>
+ <el-form-item label="婊¢鍖呴偖" prop="deliveryExpressFreePrice">
+ <el-input-number
+ v-model="formData.deliveryExpressFreePrice"
+ :min="0"
+ :precision="2"
+ class="!w-xs"
+ placeholder="璇疯緭鍏ユ弧棰濆寘閭�"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 鍟嗗煄鍟嗗搧婊″灏戦噾棰濆嵆鍙寘閭紝鍗曚綅锛氬厓
+ </el-text>
+ </el-form-item>
+ <el-form-item label="鍚敤闂ㄥ簵鑷彁" prop="deliveryPickUpEnabled">
+ <el-switch v-model="formData.deliveryPickUpEnabled" style="user-select: none" />
+ </el-form-item>
+ </el-tab-pane>
+ <!-- 鍒嗛攢 -->
+ <el-tab-pane label="鍒嗛攢">
+ <el-form-item label="鍒嗕剑鍚敤" prop="brokerageEnabled">
+ <el-switch v-model="formData.brokerageEnabled" style="user-select: none" />
+ <el-text class="w-full" size="small" type="info"> 鍟嗗煄鏄惁寮�鍚垎閿�妯″紡</el-text>
+ </el-form-item>
+ <el-form-item label="鍒嗕剑妯″紡" prop="brokerageEnabledCondition">
+ <el-radio-group v-model="formData.brokerageEnabledCondition">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_ENABLED_CONDITION)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ <el-text class="w-full" size="small" type="info">
+ 浜轰汉鍒嗛攢锛氭瘡涓敤鎴烽兘鍙互鎴愪负鎺ㄥ箍鍛�
+ </el-text>
+ <el-text class="w-full" size="small" type="info">
+ 鎸囧畾鍒嗛攢锛氫粎鍙湪鍚庡彴鎵嬪姩璁剧疆鎺ㄥ箍鍛�
+ </el-text>
+ </el-form-item>
+ <el-form-item label="鍒嗛攢鍏崇郴缁戝畾" prop="brokerageBindMode">
+ <el-radio-group v-model="formData.brokerageBindMode">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_BIND_MODE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ <el-text class="w-full" size="small" type="info">
+ 棣栨缁戝畾锛氬彧瑕佺敤鎴锋病鏈夋帹骞夸汉锛岄殢鏃堕兘鍙互缁戝畾鎺ㄥ箍鍏崇郴
+ </el-text>
+ <el-text class="w-full" size="small" type="info">
+ 娉ㄥ唽缁戝畾锛氬彧鏈夋柊鐢ㄦ埛娉ㄥ唽鏃舵垨棣栨杩涘叆绯荤粺鏃舵墠鍙互缁戝畾鎺ㄥ箍鍏崇郴
+ </el-text>
+ </el-form-item>
+ <el-form-item label="鍒嗛攢娴锋姤鍥�">
+ <UploadImgs v-model="formData.brokeragePosterUrls" height="125px" width="75px" />
+ <el-text class="w-full" size="small" type="info">
+ 涓汉涓績鍒嗛攢娴锋姤鍥剧墖锛屽缓璁昂瀵� 600x1000
+ </el-text>
+ </el-form-item>
+ <el-form-item label="涓�绾ц繑浣f瘮渚�" prop="brokerageFirstPercent">
+ <el-input-number
+ v-model="formData.brokerageFirstPercent"
+ :max="100"
+ :min="0"
+ class="!w-xs"
+ placeholder="璇疯緭鍏ヤ竴绾ц繑浣f瘮渚�"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 璁㈠崟浜ゆ槗鎴愬姛鍚庣粰鎺ㄥ箍浜鸿繑浣g殑鐧惧垎姣�
+ </el-text>
+ </el-form-item>
+ <el-form-item label="浜岀骇杩斾剑姣斾緥" prop="brokerageSecondPercent">
+ <el-input-number
+ v-model="formData.brokerageSecondPercent"
+ :max="100"
+ :min="0"
+ class="!w-xs"
+ placeholder="璇疯緭鍏ヤ簩绾ц繑浣f瘮渚�"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 璁㈠崟浜ゆ槗鎴愬姛鍚庣粰鎺ㄥ箍浜虹殑鎺ㄨ崘浜鸿繑浣g殑鐧惧垎姣�
+ </el-text>
+ </el-form-item>
+ <el-form-item label="浣i噾鍐荤粨澶╂暟" prop="brokerageFrozenDays">
+ <el-input-number
+ v-model="formData.brokerageFrozenDays"
+ :min="0"
+ class="!w-xs"
+ placeholder="璇疯緭鍏ヤ剑閲戝喕缁撳ぉ鏁�"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 闃叉鐢ㄦ埛閫�娆撅紝浣i噾琚彁鐜颁簡锛屾墍浠ラ渶瑕佽缃剑閲戝喕缁撴椂闂达紝鍗曚綅锛氬ぉ
+ </el-text>
+ </el-form-item>
+ <el-form-item label="鎻愮幇鏈�浣庨噾棰�" prop="brokerageWithdrawMinPrice">
+ <el-input-number
+ v-model="formData.brokerageWithdrawMinPrice"
+ :min="0"
+ :precision="2"
+ class="!w-xs"
+ placeholder="璇疯緭鍏ユ彁鐜版渶浣庨噾棰�"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 鐢ㄦ埛鎻愮幇鏈�浣庨噾棰濋檺鍒讹紝鍗曚綅锛氬厓
+ </el-text>
+ </el-form-item>
+ <el-form-item label="鎻愮幇鎵嬬画璐�" prop="brokerageWithdrawFeePercent">
+ <el-input-number
+ v-model="formData.brokerageWithdrawFeePercent"
+ :max="100"
+ :min="0"
+ class="!w-xs"
+ placeholder="璇疯緭鍏ユ彁鐜版墜缁垂"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 鎻愮幇鎵嬬画璐圭櫨鍒嗘瘮锛岃寖鍥� 0-100锛�0 涓烘棤鎻愮幇鎵嬬画璐广�備緥锛氳缃� 10锛屽嵆鏀跺彇 10% 鎵嬬画璐癸紝鎻愮幇
+ 10 鍏冿紝鍒拌处 9 鍏冿紝1 鍏冩墜缁垂
+ </el-text>
+ </el-form-item>
+ <el-form-item label="鎻愮幇鏂瑰紡" prop="brokerageWithdrawTypes">
+ <el-checkbox-group v-model="formData.brokerageWithdrawTypes">
+ <el-checkbox
+ v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-checkbox>
+ </el-checkbox-group>
+ <el-text class="w-full" size="small" type="info"> 鍟嗗煄寮�閫氭彁鐜扮殑浠樻鏂瑰紡</el-text>
+ </el-form-item>
+ </el-tab-pane>
+ </el-tabs>
+ <!-- 淇濆瓨 -->
+ <el-form-item>
+ <el-button :loading="formLoading" type="primary" @click="submitForm"> 淇濆瓨</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/mall/trade/config'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'TradeConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formRef = ref()
+const formData = ref({
+ id: null,
+ afterSaleRefundReasons: [],
+ afterSaleReturnReasons: [],
+ deliveryExpressFreeEnabled: false,
+ deliveryExpressFreePrice: 0,
+ deliveryPickUpEnabled: false,
+ brokerageEnabled: false,
+ brokerageEnabledCondition: undefined,
+ brokerageBindMode: undefined,
+ brokeragePosterUrls: [],
+ brokerageFirstPercent: 0,
+ brokerageSecondPercent: 0,
+ brokerageWithdrawMinPrice: 0,
+ brokerageWithdrawFeePercent: 0,
+ brokerageFrozenDays: 0,
+ brokerageWithdrawTypes: []
+})
+const formRules = reactive({
+ deliveryExpressFreePrice: [{ required: true, message: '婊¢鍖呴偖涓嶈兘涓虹┖', trigger: 'blur' }],
+ brokerageEnabledCondition: [{ required: true, message: '鍒嗕剑妯″紡涓嶈兘涓虹┖', trigger: 'blur' }],
+ brokerageBindMode: [{ required: true, message: '鍒嗛攢鍏崇郴缁戝畾妯″紡涓嶈兘涓虹┖', trigger: 'blur' }],
+ brokerageFirstPercent: [{ required: true, message: '涓�绾ц繑浣f瘮渚嬩笉鑳戒负绌�', trigger: 'blur' }],
+ brokerageSecondPercent: [{ required: true, message: '浜岀骇杩斾剑姣斾緥涓嶈兘涓虹┖', trigger: 'blur' }],
+ brokerageWithdrawMinPrice: [
+ { required: true, message: '鐢ㄦ埛鎻愮幇鏈�浣庨噾棰濅笉鑳戒负绌�', trigger: 'blur' }
+ ],
+ brokerageWithdrawFeePercent: [{ required: true, message: '鎻愮幇鎵嬬画璐逛笉鑳戒负绌�', trigger: 'blur' }],
+ brokerageFrozenDays: [{ required: true, message: '浣i噾鍐荤粨鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }],
+ brokerageWithdrawTypes: [
+ {
+ required: true,
+ message: '鎻愮幇鏂瑰紡涓嶈兘涓虹┖',
+ trigger: 'change'
+ }
+ ]
+})
+
+const submitForm = async () => {
+ if (formLoading.value) return
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = cloneDeep(unref(formData.value)) as unknown as ConfigApi.ConfigVO
+ // 閲戦鏀惧ぇ
+ data.deliveryExpressFreePrice = data.deliveryExpressFreePrice * 100
+ data.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice * 100
+ await ConfigApi.saveTradeConfig(data)
+ message.success('淇濆瓨鎴愬姛')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鏌ヨ浜ゆ槗涓績閰嶇疆 */
+const getConfig = async () => {
+ formLoading.value = true
+ try {
+ const data = await ConfigApi.getTradeConfig()
+ if (data != null) {
+ formData.value = data
+ // 閲戦缂╁皬
+ formData.value.deliveryExpressFreePrice = data.deliveryExpressFreePrice / 100
+ formData.value.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice / 100
+ }
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getConfig()
+})
+</script>
diff --git a/src/views/mall/trade/delivery/express/ExpressForm.vue b/src/views/mall/trade/delivery/express/ExpressForm.vue
new file mode 100644
index 0000000..8b759f2
--- /dev/null
+++ b/src/views/mall/trade/delivery/express/ExpressForm.vue
@@ -0,0 +1,126 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍏徃缂栫爜" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ュ揩閫掔紪鐮�" />
+ </el-form-item>
+ <el-form-item label="鍏徃鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ揩閫掑悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍏徃 logo" prop="logo">
+ <UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
+ <div style="font-size: 10px" class="pl-10px">鎺ㄨ崘 180x180 鍥剧墖鍒嗚鲸鐜�</div>
+ </el-form-item>
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+
+defineOptions({ name: 'ExpressForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ code: '',
+ name: '',
+ logo: '',
+ sort: 0,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ code: [{ required: true, message: '蹇�掔紪鐮佷笉鑳戒负绌�', trigger: 'blur' }],
+ name: [{ required: true, message: '鍒嗙被鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ logo: [{ required: true, message: '鍒嗙被鍥剧墖涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鍒嗙被鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DeliveryExpressApi.getDeliveryExpress(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as DeliveryExpressApi.DeliveryExpressVO
+ if (formType.value === 'create') {
+ await DeliveryExpressApi.createDeliveryExpress(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DeliveryExpressApi.updateDeliveryExpress(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ picUrl: '',
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/trade/delivery/express/index.vue b/src/views/mall/trade/delivery/express/index.vue
new file mode 100644
index 0000000..1cde87d
--- /dev/null
+++ b/src/views/mall/trade/delivery/express/index.vue
@@ -0,0 +1,189 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝揩閫掑彂璐�" url="https://doc.iocoder.cn/mall/trade-delivery-express/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="蹇�掑叕鍙哥紪鍙�" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭蹇�掑叕鍙哥紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="蹇�掑叕鍙稿悕绉�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭蹇�掑叕鍙稿悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['trade:delivery:express:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['trade:delivery:express:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍏徃缂栫爜" prop="code" />
+ <el-table-column label="鍏徃鍚嶇О" prop="name" />
+ <el-table-column label="鍏徃 logo " prop="logo">
+ <template #default="scope">
+ <img v-if="scope.row.logo" :src="scope.row.logo" alt="鍏徃logo" class="h-40px" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="寮�鍚姸鎬�" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['trade:delivery:express:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['trade:delivery:express:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ExpressForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import ExpressForm from './ExpressForm.vue'
+
+defineOptions({ name: 'Express' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ code: '',
+ name: ''
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeliveryExpressApi.getDeliveryExpressPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DeliveryExpressApi.deleteDeliveryExpress(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await DeliveryExpressApi.exportDeliveryExpressApi(queryParams)
+ download.excel(data, '蹇�掑叕鍙�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue b/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue
new file mode 100644
index 0000000..547aece
--- /dev/null
+++ b/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue
@@ -0,0 +1,321 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="1300px">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユā鏉垮悕绉�" />
+ </el-form-item>
+ <el-form-item label="璁¤垂鏂瑰紡" prop="chargeMode">
+ <el-radio-group v-model="formData.chargeMode" @change="changeChargeMode">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="杩愯垂" prop="charges">
+ <el-table border style="width: 100%" :data="formData.charges">
+ <el-table-column align="center" label="鍖哄煙" width="360">
+ <template #default="{ row }">
+ <el-cascader
+ v-model="row.areaIds"
+ :options="areaTree"
+ :props="defaultProps2"
+ class="w-1/1"
+ clearable
+ placeholder="璇烽�夋嫨鍦板尯"
+ filterable
+ collapse-tags
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ align="center"
+ :label="columnTitle.startCountTitle"
+ width="180"
+ prop="startCount"
+ >
+ <template #default="{ row }">
+ <el-input-number v-model="row.startCount" :min="1" />
+ </template>
+ </el-table-column>
+ <el-table-column width="180" align="center" label="杩愯垂(鍏�)" prop="startPrice">
+ <template #default="{ row }">
+ <el-input-number v-model="row.startPrice" :min="1" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ width="180"
+ align="center"
+ :label="columnTitle.extraCountTitle"
+ prop="extraCount"
+ >
+ <template #default="{ row }">
+ <el-input-number v-model="row.extraCount" :min="1" />
+ </template>
+ </el-table-column>
+ <el-table-column width="180" align="center" label="缁垂(鍏�)" prop="extraPrice">
+ <template #default="{ row }">
+ <el-input-number v-model="row.extraPrice" :min="1" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button link type="danger" @click="deleteChargeArea(scope.$index)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" plain @click="addChargeArea()">
+ <Icon icon="ep:plus" class="mr-5px" /> 娣诲姞鍖哄煙
+ </el-button>
+ </el-form-item>
+ <el-form-item label="鍖呴偖鍖哄煙" prop="frees">
+ <el-table border style="width: 100%" :data="formData.frees">
+ <el-table-column align="center" label="鍖哄煙" width="360">
+ <template #default="{ row }">
+ <el-cascader
+ v-model="row.areaIds"
+ :options="areaTree"
+ :props="defaultProps2"
+ class="w-1/1"
+ clearable
+ placeholder="璇烽�夋嫨鍟嗗搧鍒嗙被"
+ filterable
+ collapse-tags
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" :label="columnTitle.freeCountTitle" prop="freeCount">
+ <template #default="{ row }">
+ <el-input-number v-model="row.freeCount" :min="1" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍖呴偖閲戦锛堝厓锛�" prop="freePrice">
+ <template #default="{ row }">
+ <el-input-number v-model="row.freePrice" :min="1" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button link type="danger" @click="deleteFreeArea(scope.$index)"> 鍒犻櫎 </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" plain @click="addFreeArea()">
+ <Icon icon="ep:plus" class="mr-5px" /> 娣诲姞鍖哄煙
+ </el-button>
+ </el-form-item>
+ <el-form-item label="鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import { yuanToFen, fenToYuan } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const defaultProps2 = {
+ ...defaultProps,
+ multiple: true
+}
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ chargeMode: 1,
+ sort: 0,
+ charges: [],
+ frees: []
+})
+const columnTitleMap = new Map()
+const columnTitle = ref({
+ startCountTitle: '棣栦欢',
+ extraCountTitle: '缁欢',
+ freeCountTitle: '鍖呴偖浠舵暟'
+})
+const formRules = reactive({
+ name: [{ required: true, message: '妯℃澘鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ chargeMode: [{ required: true, message: '閰嶉�佽璐规柟寮忎笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鍒嗙被鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ try {
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ formData.value = await DeliveryExpressTemplateApi.getDeliveryExpressTemplate(id)
+ columnTitle.value = columnTitleMap.get(formData.value.chargeMode)
+ formData.value.charges.forEach((item) => {
+ // 鍓嶇浠锋牸浠ュ厓灞曠ず
+ item.startPrice = fenToYuan(item.startPrice)
+ item.extraPrice = fenToYuan(item.extraPrice)
+ })
+ formData.value.frees.forEach((item) => {
+ item.freePrice = fenToYuan(item.freePrice)
+ })
+ }
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = cloneDeep(formData.value) as DeliveryExpressTemplateApi.DeliveryExpressTemplateVO
+ // 鍓嶇浠锋牸浠ュ厓灞曠ず锛屾彁浜ゅ埌鍚庣銆傜敤鍒嗚绠�
+ data.charges.forEach((item) => {
+ item.startPrice = yuanToFen(item.startPrice)
+ item.extraPrice = yuanToFen(item.extraPrice)
+ })
+ data.frees.forEach((item) => {
+ item.freePrice = yuanToFen(item.freePrice)
+ })
+ if (formType.value === 'create') {
+ await DeliveryExpressTemplateApi.createDeliveryExpressTemplate(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DeliveryExpressTemplateApi.updateDeliveryExpressTemplate(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ chargeMode: 1,
+ charges: [
+ {
+ areaIds: [1],
+ startCount: 2,
+ startPrice: 5,
+ extraCount: 5,
+ extraPrice: 10
+ }
+ ],
+ frees: [],
+ sort: 0
+ }
+ columnTitle.value = columnTitleMap.get(1)
+ formRef.value?.resetFields()
+}
+
+/** 閰嶉�佽璐规柟娉曟敼鍙� */
+const changeChargeMode = (chargeMode: number) => {
+ columnTitle.value = columnTitleMap.get(chargeMode)
+}
+
+/** 鍒濆鍖栨暟鎹� */
+const areaTree = ref([])
+const initData = async () => {
+ // 琛ㄥご鏍囬鍜岃璐规柟寮忕殑鏄犲皠
+ columnTitleMap.set(1, {
+ startCountTitle: '棣栦欢',
+ extraCountTitle: '缁欢',
+ freeCountTitle: '鍖呴偖浠舵暟'
+ })
+ columnTitleMap.set(2, {
+ startCountTitle: '棣栦欢閲嶉噺(kg)',
+ extraCountTitle: '缁欢閲嶉噺(kg)',
+ freeCountTitle: '鍖呴偖閲嶉噺(kg)'
+ })
+ columnTitleMap.set(3, {
+ startCountTitle: '棣栦欢浣撶Н(m鲁)',
+ extraCountTitle: '缁欢浣撶Н(m鲁)',
+ freeCountTitle: '鍖呴偖浣撶Н(m鲁)'
+ })
+ // 鍔犺浇鍖哄煙鏁版嵁
+ areaTree.value = await AreaApi.getAreaTree()
+}
+
+/** 娣诲姞璁¤垂鍖哄煙 */
+const addChargeArea = () => {
+ const data = formData.value
+ data.charges.push({
+ areaIds: [],
+ startCount: 1,
+ startPrice: 1,
+ extraCount: 1,
+ extraPrice: 1
+ })
+}
+
+/** 鍒犻櫎璁¤垂鍖哄煙 */
+const deleteChargeArea = (index) => {
+ const data = formData.value
+ data.charges.splice(index, 1)
+}
+
+/** 娣诲姞鍖呴偖鍖哄煙 */
+const addFreeArea = () => {
+ const data = formData.value
+ data.frees.push({
+ areaIds: [],
+ freeCount: 1,
+ freePrice: 1
+ })
+}
+
+/** 鍒犻櫎鍖呴偖鍖哄煙 */
+const deleteFreeArea = (index) => {
+ const data = formData.value
+ data.frees.splice(index, 1)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ initData()
+})
+</script>
diff --git a/src/views/mall/trade/delivery/expressTemplate/index.vue b/src/views/mall/trade/delivery/expressTemplate/index.vue
new file mode 100644
index 0000000..9d0688a
--- /dev/null
+++ b/src/views/mall/trade/delivery/expressTemplate/index.vue
@@ -0,0 +1,165 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝揩閫掑彂璐�" url="https://doc.iocoder.cn/mall/trade-delivery-express/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="璁¤垂鏂瑰紡" prop="chargeMode">
+ <el-select
+ v-model="queryParams.chargeMode"
+ placeholder="璁¤垂鏂瑰紡"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['trade:delivery:express-template:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" min-width="60" prop="id" />
+ <el-table-column label="妯℃澘鍚嶇О" min-width="100" prop="name" />
+ <el-table-column label="璁¤垂鏂瑰紡" prop="chargeMode" min-width="100" align="center">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.EXPRESS_CHARGE_MODE" :value="scope.row.chargeMode" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掑簭" min-width="100" prop="sort" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['trade:delivery:express-template:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['trade:delivery:express-template:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ExpressTemplateForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import ExpressTemplateForm from './ExpressTemplateForm.vue'
+
+defineOptions({ name: 'DeliveryExpressTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ chargeMode: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeliveryExpressTemplateApi.getDeliveryExpressTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DeliveryExpressTemplateApi.deleteDeliveryExpressTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/trade/delivery/pickUpOrder/index.vue b/src/views/mall/trade/delivery/pickUpOrder/index.vue
new file mode 100644
index 0000000..5d4fc4f
--- /dev/null
+++ b/src/views/mall/trade/delivery/pickUpOrder/index.vue
@@ -0,0 +1,436 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戜氦鏄撹鍗�" url="https://doc.iocoder.cn/mall/trade-order/" />
+ <doc-alert title="銆愪氦鏄撱�戣喘鐗╄溅" url="https://doc.iocoder.cn/mall/trade-cart/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-280px"
+ end-placeholder="鑷畾涔夋椂闂�"
+ start-placeholder="鑷畾涔夋椂闂�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="鑷彁闂ㄥ簵" prop="pickUpStoreIds">
+ <el-select
+ v-model="queryParams.pickUpStoreIds"
+ class="!w-280px"
+ placeholder="鍏ㄩ儴"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="item in pickUpStoreList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑱氬悎鎼滅储">
+ <el-input
+ v-show="true"
+ v-model="queryParams[queryType.queryParam]"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏�"
+ :type="queryType.queryParam === 'userId' ? 'number' : 'text'"
+ >
+ <template #prepend>
+ <el-select
+ v-model="queryType.queryParam"
+ class="!w-110px"
+ placeholder="鍏ㄩ儴"
+ @change="inputChangeSelect"
+ >
+ <el-option
+ v-for="dict in dynamicSearchList"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ @click="handlePickup"
+ type="success"
+ plain
+ v-hasPermi="['trade:order:pick-up']"
+ :disabled="isUse"
+ >
+ <Icon class="mr-5px" icon="ep:check" />
+ 鏍搁攢
+ </el-button>
+ <el-button type="primary" @click="connectToSerialPort" :disabled="serialPort || isUse">
+ 杩炴帴鎵弿鏋�
+ </el-button>
+ <el-button type="danger" @click="cutPort" :disabled="!serialPort || isUse">
+ 鏂紑鎵弿鏋�
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 缁熻鍗$墖 -->
+ <el-row :gutter="16" class="summary">
+ <el-col :sm="6" :xs="12" v-loading="loading">
+ <SummaryCard
+ title="璁㈠崟鏁伴噺"
+ icon="icon-park-outline:transaction-order"
+ icon-color="bg-blue-100"
+ icon-bg-color="text-blue-500"
+ :value="summary?.orderCount || 0"
+ />
+ </el-col>
+ <el-col :sm="6" :xs="12" v-loading="loading">
+ <SummaryCard
+ title="璁㈠崟閲戦"
+ icon="streamline:money-cash-file-dollar-common-money-currency-cash-file"
+ icon-color="bg-purple-100"
+ icon-bg-color="text-purple-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(summary?.orderPayPrice || 0)"
+ />
+ </el-col>
+ <el-col :sm="6" :xs="12" v-loading="loading">
+ <SummaryCard
+ title="閫�娆惧崟鏁�"
+ icon="heroicons:receipt-refund"
+ icon-color="bg-yellow-100"
+ icon-bg-color="text-yellow-500"
+ :value="summary?.afterSaleCount || 0"
+ />
+ </el-col>
+ <el-col :sm="6" :xs="12" v-loading="loading">
+ <SummaryCard
+ title="閫�娆鹃噾棰�"
+ icon="ri:refund-2-line"
+ icon-color="bg-green-100"
+ icon-bg-color="text-green-500"
+ prefix="锟�"
+ :decimals="2"
+ :value="fenToYuan(summary?.afterSalePrice || 0)"
+ />
+ </el-col>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="璁㈠崟鍙�" align="center" prop="no" min-width="180" />
+ <el-table-column label="鐢ㄦ埛淇℃伅" align="center" prop="user.nickname" min-width="80" />
+ <el-table-column
+ label="鎺ㄨ崘浜轰俊鎭�"
+ align="center"
+ prop="brokerageUser.nickname"
+ min-width="100"
+ />
+ <el-table-column label="鍟嗗搧淇℃伅" align="center" prop="spuName" min-width="300">
+ <template #default="{ row }">
+ <div class="flex items-center" v-for="item in row.items" :key="item.id">
+ <el-image
+ :src="item.picUrl"
+ class="mr-10px h-30px w-30px flex-shrink-0"
+ :preview-src-list="[item.picUrl]"
+ preview-teleported
+ />
+ <span class="mr-10px">{{ item.spuName }}</span>
+ <div class="flex flex-col flex-wrap gap-1">
+ <el-tag
+ v-for="property in item.properties"
+ :key="property.propertyId"
+ class="mr-10px"
+ >
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ <span>{{ floatToFixed2(item.price) }} 鍏� x {{ item.count }}</span>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="瀹炰粯閲戦(鍏�)"
+ align="center"
+ prop="payPrice"
+ min-width="110"
+ :formatter="fenToYuanFormat"
+ />
+ <el-table-column label="鏍搁攢鍛�" align="center" prop="storeStaffName" min-width="70" />
+ <el-table-column label="鏍搁攢闂ㄥ簵" align="center" prop="pickUpStoreId" min-width="80">
+ <template #default="{ row }">
+ {{ pickUpStoreList.find((p) => p.id === row.pickUpStoreId)?.name }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀粯鐘舵��" align="center" prop="payStatus" min-width="80">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.payStatus || false" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="璁㈠崟鐘舵��" prop="status" width="120">
+ <template #default="{ row }">
+ <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓嬪崟鏃堕棿"
+ align="center"
+ prop="createTime"
+ min-width="170"
+ :formatter="dateFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 鍚勭鎿嶄綔鐨勫脊绐� -->
+ <OrderPickUpForm ref="pickUpForm" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import type { FormInstance } from 'element-plus'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { DICT_TYPE } from '@/utils/dict'
+import { fenToYuan, floatToFixed2 } from '@/utils'
+import { fenToYuanFormat } from '@/utils/formatter'
+import SummaryCard from '@/components/SummaryCard/index.vue'
+import { dateFormatter } from '@/utils/formatTime'
+import { DeliveryTypeEnum } from '@/utils/constants'
+import { TradeOrderSummaryRespVO } from '@/api/mall/trade/order'
+import { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
+import OrderPickUpForm from '@/views/mall/trade/order/form/OrderPickUpForm.vue'
+import { ref, onMounted } from 'vue'
+import { useUserStore } from '@/store/modules/user'
+const message = useMessage() // 娑堟伅寮圭獥
+
+const port = ref('')
+const ports = ref([])
+const reader = ref('')
+
+defineOptions({ name: 'PickUpOrder' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(2) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<TradeOrderApi.OrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref<FormInstance>() // 鎼滅储鐨勮〃鍗�
+const INIT_QUERY_PARAMS = {
+ // 椤垫暟
+ pageNo: 1,
+ // 姣忛〉鏄剧ず鏁伴噺
+ pageSize: 10,
+ // 鍒涘缓鏃堕棿
+ createTime: undefined,
+ // 閰嶉�佹柟寮�
+ deliveryType: DeliveryTypeEnum.PICK_UP.type,
+ // 鑷彁闂ㄥ簵
+ pickUpStoreIds: -1
+} // 鍒濆琛ㄥ崟鍙傛暟
+
+const queryParams = ref({ ...INIT_QUERY_PARAMS }) // 琛ㄥ崟鎼滅储
+const queryType = reactive({ queryParam: 'no' }) // 璁㈠崟鎼滅储绫诲瀷 queryParam
+const summary = ref<TradeOrderSummaryRespVO>() // 璁㈠崟缁熻鏁版嵁
+
+const serialPort = ref(false) // 鏄惁杩炴帴鎵爜鏋�
+const isUse = ref(true) // 鏄惁鍙牳閿�
+
+// 璁㈠崟鑱氬悎鎼滅储 select 绫诲瀷閰嶇疆锛堝姩鎬佹悳绱級
+const dynamicSearchList = ref([
+ { value: 'no', label: '璁㈠崟鍙�' },
+ { value: 'userId', label: '鐢ㄦ埛 UID' },
+ { value: 'userNickname', label: '鐢ㄦ埛鏄电О' },
+ { value: 'userMobile', label: '鐢ㄦ埛鐢佃瘽' }
+])
+/**
+ * 鑱氬悎鎼滅储鍒囨崲鏌ヨ瀵硅薄鏃惰Е鍙�
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+ dynamicSearchList.value
+ .filter((item) => item.value !== val)
+ ?.forEach((item) => {
+ // 娓呴櫎闆嗗悎鎼滅储鏃犵敤灞炴��
+ if (queryParams.value.hasOwnProperty(item.value)) {
+ delete queryParams.value[item.value]
+ }
+ })
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ // 缁熻
+ summary.value = await TradeOrderApi.getOrderSummary(unref(queryParams))
+ // 鍒嗛〉
+ const data = await TradeOrderApi.getOrderPage(unref(queryParams))
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = async () => {
+ queryParams.value.pageNo = 1
+ await getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ queryParams.value = { ...INIT_QUERY_PARAMS }
+ if (pickUpStoreList.value.length > 0) {
+ queryParams.value.pickUpStoreIds = pickUpStoreList.value[0].id
+ }
+ handleQuery()
+}
+
+/** 鑷彁闂ㄥ簵绮剧畝鍒楄〃 */
+const pickUpStoreList = ref<DeliveryPickUpStoreVO[]>([])
+const getPickUpStoreList = async () => {
+ pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
+ // 绉婚櫎鑷繁鏃犳硶鏍搁攢鐨勯棬搴�
+ const userId = useUserStore().getUser.id
+ pickUpStoreList.value = pickUpStoreList.value.filter((item) =>
+ item.verifyUserIds?.includes(userId)
+ )
+}
+
+/** 鏄剧ず鏍搁攢琛ㄥ崟 */
+const pickUpForm = ref()
+const handlePickup = () => {
+ pickUpForm.value.open()
+}
+
+/** 杩炴帴鎵爜鏋� */
+const connectToSerialPort = async () => {
+ try {
+ // 鍒ゆ柇娴忚鍣ㄦ敮鎸佷覆鍙i�氫俊
+ if (
+ 'serial' in navigator &&
+ navigator.serial != null &&
+ typeof navigator.serial === 'object' &&
+ 'requestPort' in navigator.serial
+ ) {
+ // 鎻愮ず鐢ㄦ埛閫夋嫨涓�涓覆鍙�
+ port.value = await navigator.serial.requestPort()
+ } else {
+ message.error('娴忚鍣ㄤ笉鏀寔鎵爜鏋繛鎺ワ紝璇锋洿鎹㈡祻瑙堝櫒閲嶈瘯')
+ return
+ }
+
+ // 鑾峰彇鐢ㄦ埛涔嬪墠鎺堜簣璇ョ綉绔欒闂潈闄愮殑鎵�鏈変覆鍙c��
+ ports.value = await navigator.serial.getPorts()
+
+ // console.log(port.value, ports.value);
+ // console.log(port.value)
+ // 绛夊緟涓插彛鎵撳紑
+ await port.value.open({ baudRate: 9600, dataBits: 8, stopBits: 2 })
+
+ // console.log(typeof port.value);
+ message.success('鎴愬姛杩炴帴鎵爜鏋�')
+ serialPort.value = true
+ // readData(port.value);
+ readData()
+ } catch (error) {
+ // 澶勭悊杩炴帴涓插彛鍑洪敊鐨勬儏鍐�
+ console.log('Error connecting to serial port:', error)
+ }
+}
+
+/** 鐩戝惉鎵爜鏋緭鍏� */
+const readData = async () => {
+ reader.value = port.value.readable.getReader()
+ let data = '' //鎵爜鏁版嵁
+ // 鐩戝惉鏉ヨ嚜涓插彛鐨勬暟鎹�
+ while (true) {
+ const { value, done } = await reader.value.read()
+ if (done) {
+ // 鍏佽绋嶅悗鍏抽棴涓插彛
+ reader.value.releaseLock()
+ break
+ }
+ // 鑾峰彇鍙戦�佺殑鏁版嵁
+ const serialData = new TextDecoder().decode(value)
+ data = `${data}${serialData}`
+ if (serialData.includes('\r')) {
+ //璇诲彇缁撴潫
+ let codeData = data.replace('\r', '')
+ data = '' //娓呯┖涓嬫璇诲彇涓嶄細鍙犲姞
+ console.log(`浜岀淮鐮佹暟鎹�:${codeData}`)
+ //澶勭悊鎷垮埌鏁版嵁閫昏緫
+ pickUpForm.value.open(codeData)
+ }
+ }
+}
+
+/** 鏂紑鎵爜鏋� */
+const cutPort = async () => {
+ if (port.value !== '') {
+ await reader.value.cancel()
+ await port.value.close()
+ port.value = ''
+ console.log('鏂紑鎵爜鏋繛鎺�')
+ message.success('宸叉垚鍔熸柇寮�鎵爜鏋繛鎺�')
+ serialPort.value = false
+ } else {
+ message.warning('璇峰厛杩炴帴鎴栨墦寮�鎵爜鏋�')
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getPickUpStoreList()
+ if (pickUpStoreList.value.length === 0) {
+ message.error('褰撳墠鐧诲綍浜烘病缁戝畾浠讳綍鑷彁鐐�')
+ loading.value = false
+ isUse.value = true
+ return
+ }
+
+ // 鏌ヨ
+ queryParams.value.pickUpStoreIds = pickUpStoreList.value[0].id
+ isUse.value = false
+ await getList()
+})
+</script>
+<style lang="scss" scoped>
+:deep(.order-table-col > .cell) {
+ padding: 0;
+}
+
+.summary {
+ .el-col {
+ margin-bottom: 1rem;
+ }
+}
+</style>
diff --git a/src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue b/src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue
new file mode 100644
index 0000000..2bca08b
--- /dev/null
+++ b/src/views/mall/trade/delivery/pickUpStore/DeliveryPickUpStoreBindForm.vue
@@ -0,0 +1,143 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="20%">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="闂ㄥ簵鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ棬搴楀悕绉�" readonly />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="闂ㄥ簵搴楀憳" prop="verifyUserIds">
+ <el-button type="primary" @click="storeStaffTableSelect.open()">閫夋嫨搴楀憳</el-button>
+ </el-form-item>
+ <!-- 搴楀憳鍒楄〃 -->
+ <ContentWrap v-if="formData.verifyUsers?.length > 0">
+ <el-table :data="formData.verifyUsers">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="鐢ㄦ埛鏄电О"
+ align="center"
+ prop="nickname"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="鐘舵��" align="center" key="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['trade:delivery:pick-up-store:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+
+ <!-- 閫夋嫨鍛樺伐寮圭獥 -->
+ <StoreStaffTableSelect ref="storeStaffTableSelect" @change="handleSelect" />
+</template>
+<script setup lang="ts">
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import StoreStaffTableSelect from './components/StoreStaffTableSelect.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ name: '',
+ verifyUserIds: [],
+ verifyUsers: []
+})
+const formRules = reactive({})
+const formRef = ref() // 琛ㄥ崟 Ref
+const storeStaffTableSelect = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = '缁戝畾鑷彁闂ㄥ簵鍛樺伐'
+ resetForm()
+ formLoading.value = true
+ try {
+ formData.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(id)
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = {
+ id: formData.value.id,
+ verifyUserIds: formData.value.verifyUsers.map((item: any) => item.id)
+ }
+ await DeliveryPickUpStoreApi.bindStoreStaffId(data)
+ message.success('缁戝畾鎴愬姛')
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 澶勭悊閫夋嫨鍛樺伐鎿嶄綔 */
+const handleSelect = (checkedUsers: []) => {
+ formData.value.verifyUsers = checkedUsers
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ const index = formData.value.verifyUsers.findIndex((item: any) => {
+ if (item.id == id) {
+ return true
+ }
+ })
+ formData.value.verifyUsers.splice(index, 1)
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ verifyUserIds: [],
+ verifyUsers: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/trade/delivery/pickUpStore/PickUpStoreForm.vue b/src/views/mall/trade/delivery/pickUpStore/PickUpStoreForm.vue
new file mode 100644
index 0000000..f026ff6
--- /dev/null
+++ b/src/views/mall/trade/delivery/pickUpStore/PickUpStoreForm.vue
@@ -0,0 +1,262 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="闂ㄥ簵 logo" prop="logo">
+ <UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
+ <div style="font-size: 10px" class="pl-10px">鎺ㄨ崘 180x180 鍥剧墖鍒嗚鲸鐜�</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="闂ㄥ簵鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="闂ㄥ簵鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ棬搴楀悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="闂ㄥ簵鎵嬫満" prop="phone">
+ <el-input v-model="formData.phone" placeholder="璇疯緭鍏ラ棬搴楁墜鏈�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="闂ㄥ簵绠�浠�" prop="introduction">
+ <el-input
+ v-model="formData.introduction"
+ :rows="3"
+ type="textarea"
+ placeholder="璇疯緭鍏ラ棬搴楃畝浠�"
+ />
+ </el-form-item>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="闂ㄥ簵鎵�鍦ㄥ湴鍖�" prop="areaId">
+ <el-cascader v-model="formData.areaId" :options="areaList" :props="defaultProps" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="闂ㄥ簵璇︾粏鍦板潃" prop="detailAddress">
+ <el-input v-model="formData.detailAddress" placeholder="璇疯緭鍏ラ棬搴楄缁嗗湴鍧�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="钀ヤ笟寮�濮嬫椂闂�" prop="openingTime">
+ <el-time-select
+ v-model="formData.openingTime"
+ :max-time="formData.closingTime"
+ placeholder="寮�濮嬫椂闂�"
+ start="08:30"
+ step="00:15"
+ end="23:30"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="钀ヤ笟缁撴潫鏃堕棿" prop="closingTime">
+ <el-time-select
+ v-model="formData.closingTime"
+ :min-time="formData.openingTime"
+ placeholder="缁撴潫鏃堕棿"
+ start="08:30"
+ step="00:15"
+ end="23:30"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="缁忓害" prop="longitude">
+ <el-input v-model="formData.longitude" placeholder="璇疯緭鍏ラ棬搴楃粡搴�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绾害" prop="latitude">
+ <el-input v-model="formData.latitude" placeholder="璇疯緭鍏ラ棬搴楃含搴�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鑾峰彇缁忕含搴�">
+ <el-button type="primary" @click="mapDialogVisible = true">鑾峰彇</el-button>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ <el-dialog v-model="mapDialogVisible" title="鑾峰彇缁忕含搴�" append-to-body>
+ <IFrame class="h-609px" :src="tencentLbsUrl" />
+ </el-dialog>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps } from '@/utils/tree'
+import { getAreaTree } from '@/api/system/area'
+import * as ConfigApi from '@/api/mall/trade/config'
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const mapDialogVisible = ref(false) // 鍦板浘寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ phone: '',
+ logo: '',
+ detailAddress: '',
+ introduction: '',
+ areaId: 0,
+ openingTime: undefined,
+ closingTime: undefined,
+ latitude: undefined,
+ longitude: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '闂ㄥ簵鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ logo: [{ required: true, message: '闂ㄥ簵 logo 涓嶈兘涓虹┖', trigger: 'blur' }],
+ phone: [
+ { required: true, message: '闂ㄥ簵鎵嬫満涓嶈兘涓虹┖', trigger: 'blur' },
+ { pattern: /^1[3-9]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙风爜', trigger: 'blur' }
+ ],
+ areaId: [{ required: true, message: '闂ㄥ簵鎵�鍦ㄥ尯鍩熶笉鑳戒负绌�', trigger: 'blur' }],
+ detailAddress: [{ required: true, message: '闂ㄥ簵璇︾粏鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ openingTime: [{ required: true, message: '钀ヤ笟寮�濮嬫椂闂翠笉鑳戒负绌�', trigger: 'blur' }],
+ closingTime: [{ required: true, message: '钀ヤ笟缁撴潫鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }],
+ latitude: [{ required: true, message: '绾害涓嶈兘涓虹┖', trigger: 'blur' }],
+ longitude: [{ required: true, message: '缁忓害涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const areaList = ref() // 鍖哄煙鏍�
+const tencentLbsUrl = ref('') // 鑵捐浣嶇疆鏈嶅姟 url
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as DeliveryPickUpStoreApi.DeliveryPickUpStoreVO
+ if (formType.value === 'create') {
+ await DeliveryPickUpStoreApi.createDeliveryPickUpStore(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DeliveryPickUpStoreApi.updateDeliveryPickUpStore(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ phone: '',
+ logo: '',
+ detailAddress: '',
+ introduction: '',
+ areaId: undefined,
+ openingTime: undefined,
+ closingTime: undefined,
+ latitude: undefined,
+ longitude: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+
+/** 閫夋嫨缁忕含搴� */
+const selectAddress = function (loc: any): void {
+ if (loc.latlng && loc.latlng.lat) {
+ formData.value.latitude = loc.latlng.lat
+ }
+ if (loc.latlng && loc.latlng.lng) {
+ formData.value.longitude = loc.latlng.lng
+ }
+ mapDialogVisible.value = false
+}
+
+/** 鍒濆鍖栬吘璁湴鍥� */
+const initTencentLbsMap = async () => {
+ window.selectAddress = selectAddress
+ window.addEventListener(
+ 'message',
+ function (event) {
+ // 鎺ユ敹浣嶇疆淇℃伅锛岀敤鎴烽�夋嫨纭浣嶇疆鐐瑰悗閫夌偣缁勪欢浼氳Е鍙戣浜嬩欢锛屽洖浼犵敤鎴风殑浣嶇疆淇℃伅
+ let loc = event.data
+ if (loc && loc.module === 'locationPicker') {
+ // 闃叉鍏朵粬搴旂敤涔熶細鍚戣椤甸潰 post 淇℃伅锛岄渶鍒ゆ柇 module 鏄惁涓� 'locationPicker'
+ window.parent.selectAddress(loc)
+ }
+ },
+ false
+ )
+ const data = await ConfigApi.getTradeConfig()
+ const key = data.tencentLbsKey
+ tencentLbsUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${key}&referer=myapp`
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ areaList.value = await getAreaTree()
+ // 鍔犺浇鍦板浘
+ await initTencentLbsMap()
+})
+</script>
diff --git a/src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue b/src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue
new file mode 100644
index 0000000..c5acda3
--- /dev/null
+++ b/src/views/mall/trade/delivery/pickUpStore/components/StoreStaffTableSelect.vue
@@ -0,0 +1,264 @@
+<!-- TODO 鑺嬭壙锛氳繖鍧楀悗缁娊涓嫭绔嬬殑缁勪欢鍑烘潵 -->
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="60%">
+ <el-row :gutter="20">
+ <!-- 宸︿晶閮ㄩ棬鏍� -->
+ <el-col :span="4" :xs="24">
+ <ContentWrap class="h-1/1">
+ <DeptTree @node-click="handleDeptNodeClick" />
+ </ContentWrap>
+ </el-col>
+ <el-col :span="20" :xs="24">
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="username">
+ <el-input
+ v-model="queryParams.username"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="鐢ㄦ埛鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="datetimerange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" />鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" />閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column width="55">
+ <template #header>
+ <el-checkbox
+ v-model="isCheckAll"
+ :indeterminate="isIndeterminate"
+ @change="handleCheckAll"
+ />
+ </template>
+ <template #default="{ row }">
+ <el-checkbox
+ v-model="checkedStatus[row.id]"
+ @change="(checked: boolean) => handleCheckOne(checked, row, true)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" key="id" prop="id" />
+ <el-table-column
+ label="鐢ㄦ埛鍚嶇О"
+ align="center"
+ prop="username"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="鐢ㄦ埛鏄电О"
+ align="center"
+ prop="nickname"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="閮ㄩ棬"
+ align="center"
+ key="deptName"
+ prop="deptName"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐘舵��" key="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ </el-table>
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </el-col>
+ </el-row>
+ <template #footer>
+ <el-button type="primary" @click="handleEmitChange">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserApi from '@/api/system/user'
+import DeptTree from '@/views/system/user/DeptTree.vue'
+
+// 鏄惁鍏ㄩ��
+const isCheckAll = ref(false)
+// 鍏ㄩ�夋鏄惁澶勪簬涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+const isIndeterminate = ref(false)
+// 閫変腑鐨勬椿鍔�
+const checkedUsers = ref([])
+// 閫変腑鐘舵�侊細key涓虹敤鎴稩D锛寁alue涓烘槸鍚﹂�変腑
+const checkedStatus = ref<Record<string, boolean>>({})
+
+const dialogTitle = '閫夋嫨搴楀憳'
+const dialogVisible = ref(false)
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ username: undefined,
+ mobile: undefined,
+ status: undefined,
+ deptId: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await UserApi.getUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 澶勭悊閮ㄩ棬琚偣鍑� */
+const handleDeptNodeClick = async (row) => {
+ queryParams.deptId = row.id
+ await getList()
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ loading.value = true
+ try {
+ await getList()
+ } finally {
+ loading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckAll = (checked: boolean) => {
+ isCheckAll.value = checked
+ isIndeterminate.value = false
+
+ list.value.forEach((combinationActivity) => handleCheckOne(checked, combinationActivity, false))
+}
+
+/**
+ * 閫変腑涓�琛�
+ * @param checked 鏄惁閫変腑
+ * @param combinationActivity 娲诲姩
+ * @param isCalcCheckAll 鏄惁璁$畻鍏ㄩ��
+ */
+const handleCheckOne = (checked: boolean, combinationActivity, isCalcCheckAll: boolean) => {
+ if (checked) {
+ checkedUsers.value.push(combinationActivity as never)
+ checkedStatus.value[combinationActivity.id] = true
+ } else {
+ const index = findCheckedIndex(combinationActivity)
+ if (index > -1) {
+ checkedUsers.value.splice(index, 1)
+ checkedStatus.value[combinationActivity.id] = false
+ isCheckAll.value = false
+ }
+ }
+
+ // 璁$畻鍏ㄩ�夋鐘舵��
+ if (isCalcCheckAll) {
+ calculateIsCheckAll()
+ }
+}
+
+// 鏌ユ壘娲诲姩鍦ㄥ凡閫変腑娲诲姩鍒楄〃涓殑绱㈠紩
+const findCheckedIndex = (user) => checkedUsers.value.findIndex((item) => item.id === user.id)
+
+// 璁$畻鍏ㄩ�夋鐘舵��
+const calculateIsCheckAll = () => {
+ isCheckAll.value = list.value.every((user) => checkedStatus.value[user.id])
+ // 璁$畻涓棿鐘舵�侊細涓嶆槸鍏ㄩ儴閫変腑 && 浠绘剰涓�涓�変腑
+ isIndeterminate.value =
+ !isCheckAll.value && list.value.some((user) => checkedStatus.value[user.id])
+}
+
+/** 澶氶�夊畬鎴� */
+const handleEmitChange = () => {
+ // 鍏抽棴寮圭獥
+ dialogVisible.value = false
+ emits('change', [...checkedUsers.value])
+}
+
+/** 纭閫夋嫨鏃剁殑瑙﹀彂浜嬩欢 */
+const emits = defineEmits<{
+ change: [CombinationActivityApi: any]
+}>()
+</script>
diff --git a/src/views/mall/trade/delivery/pickUpStore/index.vue b/src/views/mall/trade/delivery/pickUpStore/index.vue
new file mode 100644
index 0000000..c7684be
--- /dev/null
+++ b/src/views/mall/trade/delivery/pickUpStore/index.vue
@@ -0,0 +1,207 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戝揩閫掑彂璐�" url="https://doc.iocoder.cn/mall/trade-delivery-express/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px">
+ <el-form-item label="闂ㄥ簵鎵嬫満" prop="phone">
+ <el-input
+ v-model="queryParams.phone"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭闂ㄥ簵鎵嬫満"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="闂ㄥ簵鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭闂ㄥ簵鍚嶇О"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="闂ㄥ簵鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="闂ㄥ簵鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="datetimerange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['trade:delivery:pick-up-store:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" min-width="80" prop="id" />
+ <el-table-column label="闂ㄥ簵 logo" min-width="100" prop="logo">
+ <template #default="scope">
+ <img v-if="scope.row.logo" :src="scope.row.logo" alt="闂ㄥ簵 logo" class="h-50px" />
+ </template>
+ </el-table-column>
+ <el-table-column label="闂ㄥ簵鍚嶇О" min-width="150" prop="name" />
+ <el-table-column label="闂ㄥ簵鎵嬫満" min-width="100" prop="phone" />
+ <el-table-column label="鍦板潃" min-width="100" prop="detailAddress" />
+ <el-table-column label="钀ヤ笟鏃堕棿" min-width="180">
+ <template #default="scope">
+ {{ scope.row.openingTime }} ~ {{ scope.row.closingTime }}
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="寮�鍚姸鎬�" min-width="100" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎿嶄綔" min-width="110">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['trade:delivery:pick-up-store:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['trade:delivery:pick-up-store:update']"
+ link
+ type="primary"
+ @click="openFormBind(scope.row.id)"
+ >
+ 缁戝畾搴楀憳
+ </el-button>
+ <el-button
+ v-hasPermi="['trade:delivery:pick-up-store:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DeliveryPickUpStoreForm ref="formRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氱粦瀹氬簵鍛� -->
+ <DeliveryPickUpStoreBindForm ref="formBindRef" />
+</template>
+<script lang="ts" name="DeliveryPickUpStore" setup>
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
+import DeliveryPickUpStoreBindForm from './DeliveryPickUpStoreBindForm.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ status: undefined,
+ phone: undefined,
+ name: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+const formBindRef = ref()
+const openFormBind = (id?: number) => {
+ formBindRef.value.open(id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DeliveryPickUpStoreApi.deleteDeliveryPickUpStore(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeliveryPickUpStoreApi.getDeliveryPickUpStorePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mall/trade/order/components/OrderTableColumn.vue b/src/views/mall/trade/order/components/OrderTableColumn.vue
new file mode 100644
index 0000000..6e387cf
--- /dev/null
+++ b/src/views/mall/trade/order/components/OrderTableColumn.vue
@@ -0,0 +1,303 @@
+<template>
+ <el-table-column class-name="order-table-col">
+ <template #header>
+ <div class="flex items-center" style="width: 100%">
+ <div :style="{ width: orderTableHeadWidthList[0] + 'px' }" class="flex justify-center">
+ 鍟嗗搧淇℃伅
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[1] + 'px' }" class="flex justify-center">
+ 鍗曚环(鍏�)/鏁伴噺
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[2] + 'px' }" class="flex justify-center">
+ 鍞悗鐘舵��
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[3] + 'px' }" class="flex justify-center">
+ 瀹炰粯閲戦(鍏�)
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[4] + 'px' }" class="flex justify-center">
+ 涔板/鏀惰揣浜�
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[5] + 'px' }" class="flex justify-center">
+ 閰嶉�佹柟寮�
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[6] + 'px' }" class="flex justify-center">
+ 璁㈠崟鐘舵��
+ </div>
+ <div :style="{ width: orderTableHeadWidthList[7] + 'px' }" class="flex justify-center">
+ 鎿嶄綔
+ </div>
+ </div>
+ </template>
+ <template #default="scope">
+ <el-table
+ :ref="setOrderTableRef"
+ :border="true"
+ :data="scope.row.items"
+ :header-cell-style="headerStyle"
+ :span-method="spanMethod"
+ style="width: 100%"
+ >
+ <el-table-column min-width="300" prop="spuName">
+ <template #header>
+ <div
+ class="h-[35px] flex items-center -mx-[10px] px-[20px]"
+ style="background-color: var(--app-content-bg-color)"
+ >
+ <span class="mr-20px">璁㈠崟鍙凤細{{ scope.row.no }} </span>
+ <span class="mr-20px">涓嬪崟鏃堕棿锛歿{ formatDate(scope.row.createTime) }}</span>
+ <span>璁㈠崟鏉ユ簮锛�</span>
+ <dict-tag :type="DICT_TYPE.TERMINAL" :value="scope.row.terminal" class="mr-20px" />
+ <span>鏀粯鏂瑰紡锛�</span>
+ <dict-tag
+ v-if="scope.row.payChannelCode"
+ :type="DICT_TYPE.PAY_CHANNEL_CODE"
+ :value="scope.row.payChannelCode"
+ class="mr-20px"
+ />
+ <span v-else class="mr-20px">鏈敮浠�</span>
+ <span v-if="scope.row.payTime" class="mr-20px">
+ 鏀粯鏃堕棿锛歿{ formatDate(scope.row.payTime) }}
+ </span>
+ <span>璁㈠崟绫诲瀷锛�</span>
+ <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="scope.row.type" />
+ </div>
+ </template>
+ <template #default="{ row }">
+ <div class="flex flex-wrap">
+ <div class="mb-[10px] mr-[10px] flex items-start">
+ <div class="mr-[10px]">
+ <el-image
+ :src="row.picUrl"
+ class="!h-[45px] !w-[45px]"
+ fit="contain"
+ @click="imagePreview(row.picUrl)"
+ >
+ <template #error>
+ <div class="image-slot">
+ <icon icon="ep:picture" />
+ </div>
+ </template>
+ </el-image>
+ </div>
+ <ElTooltip :content="row.spuName" placement="top">
+ <span class="overflow-ellipsis max-h-[45px] overflow-hidden">
+ {{ row.spuName }}
+ </span>
+ </ElTooltip>
+ </div>
+ <el-tag
+ v-for="property in row.properties"
+ :key="property.propertyId"
+ class="mb-[10px] mr-[10px]"
+ >
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍘熶环*鏁伴噺" prop="price" width="150">
+ <template #default="{ row }">
+ {{ floatToFixed2(row.price) }} 鍏� / {{ row.count }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍞悗鐘舵��" prop="afterSaleStatus" width="120">
+ <template #default="{ row }">
+ <dict-tag
+ :type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS"
+ :value="row.afterSaleStatus"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹為檯鏀粯" min-width="120" prop="payPrice">
+ <template #default>
+ {{ floatToFixed2(scope.row.payPrice) + '鍏�' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="涔板/鏀惰揣浜�" min-width="160">
+ <template #default>
+ <!-- 蹇�掑彂璐� -->
+ <div
+ v-if="scope.row.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+ class="flex flex-col"
+ >
+ <span>涔板锛歿{ scope.row.user?.nickname }}</span>
+ <span>
+ 鏀惰揣浜猴細{{ scope.row.receiverName }} {{ scope.row.receiverMobile }}
+ {{ scope.row.receiverAreaName }} {{ scope.row.receiverDetailAddress }}
+ </span>
+ </div>
+ <!-- 鑷彁 -->
+ <div
+ v-if="scope.row.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+ class="flex flex-col"
+ >
+ <span>
+ 闂ㄥ簵鍚嶇О锛�
+ {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.name }}
+ </span>
+ <span>
+ 闂ㄥ簵鎵嬫満锛�
+ {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.phone }}
+ </span>
+ <span>
+ 鑷彁闂ㄥ簵:
+ {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.detailAddress }}
+ </span>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="閰嶉�佹柟寮�" width="120">
+ <template #default>
+ <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="scope.row.deliveryType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="璁㈠崟鐘舵��" width="120">
+ <template #default>
+ <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="160">
+ <template #default>
+ <slot :row="scope.row"></slot>
+ </template>
+ </el-table-column>
+ </el-table>
+ </template>
+ </el-table-column>
+</template>
+<script lang="ts" setup>
+import { nextTick, onMounted, onUnmounted, watch } from 'vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { DeliveryTypeEnum } from '@/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import { floatToFixed2 } from '@/utils'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { OrderVO } from '@/api/mall/trade/order'
+import type { TableColumnCtx, TableInstance } from 'element-plus'
+import { createImageViewer } from '@/components/ImageViewer'
+import type { DeliveryPickUpStoreVO } from '@/api/mall/trade/delivery/pickUpStore'
+
+defineOptions({ name: 'OrderTableColumn' })
+
+const props = defineProps<{
+ list: OrderVO[]
+ pickUpStoreList: DeliveryPickUpStoreVO[]
+}>()
+
+const headerStyle = ({ row, columnIndex }: any) => {
+ // 琛ㄥご绗竴琛岀涓�鍒楀崰 8
+ if (columnIndex === 0) {
+ row[columnIndex].colSpan = 8
+ } else {
+ // 鍏朵綑鐨勪笉瑕�
+ row[columnIndex].colSpan = 0
+ return {
+ display: 'none'
+ }
+ }
+}
+
+interface SpanMethodProps {
+ row: TradeOrderApi.OrderItemRespVO
+ column: TableColumnCtx<TradeOrderApi.OrderItemRespVO>
+ rowIndex: number
+ columnIndex: number
+}
+
+type spanMethodResp = number[] | { rowspan: number; colspan: number } | undefined
+const spanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps): spanMethodResp => {
+ const len = props.list.find(
+ (order) => order.items?.findIndex((item) => item.id === row.id) !== -1
+ )?.items?.length
+ // 瑕佸悎骞剁殑鍒楋紝浠庨浂寮�濮�
+ const colIndex = [3, 4, 5, 6, 7]
+ if (colIndex.includes(columnIndex)) {
+ // 闄や簡绗竴琛屽叾浣欑殑涓嶈
+ if (rowIndex !== 0) {
+ return {
+ rowspan: 0,
+ colspan: 0
+ }
+ }
+ // 鍔ㄦ�佸悎骞惰
+ return {
+ rowspan: len!,
+ colspan: 1
+ }
+ }
+}
+
+const orderTableHeadWidthList = ref([300, 150, 120, 120, 160, 120, 120, 160]) // 澶撮儴 col 瀹藉害鍒濆鍖�
+let isFirstTable = false // 鏍囪鏄惁宸插鐞嗙涓�涓〃鏍�
+let firstTableInstance: TableInstance | null = null
+
+/** 瑙e喅 ref 鍦� v-for 涓殑鑾峰彇闂*/
+const setOrderTableRef = async (el: TableInstance) => {
+ if (!el) return
+ // 鍙鐞嗙涓�涓〃鏍煎疄渚�
+ if (!isFirstTable) {
+ isFirstTable = true
+ firstTableInstance = el
+ // 浣跨敤 nextTick 纭繚 DOM 宸插畬鍏ㄦ覆鏌�
+ await nextTick()
+ tableHeadWidthAuto(el)
+ }
+}
+
+/** 澶撮儴瀹藉害鑷�傚簲 */
+const tableHeadWidthAuto = (el: TableInstance) => {
+ if (!el) return
+ const columns = el.store.states.columns.value
+ if (columns.length === 0) {
+ return
+ }
+ columns.forEach((col: TableColumnCtx<TableInstance>, index: number) => {
+ if (col.realWidth) {
+ orderTableHeadWidthList.value[index] = col.realWidth
+ }
+ })
+}
+
+/** 鐩戝惉绐楀彛澶у皬鍙樺寲锛岄噸鏂拌绠楄〃澶村搴� */
+const handleResize = async () => {
+ if (firstTableInstance) {
+ await nextTick()
+ tableHeadWidthAuto(firstTableInstance!)
+ }
+}
+
+/** 鐩戝惉鍒楄〃鏁版嵁鍙樺寲锛岄噸鏂拌绠楄〃澶村搴� */
+watch(
+ () => props.list,
+ async () => {
+ // 鏁版嵁鍙樺寲鍚庯紝绛夊緟 DOM 鏇存柊瀹屾垚鍐嶉噸鏂拌绠楀搴�
+ await nextTick()
+ if (firstTableInstance) {
+ // 寤惰繜涓�灏忔鏃堕棿锛岀‘淇濊〃鏍煎凡瀹屽叏娓叉煋
+ await new Promise((resolve) => setTimeout(resolve, 100))
+ tableHeadWidthAuto(firstTableInstance!)
+ }
+ },
+ { deep: true }
+)
+
+onMounted(() => {
+ window.addEventListener('resize', handleResize)
+})
+
+onUnmounted(() => {
+ window.removeEventListener('resize', handleResize)
+})
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+</script>
+<style lang="scss" scoped>
+:deep(.order-table-col > .cell) {
+ padding: 0;
+}
+</style>
diff --git a/src/views/mall/trade/order/components/index.ts b/src/views/mall/trade/order/components/index.ts
new file mode 100644
index 0000000..9cce9fa
--- /dev/null
+++ b/src/views/mall/trade/order/components/index.ts
@@ -0,0 +1,3 @@
+import OrderTableColumn from './OrderTableColumn.vue'
+
+export { OrderTableColumn }
diff --git a/src/views/mall/trade/order/detail/index.vue b/src/views/mall/trade/order/detail/index.vue
new file mode 100644
index 0000000..592d2c8
--- /dev/null
+++ b/src/views/mall/trade/order/detail/index.vue
@@ -0,0 +1,427 @@
+<template>
+ <ContentWrap>
+ <!-- 璁㈠崟淇℃伅 -->
+ <el-descriptions title="璁㈠崟淇℃伅">
+ <el-descriptions-item label="璁㈠崟鍙�: ">{{ formData.no }}</el-descriptions-item>
+ <el-descriptions-item label="涔板: ">{{ formData?.user?.nickname }}</el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟绫诲瀷: ">
+ <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.type!" />
+ </el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟鏉ユ簮: ">
+ <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.terminal!" />
+ </el-descriptions-item>
+ <el-descriptions-item label="涔板鐣欒█: ">{{ formData.userRemark }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗗澶囨敞: ">{{ formData.remark }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯鍗曞彿: ">{{ formData.payOrderId }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡: ">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.payChannelCode!" />
+ </el-descriptions-item>
+ <el-descriptions-item v-if="formData.brokerageUser" label="鎺ㄥ箍鐢ㄦ埛: ">
+ {{ formData.brokerageUser?.nickname }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 璁㈠崟鐘舵�� -->
+ <el-descriptions :column="1" title="璁㈠崟鐘舵��">
+ <el-descriptions-item label="璁㈠崟鐘舵��: ">
+ <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="formData.status!" />
+ </el-descriptions-item>
+ <el-descriptions-item v-hasPermi="['trade:order:update']" label-class-name="no-colon">
+ <el-button
+ v-if="formData.status! === TradeOrderStatusEnum.UNPAID.status"
+ type="primary"
+ @click="updatePrice"
+ >
+ 璋冩暣浠锋牸
+ </el-button>
+ <el-button type="primary" @click="remark">澶囨敞</el-button>
+ <!-- 寰呭彂璐� -->
+ <template v-if="formData.status! === TradeOrderStatusEnum.UNDELIVERED.status">
+ <!-- 蹇�掑彂璐� -->
+ <el-button
+ v-if="formData.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+ type="primary"
+ @click="delivery"
+ >
+ 鍙戣揣
+ </el-button>
+ <el-button
+ v-if="formData.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+ type="primary"
+ @click="updateAddress"
+ >
+ 淇敼鍦板潃
+ </el-button>
+ <!-- 鍒板簵鑷彁 -->
+ <el-button
+ v-if="formData.deliveryType === DeliveryTypeEnum.PICK_UP.type && showPickUp"
+ type="primary"
+ @click="handlePickUp"
+ >
+ 鏍搁攢
+ </el-button>
+ </template>
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label><span style="color: red">鎻愰啋: </span></template>
+ 涔板浠樻鎴愬姛鍚庯紝璐ф灏嗙洿鎺ヨ繘鍏ユ偍鐨勫晢鎴峰彿锛堝井淇°�佹敮浠樺疂锛�<br />
+ 璇峰強鏃跺叧娉ㄤ綘鍙戝嚭鐨勫寘瑁圭姸鎬侊紝纭繚鍙互閰嶉�佽嚦涔板鎵嬩腑 <br />
+ 濡傛灉涔板琛ㄧず娌℃敹鍒拌揣鎴栬揣鐗╂湁闂锛岃鍙婃椂鑱旂郴涔板澶勭悊锛屽弸濂藉崗鍟�
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鍟嗗搧淇℃伅 -->
+ <el-descriptions title="鍟嗗搧淇℃伅">
+ <el-descriptions-item labelClassName="no-colon">
+ <el-row :gutter="20">
+ <el-col :span="15">
+ <el-table :data="formData.items" border>
+ <el-table-column label="鍟嗗搧" prop="spuName" width="auto">
+ <template #default="{ row }">
+ {{ row.spuName }}
+ <el-tag v-for="property in row.properties" :key="property.propertyId">
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧鍘熶环" prop="price" width="150">
+ <template #default="{ row }">{{ fenToYuan(row.price) }}鍏�</template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="count" width="100" />
+ <el-table-column label="鍚堣" prop="payPrice" width="150">
+ <template #default="{ row }">{{ fenToYuan(row.payPrice) }}鍏�</template>
+ </el-table-column>
+ <el-table-column label="鍞悗鐘舵��" prop="afterSaleStatus" width="120">
+ <template #default="{ row }">
+ <dict-tag
+ :type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS"
+ :value="row.afterSaleStatus"
+ />
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-col>
+ <el-col :span="10" />
+ </el-row>
+ </el-descriptions-item>
+ </el-descriptions>
+ <el-descriptions :column="4">
+ <!-- 绗竴灞� -->
+ <el-descriptions-item label="鍟嗗搧鎬婚: ">
+ {{ fenToYuan(formData.totalPrice!) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item label="杩愯垂閲戦: ">
+ {{ fenToYuan(formData.deliveryPrice!) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟璋冧环: ">
+ {{ fenToYuan(formData.adjustPrice!) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item v-for="item in 1" :key="item" label-class-name="no-colon" />
+ <!-- 绗簩灞� -->
+ <el-descriptions-item>
+ <template #label><span style="color: red">浼樻儬鍔典紭鎯�: </span></template>
+ {{ fenToYuan(formData.couponPrice!) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label><span style="color: red">VIP 浼樻儬: </span></template>
+ {{ fenToYuan(formData.vipPrice!) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label><span style="color: red">娲诲姩浼樻儬: </span></template>
+ {{ fenToYuan(formData.discountPrice!) }} 鍏�
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label><span style="color: red">绉垎鎶垫墸: </span></template>
+ {{ fenToYuan(formData.pointPrice!) }} 鍏�
+ </el-descriptions-item>
+ <!-- 绗笁灞� -->
+ <el-descriptions-item v-for="item in 3" :key="item" label-class-name="no-colon" />
+ <el-descriptions-item label="搴斾粯閲戦: ">
+ {{ fenToYuan(formData.payPrice!) }} 鍏�
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鐗╂祦淇℃伅 -->
+ <el-descriptions :column="4" title="鏀惰揣淇℃伅">
+ <el-descriptions-item label="閰嶉�佹柟寮�: ">
+ <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.deliveryType!" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀惰揣浜�: ">{{ formData.receiverName }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鐢佃瘽: ">{{ formData.receiverMobile }}</el-descriptions-item>
+ <!-- 蹇�掗厤閫� -->
+ <div v-if="formData.deliveryType === DeliveryTypeEnum.EXPRESS.type">
+ <el-descriptions-item v-if="formData.receiverDetailAddress" label="鏀惰揣鍦板潃: ">
+ {{ formData.receiverAreaName }} {{ formData.receiverDetailAddress }}
+ <el-link
+ v-clipboard:copy="formData.receiverAreaName + ' ' + formData.receiverDetailAddress"
+ v-clipboard:success="clipboardSuccess"
+ icon="ep:document-copy"
+ type="primary"
+ />
+ </el-descriptions-item>
+ <el-descriptions-item v-if="formData.logisticsId" label="鐗╂祦鍏徃: ">
+ {{ deliveryExpressList.find((item) => item.id === formData.logisticsId)?.name }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="formData.logisticsId" label="杩愬崟鍙�: ">
+ {{ formData.logisticsNo }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="formatDate.deliveryTime" label="鍙戣揣鏃堕棿: ">
+ {{ formatDate(formData.deliveryTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item v-for="item in 2" :key="item" label-class-name="no-colon" />
+ <el-descriptions-item v-if="expressTrackList.length > 0" label="鐗╂祦璇︽儏: ">
+ <el-timeline>
+ <el-timeline-item
+ v-for="(express, index) in expressTrackList"
+ :key="index"
+ :timestamp="formatDate(express.time)"
+ >
+ {{ express.content }}
+ </el-timeline-item>
+ </el-timeline>
+ </el-descriptions-item>
+ </div>
+ <!-- 鑷彁闂ㄥ簵 -->
+ <div v-if="formData.deliveryType === DeliveryTypeEnum.PICK_UP.type">
+ <el-descriptions-item v-if="formData.pickUpStoreId" label="鑷彁闂ㄥ簵: ">
+ {{ pickUpStore?.name }}
+ </el-descriptions-item>
+ </div>
+ </el-descriptions>
+
+ <!-- 璁㈠崟鏃ュ織 -->
+ <el-descriptions title="璁㈠崟鎿嶄綔鏃ュ織">
+ <el-descriptions-item labelClassName="no-colon">
+ <el-timeline>
+ <el-timeline-item
+ v-for="(log, index) in formData.logs"
+ :key="index"
+ :timestamp="formatDate(log.createTime!)"
+ placement="top"
+ >
+ <div class="el-timeline-right-content">
+ {{ log.content }}
+ </div>
+ <template #dot>
+ <span
+ :style="{ backgroundColor: getUserTypeColor(log.userType!) }"
+ class="dot-node-style"
+ >
+ {{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
+ </span>
+ </template>
+ </el-timeline-item>
+ </el-timeline>
+ </el-descriptions-item>
+ </el-descriptions>
+ </ContentWrap>
+
+ <!-- 鍚勭鎿嶄綔鐨勫脊绐� -->
+ <OrderDeliveryForm ref="deliveryFormRef" @success="getDetail" />
+ <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getDetail" />
+ <OrderUpdateAddressForm ref="updateAddressFormRef" @success="getDetail" />
+ <OrderUpdatePriceForm ref="updatePriceFormRef" @success="getDetail" />
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { fenToYuan } from '@/utils'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
+import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue'
+import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue'
+import OrderUpdateAddressForm from '@/views/mall/trade/order/form/OrderUpdateAddressForm.vue'
+import OrderUpdatePriceForm from '@/views/mall/trade/order/form/OrderUpdatePriceForm.vue'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'TradeOrderDetail' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+/** 鑾峰緱 userType 棰滆壊 */
+const getUserTypeColor = (type: number) => {
+ const dict = getDictObj(DICT_TYPE.USER_TYPE, type)
+ switch (dict?.colorType) {
+ case 'success':
+ return '#67C23A'
+ case 'info':
+ return '#909399'
+ case 'warning':
+ return '#E6A23C'
+ case 'danger':
+ return '#F56C6C'
+ }
+ return '#409EFF'
+}
+
+// 璁㈠崟璇︽儏
+const formData = ref<TradeOrderApi.OrderVO>({
+ logs: []
+})
+
+/** 鍚勭鎿嶄綔 */
+const updateRemarkForm = ref() // 璁㈠崟澶囨敞琛ㄥ崟 Ref
+const remark = () => {
+ updateRemarkForm.value?.open(formData.value)
+}
+const deliveryFormRef = ref() // 鍙戣揣琛ㄥ崟 Ref
+const delivery = () => {
+ deliveryFormRef.value?.open(formData.value)
+}
+const updateAddressFormRef = ref() // 鏀惰揣鍦板潃琛ㄥ崟 Ref
+const updateAddress = () => {
+ updateAddressFormRef.value?.open(formData.value)
+}
+const updatePriceFormRef = ref() // 璁㈠崟璋冧环琛ㄥ崟 Ref
+const updatePrice = () => {
+ updatePriceFormRef.value?.open(formData.value)
+}
+
+/** 鏍搁攢 */
+const handlePickUp = async () => {
+ try {
+ // 浜屾纭
+ await message.confirm('纭鏍搁攢璁㈠崟鍚楋紵')
+ // 鎻愪氦
+ await TradeOrderApi.pickUpOrder(formData.value.id!)
+ message.success('鏍搁攢鎴愬姛')
+ // 鍒锋柊鍒楄〃
+ await getDetail()
+ } catch {}
+}
+
+/** 鑾峰緱璇︽儏 */
+const { params } = useRoute() // 鏌ヨ鍙傛暟
+const props = defineProps({
+ id: propTypes.number.def(undefined), // 璁㈠崟ID
+ showPickUp: propTypes.bool.def(true) // 鏄剧ず鏍搁攢鎸夐挳
+})
+const id = (params.id || props.id) as unknown as number
+const getDetail = async () => {
+ if (id) {
+ const res = (await TradeOrderApi.getOrder(id)) as TradeOrderApi.OrderVO
+ // 娌℃湁琛ㄥ崟淇℃伅鍒欏叧闂〉闈㈣繑鍥�
+ if (!res) {
+ message.error('浜ゆ槗璁㈠崟涓嶅瓨鍦�')
+ close()
+ }
+ formData.value = res
+ }
+}
+
+/** 鍏抽棴 tag */
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { push, currentRoute } = useRouter() // 璺敱
+const close = () => {
+ delView(unref(currentRoute))
+ push({ name: 'TradeOrder' })
+}
+
+/** 澶嶅埗 */
+const clipboardSuccess = () => {
+ message.success('澶嶅埗鎴愬姛')
+}
+
+/** 鍒濆鍖� **/
+const deliveryExpressList = ref([]) // 鐗╂祦鍏徃
+const expressTrackList = ref([]) // 鐗╂祦璇︽儏
+const pickUpStore = ref({}) // 鑷彁闂ㄥ簵
+onMounted(async () => {
+ await getDetail()
+ // 濡傛灉閰嶉�佹柟寮忎负蹇�掞紝鍒欐煡璇㈢墿娴佸叕鍙�
+ if (formData.value.deliveryType === DeliveryTypeEnum.EXPRESS.type) {
+ deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+ if (formData.value.logisticsId) {
+ expressTrackList.value = await TradeOrderApi.getExpressTrackList(formData.value.id!)
+ }
+ } else if (formData.value.deliveryType === DeliveryTypeEnum.PICK_UP.type) {
+ if (formData.value.pickUpStoreId) {
+ pickUpStore.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(formData.value.pickUpStoreId)
+ }
+ }
+})
+</script>
+<style lang="scss" scoped>
+:deep(.el-descriptions) {
+ &:not(:nth-child(1)) {
+ margin-top: 20px;
+ }
+
+ .el-descriptions__title {
+ display: flex;
+ align-items: center;
+
+ &::before {
+ display: inline-block;
+ width: 3px;
+ height: 20px;
+ margin-right: 10px;
+ background-color: #409eff;
+ content: '';
+ }
+ }
+
+ .el-descriptions-item__container {
+ margin: 0 10px;
+
+ .no-colon {
+ margin: 0;
+
+ &::after {
+ content: '';
+ }
+ }
+ }
+}
+
+// 鏃堕棿绾挎牱寮忚皟鏁�
+:deep(.el-timeline) {
+ margin: 10px 0 0 160px;
+
+ .el-timeline-item__wrapper {
+ position: relative;
+ top: -20px;
+
+ .el-timeline-item__timestamp {
+ position: absolute !important;
+ top: 10px;
+ left: -150px;
+ }
+ }
+
+ .el-timeline-right-content {
+ display: flex;
+ align-items: center;
+ min-height: 30px;
+ padding: 10px;
+ border-radius: var(--el-card-border-radius);
+ background-color: var(--app-content-bg-color);
+
+ &::before {
+ position: absolute;
+ top: 10px;
+ left: 13px; /* 灏嗕吉鍏冪礌姘村钩灞呬腑 */
+ border-color: transparent var(--app-content-bg-color) transparent transparent; /* 灏栬棰滆壊锛屽乏渚ф湞鍚� */
+ border-style: solid;
+ border-width: 8px; /* 璋冩暣灏栬澶у皬 */
+ content: ''; /* 蹇呴』璁剧疆 content 灞炴�� */
+ }
+ }
+
+ .dot-node-style {
+ position: absolute;
+ left: -5px;
+ display: flex;
+ width: 20px;
+ height: 20px;
+ font-size: 10px;
+ color: #fff;
+ border-radius: 50%;
+ justify-content: center;
+ align-items: center;
+ }
+}
+</style>
diff --git a/src/views/mall/trade/order/form/OrderDeliveryForm.vue b/src/views/mall/trade/order/form/OrderDeliveryForm.vue
new file mode 100644
index 0000000..d901c15
--- /dev/null
+++ b/src/views/mall/trade/order/form/OrderDeliveryForm.vue
@@ -0,0 +1,99 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璁㈠崟鍙戣揣" width="25%">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="鍙戣揣鏂瑰紡">
+ <el-radio-group v-model="expressType">
+ <el-radio border value="express">蹇�掔墿娴�</el-radio>
+ <el-radio border value="none">鏃犻渶鍙戣揣</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <template v-if="expressType === 'express'">
+ <el-form-item label="鐗╂祦鍏徃">
+ <el-select v-model="formData.logisticsId" placeholder="璇烽�夋嫨" style="width: 100%">
+ <el-option
+ v-for="item in deliveryExpressList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐗╂祦鍗曞彿">
+ <el-input v-model="formData.logisticsNo" />
+ </el-form-item>
+ </template>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { copyValueToTarget } from '@/utils'
+
+defineOptions({ name: 'OrderDeliveryForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const expressType = ref('express') // 濡傛灉鍊兼槸 express锛屽垯鏄揩閫掞紱none 鍒欐槸鏃狅紱鏈潵鍋氬悓鍩庨厤閫侊紱
+const formData = ref<TradeOrderApi.DeliveryVO>({
+ id: undefined, // 璁㈠崟缂栧彿
+ logisticsId: null, // 鐗╂祦鍏徃缂栧彿
+ logisticsNo: '' // 鐗╂祦缂栧彿
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+ resetForm()
+ // 璁剧疆鏁版嵁
+ copyValueToTarget(formData.value, row)
+ if (row.logisticsId === 0) {
+ expressType.value = 'none'
+ }
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = unref(formData)
+ if (expressType.value === 'none') {
+ // 鏃犻渶鍙戣揣鐨勬儏鍐�
+ data.logisticsId = 0
+ data.logisticsNo = ''
+ }
+ await TradeOrderApi.deliveryOrder(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined, // 璁㈠崟缂栧彿
+ logisticsId: null, // 鐗╂祦鍏徃缂栧彿
+ logisticsNo: '' // 鐗╂祦缂栧彿
+ }
+ formRef.value?.resetFields()
+}
+const deliveryExpressList = ref([])
+onMounted(async () => {
+ deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>
diff --git a/src/views/mall/trade/order/form/OrderPickUpForm.vue b/src/views/mall/trade/order/form/OrderPickUpForm.vue
new file mode 100644
index 0000000..ccc6c1d
--- /dev/null
+++ b/src/views/mall/trade/order/form/OrderPickUpForm.vue
@@ -0,0 +1,116 @@
+<template>
+ <!-- 鏍搁攢瀵硅瘽妗� -->
+ <Dialog v-model="dialogVisible" title="璁㈠崟鏍搁攢" width="35%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item prop="pickUpVerifyCode" label="鏍搁攢鐮�">
+ <el-input v-model="formData.pickUpVerifyCode" placeholder="璇疯緭鍏ユ牳閿�鐮�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" :disabled="formLoading" @click="getOrderByPickUpVerifyCodeClick">
+ 鏌ヨ
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ <!-- 鏍搁攢纭瀵硅瘽妗� -->
+ <Dialog v-model="detailDialogVisible" title="璁㈠崟璇︽儏" width="55%">
+ <TradeOrderDetail v-if="orderDetails.id" :id="orderDetails.id" :show-pick-up="false" />
+ <template #footer>
+ <el-button type="primary" :disabled="formLoading" @click="submitForm"> 纭鏍搁攢 </el-button>
+ <el-button @click="detailDialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { OrderVO } from '@/api/mall/trade/order'
+import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
+import TradeOrderDetail from '@/views/mall/trade/order/detail/index.vue'
+
+/** 璁㈠崟鏍搁攢琛ㄥ崟 */
+defineOptions({ name: 'OrderPickUpForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailDialogVisible = ref(false) // 璇︽儏寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formRules = reactive({
+ pickUpVerifyCode: [{ required: true, message: '鏍搁攢鐮佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formData = ref({
+ pickUpVerifyCode: '' // 鏍搁攢鐮�
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const orderDetails = ref<OrderVO>({})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (pickUpVerifyCode: string) => {
+ resetForm()
+ if(pickUpVerifyCode != null){
+ formData.value.pickUpVerifyCode = pickUpVerifyCode;
+ await getOrderByPickUpVerifyCode()
+ }else{
+ dialogVisible.value = true
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await TradeOrderApi.pickUpOrderByVerifyCode(formData.value.pickUpVerifyCode)
+ message.success('鏍搁攢鎴愬姛')
+ detailDialogVisible.value = false
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ pickUpVerifyCode: '' // 鏍搁攢鐮�
+ }
+ formRef.value?.resetFields()
+}
+
+const getOrderByPickUpVerifyCodeClick = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ await getOrderByPickUpVerifyCode()
+}
+
+/** 鏌ヨ鏍搁攢鐮佸搴旂殑璁㈠崟 */
+const getOrderByPickUpVerifyCode = async () => {
+ formLoading.value = true
+ const data = await TradeOrderApi.getOrderByPickUpVerifyCode(formData.value.pickUpVerifyCode)
+ formLoading.value = false
+ if (data?.deliveryType !== DeliveryTypeEnum.PICK_UP.type) {
+ message.error('鏈煡璇㈠埌璁㈠崟')
+ return
+ }
+ if (data?.status !== TradeOrderStatusEnum.UNDELIVERED.status) {
+ message.error('璁㈠崟涓嶆槸寰呮牳閿�鐘舵��')
+ return
+ }
+ orderDetails.value = data
+ // 鏄剧ず璇︽儏瀵硅瘽妗�
+ detailDialogVisible.value = true
+}
+</script>
diff --git a/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue b/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue
new file mode 100644
index 0000000..baedb4a
--- /dev/null
+++ b/src/views/mall/trade/order/form/OrderUpdateAddressForm.vue
@@ -0,0 +1,98 @@
+<template>
+ <Dialog v-model="dialogVisible" title="淇敼璁㈠崟鏀惰揣鍦板潃" width="35%">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="120px">
+ <el-form-item label="鏀朵欢浜�">
+ <el-input v-model="formData.receiverName" placeholder="璇疯緭鍏ユ敹浠朵汉鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙�">
+ <el-input v-model="formData.receiverMobile" placeholder="璇疯緭鍏ユ敹浠朵汉鎵嬫満鍙�" />
+ </el-form-item>
+ <el-form-item label="鎵�鍦ㄥ湴">
+ <el-tree-select
+ v-model="formData.receiverAreaId"
+ :data="areaList"
+ :props="defaultProps"
+ :render-after-expand="true"
+ />
+ </el-form-item>
+ <el-form-item label="璇︾粏鍦板潃">
+ <el-input
+ v-model="formData.receiverDetailAddress"
+ :rows="3"
+ placeholder="璇疯緭鍏ユ敹浠朵汉璇︾粏鍦板潃"
+ type="textarea"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { getAreaTree } from '@/api/system/area'
+import { copyValueToTarget } from '@/utils'
+import { defaultProps } from '@/utils/tree'
+
+defineOptions({ name: 'OrderUpdateAddressForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined, // 璁㈠崟缂栧彿
+ receiverName: '', // 鏀朵欢浜哄悕绉�
+ receiverMobile: '', // 鏀朵欢浜烘墜鏈�
+ receiverAreaId: null, //鏀朵欢浜哄湴鍖虹紪鍙�
+ receiverDetailAddress: '' //鏀朵欢浜鸿缁嗗湴鍧�
+})
+const areaList = ref([]) // 鍦板尯鍒楄〃
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+ resetForm()
+ // 璁剧疆鏁版嵁
+ copyValueToTarget(formData.value, row)
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = unref(formData)
+ await TradeOrderApi.updateOrderAddress(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined, // 璁㈠崟缂栧彿
+ receiverName: '', // 鏀朵欢浜哄悕绉�
+ receiverMobile: '', // 鏀朵欢浜烘墜鏈�
+ receiverAreaId: null, //鏀朵欢浜哄湴鍖虹紪鍙�
+ receiverDetailAddress: '' //鏀朵欢浜鸿缁嗗湴鍧�
+ }
+ formRef.value?.resetFields()
+}
+
+onMounted(async () => {
+ // 鑾峰緱鍦板尯鍒楄〃
+ areaList.value = await getAreaTree()
+})
+</script>
diff --git a/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue b/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue
new file mode 100644
index 0000000..8332e31
--- /dev/null
+++ b/src/views/mall/trade/order/form/OrderUpdatePriceForm.vue
@@ -0,0 +1,95 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璁㈠崟璋冧环" width="25%">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="100px">
+ <el-form-item label="搴斾粯閲戦(鎬�)">
+ <el-input v-model="formData.payPrice" disabled />
+ </el-form-item>
+ <el-form-item label="璁㈠崟璋冧环">
+ <el-input-number v-model="formData.adjustPrice" :precision="2" :step="0.1" class="w-100%" />
+ <el-tag class="ml-10px" type="warning">璁㈠崟璋冧环銆� 姝f暟锛屽姞浠凤紱璐熸暟锛屽噺浠�</el-tag>
+ </el-form-item>
+ <el-form-item label="璋冧环鍚�">
+ <el-input v-model="formData.newPayPrice" disabled />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'OrderUpdatePriceForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined, // 璁㈠崟缂栧彿
+ adjustPrice: 0, // 璁㈠崟璋冧环
+ payPrice: '', // 搴斾粯閲戦(鎬�)
+ newPayPrice: '' // 璋冧环鍚庡簲浠橀噾棰�(鎬�)
+})
+watch(
+ () => formData.value.adjustPrice,
+ (adjustPrice: number | string) => {
+ const numMatch = formData.value.payPrice.match(/\d+(\.\d+)?/)
+ if (numMatch) {
+ const payPriceNum = parseFloat(numMatch[0])
+ adjustPrice = typeof adjustPrice === 'string' ? parseFloat(adjustPrice) : adjustPrice
+ formData.value.newPayPrice = (payPriceNum + adjustPrice).toFixed(2) + '鍏�'
+ }
+ }
+)
+
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+ resetForm()
+ formData.value.id = row.id!
+ // 璁剧疆鏁版嵁
+ formData.value.adjustPrice = formatToFraction(row.adjustPrice!)
+ formData.value.payPrice = floatToFixed2(row.payPrice!) + '鍏�'
+ formData.value.newPayPrice = formData.value.payPrice
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = cloneDeep(unref(formData))
+ data.adjustPrice = convertToInteger(data.adjustPrice)
+ delete data.payPrice
+ delete data.newPayPrice
+ await TradeOrderApi.updateOrderPrice(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined, // 璁㈠崟缂栧彿
+ adjustPrice: 0, // 璁㈠崟璋冧环
+ payPrice: '', // 搴斾粯閲戦(鎬�)
+ newPayPrice: '' // 璋冧环鍚庡簲浠橀噾棰�(鎬�)
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue b/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue
new file mode 100644
index 0000000..e979501
--- /dev/null
+++ b/src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue
@@ -0,0 +1,70 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍟嗗澶囨敞" width="45%">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="澶囨敞">
+ <el-input
+ v-model="formData.remark"
+ :rows="3"
+ placeholder="璇疯緭鍏ヨ鍗曞娉�"
+ type="textarea"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+
+defineOptions({ name: 'OrderUpdateRemarkForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined, // 璁㈠崟缂栧彿
+ remark: '' // 璁㈠崟澶囨敞
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formData.value.id = row.id
+ formData.value.remark = row.remark
+ dialogVisible.value = true
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = unref(formData)
+ await TradeOrderApi.updateOrderRemark(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined, // 璁㈠崟缂栧彿
+ remark: '' // 璁㈠崟澶囨敞
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mall/trade/order/index.vue b/src/views/mall/trade/order/index.vue
new file mode 100644
index 0000000..9407aea
--- /dev/null
+++ b/src/views/mall/trade/order/index.vue
@@ -0,0 +1,357 @@
+<template>
+ <doc-alert title="銆愪氦鏄撱�戜氦鏄撹鍗�" url="https://doc.iocoder.cn/mall/trade-order/" />
+ <doc-alert title="銆愪氦鏄撱�戣喘鐗╄溅" url="https://doc.iocoder.cn/mall/trade-cart/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀粯鏂瑰紡" prop="payChannelCode">
+ <el-select
+ v-model="queryParams.payChannelCode"
+ class="!w-280px"
+ clearable
+ placeholder="鍏ㄩ儴"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-280px"
+ end-placeholder="鑷畾涔夋椂闂�"
+ start-placeholder="鑷畾涔夋椂闂�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏉ユ簮" prop="terminal">
+ <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟绫诲瀷" prop="type">
+ <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閰嶉�佹柟寮�" prop="deliveryType">
+ <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+ label="蹇�掑叕鍙�"
+ prop="logisticsId"
+ >
+ <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="item in deliveryExpressList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+ label="鑷彁闂ㄥ簵"
+ prop="pickUpStoreId"
+ >
+ <el-select
+ v-model="queryParams.pickUpStoreId"
+ class="!w-280px"
+ clearable
+ multiple
+ placeholder="鍏ㄩ儴"
+ >
+ <el-option
+ v-for="item in pickUpStoreList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+ label="鏍搁攢鐮�"
+ prop="pickUpVerifyCode"
+ >
+ <el-input
+ v-model="queryParams.pickUpVerifyCode"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ヨ嚜鎻愭牳閿�鐮�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鑱氬悎鎼滅储">
+ <el-input
+ v-show="true"
+ v-model="queryParams[queryType.queryParam]"
+ :type="queryType.queryParam === 'userId' ? 'number' : 'text'"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏�"
+ >
+ <template #prepend>
+ <el-select
+ v-model="queryType.queryParam"
+ class="!w-110px"
+ clearable
+ placeholder="鍏ㄩ儴"
+ @change="inputChangeSelect"
+ >
+ <el-option
+ v-for="dict in dynamicSearchList"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <!-- 娣诲姞 row-key="id" 瑙e喅鍒楁暟鎹腑鐨� table#header 鏁版嵁涓嶅埛鏂扮殑闂 -->
+ <el-table v-loading="loading" :data="list" row-key="id">
+ <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">
+ <template #default="{ row }">
+ <div class="flex items-center justify-center">
+ <el-button
+ v-hasPermi="['trade:order:query']"
+ link
+ type="primary"
+ @click="openDetail(row.id)"
+ >
+ <Icon icon="ep:notification" />
+ 璇︽儏
+ </el-button>
+ <el-dropdown
+ v-hasPermi="['trade:order:update']"
+ @command="(command) => handleCommand(command, row)"
+ >
+ <el-button link type="primary">
+ <Icon icon="ep:d-arrow-right" />
+ 鏇村
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <!-- 濡傛灉鏄�愬揩閫掋�戯紝骞朵笖銆愭湭鍙戣揣銆戯紝鍒欏睍绀恒�愬彂璐с�戞寜閽� -->
+ <el-dropdown-item
+ v-if="
+ row.deliveryType === DeliveryTypeEnum.EXPRESS.type &&
+ row.status === TradeOrderStatusEnum.UNDELIVERED.status
+ "
+ command="delivery"
+ >
+ <Icon icon="ep:takeaway-box" />
+ 鍙戣揣
+ </el-dropdown-item>
+ <el-dropdown-item command="remark">
+ <Icon icon="ep:chat-line-square" />
+ 澶囨敞
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </template>
+ </OrderTableColumn>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 鍚勭鎿嶄綔鐨勫脊绐� -->
+ <OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
+ <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import type { FormInstance } from 'element-plus'
+import OrderDeliveryForm from '@/views/mall/trade/order/form/OrderDeliveryForm.vue'
+import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
+import { OrderTableColumn } from './components'
+
+defineOptions({ name: 'TradeOrder' })
+
+const { currentRoute, push } = useRouter() // 璺敱璺宠浆
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(2) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<TradeOrderApi.OrderVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref<FormInstance>() // 鎼滅储鐨勮〃鍗�
+// 琛ㄥ崟鎼滅储
+const queryParams = ref({
+ pageNo: 1, // 椤垫暟
+ pageSize: 10, // 姣忛〉鏄剧ず鏁伴噺
+ status: undefined, // 璁㈠崟鐘舵��
+ payChannelCode: undefined, // 鏀粯鏂瑰紡
+ createTime: undefined, // 鍒涘缓鏃堕棿
+ terminal: undefined, // 璁㈠崟鏉ユ簮
+ type: undefined, // 璁㈠崟绫诲瀷
+ deliveryType: undefined, // 閰嶉�佹柟寮�
+ logisticsId: undefined, // 蹇�掑叕鍙�
+ pickUpStoreId: undefined, // 鑷彁闂ㄥ簵
+ pickUpVerifyCode: undefined // 鑷彁鏍搁攢鐮�
+})
+const queryType = reactive({ queryParam: '' }) // 璁㈠崟鎼滅储绫诲瀷 queryParam
+
+// 璁㈠崟鑱氬悎鎼滅储 select 绫诲瀷閰嶇疆锛堝姩鎬佹悳绱級
+const dynamicSearchList = ref([
+ { value: 'no', label: '璁㈠崟鍙�' },
+ { value: 'userId', label: '鐢ㄦ埛UID' },
+ { value: 'userNickname', label: '鐢ㄦ埛鏄电О' },
+ { value: 'userMobile', label: '鐢ㄦ埛鐢佃瘽' }
+])
+/**
+ * 鑱氬悎鎼滅储鍒囨崲鏌ヨ瀵硅薄鏃惰Е鍙�
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+ dynamicSearchList.value
+ .filter((item) => item.value !== val)
+ ?.forEach((item1) => {
+ // 娓呴櫎闆嗗悎鎼滅储鏃犵敤灞炴��
+ if (queryParams.value.hasOwnProperty(item1.value)) {
+ delete queryParams.value[item1.value]
+ }
+ })
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TradeOrderApi.getOrderPage(unref(queryParams))
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = async () => {
+ queryParams.value.pageNo = 1
+ await getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ queryParams.value = {
+ pageNo: 1, // 椤垫暟
+ pageSize: 10, // 姣忛〉鏄剧ず鏁伴噺
+ status: undefined, // 璁㈠崟鐘舵��
+ payChannelCode: undefined, // 鏀粯鏂瑰紡
+ createTime: undefined, // 鍒涘缓鏃堕棿
+ terminal: undefined, // 璁㈠崟鏉ユ簮
+ type: undefined, // 璁㈠崟绫诲瀷
+ deliveryType: undefined, // 閰嶉�佹柟寮�
+ logisticsId: undefined, // 蹇�掑叕鍙�
+ pickUpStoreId: undefined, // 鑷彁闂ㄥ簵
+ pickUpVerifyCode: undefined // 鑷彁鏍搁攢鐮�
+ }
+ handleQuery()
+}
+
+/** 鏌ョ湅璁㈠崟璇︽儏 */
+const openDetail = (id: number) => {
+ push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 鎿嶄綔鍒嗗彂 */
+const deliveryFormRef = ref()
+const updateRemarkForm = ref()
+const handleCommand = (command: string, row: TradeOrderApi.OrderVO) => {
+ switch (command) {
+ case 'remark':
+ updateRemarkForm.value?.open(row)
+ break
+ case 'delivery':
+ deliveryFormRef.value?.open(row)
+ break
+ }
+}
+
+// 鐩戝惉璺敱鍙樺寲鏇存柊鍒楄〃锛岃В鍐宠鍗曚繚瀛�/鏇存柊鍚庯紝鍒楄〃涓嶅埛鏂扮殑闂銆�
+watch(
+ () => currentRoute.value,
+ () => {
+ getList()
+ }
+)
+
+const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 鑷彁闂ㄥ簵绮剧畝鍒楄〃
+const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 鐗╂祦鍏徃
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
+ deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>
diff --git a/src/views/member/config/index.vue b/src/views/member/config/index.vue
new file mode 100644
index 0000000..2593509
--- /dev/null
+++ b/src/views/member/config/index.vue
@@ -0,0 +1,121 @@
+<template>
+ <doc-alert title="浼氬憳鎵嬪唽锛堝姛鑳藉紑鍚級" url="https://doc.iocoder.cn/member/build/" />
+
+ <ContentWrap>
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="hideId" v-show="false">
+ <el-input v-model="formData.id" />
+ </el-form-item>
+
+ <el-tabs>
+ <el-tab-pane label="绉垎">
+ <el-form-item label="绉垎鎶垫墸" prop="pointTradeDeductEnable">
+ <el-switch v-model="formData.pointTradeDeductEnable" style="user-select: none" />
+ <el-text class="w-full" size="small" type="info">涓嬪崟绉垎鏄惁鎶电敤璁㈠崟閲戦</el-text>
+ </el-form-item>
+ <el-form-item label="绉垎鎶垫墸" prop="pointTradeDeductUnitPrice">
+ <el-input-number
+ v-model="computedPointTradeDeductUnitPrice"
+ placeholder="璇疯緭鍏ョН鍒嗘姷鎵i噾棰�"
+ :precision="2"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 绉垎鎶电敤姣斾緥(1 绉垎鎶靛灏戦噾棰�)锛屽崟浣嶏細鍏�
+ </el-text>
+ </el-form-item>
+ <el-form-item label="绉垎鎶垫墸鏈�澶у��" prop="pointTradeDeductMaxPrice">
+ <el-input-number
+ v-model="formData.pointTradeDeductMaxPrice"
+ placeholder="璇疯緭鍏ョН鍒嗘姷鎵f渶澶у��"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 鍗曟涓嬪崟绉垎浣跨敤涓婇檺锛�0 涓嶉檺鍒�
+ </el-text>
+ </el-form-item>
+ <el-form-item label="1 鍏冭禒閫佸灏戝垎" prop="pointTradeGivePoint">
+ <el-input-number
+ v-model="formData.pointTradeGivePoint"
+ placeholder="璇疯緭鍏� 1 鍏冭禒閫佸灏戠Н鍒�"
+ />
+ <el-text class="w-full" size="small" type="info">
+ 涓嬪崟鏀粯閲戦鎸夋瘮渚嬭禒閫佺Н鍒嗭紙瀹為檯鏀粯 1 鍏冭禒閫佸灏戠Н鍒嗭級
+ </el-text>
+ </el-form-item>
+ </el-tab-pane>
+ </el-tabs>
+
+ <el-form-item>
+ <el-button type="primary" @click="onSubmit">淇濆瓨</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as ConfigApi from '@/api/member/config'
+
+defineOptions({ name: 'MemberConfig' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ pointTradeDeductEnable: true,
+ pointTradeDeductUnitPrice: 0,
+ pointTradeDeductMaxPrice: 0,
+ pointTradeGivePoint: 0
+})
+
+// 鍒涘缓涓�涓绠楀睘鎬э紝鐢ㄤ簬灏� pointTradeDeductUnitPrice 鏄剧ず涓哄甫涓や綅灏忔暟鐨勫舰寮�
+const computedPointTradeDeductUnitPrice = computed({
+ get: () => (formData.value.pointTradeDeductUnitPrice / 100).toFixed(2),
+ set: (newValue: number) => {
+ formData.value.pointTradeDeductUnitPrice = Math.round(newValue * 100)
+ }
+})
+
+const formRules = reactive({})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 淇敼绉垎閰嶇疆 */
+const onSubmit = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ConfigApi.ConfigVO
+ await ConfigApi.saveConfig(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 鑾峰緱绉垎閰嶇疆 */
+const getConfig = async () => {
+ try {
+ const data = await ConfigApi.getConfig()
+ if (data === null) {
+ return
+ }
+ formData.value = data
+ } finally {
+ }
+}
+
+onMounted(() => {
+ getConfig()
+})
+</script>
diff --git a/src/views/member/group/GroupForm.vue b/src/views/member/group/GroupForm.vue
new file mode 100644
index 0000000..f87030b
--- /dev/null
+++ b/src/views/member/group/GroupForm.vue
@@ -0,0 +1,112 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="600">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" type="textarea" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as GroupApi from '@/api/member/group'
+import { CommonStatusEnum } from '@/utils/constants'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ remark: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await GroupApi.getGroup(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as GroupApi.GroupVO
+ if (formType.value === 'create') {
+ await GroupApi.createGroup(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await GroupApi.updateGroup(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ remark: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/member/group/components/MemberGroupSelect.vue b/src/views/member/group/components/MemberGroupSelect.vue
new file mode 100644
index 0000000..78a993a
--- /dev/null
+++ b/src/views/member/group/components/MemberGroupSelect.vue
@@ -0,0 +1,45 @@
+<template>
+ <el-select v-model="groupId" placeholder="璇烽�夋嫨鐢ㄦ埛鍒嗙粍" clearable class="!w-240px">
+ <el-option
+ v-for="group in groupOptions"
+ :key="group.id"
+ :label="group.name"
+ :value="group.id"
+ />
+ </el-select>
+</template>
+<script lang="ts" setup>
+import * as GroupApi from '@/api/member/group'
+
+/** 浼氬憳鍒嗙粍閫夋嫨妗� **/
+defineOptions({ name: 'MemberGroupSelect' })
+
+const props = defineProps({
+ /** 涓嬫媺妗嗛�変腑鍊� **/
+ modelValue: {
+ type: Number,
+ default: undefined
+ }
+})
+const emit = defineEmits(['update:modelValue'])
+
+const groupId = computed({
+ get() {
+ return props.modelValue
+ },
+ set(value: any) {
+ emit('update:modelValue', value)
+ }
+})
+
+const groupOptions = ref<GroupApi.GroupVO[]>([])
+
+const getList = async () => {
+ groupOptions.value = await GroupApi.getSimpleGroupList()
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/group/index.vue b/src/views/member/group/index.vue
new file mode 100644
index 0000000..ba925d6
--- /dev/null
+++ b/src/views/member/group/index.vue
@@ -0,0 +1,176 @@
+<template>
+ <doc-alert title="浼氬憳鐢ㄦ埛銆佹爣绛俱�佸垎缁�" url="https://doc.iocoder.cn/member/user/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍒嗙粍鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ垎缁勫悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['member:group:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" min-width="60" />
+ <el-table-column label="鍚嶇О" align="center" prop="name" min-width="80" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" min-width="100" />
+ <el-table-column label="鐘舵��" align="center" prop="status" min-width="70">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ min-width="170"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="150px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['member:group:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['member:group:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <GroupForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as GroupApi from '@/api/member/group'
+import GroupForm from './GroupForm.vue'
+
+/** 鐢ㄦ埛鍒嗙粍绠$悊 **/
+defineOptions({ name: 'MemberGroup' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await GroupApi.getGroupPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await GroupApi.deleteGroup(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/level/LevelForm.vue b/src/views/member/level/LevelForm.vue
new file mode 100644
index 0000000..2aa4948
--- /dev/null
+++ b/src/views/member/level/LevelForm.vue
@@ -0,0 +1,175 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="800">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="110px"
+ v-loading="formLoading"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="绛夌骇鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ瓑绾у悕绉�" class="!w-240px" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绛夌骇" prop="level">
+ <el-input-number
+ v-model="formData.level"
+ :min="0"
+ :precision="0"
+ placeholder="璇疯緭鍏ョ瓑绾�"
+ class="!w-240px"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍗囩骇缁忛獙" prop="experience">
+ <el-input-number
+ v-model="formData.experience"
+ :min="0"
+ :precision="0"
+ placeholder="璇疯緭鍏ュ崌绾х粡楠�"
+ class="!w-240px"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜彈鎶樻墸(%)" prop="discountPercent">
+ <el-input-number
+ v-model="formData.discountPercent"
+ :min="0"
+ :max="100"
+ :precision="0"
+ placeholder="璇疯緭鍏ヤ韩鍙楁姌鎵�"
+ class="!w-240px"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="绛夌骇鍥炬爣">
+ <UploadImg v-model="formData.icon" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑳屾櫙鍥�">
+ <UploadImg v-model="formData.backgroundUrl" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as LevelApi from '@/api/member/level'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** 浼氬憳绛夌骇琛ㄥ崟 **/
+defineOptions({ name: 'MemberLevelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ experience: undefined,
+ level: undefined,
+ discountPercent: undefined,
+ icon: undefined,
+ backgroundUrl: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '绛夌骇鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ experience: [{ required: true, message: '鍗囩骇缁忛獙涓嶈兘涓虹┖', trigger: 'blur' }],
+ level: [{ required: true, message: '绛夌骇涓嶈兘涓虹┖', trigger: 'blur' }],
+ discountPercent: [{ required: true, message: '浜彈鎶樻墸涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await LevelApi.getLevel(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as LevelApi.LevelVO
+ if (formType.value === 'create') {
+ await LevelApi.createLevel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await LevelApi.updateLevel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ experience: undefined,
+ level: undefined,
+ discountPercent: undefined,
+ icon: undefined,
+ backgroundUrl: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/member/level/components/MemberLevelSelect.vue b/src/views/member/level/components/MemberLevelSelect.vue
new file mode 100644
index 0000000..2a603e6
--- /dev/null
+++ b/src/views/member/level/components/MemberLevelSelect.vue
@@ -0,0 +1,45 @@
+<template>
+ <el-select v-model="levelId" placeholder="璇烽�夋嫨鐢ㄦ埛绛夌骇" clearable class="!w-240px">
+ <el-option v-for="level in levelOptions" :key="level.id" :label="level.name" :value="level.id">
+ <span class="flex items-center gap-x-8px">
+ <el-avatar :src="level.icon" size="small" />
+ {{ level.name }}
+ </span>
+ </el-option>
+ </el-select>
+</template>
+<script lang="ts" setup>
+import * as LevelApi from '@/api/member/level'
+
+/** 浼氬憳绛夌骇閫夋嫨妗� **/
+defineOptions({ name: 'MemberLevelSelect' })
+
+const props = defineProps({
+ /** 涓嬫媺妗嗛�変腑鍊� **/
+ modelValue: {
+ type: Number,
+ default: undefined
+ }
+})
+const emit = defineEmits(['update:modelValue'])
+
+const levelId = computed({
+ get() {
+ return props.modelValue
+ },
+ set(value: any) {
+ emit('update:modelValue', value)
+ }
+})
+
+const levelOptions = ref<LevelApi.LevelVO[]>([])
+
+const getList = async () => {
+ levelOptions.value = await LevelApi.getSimpleLevelList()
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/level/index.vue b/src/views/member/level/index.vue
new file mode 100644
index 0000000..3743eac
--- /dev/null
+++ b/src/views/member/level/index.vue
@@ -0,0 +1,171 @@
+<template>
+ <doc-alert title="浼氬憳绛夌骇銆佺Н鍒嗐�佺鍒�" url="https://doc.iocoder.cn/member/level/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="绛夌骇鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ョ瓑绾у悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['member:level:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" min-width="60" />
+ <el-table-column label="绛夌骇鍥炬爣" align="center" prop="icon" min-width="80">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.icon"
+ class="h-30px w-30px"
+ :preview-src-list="[scope.row.icon]"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绛夌骇鑳屾櫙鍥�" align="center" prop="backgroundUrl" min-width="100">
+ <template #default="scope">
+ <el-image
+ :src="scope.row.backgroundUrl"
+ class="h-30px w-30px"
+ :preview-src-list="[scope.row.backgroundUrl]"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绛夌骇鍚嶇О" align="center" prop="name" min-width="100" />
+ <el-table-column label="绛夌骇" align="center" prop="level" min-width="60" />
+ <el-table-column label="鍗囩骇缁忛獙" align="center" prop="experience" min-width="80" />
+ <el-table-column label="浜彈鎶樻墸(%)" align="center" prop="discountPercent" min-width="110" />
+ <el-table-column label="鐘舵��" align="center" prop="status" min-width="70">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ min-width="170"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="110px" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['member:level:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['member:level:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <LevelForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as LevelApi from '@/api/member/level'
+import LevelForm from './LevelForm.vue'
+
+/** 浼氬憳绛夌骇绠$悊 **/
+defineOptions({ name: 'MemberLevel' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ name: null,
+ status: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ list.value = await LevelApi.getLevelList(queryParams)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await LevelApi.deleteLevel(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/point/record/index.vue b/src/views/member/point/record/index.vue
new file mode 100644
index 0000000..9676c2e
--- /dev/null
+++ b/src/views/member/point/record/index.vue
@@ -0,0 +1,161 @@
+<template>
+ <doc-alert title="浼氬憳绛夌骇銆佺Н鍒嗐�佺鍒�" url="https://doc.iocoder.cn/member/level/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ placeholder="璇疯緭鍏ョ敤鎴锋樀绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="涓氬姟绫诲瀷" prop="bizType">
+ <el-select
+ v-model="queryParams.bizType"
+ placeholder="璇烽�夋嫨涓氬姟绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="绉垎鏍囬" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ョН鍒嗘爣棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鑾峰緱鏃堕棿" prop="createDate">
+ <el-date-picker
+ v-model="queryParams.createDate"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="180" />
+ <el-table-column
+ label="鑾峰緱鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鐢ㄦ埛" align="center" prop="nickname" width="200" />
+ <el-table-column label="鑾峰緱绉垎" align="center" prop="point" width="100">
+ <template #default="scope">
+ <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+ +{{ scope.row.point }}
+ </el-tag>
+ <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎬荤Н鍒�" align="center" prop="totalPoint" width="100" />
+ <el-table-column label="鏍囬" align="center" prop="title" />
+ <el-table-column label="鎻忚堪" align="center" prop="description" />
+ <el-table-column label="涓氬姟缂栫爜" align="center" prop="bizId" />
+ <el-table-column label="涓氬姟绫诲瀷" align="center" prop="bizType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.MEMBER_POINT_BIZ_TYPE" :value="scope.row.bizType" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <RecordForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RecordApi from '@/api/member/point/record'
+
+defineOptions({ name: 'PointRecord' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ nickname: null,
+ bizType: null,
+ title: null,
+ createDate: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await RecordApi.getRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/signin/config/SignInConfigForm.vue b/src/views/member/signin/config/SignInConfigForm.vue
new file mode 100644
index 0000000..9e0a629
--- /dev/null
+++ b/src/views/member/signin/config/SignInConfigForm.vue
@@ -0,0 +1,132 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="绛惧埌澶╂暟" prop="day">
+ <el-input-number v-model="formData.day" :min="1" :max="7" :precision="0" />
+ <el-text class="mx-1" style="margin-left: 10px" type="danger">
+ 鍙厑璁歌缃� 1-7锛岄粯璁ょ鍒� 7 澶╀负涓�涓懆鏈�
+ </el-text>
+ </el-form-item>
+ <el-form-item label="濂栧姳绉垎" prop="point">
+ <el-input-number v-model="formData.point" :min="0" :precision="0" />
+ </el-form-item>
+ <el-form-item label="濂栧姳缁忛獙" prop="experience">
+ <el-input-number v-model="formData.experience" :min="0" :precision="0" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as SignInConfigApi from '@/api/member/signin/config'
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<SignInConfigApi.SignInConfigVO>({} as SignInConfigApi.SignInConfigVO)
+// 濂栧姳鏍¢獙瑙勫垯
+const awardValidator = (rule: any, _value: any, callback: any) => {
+ if (!formData.value.point && !formData.value.experience) {
+ callback(new Error('濂栧姳绉垎涓庡鍔辩粡楠岃嚦灏戦厤缃竴涓�'))
+ return
+ }
+
+ // 娓呴櫎鍙︿竴涓瓧娈电殑閿欒鎻愮ず
+ const otherAwardField = rule?.field === 'point' ? 'experience' : 'point'
+ formRef.value.validateField(otherAwardField, () => null)
+ callback()
+}
+const formRules = reactive({
+ day: [{ required: true, message: '绛惧埌澶╂暟涓嶈兘绌�', trigger: 'blur' }],
+ point: [
+ { required: true, message: '濂栧姳绉垎涓嶈兘绌�', trigger: 'blur' },
+ { validator: awardValidator, trigger: 'blur' }
+ ],
+ experience: [
+ { required: true, message: '濂栧姳缁忛獙涓嶈兘绌�', trigger: 'blur' },
+ { validator: awardValidator, trigger: 'blur' }
+ ]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SignInConfigApi.getSignInConfig(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ if (formType.value === 'create') {
+ await SignInConfigApi.createSignInConfig(formData.value)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SignInConfigApi.updateSignInConfig(formData.value)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ day: undefined,
+ point: 0,
+ experience: 0,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/member/signin/config/index.vue b/src/views/member/signin/config/index.vue
new file mode 100644
index 0000000..14a84cd
--- /dev/null
+++ b/src/views/member/signin/config/index.vue
@@ -0,0 +1,106 @@
+<template>
+ <doc-alert title="浼氬憳绛夌骇銆佺Н鍒嗐�佺鍒�" url="https://doc.iocoder.cn/member/level/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['point:sign-in-config:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column
+ label="绛惧埌澶╂暟"
+ align="center"
+ prop="day"
+ :formatter="(_, __, cellValue) => ['绗�', cellValue, '澶�'].join(' ')"
+ />
+ <el-table-column label="濂栧姳绉垎" align="center" prop="point" />
+ <el-table-column label="濂栧姳缁忛獙" align="center" prop="experience" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['point:sign-in-config:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['point:sign-in-config:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SignInConfigForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import * as SignInConfigApi from '@/api/member/signin/config'
+import SignInConfigForm from './SignInConfigForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+
+defineOptions({ name: 'SignInConfig' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SignInConfigApi.getSignInConfigList()
+ console.log(data)
+ list.value = data
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SignInConfigApi.deleteSignInConfig(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/signin/record/index.vue b/src/views/member/signin/record/index.vue
new file mode 100644
index 0000000..e80e854
--- /dev/null
+++ b/src/views/member/signin/record/index.vue
@@ -0,0 +1,134 @@
+<template>
+ <doc-alert title="浼氬憳绛夌骇銆佺Н鍒嗐�佺鍒�" url="https://doc.iocoder.cn/member/level/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="绛惧埌鐢ㄦ埛" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ placeholder="璇疯緭鍏ョ鍒扮敤鎴�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绛惧埌澶╂暟" prop="day">
+ <el-input
+ v-model="queryParams.day"
+ placeholder="璇疯緭鍏ョ鍒板ぉ鏁�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绛惧埌鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="绛惧埌鐢ㄦ埛" align="center" prop="nickname" />
+ <el-table-column
+ label="绛惧埌澶╂暟"
+ align="center"
+ prop="day"
+ :formatter="(_, __, cellValue) => ['绗�', cellValue, '澶�'].join(' ')"
+ />
+ <el-table-column label="鑾峰緱绉垎" align="center" prop="point" width="100">
+ <template #default="scope">
+ <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+ +{{ scope.row.point }}
+ </el-tag>
+ <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="绛惧埌鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as SignInRecordApi from '@/api/member/signin/record'
+
+defineOptions({ name: 'SignInRecord' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ nickname: null,
+ day: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SignInRecordApi.getSignInRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/tag/TagForm.vue b/src/views/member/tag/TagForm.vue
new file mode 100644
index 0000000..d45ea58
--- /dev/null
+++ b/src/views/member/tag/TagForm.vue
@@ -0,0 +1,91 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鏍囩鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユ爣绛惧悕绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as TagApi from '@/api/member/tag'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鏍囩鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await TagApi.getMemberTag(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as TagApi.TagVO
+ if (formType.value === 'create') {
+ await TagApi.createMemberTag(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await TagApi.updateMemberTag(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/member/tag/components/MemberTagSelect.vue b/src/views/member/tag/components/MemberTagSelect.vue
new file mode 100644
index 0000000..ebff61e
--- /dev/null
+++ b/src/views/member/tag/components/MemberTagSelect.vue
@@ -0,0 +1,68 @@
+<template>
+ <el-select v-model="tagIds" placeholder="璇烽�夋嫨鐢ㄦ埛鏍囩" clearable multiple class="!w-240px">
+ <el-option v-for="tag in tags" :key="tag.id" :label="tag.name" :value="tag.id" />
+ </el-select>
+ <el-button
+ v-if="showAdd"
+ type="primary"
+ class="ml-2"
+ link
+ @click="openForm('create')"
+ v-hasPermi="['member:tag:create']"
+ >
+ 鏂板鏍囩
+ </el-button>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔� -->
+ <TagForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import * as TagApi from '@/api/member/tag'
+import TagForm from '@/views/member/tag/TagForm.vue'
+
+defineOptions({ name: 'MemberTagSelect' })
+
+const props = defineProps({
+ /** 涓嬫媺妗嗛�変腑鍊� **/
+ modelValue: {
+ type: Array,
+ default: undefined
+ },
+ /** 鏄惁鏄剧ず鈥滄柊澧炴爣绛锯�濇寜閽� **/
+ showAdd: {
+ type: Boolean,
+ default: false
+ }
+})
+const emit = defineEmits(['update:modelValue'])
+defineExpose({
+ showAdd: props.showAdd
+})
+
+const tagIds = computed({
+ get() {
+ return props.modelValue
+ },
+ set(value: any) {
+ emit('update:modelValue', value)
+ }
+})
+
+const tags = ref<TagApi.TagVO[]>([])
+
+const getList = async () => {
+ tags.value = await TagApi.getSimpleTagList()
+}
+
+/** 娣诲姞鐢ㄦ埛鏍囩琛ㄥ崟寮规 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/tag/index.vue b/src/views/member/tag/index.vue
new file mode 100644
index 0000000..59efc5e
--- /dev/null
+++ b/src/views/member/tag/index.vue
@@ -0,0 +1,155 @@
+<template>
+ <doc-alert title="浼氬憳鐢ㄦ埛銆佹爣绛俱�佸垎缁�" url="https://doc.iocoder.cn/member/user/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏍囩鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユ爣绛惧悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['member:tag:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="150px" />
+ <el-table-column label="鏍囩鍚嶇О" align="center" prop="name" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="150px">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['member:tag:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['member:tag:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <TagForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts" name="MemberTag">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as TagApi from '@/api/member/tag'
+import TagForm from './TagForm.vue'
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TagApi.getMemberTagPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await TagApi.deleteMemberTag(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/user/UserForm.vue b/src/views/member/user/UserForm.vue
new file mode 100644
index 0000000..70d8313
--- /dev/null
+++ b/src/views/member/user/UserForm.vue
@@ -0,0 +1,179 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鎵嬫満鍙�" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ョ敤鎴锋樀绉�" />
+ </el-form-item>
+ <el-form-item label="澶村儚" prop="avatar">
+ <UploadImg v-model="formData.avatar" :limit="1" :is-show-tip="false" />
+ </el-form-item>
+ <el-form-item label="鐪熷疄鍚嶅瓧" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ湡瀹炲悕瀛�" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鎬у埆" prop="sex">
+ <el-radio-group v-model="formData.sex">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthday">
+ <el-date-picker
+ v-model="formData.birthday"
+ type="date"
+ value-format="x"
+ placeholder="閫夋嫨鍑虹敓鏃ユ湡"
+ />
+ </el-form-item>
+ <el-form-item label="鎵�鍦ㄥ湴" prop="areaId">
+ <el-tree-select
+ v-model="formData.areaId"
+ :data="areaList"
+ :props="defaultProps"
+ :render-after-expand="true"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏍囩" prop="tagIds">
+ <MemberTagSelect v-model="formData.tagIds" show-add />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍒嗙粍" prop="groupId">
+ <MemberGroupSelect v-model="formData.groupId" />
+ </el-form-item>
+ <el-form-item label="浼氬憳澶囨敞" prop="mark">
+ <el-input type="textarea" v-model="formData.mark" placeholder="璇疯緭鍏ヤ細鍛樺娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as UserApi from '@/api/member/user'
+import * as AreaApi from '@/api/system/area'
+import { defaultProps } from '@/utils/tree'
+import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ mobile: undefined,
+ password: undefined,
+ status: undefined,
+ nickname: undefined,
+ avatar: undefined,
+ name: undefined,
+ sex: undefined,
+ areaId: undefined,
+ birthday: undefined,
+ mark: undefined,
+ tagIds: [],
+ groupId: undefined
+})
+const formRules = reactive({
+ mobile: [{ required: true, message: '鎵嬫満鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const areaList = ref([]) // 鍦板尯鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await UserApi.getUser(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鍦板尯鍒楄〃
+ areaList.value = await AreaApi.getAreaTree()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as UserApi.UserVO
+ if (formType.value === 'create') {
+ // 璇存槑锛氱洰鍓嶆殏鏃舵病鏈夋柊澧炴搷浣溿�傚鏋滆嚜宸变笟鍔¢渶瑕侊紝鍙互杩涜鎵╁睍
+ // await UserApi.createUser(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await UserApi.updateUser(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ mobile: undefined,
+ password: undefined,
+ status: undefined,
+ nickname: undefined,
+ avatar: undefined,
+ name: undefined,
+ sex: undefined,
+ areaId: undefined,
+ birthday: undefined,
+ mark: undefined,
+ tagIds: [],
+ groupId: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/member/user/components/UserBalanceUpdateForm.vue b/src/views/member/user/components/UserBalanceUpdateForm.vue
new file mode 100644
index 0000000..36137cf
--- /dev/null
+++ b/src/views/member/user/components/UserBalanceUpdateForm.vue
@@ -0,0 +1,144 @@
+<template>
+ <Dialog v-model="dialogVisible" title="淇敼鐢ㄦ埛浣欓" width="600">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="130px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="id">
+ <el-input v-model="formData.id" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input v-model="formData.nickname" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鍙樺姩鍓嶄綑棰�(鍏�)" prop="balance">
+ <el-input :model-value="formData.balance" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鍙樺姩绫诲瀷" prop="changeType">
+ <el-radio-group v-model="formData.changeType">
+ <el-radio :label="1">澧炲姞</el-radio>
+ <el-radio :label="-1">鍑忓皯</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍙樺姩浣欓(鍏�)" prop="changeBalance">
+ <el-input-number
+ v-model="formData.changeBalance"
+ :min="0"
+ :precision="2"
+ :step="0.1"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍙樺姩鍚庝綑棰�(鍏�)">
+ <el-input :model-value="balanceResult" class="!w-240px" disabled />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import { convertToInteger, formatToFraction } from '@/utils'
+
+/** 淇敼鐢ㄦ埛浣欓琛ㄥ崟 */
+defineOptions({ name: 'UpdateBalanceForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ nickname: undefined,
+ balance: '0',
+ changeBalance: 0,
+ changeType: 1
+})
+const formRules = reactive({
+ changeBalance: [{ required: true, message: '鍙樺姩浣欓涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id?: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const user = await UserApi.getUser(id)
+ const wallet = await WalletApi.getWallet({ userId: user.id || 0 })
+ formData.value.id = user.id
+ formData.value.nickname = user.nickname
+ formData.value.balance = formatToFraction(wallet.balance)
+ formData.value.changeType = 1 // 榛樿澧炲姞浣欓
+ formData.value.changeBalance = 0 // 鍙樺姩浣欓榛樿0
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+
+ if (formData.value.changeBalance <= 0) {
+ message.error('鍙樺姩浣欓涓嶈兘涓洪浂')
+ return
+ }
+ if (convertToInteger(balanceResult.value) < 0) {
+ message.error('鍙樺姩鍚庣殑浣欓涓嶈兘灏忎簬 0')
+ return
+ }
+
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await WalletApi.updateWalletBalance({
+ userId: formData.value.id,
+ balance: convertToInteger(formData.value.changeBalance) * formData.value.changeType
+ })
+
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ nickname: undefined,
+ balance: '0',
+ changeBalance: 0,
+ changeType: 1
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鍙樺姩鍚庣殑浣欓 */
+const balanceResult = computed(() =>
+ formatToFraction(
+ convertToInteger(formData.value.balance) +
+ convertToInteger(formData.value.changeBalance) * formData.value.changeType
+ )
+)
+</script>
diff --git a/src/views/member/user/components/UserLevelUpdateForm.vue b/src/views/member/user/components/UserLevelUpdateForm.vue
new file mode 100644
index 0000000..e583f4a
--- /dev/null
+++ b/src/views/member/user/components/UserLevelUpdateForm.vue
@@ -0,0 +1,101 @@
+<template>
+ <Dialog title="淇敼鐢ㄦ埛绛夌骇" v-model="dialogVisible" width="600">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="id">
+ <el-input v-model="formData.id" placeholder="璇疯緭鍏ョ敤鎴锋樀绉�" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input
+ v-model="formData.nickname"
+ placeholder="璇疯緭鍏ョ敤鎴锋樀绉�"
+ class="!w-240px"
+ disabled
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绛夌骇" prop="levelId">
+ <MemberLevelSelect v-model="formData.levelId" />
+ </el-form-item>
+ <el-form-item label="淇敼鍘熷洜" prop="reason">
+ <el-input type="textarea" v-model="formData.reason" placeholder="璇疯緭鍏ヤ慨鏀瑰師鍥�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ nickname: undefined,
+ levelId: undefined,
+ reason: undefined
+})
+const formRules = reactive({
+ reason: [{ required: true, message: '淇敼鍘熷洜涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id?: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await UserApi.getUser(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await UserApi.updateUserLevel(formData.value)
+
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ nickname: undefined,
+ levelId: undefined,
+ reason: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/member/user/components/UserPointUpdateForm.vue b/src/views/member/user/components/UserPointUpdateForm.vue
new file mode 100644
index 0000000..ad673be
--- /dev/null
+++ b/src/views/member/user/components/UserPointUpdateForm.vue
@@ -0,0 +1,129 @@
+<template>
+ <Dialog v-model="dialogVisible" title="淇敼鐢ㄦ埛绉垎" width="600">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="id">
+ <el-input v-model="formData.id" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input v-model="formData.nickname" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鍙樺姩鍓嶇Н鍒�" prop="point">
+ <el-input-number v-model="formData.point" class="!w-240px" disabled />
+ </el-form-item>
+ <el-form-item label="鍙樺姩绫诲瀷" prop="changeType">
+ <el-radio-group v-model="formData.changeType">
+ <el-radio :value="1">澧炲姞</el-radio>
+ <el-radio :value="-1">鍑忓皯</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍙樺姩绉垎" prop="changePoint">
+ <el-input-number v-model="formData.changePoint" :min="0" :precision="0" class="!w-240px" />
+ </el-form-item>
+ <el-form-item label="鍙樺姩鍚庣Н鍒�">
+ <el-input-number v-model="pointResult" class="!w-240px" disabled />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/member/user'
+
+/** 淇敼鐢ㄦ埛绉垎琛ㄥ崟 */
+defineOptions({ name: 'UpdatePointForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: undefined,
+ nickname: undefined,
+ point: 0,
+ changePoint: 0,
+ changeType: 1
+})
+const formRules = reactive({
+ changePoint: [{ required: true, message: '鍙樺姩绉垎涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id?: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await UserApi.getUser(id)
+ formData.value.changeType = 1 // 榛樿澧炲姞绉垎
+ formData.value.changePoint = 0 // 鍙樺姩绉垎榛樿0
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+
+ if (formData.value.changePoint < 1) {
+ message.error('鍙樺姩绉垎涓嶈兘灏忎簬 1')
+ return
+ }
+ if (pointResult.value < 0) {
+ message.error('鍙樺姩鍚庣殑绉垎涓嶈兘灏忎簬 0')
+ return
+ }
+
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await UserApi.updateUserPoint({
+ id: formData.value.id,
+ point: formData.value.changePoint * formData.value.changeType
+ })
+
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ nickname: undefined,
+ point: 0,
+ changePoint: 0,
+ changeType: 1
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鍙樺姩鍚庣殑绉垎 */
+const pointResult = computed(
+ () => formData.value.point + formData.value.changePoint * formData.value.changeType
+)
+</script>
diff --git a/src/views/member/user/detail/UserAccountInfo.vue b/src/views/member/user/detail/UserAccountInfo.vue
new file mode 100644
index 0000000..fad174a
--- /dev/null
+++ b/src/views/member/user/detail/UserAccountInfo.vue
@@ -0,0 +1,84 @@
+<template>
+ <el-descriptions :class="{ 'kefu-descriptions': column === 1 }" :column="column">
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="svg-icon:member_level" label=" 绛夌骇 " />
+ </template>
+ {{ user.levelName || '鏃�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:suitcase" label=" 鎴愰暱鍊� " />
+ </template>
+ {{ user.experience || 0 }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:coin" label=" 褰撳墠绉垎 " />
+ </template>
+ {{ user.point || 0 }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:coin" label=" 鎬荤Н鍒� " />
+ </template>
+ {{ user.totalPoint || 0 }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="svg-icon:member_balance" label=" 褰撳墠浣欓 " />
+ </template>
+ {{ fenToYuan(wallet.balance || 0) }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="svg-icon:member_expenditure_balance" label=" 鏀嚭閲戦 " />
+ </template>
+ {{ fenToYuan(wallet.totalExpense || 0) }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="svg-icon:member_recharge_balance" label=" 鍏呭�奸噾棰� " />
+ </template>
+ {{ fenToYuan(wallet.totalRecharge || 0) }}
+ </el-descriptions-item>
+ </el-descriptions>
+</template>
+<script lang="ts" setup>
+import { DescriptionsItemLabel } from '@/components/Descriptions'
+import * as UserApi from '@/api/member/user'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import { fenToYuan } from '@/utils'
+
+withDefaults(defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO; column?: number }>(), {
+ column: 2
+}) // 鐢ㄦ埛淇℃伅
+</script>
+<style lang="scss" scoped>
+.cell-item {
+ display: inline;
+}
+
+.cell-item::after {
+ content: ':';
+}
+
+.kefu-descriptions {
+ ::v-deep(.el-descriptions__cell) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .el-descriptions__label {
+ width: 120px;
+ display: block;
+ text-align: left;
+ }
+
+ .el-descriptions__content {
+ flex: 1;
+ text-align: end;
+ }
+ }
+}
+</style>
diff --git a/src/views/member/user/detail/UserAddressList.vue b/src/views/member/user/detail/UserAddressList.vue
new file mode 100644
index 0000000..a37caba
--- /dev/null
+++ b/src/views/member/user/detail/UserAddressList.vue
@@ -0,0 +1,54 @@
+<template>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍦板潃缂栧彿" align="center" prop="id" width="150px" />
+ <el-table-column label="鏀朵欢浜哄悕绉�" align="center" prop="name" width="150px" />
+ <el-table-column label="鎵嬫満鍙�" align="center" prop="mobile" width="150px" />
+ <el-table-column label="鍦板尯缂栫爜" align="center" prop="areaId" width="150px" />
+ <el-table-column label="鏀朵欢璇︾粏鍦板潃" align="center" prop="detailAddress" />
+ <el-table-column label="鏄惁榛樿" align="center" prop="defaultStatus" width="150px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="Number(scope.row.defaultStatus)" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ </el-table>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as AddressApi from '@/api/member/address'
+
+const { userId }: { userId: number } = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+})
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ list.value = await AddressApi.getAddressList({ userId })
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/member/user/detail/UserAftersaleList.vue b/src/views/member/user/detail/UserAftersaleList.vue
new file mode 100644
index 0000000..03f3a53
--- /dev/null
+++ b/src/views/member/user/detail/UserAftersaleList.vue
@@ -0,0 +1,276 @@
+<template>
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form ref="queryFormRef" :inline="true" :model="queryParams" label-width="68px">
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="spuName">
+ <el-input
+ v-model="queryParams.spuName"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ュ晢鍝� SPU 鍚嶇О"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="閫�娆剧紪鍙�" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ラ��娆剧紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="璁㈠崟缂栧彿" prop="orderNo">
+ <el-input
+ v-model="queryParams.orderNo"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ヨ鍗曠紪鍙�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍞悗鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-280px"
+ clearable
+ placeholder="璇烽�夋嫨鍞悗鐘舵��"
+ >
+ <el-option label="鍏ㄩ儴" value="0" />
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍞悗鏂瑰紡" prop="way">
+ <el-select
+ v-model="queryParams.way"
+ class="!w-280px"
+ clearable
+ placeholder="璇烽�夋嫨鍞悗鏂瑰紡"
+ >
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍞悗绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ class="!w-280px"
+ clearable
+ placeholder="璇烽�夋嫨鍞悗绫诲瀷"
+ >
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-280px"
+ end-placeholder="鑷畾涔夋椂闂�"
+ start-placeholder="鑷畾涔夋椂闂�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-tabs v-model="queryParams.status" @tab-click="tabClick">
+ <el-tab-pane
+ v-for="item in statusTabs"
+ :key="item.label"
+ :label="item.label"
+ :name="item.value"
+ />
+ </el-tabs>
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="閫�娆剧紪鍙�" min-width="200" prop="no" />
+ <el-table-column align="center" label="璁㈠崟缂栧彿" min-width="200" prop="orderNo">
+ <template #default="{ row }">
+ <el-button link type="primary" @click="openOrderDetail(row.orderId)">
+ {{ row.orderNo }}
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍟嗗搧淇℃伅" min-width="600" prop="spuName">
+ <template #default="{ row }">
+ <div class="flex items-center">
+ <el-image
+ :src="row.picUrl"
+ class="mr-10px h-30px w-30px"
+ @click="imagePreview(row.picUrl)"
+ />
+ <span class="mr-10px">{{ row.spuName }}</span>
+ <el-tag v-for="property in row.properties" :key="property.propertyId" class="mr-10px">
+ {{ property.propertyName }}: {{ property.valueName }}
+ </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="璁㈠崟閲戦" min-width="120" prop="refundPrice">
+ <template #default="scope">
+ <span>{{ fenToYuan(scope.row.refundPrice) }} 鍏�</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐢宠鏃堕棿" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ formatDate(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍞悗鐘舵��" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鍞悗鏂瑰紡">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="scope.row.way" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" width="120">
+ <template #default="{ row }">
+ <el-button link type="primary" @click="openAfterSaleDetail(row.id)">澶勭悊閫�娆�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import { TabsPaneContext } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'UserAfterSaleList' })
+
+const { push } = useRouter() // 璺敱璺宠浆
+const props = defineProps<{
+ userId: number
+}>()
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<AfterSaleApi.TradeAfterSaleVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const statusTabs = ref([
+ {
+ label: '鍏ㄩ儴',
+ value: '0'
+ }
+])
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+// 鏌ヨ鍙傛暟
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ no: null,
+ status: '0',
+ orderNo: null,
+ spuName: null,
+ createTime: [],
+ way: null,
+ type: null,
+ userId: null
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = cloneDeep(queryParams.value)
+ // 澶勭悊鎺夊叏閮ㄧ殑鐘舵�侊紝涓嶄紶灏辨槸鍏ㄩ儴
+ if (data.status === '0') {
+ delete data.status
+ }
+ // 鎵ц鏌ヨ
+ if (props.userId) {
+ data.userId = props.userId as any
+ }
+ const res = await AfterSaleApi.getAfterSalePage(data)
+ list.value = res.list as AfterSaleApi.TradeAfterSaleVO[]
+ total.value = res.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = async () => {
+ queryParams.value.pageNo = 1
+ await getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** tab 鍒囨崲 */
+const tabClick = async (tab: TabsPaneContext) => {
+ queryParams.value.status = tab.paneName as any
+ await getList()
+}
+
+/** 澶勭悊閫�娆� */
+const openAfterSaleDetail = (id: number) => {
+ push({ name: 'TradeAfterSaleDetail', params: { id } })
+}
+
+/** 鏌ョ湅璁㈠崟璇︽儏 */
+const openOrderDetail = (id: number) => {
+ push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 鍟嗗搧鍥鹃瑙� */
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+onMounted(async () => {
+ await getList()
+ // 璁剧疆 statuses 杩囨护
+ for (const dict of getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS)) {
+ statusTabs.value.push({
+ label: dict.label,
+ value: dict.value
+ })
+ }
+})
+</script>
diff --git a/src/views/member/user/detail/UserBalanceList.vue b/src/views/member/user/detail/UserBalanceList.vue
new file mode 100644
index 0000000..08179e0
--- /dev/null
+++ b/src/views/member/user/detail/UserBalanceList.vue
@@ -0,0 +1,67 @@
+<template>
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="缂栧彿" prop="id" />
+ <el-table-column align="center" label="鍏宠仈涓氬姟鏍囬" prop="title" />
+ <el-table-column align="center" label="浜ゆ槗閲戦" prop="price">
+ <template #default="{ row }"> {{ fenToYuan(row.price) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column align="center" label="閽卞寘浣欓" prop="balance">
+ <template #default="{ row }"> {{ fenToYuan(row.balance) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浜ゆ槗鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as WalletTransactionApi from '@/api/pay/wallet/transaction'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'UserBalanceList' })
+const props = defineProps({
+ walletId: {
+ type: Number,
+ required: false
+ }
+})
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ walletId: null
+})
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ queryParams.walletId = props.walletId as any
+ const data = await WalletTransactionApi.getWalletTransactionPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/user/detail/UserBasicInfo.vue b/src/views/member/user/detail/UserBasicInfo.vue
new file mode 100644
index 0000000..0fe2538
--- /dev/null
+++ b/src/views/member/user/detail/UserBasicInfo.vue
@@ -0,0 +1,165 @@
+<template>
+ <el-card shadow="never">
+ <template #header>
+ <slot name="header"></slot>
+ </template>
+ <el-row v-if="mode === 'member'">
+ <el-col :span="4">
+ <ElAvatar :size="140" :src="user.avatar || undefined" shape="square" />
+ </el-col>
+ <el-col :span="20">
+ <el-descriptions :column="2">
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:user" label="鐢ㄦ埛鍚�" />
+ </template>
+ {{ user.name || '绌�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:user" label="鏄电О" />
+ </template>
+ {{ user.nickname }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満鍙�">
+ <template #label>
+ <descriptions-item-label icon="ep:phone" label="鎵嬫満鍙�" />
+ </template>
+ {{ user.mobile }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="fa:mars-double" label="鎬у埆" />
+ </template>
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:location" label="鎵�鍦ㄥ湴" />
+ </template>
+ {{ user.areaName }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:position" label="娉ㄥ唽 IP" />
+ </template>
+ {{ user.registerIp }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="fa:birthday-cake" label="鐢熸棩" />
+ </template>
+ {{ user.birthday ? formatDate(user.birthday as any) : '绌�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:calendar" label="娉ㄥ唽鏃堕棿" />
+ </template>
+ {{ user.createTime ? formatDate(user.createTime as any) : '绌�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:calendar" label="鏈�鍚庣櫥褰曟椂闂�" />
+ </template>
+ {{ user.loginDate ? formatDate(user.loginDate as any) : '绌�' }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-col>
+ </el-row>
+ <template v-if="mode === 'kefu'">
+ <ElAvatar :size="140" :src="user.avatar || undefined" shape="square" />
+ <el-descriptions :column="1" class="kefu-descriptions">
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:user" label="鐢ㄦ埛鍚�" />
+ </template>
+ {{ user.name || '绌�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:user" label="鏄电О" />
+ </template>
+ {{ user.nickname }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:phone" label="鎵嬫満鍙�" />
+ </template>
+ {{ user.mobile }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="fa:mars-double" label="鎬у埆" />
+ </template>
+ <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:location" label="鎵�鍦ㄥ湴" />
+ </template>
+ {{ user.areaName }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:position" label="娉ㄥ唽 IP" />
+ </template>
+ {{ user.registerIp }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="fa:birthday-cake" label="鐢熸棩" />
+ </template>
+ {{ user.birthday ? formatDate(user.birthday as any) : '绌�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:calendar" label="娉ㄥ唽鏃堕棿" />
+ </template>
+ {{ user.createTime ? formatDate(user.createTime as any) : '绌�' }}
+ </el-descriptions-item>
+ <el-descriptions-item>
+ <template #label>
+ <descriptions-item-label icon="ep:calendar" label="鏈�鍚庣櫥褰曟椂闂�" />
+ </template>
+ {{ user.loginDate ? formatDate(user.loginDate as any) : '绌�' }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </template>
+ </el-card>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DescriptionsItemLabel } from '@/components/Descriptions/index'
+
+withDefaults(defineProps<{ user: UserApi.UserVO; mode?: string }>(), {
+ mode: 'member'
+})
+</script>
+<style lang="scss" scoped>
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+::v-deep(.kefu-descriptions) {
+ .el-descriptions__cell {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .el-descriptions__label {
+ width: 120px;
+ display: block;
+ text-align: left;
+ }
+
+ .el-descriptions__content {
+ flex: 1;
+ text-align: end;
+ }
+ }
+}
+</style>
diff --git a/src/views/member/user/detail/UserBrokerageList.vue b/src/views/member/user/detail/UserBrokerageList.vue
new file mode 100644
index 0000000..68c3c84
--- /dev/null
+++ b/src/views/member/user/detail/UserBrokerageList.vue
@@ -0,0 +1,125 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="85px"
+ >
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="level">
+ <el-radio-group v-model="queryParams.level" @change="handleQuery">
+ <el-radio-button checked>鍏ㄩ儴</el-radio-button>
+ <el-radio-button value="1">涓�绾ф帹骞夸汉</el-radio-button>
+ <el-radio-button value="2">浜岀骇鎺ㄥ箍浜�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="缁戝畾鏃堕棿" prop="bindUserTime">
+ <el-date-picker
+ v-model="queryParams.bindUserTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="id" min-width="80px" />
+ <el-table-column label="澶村儚" align="center" prop="avatar" width="70px">
+ <template #default="scope">
+ <el-avatar :src="scope.row.avatar" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄电О" align="center" prop="nickname" min-width="80px" />
+ <el-table-column label="绛夌骇" align="center" prop="level" min-width="80px">
+ <template #default="scope">
+ <el-tag v-if="scope.row.bindUserId === bindUserId">涓�绾�</el-tag>
+ <el-tag v-else>浜岀骇</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="缁戝畾鏃堕棿"
+ align="center"
+ prop="bindUserTime"
+ :formatter="dateFormatter"
+ width="170px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 鎺ㄥ箍浜哄垪琛� */
+defineOptions({ name: 'UserBrokerageList' })
+
+const { bindUserId }: { bindUserId: number } = defineProps({
+ bindUserId: {
+ type: Number,
+ required: true
+ }
+}) //鐢ㄦ埛缂栧彿
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ bindUserId: null,
+ level: '',
+ bindUserTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ queryParams.bindUserId = bindUserId
+ const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/member/user/detail/UserCouponList.vue b/src/views/member/user/detail/UserCouponList.vue
new file mode 100644
index 0000000..2279b8a
--- /dev/null
+++ b/src/views/member/user/detail/UserCouponList.vue
@@ -0,0 +1,190 @@
+<template>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />鎼滅储 </el-button>
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />閲嶇疆 </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <!-- Tab 閫夐」锛氱湡姝g殑鍐呭鍦� Lab -->
+ <el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
+ <el-tab-pane
+ v-for="tab in statusTabs"
+ :key="tab.value"
+ :label="tab.label"
+ :name="tab.value"
+ />
+ </el-tabs>
+
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="浼樻儬鍔�" align="center" prop="name" />
+ <el-table-column label="浼樻儬鍒哥被鍨�" align="center" prop="discountType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="棰嗗彇鏂瑰紡" align="center" prop="takeType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="棰嗗彇鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column
+ label="浣跨敤鏃堕棿"
+ align="center"
+ prop="useTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['promotion:coupon:delete']"
+ type="danger"
+ link
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍥炴敹
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts" name="UserCouponList">
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'UserCouponList' })
+
+const { userId }: { userId: number } = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+}) //鐢ㄦ埛缂栧彿
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 瀛楀吀琛ㄦ牸鏁版嵁
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ createTime: [],
+ status: undefined,
+ userIds: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+const activeTab = ref('all') // Tab 绛涢��
+const statusTabs = reactive([
+ {
+ label: '鍏ㄩ儴',
+ value: 'all'
+ }
+])
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ // 鎵ц鏌ヨ
+ try {
+ queryParams.userIds = userId
+ const data = await getCouponPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 浜屾纭
+ await message.confirm(
+ '鍥炴敹灏嗕細鏀跺洖浼氬憳棰嗗彇鐨勫緟浣跨敤鐨勪紭鎯犲埜锛屽凡浣跨敤鐨勫皢鏃犳硶鍥炴敹锛岀‘瀹氳鍥炴敹鎵�閫変紭鎯犲埜鍚楋紵'
+ )
+ // 鍙戣捣鍒犻櫎
+ await deleteCoupon(id)
+ message.notifySuccess('鍥炴敹鎴愬姛')
+ // 閲嶆柊鍔犺浇鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** tab 鍒囨崲 */
+const onTabChange = (tabName) => {
+ queryParams.status = tabName === 'all' ? undefined : tabName
+ getList()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+ // 璁剧疆 statuses 杩囨护
+ for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
+ statusTabs.push({
+ label: dict.label,
+ value: dict.value as string
+ })
+ }
+})
+</script>
diff --git a/src/views/member/user/detail/UserExperienceRecordList.vue b/src/views/member/user/detail/UserExperienceRecordList.vue
new file mode 100644
index 0000000..64414ad
--- /dev/null
+++ b/src/views/member/user/detail/UserExperienceRecordList.vue
@@ -0,0 +1,158 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="涓氬姟绫诲瀷" prop="bizType">
+ <el-select
+ v-model="queryParams.bizType"
+ placeholder="璇烽�夋嫨涓氬姟绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏍囬" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ユ爣棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="150px" />
+ <el-table-column
+ label="鑾峰緱鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="缁忛獙" align="center" prop="experience" width="150px">
+ <template #default="scope">
+ <el-tag v-if="scope.row.experience > 0" class="ml-2" type="success" effect="dark">
+ +{{ scope.row.experience }}
+ </el-tag>
+ <el-tag v-else class="ml-2" type="danger" effect="dark">
+ {{ scope.row.experience }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎬荤粡楠�" align="center" prop="totalExperience" width="150px">
+ <template #default="scope">
+ <el-tag class="ml-2" effect="dark">
+ {{ scope.row.totalExperience }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏍囬" align="center" prop="title" width="150px" />
+ <el-table-column label="鎻忚堪" align="center" prop="description" />
+ <el-table-column label="涓氬姟缂栧彿" align="center" prop="bizId" width="150px" />
+ <el-table-column label="涓氬姟绫诲瀷" align="center" prop="bizType" width="150px">
+ <!-- TODO 鑺嬭壙锛氭澶勫簲鍒涘缓瀵瑰簲鐨勫瓧鍏� -->
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.MEMBER_EXPERIENCE_BIZ_TYPE" :value="scope.row.bizType" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as ExperienceRecordApi from '@/api/member/experience-record/index'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'UserExperienceRecordList' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ bizId: null,
+ bizType: null,
+ title: null,
+ description: null,
+ experience: null,
+ totalExperience: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ExperienceRecordApi.getExperienceRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+const { userId } = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+})
+/** 鍒濆鍖� **/
+onMounted(() => {
+ queryParams.userId = userId
+ getList()
+})
+</script>
diff --git a/src/views/member/user/detail/UserFavoriteList.vue b/src/views/member/user/detail/UserFavoriteList.vue
new file mode 100644
index 0000000..afab9a0
--- /dev/null
+++ b/src/views/member/user/detail/UserFavoriteList.vue
@@ -0,0 +1,96 @@
+<template>
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column key="id" align="center" label="鍟嗗搧缂栧彿" width="180" prop="id" />
+ <el-table-column label="鍟嗗搧鍥�" min-width="80">
+ <template #default="{ row }">
+ <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+ </template>
+ </el-table-column>
+ <el-table-column :show-overflow-tooltip="true" label="鍟嗗搧鍚嶇О" min-width="300" prop="name" />
+ <el-table-column align="center" label="鍟嗗搧鍞环" min-width="90" prop="price">
+ <template #default="{ row }"> {{ floatToFixed2(row.price) }}鍏�</template>
+ </el-table-column>
+ <el-table-column align="center" label="閿�閲�" min-width="90" prop="salesCount" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏀惰棌鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鐘舵��" min-width="80">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PRODUCT_SPU_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as FavoriteApi from '@/api/mall/product/favorite'
+import { floatToFixed2 } from '@/utils'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ createTime: [],
+ userId: NaN
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await FavoriteApi.getFavoritePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+const { userId } = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ queryParams.userId = userId
+ getList()
+})
+</script>
diff --git a/src/views/member/user/detail/UserOrderList.vue b/src/views/member/user/detail/UserOrderList.vue
new file mode 100644
index 0000000..92ee70d
--- /dev/null
+++ b/src/views/member/user/detail/UserOrderList.vue
@@ -0,0 +1,279 @@
+<template>
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="璁㈠崟鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀粯鏂瑰紡" prop="payChannelCode">
+ <el-select
+ v-model="queryParams.payChannelCode"
+ class="!w-280px"
+ clearable
+ placeholder="鍏ㄩ儴"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-280px"
+ end-placeholder="鑷畾涔夋椂闂�"
+ start-placeholder="鑷畾涔夋椂闂�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="璁㈠崟鏉ユ簮" prop="terminal">
+ <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁㈠崟绫诲瀷" prop="type">
+ <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閰嶉�佹柟寮�" prop="deliveryType">
+ <el-select v-model="queryParams.deliveryType" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+ label="蹇�掑叕鍙�"
+ prop="logisticsId"
+ >
+ <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="鍏ㄩ儴">
+ <el-option
+ v-for="item in deliveryExpressList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+ label="鑷彁闂ㄥ簵"
+ prop="pickUpStoreId"
+ >
+ <el-select
+ v-model="queryParams.pickUpStoreId"
+ class="!w-280px"
+ clearable
+ multiple
+ placeholder="鍏ㄩ儴"
+ >
+ <el-option
+ v-for="item in pickUpStoreList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+ label="鏍搁攢鐮�"
+ prop="pickUpVerifyCode"
+ >
+ <el-input
+ v-model="queryParams.pickUpVerifyCode"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏ヨ嚜鎻愭牳閿�鐮�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鑱氬悎鎼滅储">
+ <el-input
+ v-show="true"
+ v-model="queryParams[queryType.queryParam]"
+ class="!w-280px"
+ clearable
+ placeholder="璇疯緭鍏�"
+ >
+ <template #prepend>
+ <el-select
+ v-model="queryType.queryParam"
+ class="!w-110px"
+ clearable
+ placeholder="鍏ㄩ儴"
+ @change="inputChangeSelect"
+ >
+ <el-option
+ v-for="dict in dynamicSearchList"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <!-- 娣诲姞 row-key="id" 瑙e喅鍒楁暟鎹腑鐨� table#header 鏁版嵁涓嶅埛鏂扮殑闂 -->
+ <el-table v-loading="loading" :data="list" row-key="id">
+ <OrderTableColumn :list="list" :pick-up-store-list="pickUpStoreList">
+ <template #default="{ row }">
+ <el-button link type="primary" @click="openDetail(row.id)">
+ <Icon icon="ep:notification" />
+ 璇︽儏
+ </el-button>
+ </template>
+ </OrderTableColumn>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as OrderApi from '@/api/mall/trade/order/index'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { FormInstance } from 'element-plus'
+import { OrderTableColumn } from '@/views/mall/trade/order/components'
+import { DeliveryTypeEnum } from '@/utils/constants'
+
+const { push } = useRouter() // 璺敱璺宠浆
+
+const { userId } = defineProps<{
+ userId: number
+}>()
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const pickUpStoreList = ref<PickUpStoreApi.DeliveryPickUpStoreVO[]>([]) // 鑷彁闂ㄥ簵绮剧畝鍒楄〃
+const deliveryExpressList = ref<DeliveryExpressApi.DeliveryExpressVO[]>([]) // 鐗╂祦鍏徃
+const queryFormRef = ref<FormInstance>() // 鎼滅储鐨勮〃鍗�
+// 琛ㄥ崟鎼滅储
+const queryParams = ref({
+ pageNo: 1, // 椤垫暟
+ pageSize: 10, // 姣忛〉鏄剧ず鏁伴噺
+ userId: userId,
+ status: undefined, // 璁㈠崟鐘舵��
+ payChannelCode: undefined, // 鏀粯鏂瑰紡
+ createTime: undefined, // 鍒涘缓鏃堕棿
+ terminal: undefined, // 璁㈠崟鏉ユ簮
+ type: undefined, // 璁㈠崟绫诲瀷
+ deliveryType: undefined, // 閰嶉�佹柟寮�
+ logisticsId: undefined, // 蹇�掑叕鍙�
+ pickUpStoreId: undefined, // 鑷彁闂ㄥ簵
+ pickUpVerifyCode: undefined // 鑷彁鏍搁攢鐮�
+})
+const queryType = reactive({ queryParam: '' }) // 璁㈠崟鎼滅储绫诲瀷 queryParam
+
+// 璁㈠崟鑱氬悎鎼滅储 select 绫诲瀷閰嶇疆锛堝姩鎬佹悳绱級
+const dynamicSearchList = ref([
+ { value: 'no', label: '璁㈠崟鍙�' },
+ { value: 'userNickname', label: '鐢ㄦ埛鏄电О' },
+ { value: 'userMobile', label: '鐢ㄦ埛鐢佃瘽' }
+])
+/**
+ * 鑱氬悎鎼滅储鍒囨崲鏌ヨ瀵硅薄鏃惰Е鍙�
+ * @param val
+ */
+const inputChangeSelect = (val: string) => {
+ dynamicSearchList.value
+ .filter((item) => item.value !== val)
+ ?.forEach((item1) => {
+ // 娓呴櫎闆嗗悎鎼滅储鏃犵敤灞炴��
+ if (queryParams.value.hasOwnProperty(item1.value)) {
+ delete queryParams.value[item1.value]
+ }
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = async () => {
+ queryParams.value.pageNo = 1
+ await getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ queryParams.value.userId = userId
+ handleQuery()
+}
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await OrderApi.getOrderPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鏌ョ湅璁㈠崟璇︽儏 */
+const openDetail = (id: number) => {
+ push({ name: 'TradeOrderDetail', params: { id } })
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ pickUpStoreList.value = await PickUpStoreApi.getSimpleDeliveryPickUpStoreList()
+ deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>
diff --git a/src/views/member/user/detail/UserPointList.vue b/src/views/member/user/detail/UserPointList.vue
new file mode 100644
index 0000000..9754b29
--- /dev/null
+++ b/src/views/member/user/detail/UserPointList.vue
@@ -0,0 +1,152 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="涓氬姟绫诲瀷" prop="bizType">
+ <el-select
+ v-model="queryParams.bizType"
+ placeholder="璇烽�夋嫨涓氬姟绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.MEMBER_POINT_BIZ_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="绉垎鏍囬" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ョН鍒嗘爣棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鑾峰緱鏃堕棿" prop="createDate">
+ <el-date-picker
+ v-model="queryParams.createDate"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="180" />
+ <el-table-column
+ label="鑾峰緱鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鑾峰緱绉垎" align="center" prop="point" width="100">
+ <template #default="scope">
+ <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+ +{{ scope.row.point }}
+ </el-tag>
+ <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎬荤Н鍒�" align="center" prop="totalPoint" width="100" />
+ <el-table-column label="鏍囬" align="center" prop="title" />
+ <el-table-column label="鎻忚堪" align="center" prop="description" />
+ <el-table-column label="涓氬姟缂栫爜" align="center" prop="bizId" />
+ <el-table-column label="涓氬姟绫诲瀷" align="center" prop="bizType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.MEMBER_POINT_BIZ_TYPE" :value="scope.row.bizType" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RecordApi from '@/api//member/point/record'
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ bizType: undefined,
+ title: null,
+ createDate: [],
+ userId: NaN
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await RecordApi.getRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+const { userId } = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ queryParams.userId = userId
+ getList()
+})
+</script>
diff --git a/src/views/member/user/detail/UserSignList.vue b/src/views/member/user/detail/UserSignList.vue
new file mode 100644
index 0000000..c897274
--- /dev/null
+++ b/src/views/member/user/detail/UserSignList.vue
@@ -0,0 +1,135 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="绛惧埌鐢ㄦ埛" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ placeholder="璇疯緭鍏ョ鍒扮敤鎴�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绛惧埌澶╂暟" prop="day">
+ <el-input
+ v-model="queryParams.day"
+ placeholder="璇疯緭鍏ョ鍒板ぉ鏁�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绛惧埌鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="绛惧埌澶╂暟"
+ align="center"
+ prop="day"
+ :formatter="(_, __, cellValue) => ['绗�', cellValue, '澶�'].join(' ')"
+ />
+ <el-table-column label="鑾峰緱绉垎" align="center" prop="point" width="100">
+ <template #default="scope">
+ <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
+ +{{ scope.row.point }}
+ </el-tag>
+ <el-tag v-else class="ml-2" type="danger" effect="dark"> {{ scope.row.point }} </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="绛惧埌鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as SignInRecordApi from '@/api/member/signin/record'
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: NaN,
+ nickname: null,
+ day: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SignInRecordApi.getSignInRecordPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+const { userId } = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+})
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ queryParams.userId = userId
+ getList()
+})
+</script>
diff --git a/src/views/member/user/detail/index.vue b/src/views/member/user/detail/index.vue
new file mode 100644
index 0000000..9818219
--- /dev/null
+++ b/src/views/member/user/detail/index.vue
@@ -0,0 +1,160 @@
+<template>
+ <div v-loading="loading">
+ <el-row :gutter="10">
+ <!-- 宸︿笂瑙掞細鍩烘湰淇℃伅 -->
+ <el-col :span="14" class="detail-info-item">
+ <UserBasicInfo :user="user">
+ <template #header>
+ <div class="card-header">
+ <CardTitle title="鍩烘湰淇℃伅" />
+ <el-button size="small" text type="primary" @click="openForm('update')">
+ 缂栬緫
+ </el-button>
+ </div>
+ </template>
+ </UserBasicInfo>
+ </el-col>
+ <!-- 鍙充笂瑙掞細璐︽埛淇℃伅 -->
+ <el-col :span="10" class="detail-info-item">
+ <el-card class="h-full" shadow="never">
+ <template #header>
+ <CardTitle title="璐︽埛淇℃伅" />
+ </template>
+ <UserAccountInfo :user="user" :wallet="wallet" />
+ </el-card>
+ </el-col>
+ <!-- 涓嬭竟锛氳处鎴锋槑缁� -->
+ <!-- TODO 鑺嬭壙锛氥�愯鍗曠鐞嗐�戙�愬敭鍚庣鐞嗐�戙�愭敹钘忚褰曘��-->
+ <el-card header="璐︽埛鏄庣粏" shadow="never" style="width: 100%; margin-top: 20px">
+ <template #header>
+ <CardTitle title="璐︽埛鏄庣粏" />
+ </template>
+ <el-tabs>
+ <el-tab-pane label="绉垎">
+ <UserPointList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="绛惧埌" lazy>
+ <UserSignList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="鎴愰暱鍊�" lazy>
+ <UserExperienceRecordList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="浣欓" lazy>
+ <UserBalanceList :wallet-id="wallet.id" />
+ </el-tab-pane>
+ <el-tab-pane label="鏀惰揣鍦板潃" lazy>
+ <UserAddressList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="璁㈠崟绠$悊" lazy>
+ <UserOrderList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="鍞悗绠$悊" lazy>
+ <UserAfterSaleList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="鏀惰棌璁板綍" lazy>
+ <UserFavoriteList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="浼樻儬鍔�" lazy>
+ <UserCouponList :user-id="id" />
+ </el-tab-pane>
+ <el-tab-pane label="鎺ㄥ箍鐢ㄦ埛" lazy>
+ <UserBrokerageList :bind-user-id="id" />
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </el-row>
+ </div>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <UserForm ref="formRef" @success="getUserData(id)" />
+</template>
+<script lang="ts" setup>
+import * as WalletApi from '@/api/pay/wallet/balance'
+import * as UserApi from '@/api/member/user'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UserForm from '@/views/member/user/UserForm.vue'
+import UserAccountInfo from './UserAccountInfo.vue'
+import UserAddressList from './UserAddressList.vue'
+import UserBasicInfo from './UserBasicInfo.vue'
+import UserBrokerageList from './UserBrokerageList.vue'
+import UserCouponList from './UserCouponList.vue'
+import UserExperienceRecordList from './UserExperienceRecordList.vue'
+import UserOrderList from './UserOrderList.vue'
+import UserPointList from './UserPointList.vue'
+import UserSignList from './UserSignList.vue'
+import UserFavoriteList from './UserFavoriteList.vue'
+import UserAfterSaleList from './UserAftersaleList.vue'
+import UserBalanceList from './UserBalanceList.vue'
+import { CardTitle } from '@/components/Card/index'
+import { ElMessage } from 'element-plus'
+
+defineOptions({ name: 'MemberDetail' })
+
+const loading = ref(true) // 鍔犺浇涓�
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string) => {
+ formRef.value.open(type, id)
+}
+
+/** 鑾峰緱鐢ㄦ埛 */
+const getUserData = async (id: number) => {
+ loading.value = true
+ try {
+ user.value = await UserApi.getUser(id)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� */
+const { currentRoute } = useRouter() // 璺敱
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const route = useRoute()
+const id = route.params.id
+/* 鐢ㄦ埛閽卞寘鐩稿叧淇℃伅 */
+const WALLET_INIT_DATA = {
+ balance: 0,
+ totalExpense: 0,
+ totalRecharge: 0
+} as WalletApi.WalletVO // 閽卞寘鍒濆鍖栨暟鎹�
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 閽卞寘淇℃伅
+
+/** 鏌ヨ鐢ㄦ埛閽卞寘淇℃伅 */
+const getUserWallet = async () => {
+ if (!id) {
+ wallet.value = WALLET_INIT_DATA
+ return
+ }
+ const params = { userId: id }
+ wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
+onMounted(() => {
+ if (!id) {
+ ElMessage.warning('鍙傛暟閿欒锛屼細鍛樼紪鍙蜂笉鑳戒负绌猴紒')
+ delView(unref(currentRoute))
+ return
+ }
+ getUserData(id)
+ getUserWallet()
+})
+</script>
+<style lang="css" scoped>
+.detail-info-item:first-child {
+ padding-left: 0 !important;
+}
+
+/* first-child 涓嶇敓鏁堟湁娌℃湁澶т浆缁欑湅涓媞.q */
+.detail-info-item:nth-child(2) {
+ padding-right: 0 !important;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+</style>
diff --git a/src/views/member/user/index.vue b/src/views/member/user/index.vue
new file mode 100644
index 0000000..1c873bf
--- /dev/null
+++ b/src/views/member/user/index.vue
@@ -0,0 +1,317 @@
+<template>
+ <doc-alert title="浼氬憳鐢ㄦ埛銆佹爣绛俱�佸垎缁�" url="https://doc.iocoder.cn/member/user/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴锋樀绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙�" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ユ墜鏈哄彿"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="娉ㄥ唽鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="鐧诲綍鏃堕棿" prop="loginDate">
+ <el-date-picker
+ v-model="queryParams.loginDate"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏍囩" prop="tagIds">
+ <MemberTagSelect v-model="queryParams.tagIds" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绛夌骇" prop="levelId">
+ <MemberLevelSelect v-model="queryParams.levelId" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍒嗙粍" prop="groupId">
+ <MemberGroupSelect v-model="queryParams.groupId" />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['promotion:coupon:send']" @click="openCoupon">鍙戦�佷紭鎯犲埜</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ :show-overflow-tooltip="true"
+ :stripe="true"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column align="center" label="鐢ㄦ埛缂栧彿" prop="id" width="120px" />
+ <el-table-column align="center" label="澶村儚" prop="avatar" width="80px">
+ <template #default="scope">
+ <img :src="scope.row.avatar" style="width: 40px" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鎵嬫満鍙�" prop="mobile" width="120px" />
+ <el-table-column align="center" label="鏄电О" prop="nickname" width="80px" />
+ <el-table-column align="center" label="绛夌骇" prop="levelName" width="100px" />
+ <el-table-column align="center" label="鍒嗙粍" prop="groupName" width="100px" />
+ <el-table-column
+ :show-overflow-tooltip="false"
+ align="center"
+ label="鐢ㄦ埛鏍囩"
+ prop="tagNames"
+ >
+ <template #default="scope">
+ <el-tag v-for="(tagName, index) in scope.row.tagNames" :key="index" class="mr-5px">
+ {{ tagName }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="绉垎" prop="point" width="100px" />
+ <el-table-column align="center" label="鐘舵��" prop="status" width="100px">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鐧诲綍鏃堕棿"
+ prop="loginDate"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="娉ㄥ唽鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column
+ :show-overflow-tooltip="false"
+ align="center"
+ fixed="right"
+ label="鎿嶄綔"
+ width="100px"
+ >
+ <template #default="scope">
+ <div class="flex items-center justify-center">
+ <el-button link type="primary" @click="openDetail(scope.row.id)">璇︽儏</el-button>
+ <el-dropdown
+ v-hasPermi="[
+ 'member:user:update',
+ 'member:user:update-level',
+ 'member:user:update-point',
+ 'pay:wallet:update-balance'
+ ]"
+ @command="(command) => handleCommand(command, scope.row)"
+ >
+ <el-button link type="primary">
+ <Icon icon="ep:d-arrow-right" />
+ 鏇村
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item
+ v-if="checkPermi(['member:user:update'])"
+ command="handleUpdate"
+ >
+ 缂栬緫
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-if="checkPermi(['member:user:update-level'])"
+ command="handleUpdateLevel"
+ >
+ 淇敼绛夌骇
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-if="checkPermi(['member:user:update-point'])"
+ command="handleUpdatePoint"
+ >
+ 淇敼绉垎
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-if="checkPermi(['pay:wallet:update-balance'])"
+ command="handleUpdateBlance"
+ >
+ 淇敼浣欓
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <UserForm ref="formRef" @success="getList" />
+ <!-- 淇敼鐢ㄦ埛绛夌骇寮圭獥 -->
+ <UserLevelUpdateForm ref="updateLevelFormRef" @success="getList" />
+ <!-- 淇敼鐢ㄦ埛绉垎寮圭獥 -->
+ <UserPointUpdateForm ref="updatePointFormRef" @success="getList" />
+ <!-- 淇敼鐢ㄦ埛浣欓寮圭獥 -->
+ <UserBalanceUpdateForm ref="UpdateBalanceFormRef" @success="getList" />
+ <!-- 鍙戦�佷紭鎯犲埜寮圭獥 -->
+ <CouponSendForm ref="couponSendFormRef" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DICT_TYPE } from '@/utils/dict'
+import UserForm from './UserForm.vue'
+import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
+import UserLevelUpdateForm from './components/UserLevelUpdateForm.vue'
+import UserPointUpdateForm from './components/UserPointUpdateForm.vue'
+import UserBalanceUpdateForm from './components/UserBalanceUpdateForm.vue'
+import { CouponSendForm } from '@/views/mall/promotion/coupon/components'
+import { checkPermi } from '@/utils/permission'
+
+defineOptions({ name: 'MemberUser' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ nickname: null,
+ mobile: null,
+ loginDate: [],
+ createTime: [],
+ tagIds: [],
+ levelId: null,
+ groupId: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const updateLevelFormRef = ref() // 淇敼浼氬憳绛夌骇琛ㄥ崟
+const updatePointFormRef = ref() // 淇敼浼氬憳绉垎琛ㄥ崟
+const UpdateBalanceFormRef = ref() // 淇敼鐢ㄦ埛浣欓琛ㄥ崟
+const selectedIds = ref<number[]>([]) // 琛ㄦ牸鐨勯�変腑 ID 鏁扮粍
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await UserApi.getUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鎵撳紑浼氬憳璇︽儏 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+ push({ name: 'MemberUserDetail', params: { id } })
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 琛ㄦ牸閫変腑浜嬩欢 */
+const handleSelectionChange = (rows: UserApi.UserVO[]) => {
+ selectedIds.value = rows.map((row) => row.id)
+}
+
+/** 鍙戦�佷紭鎯犲埜 */
+const couponSendFormRef = ref()
+const openCoupon = () => {
+ if (selectedIds.value.length === 0) {
+ message.warning('璇烽�夋嫨瑕佸彂閫佷紭鎯犲埜鐨勭敤鎴�')
+ return
+ }
+ couponSendFormRef.value.open(selectedIds.value)
+}
+
+/** 鎿嶄綔鍒嗗彂 */
+const handleCommand = (command: string, row: UserApi.UserVO) => {
+ switch (command) {
+ case 'handleUpdate':
+ openForm('update', row.id)
+ break
+ case 'handleUpdateLevel':
+ updateLevelFormRef.value.open(row.id)
+ break
+ case 'handleUpdatePoint':
+ updatePointFormRef.value.open(row.id)
+ break
+ case 'handleUpdateBlance':
+ UpdateBalanceFormRef.value.open(row.id)
+ break
+ default:
+ break
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mp/account/AccountForm.vue b/src/views/mp/account/AccountForm.vue
new file mode 100644
index 0000000..c721013
--- /dev/null
+++ b/src/views/mp/account/AccountForm.vue
@@ -0,0 +1,160 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="rules"
+ label-width="120px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="寰俊鍙�" prop="account">
+ <template #label>
+ <span>
+ <el-tooltip
+ content="鍦ㄥ井淇″叕浼楀钩鍙帮紙mp.weixin.qq.com锛夌殑鑿滃崟 [璁剧疆涓庡紑鍙� - 鍏紬鍙疯缃� - 璐﹀彿璇︽儏] 涓兘鎵惧埌銆屽井淇″彿銆�"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" style="vertical-align: middle" />
+ </el-tooltip>
+ 寰俊鍙�
+ </span>
+ </template>
+ <el-input v-model="formData.account" placeholder="璇疯緭鍏ュ井淇″彿" />
+ </el-form-item>
+ <el-form-item label="appId" prop="appId">
+ <template #label>
+ <span>
+ <el-tooltip
+ content="鍦ㄥ井淇″叕浼楀钩鍙帮紙mp.weixin.qq.com锛夌殑鑿滃崟 [璁剧疆涓庡紑鍙� - 鍏紬鍙疯缃� - 鍩烘湰璁剧疆] 涓兘鎵惧埌銆屽紑鍙戣�匢D(AppID)銆�"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" style="vertical-align: middle" />
+ </el-tooltip>
+ appId
+ </span>
+ </template>
+ <el-input v-model="formData.appId" placeholder="璇疯緭鍏ュ叕浼楀彿 appId" />
+ </el-form-item>
+ <el-form-item label="appSecret" prop="appSecret">
+ <template #label>
+ <span>
+ <el-tooltip
+ content="鍦ㄥ井淇″叕浼楀钩鍙帮紙mp.weixin.qq.com锛夌殑鑿滃崟 [璁剧疆涓庡紑鍙� - 鍏紬鍙疯缃� - 鍩烘湰璁剧疆] 涓兘鎵惧埌銆屽紑鍙戣�呭瘑鐮�(AppSecret)銆�"
+ placement="top"
+ >
+ <Icon icon="ep:question-filled" style="vertical-align: middle" />
+ </el-tooltip>
+ appSecret
+ </span>
+ </template>
+ <el-input v-model="formData.appSecret" placeholder="璇疯緭鍏ュ叕浼楀彿 appSecret" />
+ </el-form-item>
+ <el-form-item label="token" prop="token">
+ <el-input v-model="formData.token" placeholder="璇疯緭鍏ュ叕浼楀彿token" />
+ </el-form-item>
+ <el-form-item label="娑堟伅鍔犺В瀵嗗瘑閽�" prop="aesKey">
+ <el-input v-model="formData.aesKey" placeholder="璇疯緭鍏ユ秷鎭姞瑙e瘑瀵嗛挜" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as AccountApi from '@/api/mp/account'
+
+defineOptions({ name: 'MpAccountForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ account: '',
+ appId: '',
+ appSecret: '',
+ token: '',
+ aesKey: '',
+ remark: ''
+})
+const rules = reactive({
+ name: [{ required: true, message: '鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ account: [{ required: true, message: '鍏紬鍙疯处鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+ appId: [{ required: true, message: '鍏紬鍙� appId 涓嶈兘涓虹┖', trigger: 'blur' }],
+ appSecret: [{ required: true, message: '鍏紬鍙峰瘑閽ヤ笉鑳戒负绌�', trigger: 'blur' }],
+ token: [{ required: true, message: '鍏紬鍙� token 涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await AccountApi.getAccount(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value
+ if (formType.value === 'create') {
+ await AccountApi.createAccount(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await AccountApi.updateAccount(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ account: '',
+ appId: '',
+ appSecret: '',
+ token: '',
+ aesKey: '',
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mp/account/index.vue b/src/views/mp/account/index.vue
new file mode 100644
index 0000000..6551707
--- /dev/null
+++ b/src/views/mp/account/index.vue
@@ -0,0 +1,195 @@
+<template>
+ <doc-alert title="鍏紬鍙锋帴鍏�" url="https://doc.iocoder.cn/mp/account/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />閲嶇疆</el-button>
+ <el-button type="primary" @click="openForm('create')" v-hasPermi="['mp:account:create']">
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鍚嶇О" align="center" prop="name" />
+ <el-table-column label="寰俊鍙�" align="center" prop="account" width="180" />
+ <el-table-column label="appId" align="center" prop="appId" width="180" />
+ <el-table-column label="鏈嶅姟鍣ㄥ湴鍧�(URL)" align="center" prop="appId" width="360">
+ <template #default="scope">
+ {{ 'http://鏈嶅姟绔湴鍧�/admin-api/mp/open/' + scope.row.appId }}
+ </template>
+ </el-table-column>
+ <el-table-column label="浜岀淮鐮�" align="center" prop="qrCodeUrl">
+ <template #default="scope">
+ <img
+ v-if="scope.row.qrCodeUrl"
+ :src="scope.row.qrCodeUrl"
+ alt="浜岀淮鐮�"
+ style="display: inline-block; height: 100px"
+ />
+ <el-button
+ link
+ type="primary"
+ @click="handleGenerateQrCode(scope.row)"
+ v-hasPermi="['mp:account:qr-code']"
+ >
+ 鐢熸垚浜岀淮鐮�
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['mp:account:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['mp:account:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleCleanQuota(scope.row)"
+ v-hasPermi="['mp:account:clear-quota']"
+ >
+ 娓呯┖ API 閰嶉
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 瀵硅瘽妗�(娣诲姞 / 淇敼) -->
+ <AccountForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import * as AccountApi from '@/api/mp/account'
+import AccountForm from './AccountForm.vue'
+
+defineOptions({ name: 'MpAccount' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ account: null,
+ appId: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await AccountApi.getAccountPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await AccountApi.deleteAccount(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鐢熸垚浜岀淮鐮佺殑鎸夐挳鎿嶄綔 */
+const handleGenerateQrCode = async (row) => {
+ try {
+ // 鐢熸垚浜岀淮鐮佺殑浜屾纭
+ await message.confirm('鏄惁纭鐢熸垚鍏紬鍙疯处鍙风紪鍙蜂负"' + row.name + '"鐨勪簩缁寸爜?')
+ // 鍙戣捣鐢熸垚浜岀淮鐮�
+ await AccountApi.generateAccountQrCode(row.id)
+ message.success('鐢熸垚浜岀淮鐮佹垚鍔�')
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 娓呯┖浜岀淮鐮� API 閰嶉鐨勬寜閽搷浣� */
+const handleCleanQuota = async (row) => {
+ try {
+ // 娓呯┖ API 閰嶉鐨勪簩娆$‘璁�
+ await message.confirm('鏄惁纭娓呯┖鐢熸垚鍏紬鍙疯处鍙风紪鍙蜂负"' + row.name + '"鐨� API 閰嶉?')
+ // 鍙戣捣娓呯┖ API 閰嶉
+ await AccountApi.clearAccountQuota(row.id)
+ message.success('娓呯┖ API 閰嶉鎴愬姛')
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/mp/autoReply/components/ReplyForm.vue b/src/views/mp/autoReply/components/ReplyForm.vue
new file mode 100644
index 0000000..1c9dee4
--- /dev/null
+++ b/src/views/mp/autoReply/components/ReplyForm.vue
@@ -0,0 +1,80 @@
+<template>
+ <div>
+ <el-form ref="formRef" :model="replyForm" :rules="rules" label-width="80px">
+ <el-form-item label="娑堟伅绫诲瀷" prop="requestMessageType" v-if="msgType === MsgType.Message">
+ <el-select v-model="replyForm.requestMessageType" placeholder="璇烽�夋嫨">
+ <template v-for="dict in getDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)" :key="dict.value">
+ <el-option
+ v-if="RequestMessageTypes.includes(dict.value)"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </template>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍖归厤绫诲瀷" prop="requestMatch" v-if="msgType === MsgType.Keyword">
+ <el-select v-model="replyForm.requestMatch" placeholder="璇烽�夋嫨鍖归厤绫诲瀷" clearable>
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏抽敭璇�" prop="requestKeyword" v-if="msgType === MsgType.Keyword">
+ <el-input v-model="replyForm.requestKeyword" placeholder="璇疯緭鍏ュ唴瀹�" clearable />
+ </el-form-item>
+ <el-form-item label="鍥炲娑堟伅">
+ <WxReplySelect v-model="reply" />
+ </el-form-item>
+ </el-form>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxReplySelect, { type Reply } from '@/views/mp/components/wx-reply'
+import type { FormInstance } from 'element-plus'
+import { MsgType } from './types'
+import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'ReplyForm' })
+
+const props = defineProps<{
+ modelValue: any
+ reply: Reply
+ msgType: MsgType
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:reply', v: Reply)
+ (e: 'update:modelValue', v: any)
+}>()
+
+const reply = computed<Reply>({
+ get: () => props.reply,
+ set: (val) => emit('update:reply', val)
+})
+
+const replyForm = computed<any>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const formRef = ref<FormInstance | null>(null) // 琛ㄥ崟 ref
+
+const RequestMessageTypes = ['text', 'image', 'voice', 'video', 'shortvideo', 'location', 'link'] // 鍏佽閫夋嫨鐨勮姹傛秷鎭被鍨�
+
+// 琛ㄥ崟鏍¢獙
+const rules = {
+ requestKeyword: [{ required: true, message: '璇锋眰鐨勫叧閿瓧涓嶈兘涓虹┖', trigger: 'blur' }],
+ requestMatch: [{ required: true, message: '璇锋眰鐨勫叧閿瓧鐨勫尮閰嶄笉鑳戒负绌�', trigger: 'blur' }]
+}
+
+defineExpose({
+ resetFields: () => formRef.value?.resetFields(),
+ validate: async () => formRef.value?.validate()
+})
+</script>
+
+<style scoped></style>
diff --git a/src/views/mp/autoReply/components/ReplyTable.vue b/src/views/mp/autoReply/components/ReplyTable.vue
new file mode 100644
index 0000000..2abe9f2
--- /dev/null
+++ b/src/views/mp/autoReply/components/ReplyTable.vue
@@ -0,0 +1,115 @@
+<template>
+ <el-table v-loading="props.loading" :data="props.list">
+ <el-table-column
+ label="璇锋眰娑堟伅绫诲瀷"
+ align="center"
+ prop="requestMessageType"
+ v-if="msgType === MsgType.Message"
+ />
+ <el-table-column
+ label="鍏抽敭璇�"
+ align="center"
+ prop="requestKeyword"
+ v-if="msgType === MsgType.Keyword"
+ />
+ <el-table-column
+ label="鍖归厤绫诲瀷"
+ align="center"
+ prop="requestMatch"
+ v-if="msgType === MsgType.Keyword"
+ >
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.MP_AUTO_REPLY_REQUEST_MATCH" :value="scope.row.requestMatch" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍥炲娑堟伅绫诲瀷" align="center">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.MP_MESSAGE_TYPE" :value="scope.row.responseMessageType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍥炲鍐呭" align="center">
+ <template #default="scope">
+ <div v-if="scope.row.responseMessageType === 'text'">{{ scope.row.responseContent }}</div>
+ <div v-else-if="scope.row.responseMessageType === 'voice'">
+ <WxVoicePlayer v-if="scope.row.responseMediaUrl" :url="scope.row.responseMediaUrl" />
+ </div>
+ <div v-else-if="scope.row.responseMessageType === 'image'">
+ <a target="_blank" :href="scope.row.responseMediaUrl">
+ <img :src="scope.row.responseMediaUrl" style="width: 100px" />
+ </a>
+ </div>
+ <div
+ v-else-if="
+ scope.row.responseMessageType === 'video' ||
+ scope.row.responseMessageType === 'shortvideo'
+ "
+ >
+ <WxVideoPlayer
+ v-if="scope.row.responseMediaUrl"
+ :url="scope.row.responseMediaUrl"
+ style="margin-top: 10px"
+ />
+ </div>
+ <div v-else-if="scope.row.responseMessageType === 'news'">
+ <WxNews :articles="scope.row.responseArticles" />
+ </div>
+ <div v-else-if="scope.row.responseMessageType === 'music'">
+ <WxMusic
+ :title="scope.row.responseTitle"
+ :description="scope.row.responseDescription"
+ :thumb-media-url="scope.row.responseThumbMediaUrl"
+ :music-url="scope.row.responseMusicUrl"
+ :hq-music-url="scope.row.responseHqMusicUrl"
+ />
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="emit('on-update', scope.row.id)"
+ v-hasPermi="['mp:auto-reply:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ @click="emit('on-delete', scope.row.id)"
+ v-hasPermi="['mp:auto-reply:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+<script lang="ts" setup>
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxMusic from '@/views/mp/components/wx-music'
+import WxNews from '@/views/mp/components/wx-news'
+import { dateFormatter } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+import { MsgType } from './types'
+
+const props = defineProps<{
+ loading: boolean
+ list: any[]
+ msgType: MsgType
+}>()
+
+const emit = defineEmits<{
+ (e: 'on-update', v: number)
+ (e: 'on-delete', v: number)
+}>()
+</script>
diff --git a/src/views/mp/autoReply/components/types.ts b/src/views/mp/autoReply/components/types.ts
new file mode 100644
index 0000000..68bc5c9
--- /dev/null
+++ b/src/views/mp/autoReply/components/types.ts
@@ -0,0 +1,7 @@
+// 娑堟伅绫诲瀷锛團ollow: 鍏虫敞鏃跺洖澶嶏紱Message: 娑堟伅鍥炲锛汯eyword: 鍏抽敭璇嶅洖澶嶏級
+// 浣滀负 tab.name锛宔num 鐨勬暟瀛椾笉鑳介殢鎰忎慨鏀癸紝涓� api 鍙傛暟鐩稿叧
+export enum MsgType {
+ Follow = 1,
+ Message = 2,
+ Keyword = 3
+}
diff --git a/src/views/mp/autoReply/index.vue b/src/views/mp/autoReply/index.vue
new file mode 100644
index 0000000..0b00647
--- /dev/null
+++ b/src/views/mp/autoReply/index.vue
@@ -0,0 +1,241 @@
+<template>
+ <doc-alert title="鑷姩鍥炲" url="https://doc.iocoder.cn/mp/auto-reply/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form class="-mb-15px" :model="queryParams" :inline="true" label-width="68px">
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- tab 鍒囨崲 -->
+ <ContentWrap>
+ <el-tabs v-model="msgType" @tab-change="onTabChange">
+ <!-- 鎿嶄綔宸ュ叿鏍� -->
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ @click="onCreate"
+ v-hasPermi="['mp:auto-reply:create']"
+ v-if="msgType !== MsgType.Follow || list.length <= 0"
+ >
+ <Icon icon="ep:plus" />鏂板
+ </el-button>
+ </el-col>
+ </el-row>
+ <!-- tab 椤� -->
+ <el-tab-pane :name="MsgType.Follow">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:star" class="mr-2px" /> 鍏虫敞鏃跺洖澶�</el-row>
+ </template>
+ </el-tab-pane>
+ <el-tab-pane :name="MsgType.Message">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:chat-line-round" class="mr-2px" /> 娑堟伅鍥炲</el-row>
+ </template>
+ </el-tab-pane>
+ <el-tab-pane :name="MsgType.Keyword">
+ <template #label>
+ <el-row align="middle"><Icon icon="fa:newspaper-o" class="mr-2px" /> 鍏抽敭璇嶅洖澶�</el-row>
+ </template>
+ </el-tab-pane>
+ </el-tabs>
+ <!-- 鍒楄〃 -->
+ <ReplyTable
+ :loading="loading"
+ :list="list"
+ :msg-type="msgType"
+ @on-update="onUpdate"
+ @on-delete="onDelete"
+ />
+
+ <el-dialog
+ :title="isCreating ? '鏂板鑷姩鍥炲' : '淇敼鑷姩鍥炲'"
+ v-model="showDialog"
+ width="800px"
+ destroy-on-close
+ >
+ <ReplyForm v-model="replyForm" v-model:reply="reply" :msg-type="msgType" ref="formRef" />
+ <template #footer>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ <el-button type="primary" @click="onSubmit">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import ReplyForm from '@/views/mp/autoReply/components/ReplyForm.vue'
+import { type Reply, ReplyType } from '@/views/mp/components/wx-reply'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import * as MpAutoReplyApi from '@/api/mp/autoReply'
+import { ContentWrap } from '@/components/ContentWrap'
+import type { TabPaneName } from 'element-plus'
+import ReplyTable from './components/ReplyTable.vue'
+import { MsgType } from './components/types'
+
+defineOptions({ name: 'MpAutoReply' })
+
+const message = useMessage() // 娑堟伅
+
+const accountId = ref(-1) // 鍏紬鍙稩D
+const msgType = ref<MsgType>(MsgType.Keyword) // 娑堟伅绫诲瀷
+const loading = ref(true) // 閬僵灞�
+const total = ref(0) // 鎬绘潯鏁�
+const list = ref<any[]>([]) // 鑷姩鍥炲鍒楄〃
+const formRef = ref<InstanceType<typeof ReplyForm> | null>(null) // 琛ㄥ崟 ref
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: accountId
+})
+
+const isCreating = ref(false) // 鏄惁鏂板缓锛堝惁鍒欑紪杈戯級
+const showDialog = ref(false) // 鏄惁鏄剧ず寮瑰嚭灞�
+const replyForm = ref<any>({}) // 琛ㄥ崟鍙傛暟
+// 鍥炲娑堟伅
+const reply = ref<Reply>({
+ type: ReplyType.Text,
+ accountId: -1
+})
+
+/** 渚﹀惉璐﹀彿鍙樺寲 */
+const onAccountChanged = (id: number) => {
+ accountId.value = id
+ reply.value.accountId = id
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MpAutoReplyApi.getAutoReplyPage({
+ ...queryParams,
+ type: msgType.value
+ })
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+const onTabChange = (tabName: TabPaneName) => {
+ msgType.value = tabName as MsgType
+ handleQuery()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const onCreate = () => {
+ reset()
+ // 鎵撳紑琛ㄥ崟锛屽苟璁剧疆鍒濆鍖�
+ reply.value = {
+ type: ReplyType.Text,
+ accountId: queryParams.accountId
+ }
+
+ isCreating.value = true
+ showDialog.value = true
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+const onUpdate = async (id: number) => {
+ reset()
+
+ const data = await MpAutoReplyApi.getAutoReply(id)
+ // 璁剧疆灞炴��
+ replyForm.value = { ...data }
+ delete replyForm.value['responseMessageType']
+ delete replyForm.value['responseContent']
+ delete replyForm.value['responseMediaId']
+ delete replyForm.value['responseMediaUrl']
+ delete replyForm.value['responseDescription']
+ delete replyForm.value['responseArticles']
+ reply.value = {
+ type: data.responseMessageType,
+ accountId: queryParams.accountId,
+ content: data.responseContent,
+ mediaId: data.responseMediaId,
+ url: data.responseMediaUrl,
+ title: data.responseTitle,
+ description: data.responseDescription,
+ thumbMediaId: data.responseThumbMediaId,
+ thumbMediaUrl: data.responseThumbMediaUrl,
+ articles: data.responseArticles,
+ musicUrl: data.responseMusicUrl,
+ hqMusicUrl: data.responseHqMusicUrl
+ }
+
+ // 鎵撳紑琛ㄥ崟
+ isCreating.value = false
+ showDialog.value = true
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const onDelete = async (id: number) => {
+ await message.confirm('鏄惁纭鍒犻櫎姝ゆ暟鎹�?')
+ await MpAutoReplyApi.deleteAutoReply(id)
+ await getList()
+ message.success('鍒犻櫎鎴愬姛')
+}
+
+const onSubmit = async () => {
+ await formRef.value?.validate()
+
+ // 澶勭悊鍥炲娑堟伅
+ const submitForm: any = { ...replyForm.value }
+ submitForm.responseMessageType = reply.value.type
+ submitForm.responseContent = reply.value.content
+ submitForm.responseMediaId = reply.value.mediaId
+ submitForm.responseMediaUrl = reply.value.url
+ submitForm.responseTitle = reply.value.title
+ submitForm.responseDescription = reply.value.description
+ submitForm.responseThumbMediaId = reply.value.thumbMediaId
+ submitForm.responseThumbMediaUrl = reply.value.thumbMediaUrl
+ submitForm.responseArticles = reply.value.articles
+ submitForm.responseMusicUrl = reply.value.musicUrl
+ submitForm.responseHqMusicUrl = reply.value.hqMusicUrl
+
+ if (replyForm.value.id !== undefined) {
+ await MpAutoReplyApi.updateAutoReply(submitForm)
+ message.success('淇敼鎴愬姛')
+ } else {
+ await MpAutoReplyApi.createAutoReply(submitForm)
+ message.success('鏂板鎴愬姛')
+ }
+
+ showDialog.value = false
+ await getList()
+}
+
+// 琛ㄥ崟閲嶇疆
+const reset = () => {
+ replyForm.value = {
+ id: undefined,
+ accountId: queryParams.accountId,
+ type: msgType.value,
+ requestKeyword: undefined,
+ requestMatch: msgType.value === MsgType.Keyword ? 1 : undefined,
+ requestMessageType: undefined
+ }
+ formRef.value?.resetFields()
+}
+
+// 鍙栨秷鎸夐挳
+const cancel = () => {
+ showDialog.value = false
+ reset()
+}
+</script>
diff --git a/src/views/mp/components/wx-account-select/index.ts b/src/views/mp/components/wx-account-select/index.ts
new file mode 100644
index 0000000..97556b2
--- /dev/null
+++ b/src/views/mp/components/wx-account-select/index.ts
@@ -0,0 +1,3 @@
+import WxAccountSelect from './main.vue'
+
+export default WxAccountSelect
diff --git a/src/views/mp/components/wx-account-select/main.vue b/src/views/mp/components/wx-account-select/main.vue
new file mode 100644
index 0000000..4df4cbe
--- /dev/null
+++ b/src/views/mp/components/wx-account-select/main.vue
@@ -0,0 +1,58 @@
+<template>
+ <el-select v-model="account.id" placeholder="璇烽�夋嫨鍏紬鍙�" class="!w-240px" @change="onChanged">
+ <el-option v-for="item in accountList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+</template>
+
+<script lang="ts" setup>
+import * as MpAccountApi from '@/api/mp/account'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+const { push, currentRoute } = useRouter() // 璺敱
+
+defineOptions({ name: 'WxAccountSelect' })
+
+const account: MpAccountApi.AccountVO = reactive({
+ id: -1,
+ name: ''
+})
+
+const accountList = ref<MpAccountApi.AccountVO[]>([])
+
+const emit = defineEmits<{
+ (e: 'change', id: number, name: string)
+}>()
+
+const handleQuery = async () => {
+ accountList.value = await MpAccountApi.getSimpleAccountList()
+ if (accountList.value.length == 0) {
+ message.error('鏈厤缃叕浼楀彿锛岃鍦ㄣ�愬叕浼楀彿绠$悊 -> 璐﹀彿绠$悊銆戣彍鍗曪紝杩涜閰嶇疆')
+ delView(unref(currentRoute))
+ await push({ name: 'MpAccount' })
+ return
+ }
+ // 榛樿閫変腑绗竴涓�
+ if (accountList.value.length > 0) {
+ account.id = accountList.value[0].id
+ if (account.id) {
+ account.name = accountList.value[0].name
+ emit('change', account.id, account.name)
+ }
+ }
+}
+
+const onChanged = (id?: number) => {
+ const found = accountList.value.find((v) => v.id === id)
+ if (account.id) {
+ account.name = found ? found.name : ''
+ emit('change', account.id, account.name)
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ handleQuery()
+})
+</script>
diff --git a/src/views/mp/components/wx-location/index.ts b/src/views/mp/components/wx-location/index.ts
new file mode 100644
index 0000000..14ba864
--- /dev/null
+++ b/src/views/mp/components/wx-location/index.ts
@@ -0,0 +1,3 @@
+import WxLocation from './main.vue'
+
+export default WxLocation
diff --git a/src/views/mp/components/wx-location/main.vue b/src/views/mp/components/wx-location/main.vue
new file mode 100644
index 0000000..0b68d49
--- /dev/null
+++ b/src/views/mp/components/wx-location/main.vue
@@ -0,0 +1,73 @@
+<!--
+ 銆愬井淇℃秷鎭� - 瀹氫綅銆慣ODO @Dhb52 鐩墠鏈惎鐢�
+-->
+<template>
+ <div>
+ <el-link
+ type="primary"
+ target="_blank"
+ :href="
+ 'https://map.qq.com/?type=marker&isopeninfowin=1&markertype=1&pointx=' +
+ locationY +
+ '&pointy=' +
+ locationX +
+ '&name=' +
+ label +
+ '&ref=yudao'
+ "
+ >
+ <el-col>
+ <el-row>
+ <img
+ :src="
+ 'https://apis.map.qq.com/ws/staticmap/v2/?zoom=10&markers=color:blue|label:A|' +
+ locationX +
+ ',' +
+ locationY +
+ '&key=' +
+ qqMapKey +
+ '&size=250*180'
+ "
+ />
+ </el-row>
+ <el-row>
+ <Icon icon="ep:location" />
+ {{ label }}
+ </el-row>
+ </el-col>
+ </el-link>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'WxLocation' })
+
+const props = defineProps({
+ locationX: {
+ required: true,
+ type: Number
+ },
+ locationY: {
+ required: true,
+ type: Number
+ },
+ label: {
+ // 鍦板悕
+ required: true,
+ type: String
+ },
+ qqMapKey: {
+ // QQ 鍦板浘鐨勫瘑閽� https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
+ required: false,
+ type: String,
+ default: 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E' // 闇�瑕佽嚜瀹氫箟
+ }
+})
+
+defineExpose({
+ locationX: props.locationX,
+ locationY: props.locationY,
+ label: props.label,
+ qqMapKey: props.qqMapKey
+})
+</script>
diff --git a/src/views/mp/components/wx-material-select/index.ts b/src/views/mp/components/wx-material-select/index.ts
new file mode 100644
index 0000000..eeda31d
--- /dev/null
+++ b/src/views/mp/components/wx-material-select/index.ts
@@ -0,0 +1,6 @@
+import WxMaterialSelect from './main.vue'
+import { NewsType, MaterialType } from './types'
+
+export { NewsType, MaterialType }
+
+export default WxMaterialSelect
diff --git a/src/views/mp/components/wx-material-select/main.vue b/src/views/mp/components/wx-material-select/main.vue
new file mode 100644
index 0000000..aad25ea
--- /dev/null
+++ b/src/views/mp/components/wx-material-select/main.vue
@@ -0,0 +1,279 @@
+<!--
+ - Copyright (C) 2018-2019
+ - All rights reserved, Designed By www.joolun.com
+ 鑺嬮亾婧愮爜锛�
+ 鈶� 绉婚櫎 avue 缁勪欢锛屼娇鐢� ElementUI 鍘熺敓缁勪欢
+-->
+<template>
+ <div class="pb-30px">
+ <!-- 绫诲瀷锛歩mage -->
+ <div v-if="props.type === 'image'">
+ <div class="waterfall" v-loading="loading">
+ <div class="waterfall-item" v-for="item in list" :key="item.mediaId">
+ <img class="material-img" :src="item.url" />
+ <p class="item-name">{{ item.name }}</p>
+ <el-row class="ope-row">
+ <el-button type="success" @click="selectMaterialFun(item)">
+ 閫夋嫨
+ <Icon icon="ep:circle-check" />
+ </el-button>
+ </el-row>
+ </div>
+ </div>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getMaterialPageFun"
+ />
+ </div>
+ <!-- 绫诲瀷锛歷oice -->
+ <div v-else-if="props.type === 'voice'">
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="mediaId" />
+ <el-table-column label="鏂囦欢鍚�" align="center" prop="name" />
+ <el-table-column label="璇煶" align="center">
+ <template #default="scope">
+ <WxVoicePlayer :url="scope.row.url" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓婁紶鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" link @click="selectMaterialFun(scope.row)"
+ >閫夋嫨
+ <Icon icon="ep:plus" />
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getPage"
+ />
+ </div>
+ <!-- 绫诲瀷锛歷ideo -->
+ <div v-else-if="props.type === 'video'">
+ <!-- 鍒楄〃 -->
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="mediaId" />
+ <el-table-column label="鏂囦欢鍚�" align="center" prop="name" />
+ <el-table-column label="鏍囬" align="center" prop="title" />
+ <el-table-column label="浠嬬粛" align="center" prop="introduction" />
+ <el-table-column label="瑙嗛" align="center">
+ <template #default="scope">
+ <WxVideoPlayer :url="scope.row.url" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓婁紶鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ label="鎿嶄綔"
+ align="center"
+ fixed="right"
+ class-name="small-padding fixed-width"
+ >
+ <template #default="scope">
+ <el-button type="primary" link @click="selectMaterialFun(scope.row)"
+ >閫夋嫨
+ <Icon icon="akar-icons:circle-plus" />
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getMaterialPageFun"
+ />
+ </div>
+ <!-- 绫诲瀷锛歯ews -->
+ <div v-else-if="props.type === 'news'">
+ <div class="waterfall" v-loading="loading">
+ <div class="waterfall-item" v-for="item in list" :key="item.mediaId">
+ <div v-if="item.content && item.content.newsItem">
+ <WxNews :articles="item.content.newsItem" />
+ <el-row class="ope-row">
+ <el-button type="success" @click="selectMaterialFun(item)">
+ 閫夋嫨
+ <Icon icon="ep:circle-check" />
+ </el-button>
+ </el-row>
+ </div>
+ </div>
+ </div>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getMaterialPageFun"
+ />
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxNews from '@/views/mp/components/wx-news'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import { NewsType } from './types'
+import * as MpMaterialApi from '@/api/mp/material'
+import * as MpFreePublishApi from '@/api/mp/freePublish'
+import * as MpDraftApi from '@/api/mp/draft'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'WxMaterialSelect' })
+
+const props = withDefaults(
+ defineProps<{
+ type: string
+ accountId: number
+ newsType?: NewsType
+ }>(),
+ {
+ newsType: NewsType.Published
+ }
+)
+
+const emit = defineEmits(['select-material'])
+
+// 閬僵灞�
+const loading = ref(false)
+// 鎬绘潯鏁�
+const total = ref(0)
+// 鏁版嵁鍒楄〃
+const list = ref<any[]>([])
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: props.accountId
+})
+
+const selectMaterialFun = (item) => {
+ emit('select-material', item)
+}
+
+const getPage = async () => {
+ loading.value = true
+ try {
+ if (props.type === 'news' && props.newsType === NewsType.Published) {
+ // 銆愬浘鏂囥��+ 銆愬凡鍙戝竷銆�
+ await getFreePublishPageFun()
+ } else if (props.type === 'news' && props.newsType === NewsType.Draft) {
+ // 銆愬浘鏂囥��+ 銆愯崏绋裤��
+ await getDraftPageFun()
+ } else {
+ // 銆愮礌鏉愩��
+ await getMaterialPageFun()
+ }
+ } finally {
+ loading.value = false
+ }
+}
+
+const getMaterialPageFun = async () => {
+ const data = await MpMaterialApi.getMaterialPage({
+ ...queryParams,
+ type: props.type
+ })
+ list.value = data.list
+ total.value = data.total
+}
+
+const getFreePublishPageFun = async () => {
+ const data = await MpFreePublishApi.getFreePublishPage(queryParams)
+ data.list.forEach((item: any) => {
+ const articles = item.content.newsItem
+ articles.forEach((article: any) => {
+ article.picUrl = article.thumbUrl
+ })
+ })
+ list.value = data.list
+ total.value = data.total
+}
+
+const getDraftPageFun = async () => {
+ const data = await MpDraftApi.getDraftPage(queryParams)
+ data.list.forEach((draft: any) => {
+ const articles = draft.content.newsItem
+ articles.forEach((article: any) => {
+ article.picUrl = article.thumbUrl
+ })
+ })
+ list.value = data.list
+ total.value = data.total
+}
+
+onMounted(async () => {
+ getPage()
+})
+</script>
+<style lang="scss" scoped>
+@media (width >= 992px) and (width <= 1300px) {
+ .waterfall {
+ column-count: 3;
+ }
+
+ p {
+ color: red;
+ }
+}
+
+@media (width >= 768px) and (width <= 991px) {
+ .waterfall {
+ column-count: 2;
+ }
+
+ p {
+ color: orange;
+ }
+}
+
+@media (width <= 767px) {
+ .waterfall {
+ column-count: 1;
+ }
+}
+
+.waterfall {
+ width: 100%;
+ column-gap: 10px;
+ column-count: 5;
+ margin: 0 auto;
+}
+
+.waterfall-item {
+ padding: 10px;
+ margin-bottom: 10px;
+ break-inside: avoid;
+ border: 1px solid #eaeaea;
+}
+
+.material-img {
+ width: 100%;
+}
+
+p {
+ line-height: 30px;
+}
+</style>
diff --git a/src/views/mp/components/wx-material-select/types.ts b/src/views/mp/components/wx-material-select/types.ts
new file mode 100644
index 0000000..d4add1d
--- /dev/null
+++ b/src/views/mp/components/wx-material-select/types.ts
@@ -0,0 +1,11 @@
+export enum NewsType {
+ Draft = '2',
+ Published = '1'
+}
+
+export enum MaterialType {
+ Image = 'image',
+ Voice = 'voice',
+ Video = 'video',
+ News = 'news'
+}
diff --git a/src/views/mp/components/wx-msg/card.scss b/src/views/mp/components/wx-msg/card.scss
new file mode 100644
index 0000000..7fbbe80
--- /dev/null
+++ b/src/views/mp/components/wx-msg/card.scss
@@ -0,0 +1,116 @@
+.avue-card {
+ &__item {
+ margin-bottom: 16px;
+ border: 1px solid #e8e8e8;
+ background-color: #fff;
+ box-sizing: border-box;
+ color: rgba(0, 0, 0, 0.65);
+ font-size: 14px;
+ font-variant: tabular-nums;
+ line-height: 1.5;
+ list-style: none;
+ font-feature-settings: 'tnum';
+ cursor: pointer;
+ height: 200px;
+
+ &:hover {
+ border-color: rgba(0, 0, 0, 0.09);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
+ }
+
+ &--add {
+ border: 1px dashed #000;
+ width: 100%;
+ color: rgba(0, 0, 0, 0.45);
+ background-color: #fff;
+ border-color: #d9d9d9;
+ border-radius: 2px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 16px;
+
+ i {
+ margin-right: 10px;
+ }
+
+ &:hover {
+ color: #40a9ff;
+ background-color: #fff;
+ border-color: #40a9ff;
+ }
+ }
+ }
+
+ &__body {
+ display: flex;
+ padding: 24px;
+ }
+
+ &__detail {
+ flex: 1;
+ }
+
+ &__avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 48px;
+ overflow: hidden;
+ margin-right: 12px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ &__title {
+ color: rgba(0, 0, 0, 0.85);
+ margin-bottom: 12px;
+ font-size: 16px;
+
+ &:hover {
+ color: #1890ff;
+ }
+ }
+
+ &__info {
+ color: rgba(0, 0, 0, 0.45);
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+ height: 64px;
+ }
+
+ &__menu {
+ display: flex;
+ justify-content: space-around;
+ height: 50px;
+ background: #f7f9fa;
+ color: rgba(0, 0, 0, 0.45);
+ text-align: center;
+ line-height: 50px;
+
+ &:hover {
+ color: #1890ff;
+ }
+ }
+}
+
+/** joolun 棰濆鍔犵殑 */
+.avue-comment__main {
+ flex: unset !important;
+ border-radius: 5px !important;
+ margin: 0 8px !important;
+}
+
+.avue-comment__header {
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+}
+
+.avue-comment__body {
+ border-bottom-right-radius: 5px;
+ border-bottom-left-radius: 5px;
+}
diff --git a/src/views/mp/components/wx-msg/comment.scss b/src/views/mp/components/wx-msg/comment.scss
new file mode 100644
index 0000000..7812c2a
--- /dev/null
+++ b/src/views/mp/components/wx-msg/comment.scss
@@ -0,0 +1,126 @@
+/* 鏉ヨ嚜 https://github.com/nmxiaowei/avue/blob/master/styles/src/element-ui/comment.scss */
+.avue-comment {
+ margin-bottom: 30px;
+ display: flex;
+ align-items: flex-start;
+
+ &--reverse {
+ flex-direction: row-reverse;
+
+ .avue-comment__main {
+ &:before,
+ &:after {
+ left: auto;
+ right: -8px;
+ border-width: 8px 0 8px 8px;
+ }
+
+ &:before {
+ border-left-color: #dedede;
+ }
+
+ &:after {
+ border-left-color: #f8f8f8;
+ margin-right: 1px;
+ margin-left: auto;
+ }
+ }
+ }
+
+ &__avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ border: 1px solid transparent;
+ box-sizing: border-box;
+ vertical-align: middle;
+ }
+
+ &__header {
+ padding: 5px 15px;
+ background: #f8f8f8;
+ border-bottom: 1px solid #eee;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ &__author {
+ font-weight: 700;
+ font-size: 14px;
+ color: #999;
+ }
+
+ &__main {
+ flex: 1;
+ margin: 0 20px;
+ position: relative;
+ border: 1px solid #dedede;
+ border-radius: 2px;
+
+ &:before,
+ &:after {
+ position: absolute;
+ top: 10px;
+ left: -8px;
+ right: 100%;
+ width: 0;
+ height: 0;
+ display: block;
+ content: ' ';
+ border-color: transparent;
+ border-style: solid solid outset;
+ border-width: 8px 8px 8px 0;
+ pointer-events: none;
+ }
+
+ &:before {
+ border-right-color: #dedede;
+ z-index: 1;
+ }
+
+ &:after {
+ border-right-color: #f8f8f8;
+ margin-left: 1px;
+ z-index: 2;
+ }
+ }
+
+ &__body {
+ padding: 15px;
+ overflow: hidden;
+ background: #fff;
+ font-family:
+ Segoe UI,
+ Lucida Grande,
+ Helvetica,
+ Arial,
+ Microsoft YaHei,
+ FreeSans,
+ Arimo,
+ Droid Sans,
+ wenquanyi micro hei,
+ Hiragino Sans GB,
+ Hiragino Sans GB W3,
+ FontAwesome,
+ sans-serif;
+ color: #333;
+ font-size: 14px;
+ }
+
+ blockquote {
+ margin: 0;
+ font-family:
+ Georgia,
+ Times New Roman,
+ Times,
+ Kai,
+ Kaiti SC,
+ KaiTi,
+ BiauKai,
+ FontAwesome,
+ serif;
+ padding: 1px 0 1px 15px;
+ border-left: 4px solid #ddd;
+ }
+}
diff --git a/src/views/mp/components/wx-msg/components/Msg.vue b/src/views/mp/components/wx-msg/components/Msg.vue
new file mode 100644
index 0000000..c35e268
--- /dev/null
+++ b/src/views/mp/components/wx-msg/components/Msg.vue
@@ -0,0 +1,69 @@
+<template>
+ <div>
+ <MsgEvent v-if="item.type === MsgType.Event" :item="item" />
+
+ <div v-else-if="item.type === MsgType.Text">{{ item.content }}</div>
+
+ <div v-else-if="item.type === MsgType.Voice">
+ <WxVoicePlayer :url="item.mediaUrl" :content="item.recognition" />
+ </div>
+
+ <div v-else-if="item.type === MsgType.Image">
+ <a target="_blank" :href="item.mediaUrl">
+ <img :src="item.mediaUrl" style="width: 100px" />
+ </a>
+ </div>
+
+ <div
+ v-else-if="item.type === MsgType.Video || item.type === 'shortvideo'"
+ style="text-align: center"
+ >
+ <WxVideoPlayer :url="item.mediaUrl" />
+ </div>
+
+ <div v-else-if="item.type === MsgType.Link" class="avue-card__detail">
+ <el-link type="success" :underline="false" target="_blank" :href="item.url">
+ <div class="avue-card__title"><i class="el-icon-link"></i>{{ item.title }}</div>
+ </el-link>
+ <div class="avue-card__info" style="height: unset">{{ item.description }}</div>
+ </div>
+
+ <div v-else-if="item.type === MsgType.Location">
+ <WxLocation :label="item.label" :location-y="item.locationY" :location-x="item.locationX" />
+ </div>
+
+ <div v-else-if="item.type === MsgType.News" style="width: 300px">
+ <WxNews :articles="item.articles" />
+ </div>
+
+ <div v-else-if="item.type === MsgType.Music">
+ <WxMusic
+ :title="item.title"
+ :description="item.description"
+ :thumb-media-url="item.thumbMediaUrl"
+ :music-url="item.musicUrl"
+ :hq-music-url="item.hqMusicUrl"
+ />
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxNews from '@/views/mp/components/wx-news'
+import WxLocation from '@/views/mp/components/wx-location'
+import WxMusic from '@/views/mp/components/wx-music'
+import MsgEvent from './MsgEvent.vue'
+import { MsgType } from '../types'
+
+defineOptions({ name: 'Msg' })
+
+const props = defineProps<{
+ item: any
+}>()
+
+const item = ref<any>(props.item)
+</script>
+
+<style scoped></style>
diff --git a/src/views/mp/components/wx-msg/components/MsgEvent.vue b/src/views/mp/components/wx-msg/components/MsgEvent.vue
new file mode 100644
index 0000000..32eaa09
--- /dev/null
+++ b/src/views/mp/components/wx-msg/components/MsgEvent.vue
@@ -0,0 +1,52 @@
+<template>
+ <div>
+ <div v-if="item.event === 'subscribe'">
+ <el-tag type="success">鍏虫敞</el-tag>
+ </div>
+ <div v-else-if="item.event === 'unsubscribe'">
+ <el-tag type="danger">鍙栨秷鍏虫敞</el-tag>
+ </div>
+ <div v-else-if="item.event === 'CLICK'">
+ <el-tag>鐐瑰嚮鑿滃崟</el-tag>
+ 銆恵{ item.eventKey }}銆�
+ </div>
+ <div v-else-if="item.event === 'VIEW'">
+ <el-tag>鐐瑰嚮鑿滃崟閾炬帴</el-tag>
+ 銆恵{ item.eventKey }}銆�
+ </div>
+ <div v-else-if="item.event === 'scancode_waitmsg'">
+ <el-tag>鎵爜缁撴灉</el-tag>
+ 銆恵{ item.eventKey }}銆�
+ </div>
+ <div v-else-if="item.event === 'scancode_push'">
+ <el-tag>鎵爜缁撴灉</el-tag>
+ 銆恵{ item.eventKey }}銆�
+ </div>
+ <div v-else-if="item.event === 'pic_sysphoto'">
+ <el-tag>绯荤粺鎷嶇収鍙戝浘</el-tag>
+ </div>
+ <div v-else-if="item.event === 'pic_photo_or_album'">
+ <el-tag>鎷嶇収鎴栬�呯浉鍐�</el-tag>
+ </div>
+ <div v-else-if="item.event === 'pic_weixin'">
+ <el-tag>寰俊鐩稿唽</el-tag>
+ </div>
+ <div v-else-if="item.event === 'location_select'">
+ <el-tag>閫夋嫨鍦扮悊浣嶇疆</el-tag>
+ </div>
+ <div v-else-if="item.event === 'SCAN'">
+ <el-tag>鎵爜</el-tag>
+ </div>
+ <div v-else>
+ <el-tag type="danger">鏈煡浜嬩欢绫诲瀷</el-tag>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+const props = defineProps<{
+ item: any
+}>()
+
+const item = ref(props.item)
+</script>
diff --git a/src/views/mp/components/wx-msg/components/MsgList.vue b/src/views/mp/components/wx-msg/components/MsgList.vue
new file mode 100644
index 0000000..ce7063b
--- /dev/null
+++ b/src/views/mp/components/wx-msg/components/MsgList.vue
@@ -0,0 +1,62 @@
+<template>
+ <div class="execution" v-for="item in props.list" :key="item.id">
+ <div
+ class="avue-comment"
+ :class="{ 'avue-comment--reverse': item.sendFrom === SendFrom.MpBot }"
+ >
+ <div class="avatar-div">
+ <img :src="getAvatar(item.sendFrom)" class="avue-comment__avatar" />
+ <div class="avue-comment__author">
+ {{ getNickname(item.sendFrom) }}
+ </div>
+ </div>
+ <div class="avue-comment__main">
+ <div class="avue-comment__header">
+ <div class="avue-comment__create_time">{{ formatDate(item.createTime) }}</div>
+ </div>
+ <div
+ class="avue-comment__body"
+ :style="item.sendFrom === SendFrom.MpBot ? 'background: #6BED72;' : ''"
+ >
+ <Msg :item="item" />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+<script lang="ts" setup>
+import Msg from './Msg.vue'
+import { formatDate } from '@/utils/formatTime'
+import { User } from '../types'
+import avatarWechat from '@/assets/imgs/wechat.png'
+
+defineOptions({ name: 'MsgList' })
+
+const props = defineProps<{
+ list: any[]
+ accountId: number
+ user: User
+}>()
+
+enum SendFrom {
+ User = 1,
+ MpBot = 2
+}
+
+const getAvatar = (sendFrom: SendFrom) =>
+ sendFrom === SendFrom.User ? props.user.avatar : avatarWechat
+
+const getNickname = (sendFrom: SendFrom) =>
+ sendFrom === SendFrom.User ? props.user.nickname : '鍏紬鍙�'
+</script>
+
+<style lang="scss" scoped>
+/* 鍥犱负 joolun 瀹炵幇渚濊禆 avue 缁勪欢锛岃椤甸潰浣跨敤浜� comment.scss銆乧ard.scc */
+@import url('../comment.scss');
+@import url('../card.scss');
+
+.avatar-div {
+ width: 80px;
+ text-align: center;
+}
+</style>
diff --git a/src/views/mp/components/wx-msg/index.ts b/src/views/mp/components/wx-msg/index.ts
new file mode 100644
index 0000000..fd9eddd
--- /dev/null
+++ b/src/views/mp/components/wx-msg/index.ts
@@ -0,0 +1,6 @@
+import WxMsg from './main.vue'
+import { MsgType } from './types'
+
+export { MsgType }
+
+export default WxMsg
diff --git a/src/views/mp/components/wx-msg/main.vue b/src/views/mp/components/wx-msg/main.vue
new file mode 100644
index 0000000..5223113
--- /dev/null
+++ b/src/views/mp/components/wx-msg/main.vue
@@ -0,0 +1,192 @@
+<!--
+ - Copyright (C) 2018-2019
+ - All rights reserved, Designed By www.joolun.com
+ 鑺嬮亾婧愮爜锛�
+ 鈶� 绉婚櫎鏆傛椂鐢ㄤ笉鍒扮殑 websocket
+ 鈶� 浠g爜浼樺寲锛岃ˉ鍏呮敞閲婏紝鎻愬崌闃呰鎬�
+-->
+<template>
+ <ContentWrap>
+ <div class="msg-div" ref="msgDivRef">
+ <!-- 鍔犺浇鏇村 -->
+ <div v-loading="loading"></div>
+ <div v-if="!loading">
+ <div class="el-table__empty-block" v-if="hasMore" @click="loadMore"
+ ><span class="el-table__empty-text">鐐瑰嚮鍔犺浇鏇村</span></div
+ >
+ <div class="el-table__empty-block" v-if="!hasMore"
+ ><span class="el-table__empty-text">娌℃湁鏇村浜�</span></div
+ >
+ </div>
+
+ <!-- 娑堟伅鍒楄〃 -->
+ <MsgList :list="list" :account-id="accountId" :user="user" />
+ </div>
+
+ <div class="msg-send" v-loading="sendLoading">
+ <WxReplySelect ref="replySelectRef" v-model="reply" />
+ <el-button type="success" class="send-but" @click="sendMsg">鍙戦��(S)</el-button>
+ </div>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import WxReplySelect, { Reply, ReplyType } from '@/views/mp/components/wx-reply'
+import MsgList from './components/MsgList.vue'
+import { getMessagePage, sendMessage } from '@/api/mp/message'
+import { getUser } from '@/api/mp/user'
+import profile from '@/assets/imgs/profile.jpg'
+import { User } from './types'
+
+defineOptions({ name: 'WxMsg' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const props = defineProps({
+ userId: {
+ type: Number,
+ required: true
+ }
+})
+
+const accountId = ref(-1) // 鍏紬鍙稩D锛岄渶瑕侀�氳繃userId鍒濆鍖�
+const loading = ref(false) // 娑堟伅鍒楄〃鏄惁姝e湪鍔犺浇涓�
+const hasMore = ref(true) // 鏄惁鍙互鍔犺浇鏇村
+const list = ref<any[]>([]) // 娑堟伅鍒楄〃
+const queryParams = reactive({
+ pageNo: 1, // 褰撳墠椤垫暟
+ pageSize: 14, // 姣忛〉鏄剧ず澶氬皯鏉�
+ accountId: accountId
+})
+
+// 鐢变簬寰俊涓嶅啀鎻愪緵鏄电О锛岀洿鎺ヤ娇鐢ㄢ�滅敤鎴封�濆睍绀�
+const user: User = reactive({
+ nickname: '鐢ㄦ埛',
+ avatar: profile,
+ accountId: accountId // 鍏紬鍙疯处鍙风紪鍙�
+})
+
+// ========= 娑堟伅鍙戦�� =========
+const sendLoading = ref(false) // 鍙戦�佹秷鎭槸鍚﹀姞杞戒腑
+// 寰俊鍙戦�佹秷鎭�
+const reply = ref<Reply>({
+ type: ReplyType.Text,
+ accountId: -1,
+ articles: []
+})
+
+const replySelectRef = ref<InstanceType<typeof WxReplySelect> | null>(null) // WxReplySelect缁勪欢ref锛岀敤浜庢秷鎭彂閫佹垚鍔熷悗娓呴櫎鍐呭
+const msgDivRef = ref<HTMLDivElement | null>(null) // 娑堟伅鏄剧ず绐楀彛ref锛岀敤浜庢粴鍔ㄥ埌搴曢儴
+
+/** 瀹屾垚鍔犺浇 */
+onMounted(async () => {
+ const data = await getUser(props.userId)
+ user.nickname = data.nickname?.length > 0 ? data.nickname : user.nickname
+ user.avatar = data.headImageUrl?.length > 0 ? data.headImageUrl : user.avatar
+ accountId.value = data.accountId
+ reply.value.accountId = data.accountId
+
+ refreshChange()
+})
+
+// 鎵ц鍙戦��
+const sendMsg = async () => {
+ if (!unref(reply)) {
+ return
+ }
+ // 鍏紬鍙烽檺鍒讹細瀹㈡湇娑堟伅锛屽叕浼楀彿鍙厑璁稿彂閫佷竴鏉�
+ if (
+ reply.value.type === ReplyType.News &&
+ reply.value.articles &&
+ reply.value.articles.length > 1
+ ) {
+ reply.value.articles = [reply.value.articles[0]]
+ message.success('鍥炬枃娑堟伅鏉℃暟闄愬埗鍦� 1 鏉′互鍐咃紝宸查粯璁ゅ彂閫佺涓�鏉�')
+ }
+
+ const data = await sendMessage({ userId: props.userId, ...reply.value })
+ sendLoading.value = false
+
+ list.value = [...list.value, ...[data]]
+ await scrollToBottom()
+
+ // 鍙戦�佸悗娓呯┖鏁版嵁
+ replySelectRef.value?.clear()
+}
+
+const loadMore = () => {
+ queryParams.pageNo++
+ getPage(queryParams, null)
+}
+
+const getPage = async (page: any, params: any = null) => {
+ loading.value = true
+ const dataTemp = await getMessagePage(
+ Object.assign(
+ {
+ pageNo: page.pageNo,
+ pageSize: page.pageSize,
+ userId: props.userId,
+ accountId: page.accountId
+ },
+ params
+ )
+ )
+
+ const scrollHeight = msgDivRef.value?.scrollHeight ?? 0
+ // 澶勭悊鏁版嵁
+ const data = dataTemp.list.reverse()
+ list.value = [...data, ...list.value]
+ loading.value = false
+ if (data.length < queryParams.pageSize || data.length === 0) {
+ hasMore.value = false
+ }
+ queryParams.pageNo = page.pageNo
+ queryParams.pageSize = page.pageSize
+ // 婊氬姩鍒板師鏉ョ殑浣嶇疆
+ if (queryParams.pageNo === 1) {
+ // 瀹氫綅鍒版秷鎭簳閮�
+ await scrollToBottom()
+ } else if (data.length !== 0) {
+ // 瀹氫綅婊氬姩鏉�
+ await nextTick()
+ if (scrollHeight !== 0) {
+ if (msgDivRef.value) {
+ msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight - scrollHeight - 100
+ }
+ }
+ }
+}
+
+const refreshChange = () => {
+ getPage(queryParams)
+}
+
+/** 瀹氫綅鍒版秷鎭簳閮� */
+const scrollToBottom = async () => {
+ await nextTick()
+ if (msgDivRef.value) {
+ msgDivRef.value.scrollTop = msgDivRef.value.scrollHeight
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.msg-div {
+ height: 50vh;
+ margin-right: 10px;
+ margin-left: 10px;
+ overflow: auto;
+ background-color: #eaeaea;
+}
+
+.msg-send {
+ padding: 10px;
+}
+
+.send-but {
+ float: right;
+ margin-top: 8px;
+ margin-bottom: 8px;
+}
+</style>
diff --git a/src/views/mp/components/wx-msg/types.ts b/src/views/mp/components/wx-msg/types.ts
new file mode 100644
index 0000000..38a0ff8
--- /dev/null
+++ b/src/views/mp/components/wx-msg/types.ts
@@ -0,0 +1,17 @@
+export enum MsgType {
+ Event = 'event',
+ Text = 'text',
+ Voice = 'voice',
+ Image = 'image',
+ Video = 'video',
+ Link = 'link',
+ Location = 'location',
+ Music = 'music',
+ News = 'news'
+}
+
+export interface User {
+ nickname: string
+ avatar: string
+ accountId: number
+}
diff --git a/src/views/mp/components/wx-music/index.ts b/src/views/mp/components/wx-music/index.ts
new file mode 100644
index 0000000..c421126
--- /dev/null
+++ b/src/views/mp/components/wx-music/index.ts
@@ -0,0 +1,3 @@
+import WxMusic from './main.vue'
+
+export default WxMusic
diff --git a/src/views/mp/components/wx-music/main.vue b/src/views/mp/components/wx-music/main.vue
new file mode 100644
index 0000000..6b44f44
--- /dev/null
+++ b/src/views/mp/components/wx-music/main.vue
@@ -0,0 +1,62 @@
+<!--
+ 銆愬井淇℃秷鎭� - 闊充箰銆�
+-->
+<template>
+ <div>
+ <el-link
+ type="success"
+ :underline="false"
+ target="_blank"
+ :href="hqMusicUrl ? hqMusicUrl : musicUrl"
+ >
+ <div
+ class="avue-card__body"
+ style="padding: 10px; background-color: #fff; border-radius: 5px"
+ >
+ <div class="avue-card__avatar">
+ <img :src="thumbMediaUrl" alt="" />
+ </div>
+ <div class="avue-card__detail">
+ <div class="avue-card__title" style="margin-bottom: unset">{{ title }}</div>
+ <div class="avue-card__info" style="height: unset">{{ description }}</div>
+ </div>
+ </div>
+ </el-link>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'WxMusic' })
+
+const props = defineProps({
+ title: {
+ required: false,
+ type: String
+ },
+ description: {
+ required: false,
+ type: String
+ },
+ musicUrl: {
+ required: false,
+ type: String
+ },
+ hqMusicUrl: {
+ required: false,
+ type: String
+ },
+ thumbMediaUrl: {
+ required: true,
+ type: String
+ }
+})
+
+defineExpose({
+ musicUrl: props.musicUrl
+})
+</script>
+
+<style lang="scss" scoped>
+/* 鍥犱负 joolun 瀹炵幇渚濊禆 avue 缁勪欢锛岃椤甸潰浣跨敤浜� card.scss */
+@import url('../wx-msg/card.scss');
+</style>
diff --git a/src/views/mp/components/wx-news/index.ts b/src/views/mp/components/wx-news/index.ts
new file mode 100644
index 0000000..e68f4d5
--- /dev/null
+++ b/src/views/mp/components/wx-news/index.ts
@@ -0,0 +1,3 @@
+import WxNews from './main.vue'
+
+export default WxNews
diff --git a/src/views/mp/components/wx-news/main.vue b/src/views/mp/components/wx-news/main.vue
new file mode 100644
index 0000000..033adca
--- /dev/null
+++ b/src/views/mp/components/wx-news/main.vue
@@ -0,0 +1,119 @@
+<!--
+ - Copyright (C) 2018-2019
+ - All rights reserved, Designed By www.joolun.com
+ 銆愬井淇℃秷鎭� - 鍥炬枃銆�
+ 鑺嬮亾婧愮爜锛�
+ 鈶� 浠g爜浼樺寲锛岃ˉ鍏呮敞閲婏紝鎻愬崌闃呰鎬�
+-->
+<template>
+ <div class="news-home">
+ <div v-for="(article, index) in articles" :key="index" class="news-div">
+ <!-- 澶存潯 -->
+ <a v-if="index === 0" :href="article.url" target="_blank">
+ <div class="news-main">
+ <div class="news-content">
+ <el-image
+ :src="article.picUrl || article.thumbUrl"
+ class="material-img"
+ style="width: 100%; height: 120px"
+ />
+ <div class="news-content-title">
+ <span>{{ article.title }}</span>
+ </div>
+ </div>
+ </div>
+ </a>
+ <!-- 浜屾潯/涓夋潯绛夌瓑 -->
+ <a v-else :href="article.url" target="_blank">
+ <div class="news-main-item">
+ <div class="news-content-item">
+ <div class="news-content-item-title">{{ article.title }}</div>
+ <div class="news-content-item-img">
+ <img :src="article.picUrl || article.thumbUrl" class="material-img" height="100%" />
+ </div>
+ </div>
+ </div>
+ </a>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+defineOptions({ name: 'WxNews' })
+
+const props = withDefaults(
+ defineProps<{
+ articles: any[] | null
+ }>(),
+ {
+ articles: null
+ }
+)
+
+defineExpose({
+ articles: props.articles
+})
+</script>
+
+<style lang="scss" scoped>
+.news-home {
+ width: 100%;
+ margin: auto;
+ background-color: #fff;
+}
+
+.news-main {
+ width: 100%;
+ margin: auto;
+}
+
+.news-content {
+ position: relative;
+ width: 100%;
+ background-color: #acadae;
+}
+
+.news-content-title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ display: inline-block;
+ width: 98%;
+ padding: 1%;
+ font-size: 12px;
+ color: #fff;
+ white-space: normal;
+ background-color: black;
+ opacity: 0.65;
+ box-sizing: unset !important;
+}
+
+.news-main-item {
+ padding: 5px 0;
+ background-color: #fff;
+ border-top: 1px solid #eaeaea;
+}
+
+.news-content-item {
+ position: relative;
+}
+
+.news-content-item-title {
+ display: inline-block;
+ width: 70%;
+ margin-left: 1%;
+ font-size: 10px;
+ white-space: normal;
+}
+
+.news-content-item-img {
+ display: inline-block;
+ width: 25%;
+ margin-right: 1%;
+ background-color: #acadae;
+}
+
+.material-img {
+ width: 100%;
+}
+</style>
diff --git a/src/views/mp/components/wx-reply/components/TabImage.vue b/src/views/mp/components/wx-reply/components/TabImage.vue
new file mode 100644
index 0000000..6dbfeed
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/TabImage.vue
@@ -0,0 +1,171 @@
+<template>
+ <div>
+ <!-- 鎯呭喌涓�锛氬凡缁忛�夋嫨濂界礌鏉愩�佹垨鑰呬笂浼犲ソ鍥剧墖 -->
+ <div class="select-item" v-if="reply.url">
+ <img class="material-img" :src="reply.url" />
+ <p class="item-name" v-if="reply.name">{{ reply.name }}</p>
+ <el-row class="ope-row" justify="center">
+ <el-button type="danger" circle @click="onDelete">
+ <Icon icon="ep:delete" />
+ </el-button>
+ </el-row>
+ </div>
+ <!-- 鎯呭喌浜岋細鏈仛瀹屼笂杩版搷浣� -->
+ <el-row v-else style="text-align: center" align="middle">
+ <!-- 閫夋嫨绱犳潗 -->
+ <el-col :span="12" class="col-select">
+ <el-button type="success" @click="showDialog = true">
+ 绱犳潗搴撻�夋嫨 <Icon icon="ep:circle-check" />
+ </el-button>
+ <el-dialog
+ title="閫夋嫨鍥剧墖"
+ v-model="showDialog"
+ width="90%"
+ append-to-body
+ destroy-on-close
+ >
+ <WxMaterialSelect
+ type="image"
+ :account-id="reply.accountId"
+ @select-material="selectMaterial"
+ />
+ </el-dialog>
+ </el-col>
+ <!-- 鏂囦欢涓婁紶 -->
+ <el-col :span="12" class="col-add">
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :before-upload="beforeImageUpload"
+ :on-success="onUploadSuccess"
+ >
+ <el-button type="primary">涓婁紶鍥剧墖</el-button>
+ <template #tip>
+ <span>
+ <div class="el-upload__tip">鏀寔 bmp/png/jpeg/jpg/gif 鏍煎紡锛屽ぇ灏忎笉瓒呰繃 2M</div>
+ </span>
+ </template>
+ </el-upload>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import type { UploadRawFile } from 'element-plus'
+import { getAccessToken } from '@/utils/auth'
+import { Reply } from './types'
+const message = useMessage()
+
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
+const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+
+const props = defineProps<{
+ modelValue: Reply
+}>()
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Reply)
+}>()
+const reply = computed<Reply>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const showDialog = ref(false)
+const fileList = ref([])
+const uploadData = reactive({
+ accountId: reply.value.accountId,
+ type: 'image',
+ title: '',
+ introduction: ''
+})
+
+const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
+
+const onUploadSuccess = (res: any) => {
+ if (res.code !== 0) {
+ message.error('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 娓呯┖涓婁紶鏃剁殑鍚勭鏁版嵁
+ fileList.value = []
+ uploadData.title = ''
+ uploadData.introduction = ''
+
+ // 涓婁紶濂界殑鏂囦欢锛屾湰璐ㄦ槸涓礌鏉愶紝鎵�浠ュ彲浠ヨ繘琛岄�変腑
+ selectMaterial(res.data)
+}
+
+const onDelete = () => {
+ reply.value.mediaId = null
+ reply.value.url = null
+ reply.value.name = null
+}
+
+const selectMaterial = (item) => {
+ showDialog.value = false
+
+ // reply.value.type = 'image'
+ reply.value.mediaId = item.mediaId
+ reply.value.url = item.url
+ reply.value.name = item.name
+}
+</script>
+
+<style lang="scss" scoped>
+.select-item {
+ width: 280px;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+
+ .material-img {
+ width: 100%;
+ }
+
+ .item-name {
+ overflow: hidden;
+ font-size: 12px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ .item-infos {
+ width: 30%;
+ margin: auto;
+ }
+
+ .ope-row {
+ padding-top: 10px;
+ text-align: center;
+ }
+ }
+
+ .col-select {
+ width: 49.5%;
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+ }
+
+ .col-add {
+ float: right;
+ width: 49.5%;
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+
+ .el-upload__tip {
+ line-height: 18px;
+ text-align: center;
+ }
+ }
+}
+</style>
diff --git a/src/views/mp/components/wx-reply/components/TabMusic.vue b/src/views/mp/components/wx-reply/components/TabMusic.vue
new file mode 100644
index 0000000..6421d24
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/TabMusic.vue
@@ -0,0 +1,116 @@
+<template>
+ <div>
+ <el-row align="middle" justify="center">
+ <el-col :span="6">
+ <el-row align="middle" justify="center" class="thumb-div">
+ <el-col :span="24">
+ <el-row align="middle" justify="center">
+ <img style="width: 100px" v-if="reply.thumbMediaUrl" :src="reply.thumbMediaUrl" />
+ <icon v-else icon="ep:plus" />
+ </el-row>
+ <el-row align="middle" justify="center" style="margin-top: 2%">
+ <div class="thumb-but">
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :before-upload="beforeImageUpload"
+ :on-success="onUploadSuccess"
+ >
+ <template #trigger>
+ <el-button type="primary" link>鏈湴涓婁紶</el-button>
+ </template>
+ <el-button type="primary" link @click="showDialog = true" style="margin-left: 5px"
+ >绱犳潗搴撻�夋嫨
+ </el-button>
+ </el-upload>
+ </div>
+ </el-row>
+ </el-col>
+ </el-row>
+ <el-dialog
+ title="閫夋嫨鍥剧墖"
+ v-model="showDialog"
+ width="80%"
+ append-to-body
+ destroy-on-close
+ >
+ <WxMaterialSelect
+ type="image"
+ :account-id="reply.accountId"
+ @select-material="selectMaterial"
+ />
+ </el-dialog>
+ </el-col>
+ <el-col :span="18">
+ <el-input v-model="reply.title" placeholder="璇疯緭鍏ユ爣棰�" />
+ <div style="margin: 20px 0"></div>
+ <el-input v-model="reply.description" placeholder="璇疯緭鍏ユ弿杩�" />
+ </el-col>
+ </el-row>
+ <div style="margin: 20px 0"></div>
+ <el-input v-model="reply.musicUrl" placeholder="璇疯緭鍏ラ煶涔愰摼鎺�" />
+ <div style="margin: 20px 0"></div>
+ <el-input v-model="reply.hqMusicUrl" placeholder="璇疯緭鍏ラ珮璐ㄩ噺闊充箰閾炬帴" />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import type { UploadRawFile } from 'element-plus'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import { getAccessToken } from '@/utils/auth'
+import { Reply } from './types'
+
+const message = useMessage()
+
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
+const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+
+const props = defineProps<{
+ modelValue: Reply
+}>()
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Reply)
+}>()
+const reply = computed<Reply>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const showDialog = ref(false)
+const fileList = ref([])
+const uploadData = reactive({
+ accountId: reply.value.accountId,
+ type: 'thumb', // 闊充箰绫诲瀷涓簍humb
+ title: '',
+ introduction: ''
+})
+
+const beforeImageUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Image, 2)(rawFile)
+
+const onUploadSuccess = (res: any) => {
+ if (res.code !== 0) {
+ message.error('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 娓呯┖涓婁紶鏃剁殑鍚勭鏁版嵁
+ fileList.value = []
+ uploadData.title = ''
+ uploadData.introduction = ''
+
+ // 涓婁紶濂界殑鏂囦欢锛屾湰璐ㄦ槸涓礌鏉愶紝鎵�浠ュ彲浠ヨ繘琛岄�変腑
+ selectMaterial(res.data)
+}
+
+const selectMaterial = (item: any) => {
+ showDialog.value = false
+
+ reply.value.thumbMediaId = item.mediaId
+ reply.value.thumbMediaUrl = item.url
+}
+</script>
diff --git a/src/views/mp/components/wx-reply/components/TabNews.vue b/src/views/mp/components/wx-reply/components/TabNews.vue
new file mode 100644
index 0000000..565b1fb
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/TabNews.vue
@@ -0,0 +1,76 @@
+<template>
+ <div>
+ <el-row>
+ <div class="select-item" v-if="reply.articles && reply.articles.length > 0">
+ <WxNews :articles="reply.articles" />
+ <el-col class="ope-row">
+ <el-button type="danger" circle @click="onDelete">
+ <Icon icon="ep:delete" />
+ </el-button>
+ </el-col>
+ </div>
+ <!-- 閫夋嫨绱犳潗 -->
+ <el-col :span="24" v-if="!reply.content">
+ <el-row style="text-align: center" align="middle">
+ <el-col :span="24">
+ <el-button type="success" @click="showDialog = true">
+ {{ newsType === NewsType.Published ? '閫夋嫨宸插彂甯冨浘鏂�' : '閫夋嫨鑽夌绠卞浘鏂�' }}
+ <Icon icon="ep:circle-check" />
+ </el-button>
+ </el-col>
+ </el-row>
+ </el-col>
+ <el-dialog title="閫夋嫨鍥炬枃" v-model="showDialog" width="90%" append-to-body destroy-on-close>
+ <WxMaterialSelect
+ type="news"
+ :account-id="reply.accountId"
+ :newsType="newsType"
+ @select-material="selectMaterial"
+ />
+ </el-dialog>
+ </el-row>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxNews from '@/views/mp/components/wx-news'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import { Reply, NewsType } from './types'
+
+const props = defineProps<{
+ modelValue: Reply
+ newsType: NewsType
+}>()
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Reply)
+}>()
+const reply = computed<Reply>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const showDialog = ref(false)
+
+const selectMaterial = (item: any) => {
+ showDialog.value = false
+ reply.value.articles = item.content.newsItem
+}
+
+const onDelete = () => {
+ reply.value.articles = []
+}
+</script>
+
+<style lang="scss" scoped>
+.select-item {
+ width: 280px;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+
+ .ope-row {
+ padding-top: 10px;
+ text-align: center;
+ }
+}
+</style>
diff --git a/src/views/mp/components/wx-reply/components/TabText.vue b/src/views/mp/components/wx-reply/components/TabText.vue
new file mode 100644
index 0000000..307e48f
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/TabText.vue
@@ -0,0 +1,22 @@
+<template>
+ <el-input type="textarea" :rows="5" placeholder="璇疯緭鍏ュ唴瀹�" v-model="content" />
+</template>
+
+<script lang="ts" setup>
+const props = defineProps<{
+ modelValue?: string | null
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: string | null)
+ (e: 'input', v: string | null)
+}>()
+
+const content = computed<string | null | undefined>({
+ get: () => props.modelValue,
+ set: (val: string | null) => {
+ emit('update:modelValue', val)
+ emit('input', val)
+ }
+})
+</script>
diff --git a/src/views/mp/components/wx-reply/components/TabVideo.vue b/src/views/mp/components/wx-reply/components/TabVideo.vue
new file mode 100644
index 0000000..adb8fa3
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/TabVideo.vue
@@ -0,0 +1,128 @@
+<template>
+ <div>
+ <el-row>
+ <el-input v-model="reply.title" class="input-margin-bottom" placeholder="璇疯緭鍏ユ爣棰�" />
+ <el-input class="input-margin-bottom" v-model="reply.description" placeholder="璇疯緭鍏ユ弿杩�" />
+ <el-row class="ope-row" justify="center">
+ <WxVideoPlayer v-if="reply.url" :url="reply.url" />
+ </el-row>
+ <el-col>
+ <el-row style="text-align: center" align="middle">
+ <!-- 閫夋嫨绱犳潗 -->
+ <el-col :span="12">
+ <el-button type="success" @click="showDialog = true">
+ 绱犳潗搴撻�夋嫨 <Icon icon="ep:circle-check" />
+ </el-button>
+ <el-dialog
+ title="閫夋嫨瑙嗛"
+ v-model="showDialog"
+ width="90%"
+ append-to-body
+ destroy-on-close
+ >
+ <WxMaterialSelect
+ type="video"
+ :account-id="reply.accountId"
+ @select-material="selectMaterial"
+ />
+ </el-dialog>
+ </el-col>
+ <!-- 鏂囦欢涓婁紶 -->
+ <el-col :span="12">
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :before-upload="beforeVideoUpload"
+ :on-success="onUploadSuccess"
+ >
+ <el-button type="primary">鏂板缓瑙嗛 <Icon icon="ep:upload" /></el-button>
+ </el-upload>
+ </el-col>
+ </el-row>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import type { UploadRawFile } from 'element-plus'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import { getAccessToken } from '@/utils/auth'
+import { Reply } from './types'
+
+const message = useMessage()
+
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
+const HEADERS = { Authorization: 'Bearer ' + getAccessToken() }
+
+const props = defineProps<{
+ modelValue: Reply
+}>()
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Reply)
+}>()
+const reply = computed<Reply>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const showDialog = ref(false)
+const fileList = ref([])
+const uploadData = reactive({
+ accountId: reply.value.accountId,
+ type: 'video',
+ title: '',
+ introduction: ''
+})
+
+const beforeVideoUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Video, 10)(rawFile)
+
+const onUploadSuccess = (res: any) => {
+ if (res.code !== 0) {
+ message.error('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 娓呯┖涓婁紶鏃剁殑鍚勭鏁版嵁
+ fileList.value = []
+ uploadData.title = ''
+ uploadData.introduction = ''
+
+ selectMaterial(res.data)
+}
+
+/** 閫夋嫨绱犳潗鍚庤缃� */
+const selectMaterial = (item: any) => {
+ showDialog.value = false
+
+ reply.value.mediaId = item.mediaId
+ reply.value.url = item.url
+ reply.value.name = item.name
+
+ // title銆乮ntroduction锛氫粠 item 鍒� tempObjItem锛屽洜涓虹礌鏉愰噷鏈� title銆乮ntroduction
+ if (item.title) {
+ reply.value.title = item.title || ''
+ }
+ if (item.introduction) {
+ reply.value.description = item.introduction || ''
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.input-margin-bottom {
+ margin-bottom: 2%;
+}
+
+.ope-row {
+ width: 100%;
+ padding-top: 10px;
+ text-align: center;
+}
+</style>
diff --git a/src/views/mp/components/wx-reply/components/TabVoice.vue b/src/views/mp/components/wx-reply/components/TabVoice.vue
new file mode 100644
index 0000000..5dbe9a0
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/TabVoice.vue
@@ -0,0 +1,160 @@
+<template>
+ <div>
+ <div class="select-item2" v-if="reply.url">
+ <p class="item-name">{{ reply.name }}</p>
+ <el-row class="ope-row" justify="center">
+ <WxVoicePlayer :url="reply.url" />
+ </el-row>
+ <el-row class="ope-row" justify="center">
+ <el-button type="danger" circle @click="onDelete"><Icon icon="ep:delete" /></el-button>
+ </el-row>
+ </div>
+ <el-row v-else style="text-align: center">
+ <!-- 閫夋嫨绱犳潗 -->
+ <el-col :span="12" class="col-select">
+ <el-button type="success" @click="showDialog = true">
+ 绱犳潗搴撻�夋嫨<Icon icon="ep:circle-check" />
+ </el-button>
+ <el-dialog
+ title="閫夋嫨璇煶"
+ v-model="showDialog"
+ width="90%"
+ append-to-body
+ destroy-on-close
+ >
+ <WxMaterialSelect
+ type="voice"
+ :account-id="reply.accountId"
+ @select-material="selectMaterial"
+ />
+ </el-dialog>
+ </el-col>
+ <!-- 鏂囦欢涓婁紶 -->
+ <el-col :span="12" class="col-add">
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :before-upload="beforeVoiceUpload"
+ :on-success="onUploadSuccess"
+ >
+ <el-button type="primary">鐐瑰嚮涓婁紶</el-button>
+ <template #tip>
+ <div class="el-upload__tip">
+ 鏍煎紡鏀寔 mp3/wma/wav/amr锛屾枃浠跺ぇ灏忎笉瓒呰繃 2M锛屾挱鏀鹃暱搴︿笉瓒呰繃 60s
+ </div>
+ </template>
+ </el-upload>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+<script lang="ts" setup>
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import type { UploadRawFile } from 'element-plus'
+import { getAccessToken } from '@/utils/auth'
+import { Reply } from './types'
+const message = useMessage()
+
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-temporary'
+const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+
+const props = defineProps<{
+ modelValue: Reply
+}>()
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Reply)
+}>()
+const reply = computed<Reply>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const showDialog = ref(false)
+const fileList = ref([])
+const uploadData = reactive({
+ accountId: reply.value.accountId,
+ type: 'voice',
+ title: '',
+ introduction: ''
+})
+
+const beforeVoiceUpload = (rawFile: UploadRawFile) => useBeforeUpload(UploadType.Voice, 10)(rawFile)
+
+const onUploadSuccess = (res: any) => {
+ if (res.code !== 0) {
+ message.error('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 娓呯┖涓婁紶鏃剁殑鍚勭鏁版嵁
+ fileList.value = []
+ uploadData.title = ''
+ uploadData.introduction = ''
+
+ // 涓婁紶濂界殑鏂囦欢锛屾湰璐ㄦ槸涓礌鏉愶紝鎵�浠ュ彲浠ヨ繘琛岄�変腑
+ selectMaterial(res.data)
+}
+
+const onDelete = () => {
+ reply.value.mediaId = null
+ reply.value.url = null
+ reply.value.name = null
+}
+
+const selectMaterial = (item: Reply) => {
+ showDialog.value = false
+
+ // reply.value.type = ReplyType.Voice
+ reply.value.mediaId = item.mediaId
+ reply.value.url = item.url
+ reply.value.name = item.name
+}
+</script>
+
+<style lang="scss" scoped>
+.select-item2 {
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+
+ .item-name {
+ overflow: hidden;
+ font-size: 12px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ .ope-row {
+ width: 100%;
+ padding-top: 10px;
+ text-align: center;
+ }
+ }
+
+ .col-select {
+ width: 49.5%;
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+ }
+
+ .col-add {
+ float: right;
+ width: 49.5%;
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+
+ .el-upload__tip {
+ line-height: 18px;
+ text-align: center;
+ }
+ }
+}
+</style>
diff --git a/src/views/mp/components/wx-reply/components/types.ts b/src/views/mp/components/wx-reply/components/types.ts
new file mode 100644
index 0000000..3e07d6e
--- /dev/null
+++ b/src/views/mp/components/wx-reply/components/types.ts
@@ -0,0 +1,54 @@
+enum ReplyType {
+ News = 'news',
+ Image = 'image',
+ Voice = 'voice',
+ Video = 'video',
+ Music = 'music',
+ Text = 'text'
+}
+
+interface _Reply {
+ accountId: number
+ type: ReplyType
+ name?: string | null
+ content?: string | null
+ mediaId?: string | null
+ url?: string | null
+ title?: string | null
+ description?: string | null
+ thumbMediaId?: string | null
+ thumbMediaUrl?: string | null
+ musicUrl?: string | null
+ hqMusicUrl?: string | null
+ introduction?: string | null
+ articles?: any[]
+}
+
+type Reply = _Reply //Partial<_Reply>
+
+enum NewsType {
+ Published = '1',
+ Draft = '2'
+}
+
+/** 鍒╃敤鏃х殑reply[accountId, type]鍒濆鍖栨柊鐨凴eply */
+const createEmptyReply = (old: Reply | Ref<Reply>): Reply => {
+ return {
+ accountId: unref(old).accountId,
+ type: unref(old).type,
+ name: null,
+ content: null,
+ mediaId: null,
+ url: null,
+ title: null,
+ description: null,
+ thumbMediaId: null,
+ thumbMediaUrl: null,
+ musicUrl: null,
+ hqMusicUrl: null,
+ introduction: null,
+ articles: []
+ }
+}
+
+export { Reply, NewsType, ReplyType, createEmptyReply }
diff --git a/src/views/mp/components/wx-reply/index.ts b/src/views/mp/components/wx-reply/index.ts
new file mode 100644
index 0000000..d1da217
--- /dev/null
+++ b/src/views/mp/components/wx-reply/index.ts
@@ -0,0 +1,7 @@
+import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
+
+import WxReplySelect from './main.vue'
+
+export type { Reply }
+export { createEmptyReply, NewsType, ReplyType }
+export default WxReplySelect
diff --git a/src/views/mp/components/wx-reply/main.vue b/src/views/mp/components/wx-reply/main.vue
new file mode 100644
index 0000000..89ffe47
--- /dev/null
+++ b/src/views/mp/components/wx-reply/main.vue
@@ -0,0 +1,208 @@
+<!--
+ - Copyright (C) 2018-2019
+ - All rights reserved, Designed By www.joolun.com
+ 鑺嬮亾婧愮爜锛�
+ 鈶� 绉婚櫎澶氫綑鐨� rep 涓哄墠缂�鐨勫彉閲忥紝璁� message 娑堟伅鏇寸畝鍗�
+ 鈶� 浠g爜浼樺寲锛岃ˉ鍏呮敞閲婏紝鎻愬崌闃呰鎬�
+ 鈶� 浼樺寲娑堟伅鐨勪复鏃剁紦瀛樼瓥鐣ワ紝鍙戦�佹秷鎭椂锛屽彧娓呯悊琚彂閫佹秷鎭殑 tab锛屼笉浼氬己鍒跺垏鍥炲埌 text 杈撳叆
+ 鈶� 鏀寔鍙戦�併�愯棰戙�戞秷鎭椂锛屾敮鎸佹柊寤鸿棰�
+-->
+<template>
+ <el-tabs type="border-card" v-model="currentTab">
+ <!-- 绫诲瀷 1锛氭枃鏈� -->
+ <el-tab-pane :name="ReplyType.Text">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:document" /> 鏂囨湰</el-row>
+ </template>
+ <TabText v-model="reply.content" />
+ </el-tab-pane>
+
+ <!-- 绫诲瀷 2锛氬浘鐗� -->
+ <el-tab-pane :name="ReplyType.Image">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:picture" class="mr-5px" /> 鍥剧墖</el-row>
+ </template>
+ <TabImage v-model="reply" />
+ </el-tab-pane>
+
+ <!-- 绫诲瀷 3锛氳闊� -->
+ <el-tab-pane :name="ReplyType.Voice">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:phone" /> 璇煶</el-row>
+ </template>
+ <TabVoice v-model="reply" />
+ </el-tab-pane>
+
+ <!-- 绫诲瀷 4锛氳棰� -->
+ <el-tab-pane :name="ReplyType.Video">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:share" /> 瑙嗛</el-row>
+ </template>
+ <TabVideo v-model="reply" />
+ </el-tab-pane>
+
+ <!-- 绫诲瀷 5锛氬浘鏂� -->
+ <el-tab-pane :name="ReplyType.News">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:reading" /> 鍥炬枃</el-row>
+ </template>
+ <TabNews v-model="reply" :news-type="newsType" />
+ </el-tab-pane>
+
+ <!-- 绫诲瀷 6锛氶煶涔� -->
+ <el-tab-pane :name="ReplyType.Music">
+ <template #label>
+ <el-row align="middle"><Icon icon="ep:service" />闊充箰</el-row>
+ </template>
+ <TabMusic v-model="reply" />
+ </el-tab-pane>
+ </el-tabs>
+</template>
+
+<script lang="ts" setup>
+import { Reply, NewsType, ReplyType, createEmptyReply } from './components/types'
+import TabText from './components/TabText.vue'
+import TabImage from './components/TabImage.vue'
+import TabVoice from './components/TabVoice.vue'
+import TabVideo from './components/TabVideo.vue'
+import TabNews from './components/TabNews.vue'
+import TabMusic from './components/TabMusic.vue'
+
+defineOptions({ name: 'WxReplySelect' })
+
+interface Props {
+ modelValue: Reply
+ newsType?: NewsType
+}
+const props = withDefaults(defineProps<Props>(), {
+ newsType: () => NewsType.Published
+})
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Reply)
+}>()
+
+const reply = computed<Reply>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+// 浣滀负澶氫釜鏍囩淇濆瓨鍚勮嚜Reply鐨勭紦瀛�
+const tabCache = new Map<ReplyType, Reply>()
+// 閲囩敤鐙珛鐨剅ef鏉ヤ繚瀛樺綋鍓峵ab锛岄伩鍏嶅湪watch鏍囩鍙樺寲锛屽reply杩涜璧嬪�间細浜х敓浜嗗惊鐜皟鐢�
+const currentTab = ref<ReplyType>(props.modelValue.type || ReplyType.Text)
+
+watch(
+ currentTab,
+ (newTab, oldTab) => {
+ // 绗竴娆¤繘鍏ワ細oldTab 涓� undefined
+ // 鍒ゆ柇 newTab 鏄洜涓� Reply 涓� Partial
+ if (oldTab === undefined || newTab === undefined) {
+ return
+ }
+
+ tabCache.set(oldTab, unref(reply))
+
+ // 浠庣紦瀛橀噷闈㈠彇鍑烘柊tab鍐呭锛屾湁鍒欒鐩朢eply锛屾病鏈夊垯鍒涘缓绌篟eply
+ const temp = tabCache.get(newTab)
+ if (temp) {
+ reply.value = temp
+ } else {
+ const newData = createEmptyReply(reply)
+ newData.type = newTab
+ reply.value = newData
+ }
+ },
+ {
+ immediate: true
+ }
+)
+
+/** 娓呴櫎闄や簡`type`, `accountId`鐨勫瓧娈� */
+const clear = () => {
+ reply.value = createEmptyReply(reply)
+}
+
+defineExpose({
+ clear
+})
+</script>
+
+<style lang="scss" scoped>
+.select-item {
+ width: 280px;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+}
+
+.select-item2 {
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+}
+
+.ope-row {
+ padding-top: 10px;
+ text-align: center;
+}
+
+.input-margin-bottom {
+ margin-bottom: 2%;
+}
+
+.item-name {
+ overflow: hidden;
+ font-size: 12px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.el-form-item__content {
+ line-height: unset !important;
+}
+
+.col-select {
+ width: 49.5%;
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+}
+
+.col-select2 {
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+}
+
+.col-add {
+ float: right;
+ width: 49.5%;
+ height: 160px;
+ padding: 50px 0;
+ border: 1px solid rgb(234 234 234);
+}
+
+.avatar-uploader-icon {
+ width: 100px !important;
+ height: 100px !important;
+ font-size: 28px;
+ line-height: 100px !important;
+ color: #8c939d;
+ text-align: center;
+ border: 1px solid #d9d9d9;
+}
+
+.material-img {
+ width: 100%;
+}
+
+.thumb-div {
+ display: inline-block;
+ text-align: center;
+}
+
+.item-infos {
+ width: 30%;
+ margin: auto;
+}
+</style>
diff --git a/src/views/mp/components/wx-video-play/index.ts b/src/views/mp/components/wx-video-play/index.ts
new file mode 100644
index 0000000..91e00ef
--- /dev/null
+++ b/src/views/mp/components/wx-video-play/index.ts
@@ -0,0 +1,3 @@
+import WxVideoPlayer from './main.vue'
+
+export default WxVideoPlayer
diff --git a/src/views/mp/components/wx-video-play/main.vue b/src/views/mp/components/wx-video-play/main.vue
new file mode 100644
index 0000000..d544bbe
--- /dev/null
+++ b/src/views/mp/components/wx-video-play/main.vue
@@ -0,0 +1,73 @@
+<!--
+ - Copyright (C) 2018-2019
+ - All rights reserved, Designed By www.joolun.com
+ 銆愬井淇℃秷鎭� - 瑙嗛銆�
+ 鑺嬮亾婧愮爜锛�
+ 鈶� bug 淇锛�
+ 1锛塲oolun 鐨勫仛娉曪細浣跨敤 mediaId 浠庡井淇″叕浼楀彿锛屼笅杞藉搴旂殑 mp4 绱犳潗锛屼粠鑰屾挱鏀惧唴瀹癸紱
+ 瀛樺湪鐨勯棶棰橈細mediaId 鏈夋晥鏈熸槸 3 澶╋紝瓒呰繃鏃堕棿鍚庢棤娉曟挱鏀�
+ 2锛夐噸鏋勫悗鐨勫仛娉曪細鍚庣鎺ユ敹鍒板井淇″叕浼楀彿鐨勮棰戞秷鎭悗锛屽皢瑙嗛娑堟伅鐨� media_id 鐨勬枃浠跺唴瀹逛繚瀛樺埌鏂囦欢鏈嶅姟鍣ㄤ腑锛岃繖鏍峰墠绔彲浠ョ洿鎺ヤ娇鐢� URL 鎾斁銆�
+ 鈶� 浣撻獙浼樺寲锛氬脊绐楀叧闂悗锛岃嚜鍔ㄦ殏鍋滆棰戠殑鎾斁
+
+-->
+<template>
+ <div @click="playVideo()">
+ <!-- 鎻愮ず -->
+ <div>
+ <Icon icon="ep:video-play" :size="32" class="mr-5px" />
+ <p class="text-sm">鐐瑰嚮鎾斁瑙嗛</p>
+ </div>
+
+ <!-- 寮圭獥鎾斁 -->
+ <el-dialog v-model="dialogVideo" title="瑙嗛鎾斁" append-to-body>
+ <video-player
+ v-if="dialogVideo"
+ class="video-player vjs-big-play-centered"
+ :src="props.url"
+ poster=""
+ crossorigin="anonymous"
+ controls
+ playsinline
+ :volume="0.6"
+ :width="800"
+ :playback-rates="[0.7, 1.0, 1.5, 2.0]"
+ />
+ <!-- 浜嬩欢锛屾毇鏅傛矑鐢�
+ @mounted="handleMounted"-->
+ <!-- @ready="handleEvent($event)"-->
+ <!-- @play="handleEvent($event)"-->
+ <!-- @pause="handleEvent($event)"-->
+ <!-- @ended="handleEvent($event)"-->
+ <!-- @loadeddata="handleEvent($event)"-->
+ <!-- @waiting="handleEvent($event)"-->
+ <!-- @playing="handleEvent($event)"-->
+ <!-- @canplay="handleEvent($event)"-->
+ <!-- @canplaythrough="handleEvent($event)"-->
+ <!-- @timeupdate="handleEvent(player?.currentTime())"-->
+ </el-dialog>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import 'video.js/dist/video-js.css'
+import { VideoPlayer } from '@videojs-player/vue'
+
+defineOptions({ name: 'WxVideoPlayer' })
+
+const props = defineProps({
+ url: {
+ type: String,
+ required: true
+ }
+})
+
+const dialogVideo = ref(false)
+
+// const handleEvent = (log) => {
+// console.log('Basic player event', log)
+// }
+
+const playVideo = () => {
+ dialogVideo.value = true
+}
+</script>
diff --git a/src/views/mp/components/wx-voice-play/index.ts b/src/views/mp/components/wx-voice-play/index.ts
new file mode 100644
index 0000000..9eb78e0
--- /dev/null
+++ b/src/views/mp/components/wx-voice-play/index.ts
@@ -0,0 +1,3 @@
+import WxVoicePlayer from './main.vue'
+
+export default WxVoicePlayer
diff --git a/src/views/mp/components/wx-voice-play/main.vue b/src/views/mp/components/wx-voice-play/main.vue
new file mode 100644
index 0000000..fe7f0ca
--- /dev/null
+++ b/src/views/mp/components/wx-voice-play/main.vue
@@ -0,0 +1,105 @@
+<!--
+ - Copyright (C) 2018-2019
+ - All rights reserved, Designed By www.joolun.com
+ 銆愬井淇℃秷鎭� - 璇煶銆�
+ 鑺嬮亾婧愮爜锛�
+ 鈶� bug 淇锛�
+ 1锛塲oolun 鐨勫仛娉曪細浣跨敤 mediaId 浠庡井淇″叕浼楀彿锛屼笅杞藉搴旂殑 mp4 绱犳潗锛屼粠鑰屾挱鏀惧唴瀹癸紱
+ 瀛樺湪鐨勯棶棰橈細mediaId 鏈夋晥鏈熸槸 3 澶╋紝瓒呰繃鏃堕棿鍚庢棤娉曟挱鏀�
+ 2锛夐噸鏋勫悗鐨勫仛娉曪細鍚庣鎺ユ敹鍒板井淇″叕浼楀彿鐨勮棰戞秷鎭悗锛屽皢瑙嗛娑堟伅鐨� media_id 鐨勬枃浠跺唴瀹逛繚瀛樺埌鏂囦欢鏈嶅姟鍣ㄤ腑锛岃繖鏍峰墠绔彲浠ョ洿鎺ヤ娇鐢� URL 鎾斁銆�
+ 鈶� 浠g爜浼樺寲锛氬皢 props 涓殑 reply 璋冩垚涓� data 涓搴旂殑灞炴�э紝骞惰ˉ鍏呯浉鍏虫敞閲�
+-->
+<template>
+ <div class="wx-voice-div" @click="playVoice">
+ <el-icon>
+ <Icon v-if="playing !== true" icon="ep:video-play" :size="32" />
+ <Icon v-else icon="ep:video-pause" :size="32" />
+ <span class="amr-duration" v-if="duration">{{ duration }} 绉�</span>
+ </el-icon>
+ <div v-if="content">
+ <el-tag type="success" size="small">璇煶璇嗗埆</el-tag>
+ {{ content }}
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+// 鍥犱负寰俊璇煶鏄� amr 鏍煎紡锛屾墍浠ラ渶瑕佺敤鍒� amr 瑙g爜鍣細https://www.npmjs.com/package/benz-amr-recorder
+import BenzAMRRecorder from 'benz-amr-recorder'
+
+defineOptions({ name: 'WxVoicePlayer' })
+
+const props = defineProps({
+ url: {
+ type: String, // 璇煶鍦板潃锛屼緥濡傝锛歨ttps://www.iocoder.cn/xxx.amr
+ required: true
+ },
+ content: {
+ type: String, // 璇煶鏂囨湰
+ required: false
+ }
+})
+
+const amr = ref()
+const playing = ref(false)
+const duration = ref()
+
+/** 澶勭悊鐐瑰嚮锛屾挱鏀炬垨鏆傚仠 */
+const playVoice = () => {
+ // 鎯呭喌涓�锛氭湭鍒濆鍖栵紝鍒欏垱寤� BenzAMRRecorder
+ if (amr.value === undefined) {
+ amrInit()
+ return
+ }
+ // 鎯呭喌浜岋細宸茬粡鍒濆鍖栵紝鍒欐牴鎹儏鍐垫挱鏀炬垨鏆傛椂
+ if (amr.value.isPlaying()) {
+ amrStop()
+ } else {
+ amrPlay()
+ }
+}
+
+/** 闊抽鍒濆鍖� */
+const amrInit = () => {
+ amr.value = new BenzAMRRecorder()
+ // 璁剧疆鎾斁
+ amr.value.initWithUrl(props.url).then(function () {
+ amrPlay()
+ duration.value = amr.value.getDuration()
+ })
+ // 鐩戝惉鏆傚仠
+ amr.value.onEnded(function () {
+ playing.value = false
+ })
+}
+
+/** 闊抽鎾斁 */
+const amrPlay = () => {
+ playing.value = true
+ amr.value.play()
+}
+
+/** 闊抽鏆傚仠 */
+const amrStop = () => {
+ playing.value = false
+ amr.value.stop()
+}
+// TODO 鑺嬭壙锛氫笅闈㈡牱寮忔湁鐐归棶棰�
+</script>
+<style lang="scss" scoped>
+.wx-voice-div {
+ display: flex;
+ width: 120px;
+ height: 50px;
+ padding: 5px;
+ background-color: #eaeaea;
+ border-radius: 10px;
+ justify-content: center;
+ align-items: center;
+}
+
+.amr-duration {
+ margin-left: 5px;
+ font-size: 11px;
+}
+</style>
diff --git a/src/views/mp/draft/components/CoverSelect.vue b/src/views/mp/draft/components/CoverSelect.vue
new file mode 100644
index 0000000..499f1a6
--- /dev/null
+++ b/src/views/mp/draft/components/CoverSelect.vue
@@ -0,0 +1,166 @@
+<template>
+ <div>
+ <p>灏侀潰:</p>
+ <div class="thumb-div">
+ <el-image
+ v-if="newsItem.thumbUrl"
+ style="width: 300px; max-height: 300px"
+ :src="newsItem.thumbUrl"
+ fit="contain"
+ />
+ <Icon
+ v-else
+ icon="ep:plus"
+ class="avatar-uploader-icon"
+ :class="isFirst ? 'avatar' : 'avatar1'"
+ />
+ <div class="thumb-but">
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :before-upload="onBeforeUpload"
+ :on-error="onUploadError"
+ :on-success="onUploadSuccess"
+ >
+ <template #trigger>
+ <el-button size="small" type="primary">鏈湴涓婁紶</el-button>
+ </template>
+ <el-button
+ size="small"
+ type="primary"
+ @click="showImageDialog = true"
+ style="margin-left: 5px"
+ >
+ 绱犳潗搴撻�夋嫨
+ </el-button>
+ <template #tip>
+ <div class="el-upload__tip">鏀寔 bmp/png/jpeg/jpg/gif 鏍煎紡锛屽ぇ灏忎笉瓒呰繃 2M</div>
+ </template>
+ </el-upload>
+ </div>
+ <el-dialog
+ title="閫夋嫨鍥剧墖"
+ v-model="showImageDialog"
+ width="80%"
+ append-to-body
+ destroy-on-close
+ >
+ <WxMaterialSelect
+ type="image"
+ :account-id="accountId!"
+ @select-material="onMaterialSelected"
+ />
+ </el-dialog>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import { getAccessToken } from '@/utils/auth'
+import type { UploadFiles, UploadProps, UploadRawFile } from 'element-plus'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+import { NewsItem } from './types'
+const message = useMessage()
+
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 涓婁紶姘镐箙绱犳潗鐨勫湴鍧�
+const HEADERS = { Authorization: 'Bearer ' + getAccessToken() } // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+
+const props = defineProps<{
+ modelValue: NewsItem
+ isFirst: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: NewsItem)
+}>()
+const newsItem = computed<NewsItem>({
+ get() {
+ return props.modelValue
+ },
+ set(val) {
+ emit('update:modelValue', val)
+ }
+})
+
+const accountId = inject<number>('accountId')
+const showImageDialog = ref(false)
+
+const fileList = ref<UploadFiles>([])
+interface UploadData {
+ type: UploadType
+ accountId: number
+}
+const uploadData: UploadData = reactive({
+ type: UploadType.Image,
+ accountId: accountId!
+})
+
+/** 绱犳潗閫夋嫨瀹屾垚浜嬩欢*/
+const onMaterialSelected = (item: any) => {
+ showImageDialog.value = false
+ newsItem.value.thumbMediaId = item.mediaId
+ newsItem.value.thumbUrl = item.url
+}
+
+const onBeforeUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+ useBeforeUpload(UploadType.Image, 2)(rawFile)
+
+const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
+ if (res.code !== 0) {
+ message.error('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 閲嶇疆涓婁紶鏂囦欢鐨勮〃鍗�
+ fileList.value = []
+
+ // 璁剧疆鑽夌鐨勫皝闈㈠瓧娈�
+ newsItem.value.thumbMediaId = res.data.mediaId
+ newsItem.value.thumbUrl = res.data.url
+}
+
+const onUploadError = (err: Error) => {
+ message.error('涓婁紶澶辫触: ' + err.message)
+}
+</script>
+
+<style lang="scss" scoped>
+.el-upload__tip {
+ margin-left: 5px;
+}
+
+.thumb-div {
+ display: inline-block;
+ width: 100%;
+ text-align: center;
+
+ .avatar-uploader-icon {
+ width: 120px;
+ height: 120px;
+ font-size: 28px;
+ line-height: 120px;
+ color: #8c939d;
+ text-align: center;
+ border: 1px solid #d9d9d9;
+ }
+
+ .avatar {
+ width: 230px;
+ height: 120px;
+ }
+
+ .avatar1 {
+ width: 120px;
+ height: 120px;
+ }
+
+ .thumb-but {
+ margin: 5px;
+ }
+}
+</style>
diff --git a/src/views/mp/draft/components/DraftTable.vue b/src/views/mp/draft/components/DraftTable.vue
new file mode 100644
index 0000000..b0f4fa0
--- /dev/null
+++ b/src/views/mp/draft/components/DraftTable.vue
@@ -0,0 +1,87 @@
+<template>
+ <div class="waterfall" v-loading="props.loading">
+ <template v-for="(item, index) in props.list" :key="index">
+ <div class="waterfall-item" v-if="item.content && item.content.newsItem">
+ <WxNews :articles="item.content.newsItem" />
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <el-row>
+ <el-button
+ type="success"
+ circle
+ @click="emit('publish', item)"
+ v-hasPermi="['mp:free-publish:submit']"
+ >
+ <Icon icon="fa:upload" />
+ </el-button>
+ <el-button
+ type="primary"
+ circle
+ @click="emit('update', item)"
+ v-hasPermi="['mp:draft:update']"
+ >
+ <Icon icon="ep:edit" />
+ </el-button>
+ <el-button
+ type="danger"
+ circle
+ @click="emit('delete', item)"
+ v-hasPermi="['mp:draft:delete']"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </el-row>
+ </div>
+ </template>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxNews from '@/views/mp/components/wx-news'
+
+import { Article } from './types'
+
+const props = defineProps<{
+ list: Article[]
+ loading: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'publish', v: Article)
+ (e: 'update', v: Article)
+ (e: 'delete', v: Article)
+}>()
+</script>
+
+<style lang="scss" scoped>
+.waterfall {
+ width: 100%;
+ column-gap: 10px;
+ column-count: 5;
+ margin: 0 auto;
+
+ .waterfall-item {
+ padding: 10px;
+ margin-bottom: 10px;
+ break-inside: avoid;
+ border: 1px solid #eaeaea;
+ }
+}
+
+@media (width >= 992px) and (width <= 1300px) {
+ .waterfall {
+ column-count: 3;
+ }
+}
+
+@media (width >= 768px) and (width <= 991px) {
+ .waterfall {
+ column-count: 2;
+ }
+}
+
+@media (width <= 767px) {
+ .waterfall {
+ column-count: 1;
+ }
+}
+</style>
diff --git a/src/views/mp/draft/components/NewsForm.vue b/src/views/mp/draft/components/NewsForm.vue
new file mode 100644
index 0000000..9711334
--- /dev/null
+++ b/src/views/mp/draft/components/NewsForm.vue
@@ -0,0 +1,306 @@
+<template>
+ <el-container>
+ <el-aside width="40%">
+ <div class="select-item">
+ <div v-for="(news, index) in newsList" :key="index">
+ <div
+ class="news-main father"
+ v-if="index === 0"
+ :class="{ activeAddNews: activeNewsIndex === index }"
+ @click="activeNewsIndex = index"
+ >
+ <div class="news-content">
+ <img class="material-img" :src="news.thumbUrl" />
+ <div class="news-content-title">{{ news.title }}</div>
+ </div>
+ <div class="child" v-if="newsList.length > 1">
+ <el-button type="info" circle size="small" @click="() => moveDownNews(index)">
+ <Icon icon="ep:arrow-down-bold" />
+ </el-button>
+ <el-button
+ v-if="isCreating"
+ type="danger"
+ circle
+ size="small"
+ @click="() => removeNews(index)"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </div>
+ </div>
+ <div
+ class="news-main-item father"
+ v-if="index > 0"
+ :class="{ activeAddNews: activeNewsIndex === index }"
+ @click="activeNewsIndex = index"
+ >
+ <div class="news-content-item">
+ <div class="news-content-item-title">{{ news.title }}</div>
+ <div class="news-content-item-img">
+ <img class="material-img" :src="news.thumbUrl" width="100%" />
+ </div>
+ </div>
+ <div class="child">
+ <el-button
+ v-if="newsList.length > index + 1"
+ circle
+ type="info"
+ size="small"
+ @click="() => moveDownNews(index)"
+ >
+ <Icon icon="ep:arrow-down-bold" />
+ </el-button>
+ <el-button
+ v-if="index > 0"
+ type="info"
+ circle
+ size="small"
+ @click="() => moveUpNews(index)"
+ >
+ <Icon icon="ep:arrow-up-bold" />
+ </el-button>
+ <el-button
+ v-if="isCreating"
+ type="danger"
+ size="small"
+ circle
+ @click="() => removeNews(index)"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </div>
+ </div>
+ </div>
+ <el-row justify="center" class="ope-row">
+ <el-button
+ type="primary"
+ circle
+ @click="plusNews"
+ v-if="newsList.length < 8 && isCreating"
+ >
+ <Icon icon="ep:plus" />
+ </el-button>
+ </el-row>
+ </div>
+ </el-aside>
+ <el-main>
+ <div v-if="newsList.length > 0">
+ <!-- 鏍囬銆佷綔鑰呫�佸師鏂囧湴鍧� -->
+ <el-row :gutter="20">
+ <el-input v-model="activeNewsItem.title" placeholder="璇疯緭鍏ユ爣棰橈紙蹇呭~锛�" />
+ <el-input
+ v-model="activeNewsItem.author"
+ placeholder="璇疯緭鍏ヤ綔鑰�"
+ style="margin-top: 5px"
+ />
+ <el-input
+ v-model="activeNewsItem.contentSourceUrl"
+ placeholder="璇疯緭鍏ュ師鏂囧湴鍧�"
+ style="margin-top: 5px"
+ />
+ </el-row>
+ <!-- 灏侀潰鍜屾憳瑕� -->
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <CoverSelect v-model="activeNewsItem" :is-first="activeNewsIndex === 0" />
+ </el-col>
+ <el-col :span="12">
+ <p>鎽樿:</p>
+ <el-input
+ :rows="8"
+ type="textarea"
+ v-model="activeNewsItem.digest"
+ placeholder="璇疯緭鍏ユ憳瑕�"
+ class="digest"
+ maxlength="120"
+ />
+ </el-col>
+ </el-row>
+ <!--瀵屾枃鏈紪杈戝櫒缁勪欢-->
+ <el-row>
+ <Editor v-model="activeNewsItem.content" :editor-config="editorConfig" />
+ </el-row>
+ </div>
+ </el-main>
+ </el-container>
+</template>
+
+<script lang="ts" setup>
+import { Editor } from '@/components/Editor'
+import { createEditorConfig } from '../editor-config'
+import CoverSelect from './CoverSelect.vue'
+import { type NewsItem, createEmptyNewsItem } from './types'
+
+defineOptions({ name: 'NewsForm' })
+
+const message = useMessage()
+
+const props = defineProps<{
+ isCreating: boolean
+ modelValue: NewsItem[] | null
+}>()
+
+const accountId = inject<number>('accountId')
+
+// ========== 鏂囦欢涓婁紶 ==========
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 涓婁紶姘镐箙绱犳潗鐨勫湴鍧�
+const editorConfig = createEditorConfig(UPLOAD_URL, unref(accountId))
+
+// v-model=newsList
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: NewsItem[])
+}>()
+const newsList = computed<NewsItem[]>({
+ get() {
+ return props.modelValue === null ? [createEmptyNewsItem()] : props.modelValue
+ },
+ set(val) {
+ emit('update:modelValue', val)
+ }
+})
+
+const activeNewsIndex = ref(0)
+const activeNewsItem = computed<NewsItem>(() => newsList.value[activeNewsIndex.value])
+
+// 灏嗗浘鏂囧悜涓嬬Щ鍔�
+const moveDownNews = (index: number) => {
+ const temp = newsList.value[index]
+ newsList.value[index] = newsList.value[index + 1]
+ newsList.value[index + 1] = temp
+ activeNewsIndex.value = index + 1
+}
+
+// 灏嗗浘鏂囧悜涓婄Щ鍔�
+const moveUpNews = (index: number) => {
+ const temp = newsList.value[index]
+ newsList.value[index] = newsList.value[index - 1]
+ newsList.value[index - 1] = temp
+ activeNewsIndex.value = index - 1
+}
+
+// 鍒犻櫎鎸囧畾 index 鐨勫浘鏂�
+const removeNews = async (index: number) => {
+ try {
+ await message.confirm('纭畾鍒犻櫎璇ュ浘鏂囧悧?')
+ newsList.value.splice(index, 1)
+ if (activeNewsIndex.value === index) {
+ activeNewsIndex.value = 0
+ }
+ } catch {
+ // empty
+ }
+}
+
+// 娣诲姞涓�涓浘鏂�
+const plusNews = () => {
+ newsList.value.push(createEmptyNewsItem())
+ activeNewsIndex.value = newsList.value.length - 1
+}
+</script>
+
+<style lang="scss" scoped>
+.ope-row {
+ padding-top: 5px;
+ margin-top: 5px;
+ text-align: center;
+ border-top: 1px solid #eaeaea;
+}
+
+.el-row {
+ margin-bottom: 20px;
+}
+
+.el-row:last-child {
+ margin-bottom: 0;
+}
+
+.digest {
+ display: inline-block;
+ width: 100%;
+ vertical-align: top;
+}
+
+/* 鏂板鍥炬枃 */
+.news-main {
+ width: 100%;
+ height: 120px;
+ margin: auto;
+ background-color: #fff;
+}
+
+.news-content {
+ position: relative;
+ width: 100%;
+ height: 120px;
+ background-color: #acadae;
+}
+
+.news-content-title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ display: inline-block;
+ width: 98%;
+ height: 25px;
+ padding: 1%;
+ overflow: hidden;
+ font-size: 15px;
+ color: #fff;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ background-color: black;
+ opacity: 0.65;
+}
+
+.news-main-item {
+ width: 100%;
+ padding: 5px 0;
+ margin: auto;
+ background-color: #fff;
+ border-top: 1px solid #eaeaea;
+}
+
+.news-content-item {
+ position: relative;
+ margin-left: -3px;
+}
+
+.news-content-item-title {
+ display: inline-block;
+ width: 70%;
+ font-size: 12px;
+}
+
+.news-content-item-img {
+ display: inline-block;
+ width: 25%;
+ background-color: #acadae;
+}
+
+.select-item {
+ width: 60%;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+
+ .activeAddNews {
+ border: 5px solid #2bb673;
+ }
+}
+
+.father .child {
+ position: relative;
+ bottom: 25px;
+ display: none;
+ text-align: center;
+}
+
+.father:hover .child {
+ display: block;
+}
+
+.material-img {
+ width: 100%;
+ height: 100%;
+}
+</style>
diff --git a/src/views/mp/draft/components/index.ts b/src/views/mp/draft/components/index.ts
new file mode 100644
index 0000000..51e843d
--- /dev/null
+++ b/src/views/mp/draft/components/index.ts
@@ -0,0 +1,7 @@
+import type { Article, NewsItem, NewsItemList } from './types'
+import { createEmptyNewsItem } from './types'
+import DraftTable from './DraftTable.vue'
+import NewsForm from './NewsForm.vue'
+
+export { DraftTable, NewsForm, createEmptyNewsItem }
+export type { Article, NewsItem, NewsItemList }
diff --git a/src/views/mp/draft/components/types.ts b/src/views/mp/draft/components/types.ts
new file mode 100644
index 0000000..a8cf00c
--- /dev/null
+++ b/src/views/mp/draft/components/types.ts
@@ -0,0 +1,40 @@
+interface NewsItem {
+ title: string
+ thumbMediaId: string
+ author: string
+ digest: string
+ showCoverPic: string
+ content: string
+ contentSourceUrl: string
+ needOpenComment: string
+ onlyFansCanComment: string
+ thumbUrl: string
+}
+
+interface NewsItemList {
+ newsItem: NewsItem[]
+}
+
+interface Article {
+ mediaId: string
+ content: NewsItemList
+ updateTime: number
+}
+
+const createEmptyNewsItem = (): NewsItem => {
+ return {
+ title: '',
+ thumbMediaId: '',
+ author: '',
+ digest: '',
+ showCoverPic: '',
+ content: '',
+ contentSourceUrl: '',
+ needOpenComment: '',
+ onlyFansCanComment: '',
+ thumbUrl: ''
+ }
+}
+
+export type { Article, NewsItem, NewsItemList }
+export { createEmptyNewsItem }
diff --git a/src/views/mp/draft/editor-config.ts b/src/views/mp/draft/editor-config.ts
new file mode 100644
index 0000000..a8bd4e7
--- /dev/null
+++ b/src/views/mp/draft/editor-config.ts
@@ -0,0 +1,75 @@
+import { IEditorConfig } from '@wangeditor-next/editor'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+
+const message = useMessage()
+
+type InsertFnType = (url: string, alt: string, href: string) => void
+
+export const createEditorConfig = (
+ server: string,
+ accountId: number | undefined
+): Partial<IEditorConfig> => {
+ return {
+ MENU_CONF: {
+ ['uploadImage']: {
+ server,
+ // 鍗曚釜鏂囦欢鐨勬渶澶т綋绉檺鍒讹紝榛樿涓� 2M
+ maxFileSize: 5 * 1024 * 1024,
+ // 鏈�澶氬彲涓婁紶鍑犱釜鏂囦欢锛岄粯璁や负 100
+ maxNumberOfFiles: 10,
+ // 閫夋嫨鏂囦欢鏃剁殑绫诲瀷闄愬埗锛岄粯璁や负 ['image/*'] 銆傚涓嶆兂闄愬埗锛屽垯璁剧疆涓� []
+ allowedFileTypes: ['image/*'],
+
+ // 鑷畾涔変笂浼犲弬鏁帮紝渚嬪浼犻�掗獙璇佺殑 token 绛夈�傚弬鏁颁細琚坊鍔犲埌 formData 涓紝涓�璧蜂笂浼犲埌鏈嶅姟绔��
+ meta: {
+ accountId: accountId,
+ type: 'image'
+ },
+ // 灏� meta 鎷兼帴鍒� url 鍙傛暟涓紝榛樿 false
+ metaWithUrl: true,
+
+ // 鑷畾涔夊鍔� http header
+ headers: {
+ Accept: '*',
+ Authorization: 'Bearer ' + getAccessToken(),
+ 'tenant-id': getTenantId()
+ },
+
+ // 璺ㄥ煙鏄惁浼犻�� cookie 锛岄粯璁や负 false
+ withCredentials: true,
+
+ // 瓒呮椂鏃堕棿锛岄粯璁や负 10 绉�
+ timeout: 5 * 1000, // 5 绉�
+
+ // form-data fieldName锛屽悗绔帴鍙e弬鏁板悕绉帮紝榛樿鍊紈angeditor-uploaded-image
+ fieldName: 'file',
+
+ // 涓婁紶涔嬪墠瑙﹀彂
+ onBeforeUpload(file: File) {
+ console.log(file)
+ return file
+ },
+ // 涓婁紶杩涘害鐨勫洖璋冨嚱鏁�
+ onProgress(progress: number) {
+ // progress 鏄� 0-100 鐨勬暟瀛�
+ console.log('progress', progress)
+ },
+ onSuccess(file: File, res: any) {
+ console.log('onSuccess', file, res)
+ },
+ onFailed(file: File, res: any) {
+ message.alertError(res.message)
+ console.log('onFailed', file, res)
+ },
+ onError(file: File, err: any, res: any) {
+ message.alertError(err.message)
+ console.error('onError', file, err, res)
+ },
+ // 鑷畾涔夋彃鍏ュ浘鐗�
+ customInsert(res: any, insertFn: InsertFnType) {
+ insertFn(res.data.url, 'image', res.data.url)
+ }
+ }
+ }
+ }
+}
diff --git a/src/views/mp/draft/index.vue b/src/views/mp/draft/index.vue
new file mode 100644
index 0000000..5209d22
--- /dev/null
+++ b/src/views/mp/draft/index.vue
@@ -0,0 +1,208 @@
+<template>
+ <doc-alert title="鍏紬鍙峰浘鏂�" url="https://doc.iocoder.cn/mp/article/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ type="primary"
+ plain
+ @click="handleAdd"
+ v-hasPermi="['mp:draft:create']"
+ :disabled="accountId === 0"
+ >
+ <Icon icon="ep:plus" />鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <DraftTable
+ :loading="loading"
+ :list="list"
+ @update="onUpdate"
+ @delete="onDelete"
+ @publish="onPublish"
+ />
+ <!-- 鍒嗛〉璁板綍 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 娣诲姞鎴栦慨鏀硅崏绋垮璇濇 -->
+ <el-dialog
+ :title="isCreating ? '鏂板缓鍥炬枃' : '淇敼鍥炬枃'"
+ width="80%"
+ v-model="showDialog"
+ :before-close="onBeforeDialogClose"
+ destroy-on-close
+ >
+ <NewsForm v-model="newsList" v-loading="isSubmitting" :is-creating="isCreating" />
+ <template #footer>
+ <el-button @click="showDialog = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="onSubmitNewsItem">鎻� 浜�</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import * as MpDraftApi from '@/api/mp/draft'
+import * as MpFreePublishApi from '@/api/mp/freePublish'
+import {
+ type Article,
+ type NewsItem,
+ NewsForm,
+ DraftTable,
+ createEmptyNewsItem
+} from './components/'
+// import drafts from './mock' // 鍙互鐢ㄦ敼鏈湴鏁版嵁妯℃嫙锛岄伩鍏岮PI璋冪敤瓒呴檺
+
+defineOptions({ name: 'MpDraft' })
+
+const message = useMessage() // 娑堟伅
+
+const accountId = ref(-1)
+provide('accountId', accountId)
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: accountId
+})
+
+// ========== 鑽夌鏂板缓 or 淇敼 ==========
+const showDialog = ref(false)
+const newsList = ref<NewsItem[]>([])
+const mediaId = ref('')
+const isCreating = ref(true)
+const isSubmitting = ref(false)
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number) => {
+ accountId.value = id
+ queryParams.pageNo = 1
+ getList()
+}
+
+// 鍏抽棴寮圭獥
+const onBeforeDialogClose = async (onDone: () => {}) => {
+ try {
+ await message.confirm('淇敼鍐呭鍙兘杩樻湭淇濆瓨锛岀‘瀹氬叧闂悧?')
+ onDone()
+ } catch {
+ //
+ }
+}
+
+// ======================== 鍒楄〃鏌ヨ ========================
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const drafts = await MpDraftApi.getDraftPage(queryParams)
+ drafts.list.forEach((draft) => {
+ const newsList = draft.content.newsItem
+ // 灏� thumbUrl 杞垚 picUrl锛屼繚璇� wx-news 缁勪欢鍙互棰勮灏侀潰
+ newsList.forEach((item) => {
+ item.picUrl = item.thumbUrl
+ })
+ })
+ list.value = drafts.list
+ total.value = drafts.total
+ } finally {
+ loading.value = false
+ }
+}
+
+// ======================== 鏂板/淇敼鑽夌 ========================
+/** 鏂板鎸夐挳鎿嶄綔 */
+const handleAdd = () => {
+ isCreating.value = true
+ newsList.value = [createEmptyNewsItem()]
+ showDialog.value = true
+}
+
+/** 鏇存柊鎸夐挳鎿嶄綔 */
+const onUpdate = (item: Article) => {
+ mediaId.value = item.mediaId
+ newsList.value = JSON.parse(JSON.stringify(item.content.newsItem))
+ isCreating.value = false
+ showDialog.value = true
+}
+
+/** 鎻愪氦鎸夐挳 */
+const onSubmitNewsItem = async () => {
+ isSubmitting.value = true
+ try {
+ if (isCreating.value) {
+ await MpDraftApi.createDraft(accountId.value, newsList.value)
+ message.notifySuccess('鏂板鎴愬姛')
+ } else {
+ await MpDraftApi.updateDraft(accountId.value, mediaId.value, newsList.value)
+ message.notifySuccess('鏇存柊鎴愬姛')
+ }
+ } finally {
+ showDialog.value = false
+ isSubmitting.value = false
+ await getList()
+ }
+}
+
+// ======================== 鑽夌绠卞彂甯� ========================
+const onPublish = async (item: Article) => {
+ const mediaId = item.mediaId
+ const content =
+ '浣犳鍦ㄩ�氳繃鍙戝竷鐨勬柟寮忓彂琛ㄥ唴瀹广�� 鍙戝竷涓嶅崰鐢ㄧ兢鍙戞鏁帮紝涓�澶╁彲澶氭鍙戝竷銆�' +
+ '宸插彂甯冨唴瀹逛笉浼氭帹閫佺粰鐢ㄦ埛锛屼篃涓嶄細灞曠ず鍦ㄥ叕浼楀彿涓婚〉涓�� ' +
+ '鍙戝竷鍚庯紝浣犲彲浠ュ墠寰�鍙戣〃璁板綍鑾峰彇閾炬帴锛屼篃鍙互灏嗗彂甯冨唴瀹规坊鍔犲埌鑷畾涔夎彍鍗曘�佽嚜鍔ㄥ洖澶嶃�佽瘽棰樺拰椤甸潰妯℃澘涓��'
+ try {
+ await message.confirm(content)
+ await MpFreePublishApi.submitFreePublish(accountId.value, mediaId)
+ message.notifySuccess('鍙戝竷鎴愬姛')
+ await getList()
+ } catch {
+ //
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const onDelete = async (item: Article) => {
+ const mediaId = item.mediaId
+ try {
+ await message.confirm('姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ヨ崏绋�, 鏄惁缁х画?')
+ await MpDraftApi.deleteDraft(accountId.value, mediaId)
+ message.notifySuccess('鍒犻櫎鎴愬姛')
+ await getList()
+ } catch {
+ //
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.pagination {
+ float: right;
+ margin-right: 25px;
+}
+</style>
diff --git a/src/views/mp/draft/mock.js b/src/views/mp/draft/mock.js
new file mode 100644
index 0000000..e8493f6
--- /dev/null
+++ b/src/views/mp/draft/mock.js
@@ -0,0 +1,151 @@
+export default {
+ list: [
+ {
+ mediaId: 'r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW',
+ content: {
+ newsItem: [
+ {
+ title: '鎴戞槸鏍囬锛圤OO锛�',
+ author: '鎴戞槸浣滆��',
+ digest: '鎴戞槸鎽樿',
+ content: '鎴戞槸鍐呭',
+ contentSourceUrl: 'https://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9XaFphcmtJVFh3VEc4Q1MxQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN2QxTE56SFBCYXc2RE9NcUxIeS1CQjJuUHhTWjBlN2VOeGRpRi1fZUhwN1FNQjdrQV9yRU9EU0hibHREZmZoVW5acnZrN3ZjaWsxejR3RGpKczBzTHFIM0dFNFZWVkpBc0dWWlAzUEhlVmpnfn4%3D&chksm=1f6354802814dd969ef83c0f3babe555c614270b30bc383beaf7ffd13b0257f0fe5ced9af694#rd',
+ thumbUrl:
+ 'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
+ },
+ {
+ title: '鎴戞槸鏍囬锛圶XX锛�',
+ author: '鎴戞槸浣滆��',
+ digest: '鎴戞槸鎽樿',
+ content: '鎴戞槸鍐呭',
+ contentSourceUrl: 'https://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9yTlYwOEs1clpwcE5OUEhCQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN0NSMjFqN3N1aUZMbFNVLTZHN2ZDME9qOGp2THk2RFNlSTlKZ3Y1czFVZDdQQm5IeUg3dEppSUtpQUh5SExOOTRkT3dHNUdBdHdWSWlOendlREV3dS1jUEVQbFpiVTZmVW5iRWhZcGdkNTFRfn4%3D&chksm=1f6354802814dd96a403151cd44c7da4eecf0e475d25423e46ecd795b513bafd829a75daef9b#rd',
+ thumbUrl:
+ 'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
+ }
+ ]
+ },
+ updateTime: 1673655730
+ },
+ {
+ mediaId: 'r6ryvl6LrxBU0miaST4Y-jGpXnO73ihN0lsNXknCRQHapp2xgHMRxHKG50LituFe',
+ content: {
+ newsItem: [
+ {
+ title: '鎴戞槸鏍囬锛堜慨鏀癸級',
+ author: '鎴戞槸浣滆��',
+ digest: '鎴戞槸鎽樿',
+ content: '鎴戞槸鍐呭',
+ contentSourceUrl: 'https://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl95WVFXYndIZnZJd0t5cjgvQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN1dlNURPbWswbEF4RDd5dVJTdjQ4cm9Cc0Q1TWhpMUh6SE1hVEE3ZHljaHhlZjZYSGF5N2JNSHpDTlh6ajNZbkpGTGpTcUQ4M3NMdW41ZUpXNFZZQ1VKbVlaMVp5ekxEV1czREdsY1dOYTZnfn4%3D&chksm=1f6354be2814dda8e6238037c2ebd52b1c8e80e93249a861ad80e4d40e5ca7207233475ca689#rd',
+ thumbUrl:
+ 'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
+ }
+ ]
+ },
+ updateTime: 1673655584
+ },
+ {
+ mediaId: 'r6ryvl6LrxBU0miaST4Y-v5SrbNCPpD6M_p3TmSrYwTjKogs-0DMJgmjMyNZPeMO',
+ content: {
+ newsItem: [
+ {
+ title: '1321',
+ author: '3232',
+ digest: '1333',
+ content: '<p>444</p>',
+ contentSourceUrl: 'http://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-tlQmcl3RdC-Jcgns6IQtf7zenGy3b86WLT7GzUcrb1T',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9jelJiaDAzbmdpSkJOZ2M2QWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNDNXVVc2ZDRYeTY0Zm1weXR6dE9vQWh1TzEwbEpUVnRfVzJyaGFDNXBkZ0ZXM2JFOTNaRHNhOHRUeFdEanhMeS01X01kMUNWQ1BpRER3cjYwTl9pMnpFLUJhZXFucVVfM1pDUXlTUEl1S25nfn4%3D&chksm=1f6354bc2814ddaa56a90ad5bc3d078601c8d1589ba01827a8170587bc830ff9747b5f59c3a0#rd',
+ thumbUrl:
+ 'http://mmbiz.qpic.cn/mmbiz_png/btUmCVHwbJUoicwBiacjVeQbu6QxgBVrukfSJXz509boa21SpH8OVHAqXCJiaiaAaHQJNxwwsa0gHRXVr0G5EZYamw/0?wx_fmt=png'
+ }
+ ]
+ },
+ updateTime: 1673628969
+ },
+ {
+ mediaId: 'r6ryvl6LrxBU0miaST4Y-vdWrisK5EZbk4Y3tzh8P0PG0eEUbnQrh0BcsEb3WNP0',
+ content: {
+ newsItem: [
+ {
+ title: 'tudou',
+ author: 'haha',
+ digest: '312',
+ content: '<p>132312</p>',
+ contentSourceUrl: 'http://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qdkJ1ZjBoUmg2Uk9TS3RlQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNVg2aTJsaC1fMkU2eXNacUplN3VDTTZFZkhtMjhuTUZvWkxsNDBRSXExY2tiVXRHb09TaHgtREhzY3doZ0JYeC1TSTZ5eWZldXJsOWtfbV8yMi1aYkcyZ2pOY0haM0Ntb3VSWEtxUGVFRlNBfn4%3D&chksm=1f6354ba2814ddacf0184b24d310483641ef190b1faac098c285eb416c70017e2f54decfa1af#rd',
+ thumbUrl:
+ 'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG.png'
+ }
+ ]
+ },
+ updateTime: 1673628760
+ },
+ {
+ mediaId: 'r6ryvl6LrxBU0miaST4Y-u9kTIm1DhWZDdXyxsxUVv2Z5DAB99IPxkIRTUUD206k',
+ content: {
+ newsItem: [
+ {
+ title: '12',
+ author: '333',
+ digest: '123',
+ content: '123',
+ contentSourceUrl: 'https://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qVVhpSDZUaFJWTzBBWWRVQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNWRnTDJWYmF2NER0clV1bThmQ0xUR3hqQnJkZ3BJSUNmNDJmc0lCZ1dadkVnZ3Z5bkN4YWtVUjhoaWZWYzZURUR4NnpMd0Y4Z3U5aUdib0lkMzI4Rjg3SG9JX2FycTMxbUctOHplaTlQVVhnfn4%3D&chksm=1f6354b62814dda076c778af33f06580165d8aa81f7798d55cfabb1886b5c74d9b2124a3535c#rd',
+ thumbUrl:
+ 'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8.jpg'
+ }
+ ]
+ },
+ updateTime: 1673626494
+ },
+ {
+ mediaId: 'r6ryvl6LrxBU0miaST4Y-sO24upobaENDmeByfBTfaozB3aOqSMAV0lGy-UkHXE7',
+ content: {
+ newsItem: [
+ {
+ title: '鎴戞槸鏍囬',
+ author: '鎴戞槸浣滆��',
+ digest: '鎴戞槸鎽樿',
+ content: '鎴戞槸鍐呭',
+ contentSourceUrl: 'https://www.iocoder.cn',
+ thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
+ showCoverPic: 0,
+ needOpenComment: 0,
+ onlyFansCanComment: 0,
+ url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9LT2dqRnpMNUpsR0hjYWtBQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNGNmazZTdlE5WkxvU0tfX2V5cjV2WjJiR0xjQUhyREFSZWo2eWNrUW9EYVh6ZkpWRXBLR3FmTEV6YldBMno3Q2ZvVXBSdzlaVDc3aFhndEpQWUwzWmFMUWt0YVVURE1VZ1FsQTdPMlRtc3JBfn4%3D&chksm=1f6354aa2814ddbcc2637382f963a8742993ac38ebcebe6e3411df5ac82ac7bbdb391be6494a#rd',
+ thumbUrl:
+ 'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
+ }
+ ]
+ },
+ updateTime: 1673534279
+ }
+ ],
+ total: 6
+}
diff --git a/src/views/mp/freePublish/index.vue b/src/views/mp/freePublish/index.vue
new file mode 100644
index 0000000..c5639ec
--- /dev/null
+++ b/src/views/mp/freePublish/index.vue
@@ -0,0 +1,338 @@
+<template>
+ <doc-alert title="鍏紬鍙峰浘鏂�" url="https://doc.iocoder.cn/mp/article/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <div class="waterfall" v-loading="loading">
+ <div
+ class="waterfall-item"
+ v-show="item.content && item.content.newsItem"
+ v-for="item in list"
+ :key="item.articleId"
+ >
+ <wx-news :articles="item.content.newsItem" />
+ <el-row justify="center" class="ope-row">
+ <el-button
+ type="danger"
+ circle
+ @click="handleDelete(item)"
+ v-hasPermi="['mp:free-publish:delete']"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </el-row>
+ </div>
+ </div>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import * as FreePublishApi from '@/api/mp/freePublish'
+import WxNews from '@/views/mp/components/wx-news'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+
+defineOptions({ name: 'MpFreePublish' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: -1
+})
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number) => {
+ queryParams.accountId = id
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ try {
+ loading.value = true
+ const data = await FreePublishApi.getFreePublishPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (item: any) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm('鍒犻櫎鍚庣敤鎴峰皢鏃犳硶璁块棶姝ら〉闈紝纭畾鍒犻櫎锛�')
+ // 鍙戣捣鍒犻櫎
+ await FreePublishApi.deleteFreePublish(queryParams.accountId, item.articleId)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ //
+ }
+}
+</script>
+<style lang="scss" scoped>
+@media (width >= 992px) and (width <= 1300px) {
+ .waterfall {
+ column-count: 3;
+ }
+
+ p {
+ color: red;
+ }
+}
+
+@media (width >= 768px) and (width <= 991px) {
+ .waterfall {
+ column-count: 2;
+ }
+
+ p {
+ color: orange;
+ }
+}
+
+@media (width <= 767px) {
+ .waterfall {
+ column-count: 1;
+ }
+}
+
+.ope-row {
+ padding-top: 5px;
+ margin-top: 5px;
+ text-align: center;
+ border-top: 1px solid #eaeaea;
+}
+
+.item-name {
+ overflow: hidden;
+ font-size: 12px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.el-upload__tip {
+ margin-left: 5px;
+}
+
+/* 鏂板鍥炬枃 */
+.left {
+ display: inline-block;
+ width: 35%;
+ margin-top: 200px;
+ vertical-align: top;
+}
+
+.right {
+ display: inline-block;
+ width: 60%;
+ margin-top: -40px;
+}
+
+.avatar-uploader {
+ display: inline-block;
+ width: 20%;
+}
+
+.avatar-uploader .el-upload {
+ position: relative;
+ overflow: hidden;
+ text-align: unset !important;
+ cursor: pointer;
+ border-radius: 6px;
+}
+
+.avatar-uploader .el-upload:hover {
+ border-color: #165dff;
+}
+
+.avatar-uploader-icon {
+ width: 120px;
+ height: 120px;
+ font-size: 28px;
+ line-height: 120px;
+ color: #8c939d;
+ text-align: center;
+ border: 1px solid #d9d9d9;
+}
+
+.avatar {
+ width: 230px;
+ height: 120px;
+}
+
+.avatar1 {
+ width: 120px;
+ height: 120px;
+}
+
+.digest {
+ display: inline-block;
+ width: 60%;
+ vertical-align: top;
+}
+
+/* 鏂板鍥炬枃 */
+
+/* 鐎戝竷娴佹牱寮� */
+.waterfall {
+ width: 100%;
+ column-gap: 10px;
+ column-count: 5;
+ margin: 0 auto;
+}
+
+.waterfall-item {
+ padding: 10px;
+ margin-bottom: 10px;
+ break-inside: avoid;
+ border: 1px solid #eaeaea;
+}
+
+p {
+ line-height: 30px;
+}
+
+/* 鐎戝竷娴佹牱寮� */
+.news-main {
+ width: 100%;
+ height: 120px;
+ margin: auto;
+ background-color: #fff;
+}
+
+.news-content {
+ position: relative;
+ width: 100%;
+ height: 120px;
+ background-color: #acadae;
+}
+
+.news-content-title {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ display: inline-block;
+ width: 98%;
+ height: 25px;
+ padding: 1%;
+ overflow: hidden;
+ font-size: 15px;
+ color: #fff;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ background-color: black;
+ opacity: 0.65;
+}
+
+.news-main-item {
+ width: 100%;
+ padding: 5px 0;
+ margin: auto;
+ background-color: #fff;
+ border-top: 1px solid #eaeaea;
+}
+
+.news-content-item {
+ position: relative;
+ margin-left: -3px;
+}
+
+.news-content-item-title {
+ display: inline-block;
+ width: 70%;
+ font-size: 12px;
+}
+
+.news-content-item-img {
+ display: inline-block;
+ width: 25%;
+ background-color: #acadae;
+}
+
+.input-tt {
+ padding: 5px;
+}
+
+.activeAddNews {
+ border: 5px solid #2bb673;
+}
+
+.news-main-plus {
+ width: 280px;
+ height: 50px;
+ margin: auto;
+ text-align: center;
+}
+
+.icon-plus {
+ margin: 10px;
+ font-size: 25px;
+}
+
+.select-item {
+ width: 60%;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+}
+
+.father .child {
+ position: relative;
+ bottom: 25px;
+ display: none;
+ text-align: center;
+}
+
+.father:hover .child {
+ display: block;
+}
+
+.thumb-div {
+ display: inline-block;
+ width: 30%;
+ text-align: center;
+}
+
+.thumb-but {
+ margin: 5px;
+}
+
+.material-img {
+ width: 100%;
+ height: 100%;
+}
+</style>
diff --git a/src/views/mp/hooks/useUpload.ts b/src/views/mp/hooks/useUpload.ts
new file mode 100644
index 0000000..b0e7053
--- /dev/null
+++ b/src/views/mp/hooks/useUpload.ts
@@ -0,0 +1,50 @@
+import type { UploadRawFile } from 'element-plus'
+
+const message = useMessage() // 娑堟伅
+
+enum UploadType {
+ Image = 'image',
+ Voice = 'voice',
+ Video = 'video'
+}
+
+const useBeforeUpload = (type: UploadType, maxSizeMB: number) => {
+ const fn = (rawFile: UploadRawFile): boolean => {
+ let allowTypes: string[] = []
+ let name = ''
+
+ switch (type) {
+ case UploadType.Image:
+ allowTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']
+ maxSizeMB = 2
+ name = '鍥剧墖'
+ break
+ case UploadType.Voice:
+ allowTypes = ['audio/mp3', 'audio/mpeg', 'audio/wma', 'audio/wav', 'audio/amr']
+ maxSizeMB = 2
+ name = '璇煶'
+ break
+ case UploadType.Video:
+ allowTypes = ['video/mp4']
+ maxSizeMB = 10
+ name = '瑙嗛'
+ break
+ }
+ // 鏍煎紡涓嶆纭�
+ if (!allowTypes.includes(rawFile.type)) {
+ message.error(`涓婁紶${name}鏍煎紡涓嶅!`)
+ return false
+ }
+ // 澶у皬涓嶆纭�
+ if (rawFile.size / 1024 / 1024 > maxSizeMB) {
+ message.error(`涓婁紶${name}澶у皬涓嶈兘瓒呰繃${maxSizeMB}M!`)
+ return false
+ }
+
+ return true
+ }
+
+ return fn
+}
+
+export { UploadType, useBeforeUpload }
diff --git a/src/views/mp/material/components/ImageTable.vue b/src/views/mp/material/components/ImageTable.vue
new file mode 100644
index 0000000..52c608f
--- /dev/null
+++ b/src/views/mp/material/components/ImageTable.vue
@@ -0,0 +1,83 @@
+<template>
+ <div class="waterfall" v-loading="props.loading">
+ <div class="waterfall-item" v-for="item in props.list" :key="item.id">
+ <a target="_blank" :href="item.url">
+ <img class="material-img" :src="item.url" />
+ <div class="item-name">{{ item.name }}</div>
+ </a>
+ <el-row justify="center">
+ <el-button
+ type="danger"
+ circle
+ @click="emit('delete', item.id)"
+ v-hasPermi="['mp:material:delete']"
+ >
+ <Icon icon="ep:delete" />
+ </el-button>
+ </el-row>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+const props = defineProps<{
+ list: any[]
+ loading: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'delete', v: number)
+}>()
+</script>
+
+<style lang="scss" scoped>
+@media (width >= 992px) and (width <= 1300px) {
+ .waterfall {
+ column-count: 3;
+ }
+
+ p {
+ color: red;
+ }
+}
+
+@media (width >= 768px) and (width <= 991px) {
+ .waterfall {
+ column-count: 2;
+ }
+
+ p {
+ color: orange;
+ }
+}
+
+@media (width <= 767px) {
+ .waterfall {
+ column-count: 1;
+ }
+}
+
+.waterfall {
+ width: 100%;
+ column-gap: 10px;
+ column-count: 5;
+ margin-top: 10px;
+
+ /* 鑺嬮亾婧愮爜锛氬鍔� 10px锛岄伩鍏嶉《鐫�涓婇潰 */
+}
+
+.waterfall-item {
+ padding: 10px;
+ margin-bottom: 10px;
+ break-inside: avoid;
+ border: 1px solid #eaeaea;
+}
+
+.material-img {
+ width: 100%;
+}
+
+p {
+ line-height: 30px;
+}
+</style>
diff --git a/src/views/mp/material/components/UploadFile.vue b/src/views/mp/material/components/UploadFile.vue
new file mode 100644
index 0000000..276a798
--- /dev/null
+++ b/src/views/mp/material/components/UploadFile.vue
@@ -0,0 +1,77 @@
+<template>
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :on-error="onUploadError"
+ :before-upload="onBeforeUpload"
+ :on-success="onUploadSuccess"
+ >
+ <el-button type="primary" plain> 鐐瑰嚮涓婁紶 </el-button>
+ <template #tip>
+ <span class="el-upload__tip" style="margin-left: 5px">
+ <slot></slot>
+ </span>
+ </template>
+ </el-upload>
+</template>
+<script lang="ts" setup>
+import type { UploadProps, UploadUserFile } from 'element-plus'
+import {
+ HEADERS,
+ UPLOAD_URL,
+ UploadData,
+ UploadType,
+ beforeImageUpload,
+ beforeVoiceUpload
+} from './upload'
+
+const message = useMessage()
+
+const props = defineProps<{ type: UploadType }>()
+
+const accountId = inject<number>('accountId')
+
+const fileList = ref<UploadUserFile[]>([])
+const emit = defineEmits<{
+ (e: 'uploaded', v: void)
+}>()
+
+const uploadData: UploadData = reactive({
+ type: UploadType.Image,
+ title: '',
+ introduction: '',
+ accountId: accountId!
+})
+
+/** 涓婁紶鍓嶆鏌� */
+const onBeforeUpload = props.type === UploadType.Image ? beforeImageUpload : beforeVoiceUpload
+
+/** 涓婁紶鎴愬姛澶勭悊 */
+const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
+ if (res.code !== 0) {
+ message.alertError('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 娓呯┖涓婁紶鏃剁殑鍚勭鏁版嵁
+ fileList.value = []
+ uploadData.title = ''
+ uploadData.introduction = ''
+
+ message.notifySuccess('涓婁紶鎴愬姛')
+ emit('uploaded')
+}
+
+/** 涓婁紶澶辫触澶勭悊 */
+const onUploadError = (err: Error) => message.error('涓婁紶澶辫触: ' + err.message)
+</script>
+
+<style lang="scss" scoped>
+.el-upload__tip {
+ margin-left: 5px;
+}
+</style>
diff --git a/src/views/mp/material/components/UploadVideo.vue b/src/views/mp/material/components/UploadVideo.vue
new file mode 100644
index 0000000..22347fd
--- /dev/null
+++ b/src/views/mp/material/components/UploadVideo.vue
@@ -0,0 +1,129 @@
+<template>
+ <el-dialog title="鏂板缓瑙嗛" v-model="showDialog" width="600px">
+ <el-upload
+ :action="UPLOAD_URL"
+ :headers="HEADERS"
+ multiple
+ :limit="1"
+ :file-list="fileList"
+ :data="uploadData"
+ :before-upload="beforeVideoUpload"
+ :on-error="onUploadError"
+ :on-success="onUploadSuccess"
+ ref="uploadVideoRef"
+ :auto-upload="false"
+ class="mb-5"
+ >
+ <template #trigger>
+ <el-button type="primary" plain>閫夋嫨瑙嗛</el-button>
+ </template>
+ <template #tip>
+ <span class="el-upload__tip" style="margin-left: 10px"
+ >鏍煎紡鏀寔 MP4锛屾枃浠跺ぇ灏忎笉瓒呰繃 10MB</span
+ >
+ </template>
+ </el-upload>
+ <el-divider />
+ <el-form :model="uploadData" :rules="uploadRules" ref="uploadFormRef">
+ <el-form-item label="鏍囬" prop="title">
+ <el-input
+ v-model="uploadData.title"
+ placeholder="鏍囬灏嗗睍绀哄湪鐩稿叧鎾斁椤甸潰锛屽缓璁~鍐欐竻鏅般�佸噯纭�佺敓鍔ㄧ殑鏍囬"
+ />
+ </el-form-item>
+ <el-form-item label="鎻忚堪" prop="introduction">
+ <el-input
+ :rows="3"
+ type="textarea"
+ v-model="uploadData.introduction"
+ placeholder="浠嬬粛璇皢灞曠ず鍦ㄧ浉鍏虫挱鏀鹃〉闈紝寤鸿濉啓绠�娲佹槑纭�佹湁淇℃伅閲忕殑鍐呭"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="showDialog = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitVideo">鎻� 浜�</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import type {
+ FormInstance,
+ FormRules,
+ UploadInstance,
+ UploadProps,
+ UploadUserFile
+} from 'element-plus'
+import { HEADERS, UploadData, UPLOAD_URL, UploadType, beforeVideoUpload } from './upload'
+
+const message = useMessage()
+
+const accountId = inject<number>('accountId')
+
+const uploadRules: FormRules = {
+ title: [{ required: true, message: '璇疯緭鍏ユ爣棰�', trigger: 'blur' }],
+ introduction: [{ required: true, message: '璇疯緭鍏ユ弿杩�', trigger: 'blur' }]
+}
+
+const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false
+ }
+})
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: boolean)
+ (e: 'uploaded', v: void)
+}>()
+
+const showDialog = computed<boolean>({
+ get() {
+ return props.modelValue
+ },
+ set(val) {
+ emit('update:modelValue', val)
+ }
+})
+
+const fileList = ref<UploadUserFile[]>([])
+
+const uploadData: UploadData = reactive({
+ type: UploadType.Video,
+ title: '',
+ introduction: '',
+ accountId: accountId!
+})
+
+const uploadFormRef = ref<FormInstance | null>(null)
+const uploadVideoRef = ref<UploadInstance | null>(null)
+
+const submitVideo = () => {
+ uploadFormRef.value?.validate((valid) => {
+ if (!valid) {
+ return
+ }
+ uploadVideoRef.value?.submit()
+ })
+}
+
+/** 涓婁紶鎴愬姛澶勭悊 */
+const onUploadSuccess: UploadProps['onSuccess'] = (res: any) => {
+ if (res.code !== 0) {
+ message.error('涓婁紶鍑洪敊锛�' + res.msg)
+ return false
+ }
+
+ // 娓呯┖涓婁紶鏃剁殑鍚勭鏁版嵁
+ fileList.value = []
+ uploadData.title = ''
+ uploadData.introduction = ''
+
+ showDialog.value = false
+ message.notifySuccess('涓婁紶鎴愬姛')
+ emit('uploaded')
+}
+
+/** 涓婁紶澶辫触澶勭悊 */
+const onUploadError = (err: Error) => message.error(`涓婁紶澶辫触: ${err.message}`)
+</script>
diff --git a/src/views/mp/material/components/VideoTable.vue b/src/views/mp/material/components/VideoTable.vue
new file mode 100644
index 0000000..cbaa902
--- /dev/null
+++ b/src/views/mp/material/components/VideoTable.vue
@@ -0,0 +1,59 @@
+<template>
+ <el-table :data="props.list" stripe border v-loading="props.loading" style="margin-top: 10px">
+ <el-table-column label="缂栧彿" align="center" prop="mediaId" />
+ <el-table-column label="鏂囦欢鍚�" align="center" prop="name" />
+ <el-table-column label="鏍囬" align="center" prop="title" />
+ <el-table-column label="浠嬬粛" align="center" prop="introduction" />
+ <el-table-column label="瑙嗛" align="center">
+ <template #default="scope">
+ <WxVideoPlayer v-if="scope.row.url" :url="scope.row.url" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓婁紶鏃堕棿"
+ align="center"
+ :formatter="dateFormatter"
+ prop="createTime"
+ width="180"
+ >
+ <template #default="scope">
+ <span>{{ scope.row.createTime }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" link @click="handleDownload(scope.row.url)">
+ <Icon icon="ep:download" />涓嬭浇
+ </el-button>
+ <el-button
+ type="primary"
+ link
+ @click="emit('delete', scope.row.id)"
+ v-hasPermi="['mp:material:delete']"
+ >
+ <Icon icon="ep:delete" />鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+
+<script lang="ts" setup>
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import { dateFormatter } from '@/utils/formatTime'
+
+const props = defineProps<{
+ list: any[]
+ loading: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'delete', v: number)
+ (e: 'download', v: string)
+}>()
+
+// 涓嬭浇鏂囦欢
+const handleDownload = (url: string) => {
+ window.open(url, '_blank')
+}
+</script>
diff --git a/src/views/mp/material/components/VoiceTable.vue b/src/views/mp/material/components/VoiceTable.vue
new file mode 100644
index 0000000..76fab7a
--- /dev/null
+++ b/src/views/mp/material/components/VoiceTable.vue
@@ -0,0 +1,51 @@
+<template>
+ <el-table :data="props.list" stripe border v-loading="props.loading" style="margin-top: 10px">
+ <el-table-column label="缂栧彿" align="center" prop="mediaId" />
+ <el-table-column label="鏂囦欢鍚�" align="center" prop="name" />
+ <el-table-column label="璇煶" align="center">
+ <template #default="scope">
+ <WxVoicePlayer v-if="scope.row.url" :url="scope.row.url" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="涓婁紶鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ >
+ <template #default="scope">
+ <span>{{ scope.row.createTime }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button type="primary" link @click="emit('delete', scope.row.id)">
+ <Icon icon="ep:download" />涓嬭浇
+ </el-button>
+ <el-button
+ type="primary"
+ link
+ @click="emit('delete', scope.row.id)"
+ v-hasPermi="['mp:material:delete']"
+ >
+ <Icon icon="ep:delete" />鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+</template>
+
+<script lang="ts" setup>
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import { dateFormatter } from '@/utils/formatTime'
+
+const props = defineProps<{
+ list: any[]
+ loading: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'delete', v: number)
+}>()
+</script>
diff --git a/src/views/mp/material/components/upload.ts b/src/views/mp/material/components/upload.ts
new file mode 100644
index 0000000..724d545
--- /dev/null
+++ b/src/views/mp/material/components/upload.ts
@@ -0,0 +1,32 @@
+import type { UploadProps, UploadRawFile } from 'element-plus'
+import { getRefreshToken } from '@/utils/auth'
+import { UploadType, useBeforeUpload } from '@/views/mp/hooks/useUpload'
+
+const HEADERS = { Authorization: 'Bearer ' + getRefreshToken() } // 璇锋眰澶达紙瑙e喅 el-upload 涓婁紶杩囩▼涓紝鏃犳硶鍒锋柊浠ょ墝鐨勯棶棰橈級
+const UPLOAD_URL = import.meta.env.VITE_BASE_URL + '/admin-api/mp/material/upload-permanent' // 涓婁紶鍦板潃
+
+interface UploadData {
+ type: UploadType
+ title: string
+ introduction: string
+ accountId: number
+}
+
+const beforeImageUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+ useBeforeUpload(UploadType.Image, 2)(rawFile)
+
+const beforeVoiceUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+ useBeforeUpload(UploadType.Voice, 2)(rawFile)
+
+const beforeVideoUpload: UploadProps['beforeUpload'] = (rawFile: UploadRawFile) =>
+ useBeforeUpload(UploadType.Video, 10)(rawFile)
+
+export {
+ HEADERS,
+ UPLOAD_URL,
+ UploadType,
+ UploadData,
+ beforeImageUpload,
+ beforeVoiceUpload,
+ beforeVideoUpload
+}
diff --git a/src/views/mp/material/index.vue b/src/views/mp/material/index.vue
new file mode 100644
index 0000000..de06042
--- /dev/null
+++ b/src/views/mp/material/index.vue
@@ -0,0 +1,159 @@
+<template>
+ <doc-alert title="鍏紬鍙风礌鏉�" url="https://doc.iocoder.cn/mp/material/" />
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form class="-mb-15px" :inline="true" label-width="68px">
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <el-tabs v-model="type" @tab-change="onTabChange">
+ <!-- tab 1锛氬浘鐗� -->
+ <el-tab-pane :name="UploadType.Image">
+ <template #label>
+ <el-row align="middle"> <Icon icon="ep:picture" />鍥剧墖 </el-row>
+ </template>
+ <UploadFile
+ v-hasPermi="['mp:material:upload-permanent']"
+ :type="UploadType.Image"
+ @uploaded="getList"
+ >
+ 鏀寔 bmp/png/jpeg/jpg/gif 鏍煎紡锛屽ぇ灏忎笉瓒呰繃 2M
+ </UploadFile>
+ <!-- 鍒楄〃 -->
+ <ImageTable :loading="loading" :list="list" @delete="handleDelete" />
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-tab-pane>
+
+ <!-- tab 2锛氳闊� -->
+ <el-tab-pane :name="UploadType.Voice">
+ <template #label>
+ <el-row align="middle"> <Icon icon="ep:microphone" />璇煶 </el-row>
+ </template>
+ <UploadFile
+ v-hasPermi="['mp:material:upload-permanent']"
+ :type="UploadType.Voice"
+ @uploaded="getList"
+ >
+ 鏍煎紡鏀寔 mp3/wma/wav/amr锛屾枃浠跺ぇ灏忎笉瓒呰繃 2M锛屾挱鏀鹃暱搴︿笉瓒呰繃 60s
+ </UploadFile>
+ <!-- 鍒楄〃 -->
+ <VoiceTable :list="list" :loading="loading" @delete="handleDelete" />
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-tab-pane>
+
+ <!-- tab 3锛氳棰� -->
+ <el-tab-pane :name="UploadType.Video">
+ <template #label>
+ <el-row align="middle"> <Icon icon="ep:video-play" /> 瑙嗛 </el-row>
+ </template>
+ <el-button
+ v-hasPermi="['mp:material:upload-permanent']"
+ type="primary"
+ plain
+ @click="showCreateVideo = true"
+ >鏂板缓瑙嗛</el-button
+ >
+ <!-- 鏂板缓瑙嗛鐨勫脊绐� -->
+ <UploadVideo v-model="showCreateVideo" />
+ <!-- 鍒楄〃 -->
+ <VideoTable :list="list" :loading="loading" @delete="handleDelete" />
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-tab-pane>
+ </el-tabs>
+ </ContentWrap>
+</template>
+<script lang="ts" setup name="MpMaterial">
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import ImageTable from './components/ImageTable.vue'
+import VoiceTable from './components/VoiceTable.vue'
+import VideoTable from './components/VideoTable.vue'
+import UploadFile from './components/UploadFile.vue'
+import UploadVideo from './components/UploadVideo.vue'
+import { UploadType } from './components/upload'
+import * as MpMaterialApi from '@/api/mp/material'
+const message = useMessage() // 娑堟伅
+
+const type = ref<UploadType>(UploadType.Image) // 绱犳潗绫诲瀷
+const loading = ref(false) // 閬僵灞�
+const list = ref<any[]>([]) // 鎬绘潯鏁�
+const total = ref(0) // 鏁版嵁鍒楄〃
+
+const accountId = ref(-1)
+provide('accountId', accountId)
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: accountId,
+ permanent: true
+})
+const showCreateVideo = ref(false) // 鏄惁鏂板缓瑙嗛鐨勫脊绐�
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number) => {
+ accountId.value = id
+ queryParams.accountId = id
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MpMaterialApi.getMaterialPage({
+ ...queryParams,
+ type: type.value
+ })
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 澶勭悊 table 鍒囨崲 */
+const onTabChange = () => {
+ // 鎻愬墠鎯呭喌鏁版嵁锛岄伩鍏� tab 鍒囨崲鍚庢樉绀哄瀮鍦炬暟鎹�
+ list.value = []
+ total.value = 0
+ // 浠庣涓�椤靛紑濮嬫煡璇�
+ handleQuery()
+}
+
+/** 澶勭悊鍒犻櫎鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ await message.confirm('姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ユ枃浠�, 鏄惁缁х画?')
+ await MpMaterialApi.deletePermanentMaterial(id)
+ message.alertSuccess('鍒犻櫎鎴愬姛')
+}
+</script>
diff --git a/src/views/mp/menu/assets/iphone_backImg.png b/src/views/mp/menu/assets/iphone_backImg.png
new file mode 100644
index 0000000..bb09591
--- /dev/null
+++ b/src/views/mp/menu/assets/iphone_backImg.png
Binary files differ
diff --git a/src/views/mp/menu/assets/menu_foot.png b/src/views/mp/menu/assets/menu_foot.png
new file mode 100644
index 0000000..4a89d4b
--- /dev/null
+++ b/src/views/mp/menu/assets/menu_foot.png
Binary files differ
diff --git a/src/views/mp/menu/assets/menu_head.png b/src/views/mp/menu/assets/menu_head.png
new file mode 100644
index 0000000..248cfb7
--- /dev/null
+++ b/src/views/mp/menu/assets/menu_head.png
Binary files differ
diff --git a/src/views/mp/menu/components/MenuEditor.vue b/src/views/mp/menu/components/MenuEditor.vue
new file mode 100644
index 0000000..5df1785
--- /dev/null
+++ b/src/views/mp/menu/components/MenuEditor.vue
@@ -0,0 +1,244 @@
+<template>
+ <div>
+ <div class="configure_page">
+ <div class="delete_btn">
+ <el-button type="danger" @click="emit('delete')">
+ <Icon icon="ep:delete" />
+ 鍒犻櫎褰撳墠鑿滃崟
+ </el-button>
+ </div>
+ <div>
+ <span>鑿滃崟鍚嶇О锛�</span>
+ <el-input
+ class="input_width"
+ v-model="menu.name"
+ placeholder="璇疯緭鍏ヨ彍鍗曞悕绉�"
+ :maxlength="isParent ? 4 : 7"
+ clearable
+ />
+ </div>
+ <div v-if="isLeave">
+ <div class="menu_content">
+ <span>鑿滃崟鏍囪瘑锛�</span>
+ <el-input
+ class="input_width"
+ v-model="menu.menuKey"
+ placeholder="璇疯緭鍏ヨ彍鍗� KEY"
+ clearable
+ />
+ </div>
+ <div class="menu_content">
+ <span>鑿滃崟鍐呭锛�</span>
+ <el-select v-model="menu.type" clearable placeholder="璇烽�夋嫨" class="menu_option">
+ <el-option
+ v-for="item in menuOptions"
+ :label="item.label"
+ :value="item.value"
+ :key="item.value"
+ />
+ </el-select>
+ </div>
+ <div class="configur_content" v-if="menu.type === 'view'">
+ <span>璺宠浆閾炬帴锛�</span>
+ <el-input class="input_width" v-model="menu.url" placeholder="璇疯緭鍏ラ摼鎺�" clearable />
+ </div>
+ <div class="configur_content" v-if="menu.type === 'miniprogram'">
+ <div class="applet">
+ <span>灏忕▼搴忕殑 appid 锛�</span>
+ <el-input
+ class="input_width"
+ v-model="menu.miniProgramAppId"
+ placeholder="璇疯緭鍏ュ皬绋嬪簭鐨刟ppid"
+ clearable
+ />
+ </div>
+ <div class="applet">
+ <span>灏忕▼搴忕殑椤甸潰璺緞锛�</span>
+ <el-input
+ class="input_width"
+ v-model="menu.miniProgramPagePath"
+ placeholder="璇疯緭鍏ュ皬绋嬪簭鐨勯〉闈㈣矾寰勶紝濡傦細pages/index"
+ clearable
+ />
+ </div>
+ <div class="applet">
+ <span>灏忕▼搴忕殑澶囩敤缃戦〉锛�</span>
+ <el-input
+ class="input_width"
+ v-model="menu.url"
+ placeholder="涓嶆敮鎸佸皬绋嬪簭鐨勮�佺増鏈鎴风灏嗘墦寮�鏈綉椤�"
+ clearable
+ />
+ </div>
+ <p class="blue">tips:闇�瑕佸拰鍏紬鍙疯繘琛屽叧鑱旀墠鍙互鎶婂皬绋嬪簭缁戝畾甯﹀井淇¤彍鍗曚笂鍝燂紒</p>
+ </div>
+ <div class="configur_content" v-if="menu.type === 'article_view_limited'">
+ <el-row>
+ <div class="select-item" v-if="menu && menu.replyArticles">
+ <WxNews :articles="menu.replyArticles" />
+ <el-row class="ope-row" justify="center" align="middle">
+ <el-button type="danger" circle @click="deleteMaterial">
+ <icon icon="ep:delete" />
+ </el-button>
+ </el-row>
+ </div>
+ <div v-else>
+ <el-row justify="center">
+ <el-col :span="24" style="text-align: center">
+ <el-button type="success" @click="showNewsDialog = true">
+ 绱犳潗搴撻�夋嫨
+ <Icon icon="ep:circle-check" />
+ </el-button>
+ </el-col>
+ </el-row>
+ </div>
+ <el-dialog title="閫夋嫨鍥炬枃" v-model="showNewsDialog" width="80%" destroy-on-close>
+ <WxMaterialSelect
+ type="news"
+ :account-id="props.accountId"
+ @select-material="selectMaterial"
+ />
+ </el-dialog>
+ </el-row>
+ </div>
+ <div
+ class="configur_content"
+ v-if="menu.type === 'click' || menu.type === 'scancode_waitmsg'"
+ >
+ <WxReplySelect v-if="hackResetWxReplySelect" v-model="menu.reply" />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxReplySelect from '@/views/mp/components/wx-reply'
+import WxNews from '@/views/mp/components/wx-news'
+import WxMaterialSelect from '@/views/mp/components/wx-material-select'
+import menuOptions from './menuOptions'
+
+const message = useMessage()
+
+const props = defineProps<{
+ accountId: number
+ modelValue: any
+ isParent: boolean
+}>()
+
+const emit = defineEmits<{
+ (e: 'delete', v: void)
+ (e: 'update:modelValue', v: any)
+}>()
+
+const menu = computed({
+ get() {
+ return props.modelValue
+ },
+ set(val) {
+ emit('update:modelValue', val)
+ }
+})
+const showNewsDialog = ref(false)
+const hackResetWxReplySelect = ref(false)
+const isLeave = computed<boolean>(() => !(menu.value.children?.length > 0))
+
+watch(menu, () => {
+ hackResetWxReplySelect.value = false // 閿�姣佺粍浠�
+ nextTick(() => {
+ hackResetWxReplySelect.value = true // 閲嶅缓缁勪欢
+ })
+})
+
+// ======================== 鑿滃崟缂栬緫锛堢礌鏉愰�夋嫨锛� ========================
+const selectMaterial = (item: any) => {
+ const articleId = item.articleId
+ const articles = item.content.newsItem
+ // 鎻愮ず锛岄拡瀵瑰鍥炬枃
+ if (articles.length > 1) {
+ message.alertWarning('鎮ㄩ�夋嫨鐨勬槸澶氬浘鏂囷紝灏嗛粯璁よ烦杞涓�绡�')
+ }
+ showNewsDialog.value = false
+
+ // 璁剧疆鑿滃崟鐨勫洖澶�
+ menu.value.articleId = articleId
+ menu.value.replyArticles = []
+ articles.forEach((article) => {
+ menu.value.replyArticles.push({
+ title: article.title,
+ description: article.digest,
+ picUrl: article.picUrl,
+ url: article.url
+ })
+ })
+}
+
+const deleteMaterial = () => {
+ delete menu.value['articleId']
+ delete menu.value['replyArticles']
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input {
+ width: 70%;
+ margin-right: 2%;
+}
+
+.configure_page {
+ .delete_btn {
+ margin-bottom: 15px;
+ text-align: right;
+ }
+
+ .menu_content {
+ margin-top: 20px;
+ }
+
+ .configur_content {
+ padding: 20px 10px;
+ margin-top: 20px;
+ background-color: #fff;
+ border-radius: 5px;
+
+ .select-item {
+ width: 280px;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+
+ .ope-row {
+ padding-top: 10px;
+ text-align: center;
+ }
+ }
+ }
+
+ .blue {
+ margin-top: 10px;
+ color: #29b6f6;
+ }
+
+ .applet {
+ margin-bottom: 20px;
+
+ span {
+ width: 20%;
+ }
+ }
+
+ .input_width {
+ width: 40%;
+ }
+
+ .material {
+ .input_width {
+ width: 30%;
+ }
+
+ .el-textarea {
+ width: 80%;
+ }
+ }
+}
+</style>
diff --git a/src/views/mp/menu/components/MenuPreviewer.vue b/src/views/mp/menu/components/MenuPreviewer.vue
new file mode 100644
index 0000000..77b024a
--- /dev/null
+++ b/src/views/mp/menu/components/MenuPreviewer.vue
@@ -0,0 +1,226 @@
+<template>
+ <draggable
+ v-model="menuList"
+ item-key="id"
+ ghost-class="draggable-ghost"
+ :animation="400"
+ @end="onParentDragEnd"
+ >
+ <template #item="{ element: parent, index: x }">
+ <div class="menu_bottom">
+ <!-- 涓�绾ц彍鍗� -->
+ <div
+ @click="menuClicked(parent, x)"
+ class="menu_item"
+ :class="{ active: props.activeIndex === `${x}` }"
+ >
+ <Icon icon="ep:fold" color="black" />{{ parent.name }}
+ </div>
+ <!-- 浠ヤ笅涓轰簩绾ц彍鍗�-->
+ <div class="submenu" v-if="props.parentIndex === x && parent.children">
+ <draggable
+ v-model="parent.children"
+ item-key="id"
+ ghost-class="draggable-ghost"
+ :animation="400"
+ @end="onChildDragEnd"
+ >
+ <template #item="{ element: child, index: y }">
+ <div class="menu_bottom subtitle">
+ <div
+ class="menu_subItem"
+ v-if="parent.children"
+ :class="{ active: props.activeIndex === `${x}-${y}` }"
+ @click="subMenuClicked(child, x, y)"
+ >
+ {{ child.name }}
+ </div>
+ </div>
+ </template>
+ </draggable>
+ <!-- 浜岀骇鑿滃崟鍔犲彿锛� 褰撻暱搴� 灏忎簬 5 鎵嶆樉绀轰簩绾ц彍鍗曠殑鍔犲彿 -->
+ <div
+ class="menu_bottom menu_addicon"
+ v-if="!parent.children || parent.children.length < 5"
+ @click="addSubMenu(x, parent)"
+ >
+ <Icon icon="ep:plus" class="plus" />
+ </div>
+ </div>
+ </div>
+ </template>
+ </draggable>
+
+ <!-- 涓�绾ц彍鍗曞姞鍙� -->
+ <div class="menu_bottom menu_addicon" v-if="menuList.length < 3" @click="addMenu">
+ <Icon icon="ep:plus" class="plus" />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { Menu } from './types'
+import draggable from 'vuedraggable'
+
+const props = defineProps<{
+ modelValue: Menu[]
+ activeIndex: string
+ parentIndex: number
+ accountId: number
+}>()
+
+const emit = defineEmits<{
+ (e: 'update:modelValue', v: Menu[])
+ (e: 'menu-clicked', parent: Menu, x: number)
+ (e: 'submenu-clicked', child: Menu, x: number, y: number)
+}>()
+
+const menuList = computed<Menu[]>({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+// 娣诲姞妯悜涓�绾ц彍鍗�
+const addMenu = () => {
+ const index = menuList.value.length
+ const menu = {
+ name: '鑿滃崟鍚嶇О',
+ children: [],
+ reply: {
+ // 鐢ㄤ簬瀛樺偍鍥炲鍐呭
+ type: 'text',
+ accountId: props.accountId // 淇濊瘉缁勪欢閲岋紝鍙互浣跨敤鍒板搴旂殑鍏紬鍙�
+ }
+ }
+ menuList.value[index] = menu
+ menuClicked(menu, index - 1)
+}
+
+// 娣诲姞妯悜浜岀骇鑿滃崟锛沺arent 琛ㄧず瑕佹搷浣滅殑鐖惰彍鍗�
+const addSubMenu = (i: number, parent: any) => {
+ const subMenuKeyLength = parent.children.length // 鑾峰彇浜岀骇鑿滃崟key闀垮害
+ const addButton = {
+ name: '瀛愯彍鍗曞悕绉�',
+ reply: {
+ // 鐢ㄤ簬瀛樺偍鍥炲鍐呭
+ type: 'text',
+ accountId: props.accountId // 淇濊瘉缁勪欢閲岋紝鍙互浣跨敤鍒板搴旂殑鍏紬鍙�
+ }
+ }
+ parent.children[subMenuKeyLength] = addButton
+ subMenuClicked(parent.children[subMenuKeyLength], i, subMenuKeyLength)
+}
+
+const menuClicked = (parent: Menu, x: number) => {
+ emit('menu-clicked', parent, x)
+}
+
+const subMenuClicked = (child: Menu, x: number, y: number) => {
+ emit('submenu-clicked', child, x, y)
+}
+
+/**
+ * 澶勭悊涓�绾ц彍鍗曞睍寮�鍚庤鎷栧姩锛屾縺娲�(灞曞紑)鍘熸潵娲诲姩鐨勪竴绾ц彍鍗�
+ *
+ * @param oldIndex: 涓�绾ц彍鍗曟嫋鍔ㄥ墠鐨勪綅缃�
+ * @param newIndex: 涓�绾ц彍鍗曟嫋鍔ㄥ悗鐨勪綅缃�
+ */
+const onParentDragEnd = ({ oldIndex, newIndex }) => {
+ // 浜岀骇鑿滃崟娌℃湁灞曞紑锛岀洿鎺ヨ繑鍥�
+ if (props.activeIndex === '__MENU_NOT_SELECTED__') {
+ return
+ }
+
+ // 浣跨敤涓�涓緟鍔╂暟缁勬潵妯℃嫙鑿滃崟绉诲姩锛岀劧鍚庢壘鍒板睍寮�鐨勪簩绾ц彍鍗曠殑鏂颁笅鏍嘸newParent`
+ const positions = new Array<boolean>(menuList.value.length).fill(false)
+ positions[props.parentIndex] = true
+ const [out] = positions.splice(oldIndex, 1) // 绉诲嚭鑿滃崟锛屼繚瀛樺埌鍙橀噺out
+ positions.splice(newIndex, 0, out) // 鎶妎ut鍙橀噺鎻掑叆琚Щ鍑虹殑鑿滃崟
+ const newParentIndex = positions.indexOf(true)
+
+ // 鎵惧埌鑿滃崟鍏冪礌锛岃Е鍙戜竴绾ц彍鍗曠偣鍑�
+ const parent = menuList.value[newParentIndex]
+ emit('menu-clicked', parent, newParentIndex)
+}
+
+/**
+ * 澶勭悊浜岀骇鑿滃崟灞曞紑鍚庤鎷栧姩锛屾縺娲昏鎷栧姩鐨勮彍鍗�
+ *
+ * @param newIndex 浜岀骇鑿滃崟鎷栧姩鍚庣殑浣嶇疆
+ */
+const onChildDragEnd = ({ newIndex }) => {
+ const x = props.parentIndex
+ const y = newIndex
+ const children = menuList.value[x]?.children
+ if (children && children?.length > 0) {
+ const child = children[y]
+ emit('submenu-clicked', child, x, y)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.menu_bottom {
+ position: relative;
+ display: block;
+ float: left;
+ width: 85.5px;
+ text-align: center;
+ cursor: pointer;
+ background-color: #fff;
+ border: 1px solid #ebedee;
+ box-sizing: border-box;
+
+ &.menu_addicon {
+ height: 46px;
+ line-height: 46px;
+
+ .plus {
+ color: #2bb673;
+ }
+ }
+
+ .menu_item {
+ display: flex;
+ width: 100%;
+ height: 44px;
+ line-height: 44px;
+ // text-align: center;
+ box-sizing: border-box;
+ align-items: center;
+ justify-content: center;
+
+ &.active {
+ border: 1px solid #2bb673;
+ }
+ }
+
+ .menu_subItem {
+ height: 44px;
+ line-height: 44px;
+ text-align: center;
+ box-sizing: border-box;
+
+ &.active {
+ border: 1px solid #2bb673;
+ }
+ }
+}
+
+/* 绗簩绾ц彍鍗� */
+.submenu {
+ position: absolute;
+ bottom: 45px;
+ width: 85.5px;
+
+ .subtitle {
+ background-color: #fff;
+ box-sizing: border-box;
+ }
+}
+
+.draggable-ghost {
+ background: #f7fafc;
+ border: 1px solid #4299e1;
+ opacity: 0.5;
+}
+</style>
diff --git a/src/views/mp/menu/components/menuOptions.ts b/src/views/mp/menu/components/menuOptions.ts
new file mode 100644
index 0000000..d86dd78
--- /dev/null
+++ b/src/views/mp/menu/components/menuOptions.ts
@@ -0,0 +1,42 @@
+export default [
+ {
+ value: 'view',
+ label: '璺宠浆缃戦〉'
+ },
+ {
+ value: 'miniprogram',
+ label: '璺宠浆灏忕▼搴�'
+ },
+ {
+ value: 'click',
+ label: '鐐瑰嚮鍥炲'
+ },
+ {
+ value: 'article_view_limited',
+ label: '璺宠浆鍥炬枃娑堟伅'
+ },
+ {
+ value: 'scancode_push',
+ label: '鎵爜鐩存帴杩斿洖缁撴灉'
+ },
+ {
+ value: 'scancode_waitmsg',
+ label: '鎵爜鍥炲'
+ },
+ {
+ value: 'pic_sysphoto',
+ label: '绯荤粺鎷嶇収鍙戝浘'
+ },
+ {
+ value: 'pic_photo_or_album',
+ label: '鎷嶇収鎴栬�呯浉鍐�'
+ },
+ {
+ value: 'pic_weixin',
+ label: '寰俊鐩稿唽'
+ },
+ {
+ value: 'location_select',
+ label: '閫夋嫨鍦扮悊浣嶇疆'
+ }
+]
diff --git a/src/views/mp/menu/components/types.ts b/src/views/mp/menu/components/types.ts
new file mode 100644
index 0000000..b9f7659
--- /dev/null
+++ b/src/views/mp/menu/components/types.ts
@@ -0,0 +1,73 @@
+export interface Replay {
+ title: string
+ description: string
+ picUrl: string
+ url: string
+}
+
+export type MenuType =
+ | ''
+ | 'click'
+ | 'view'
+ | 'scancode_waitmsg'
+ | 'scancode_push'
+ | 'pic_sysphoto'
+ | 'pic_photo_or_album'
+ | 'pic_weixin'
+ | 'location_select'
+ | 'article_view_limited'
+
+interface _RawMenu {
+ // db
+ id: number
+ parentId: number
+ accountId: number
+ appId: string
+ createTime: number
+
+ // mp-native
+ name: string
+ menuKey: string
+ type: MenuType
+ url: string
+ miniProgramAppId: string
+ miniProgramPagePath: string
+ articleId: string
+ replyMessageType: string
+ replyContent: string
+ replyMediaId: string
+ replyMediaUrl: string
+ replyThumbMediaId: string
+ replyThumbMediaUrl: string
+ replyTitle: string
+ replyDescription: string
+ replyArticles: Replay
+ replyMusicUrl: string
+ replyHqMusicUrl: string
+}
+
+export type RawMenu = Partial<_RawMenu>
+
+interface _Reply {
+ type: string
+ accountId: number
+ content: string
+ mediaId: string
+ url: string
+ thumbMediaId: string
+ thumbMediaUrl: string
+ title: string
+ description: string
+ articles: null | Replay[]
+ musicUrl: string
+ hqMusicUrl: string
+}
+
+export type Reply = Partial<_Reply>
+
+interface _Menu extends RawMenu {
+ children: _Menu[]
+ reply: Reply
+}
+
+export type Menu = Partial<_Menu>
diff --git a/src/views/mp/menu/index.vue b/src/views/mp/menu/index.vue
new file mode 100644
index 0000000..b86286a
--- /dev/null
+++ b/src/views/mp/menu/index.vue
@@ -0,0 +1,403 @@
+<template>
+ <doc-alert title="鍏紬鍙疯彍鍗�" url="https://doc.iocoder.cn/mp/menu/" />
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form class="-mb-15px" ref="queryFormRef" :inline="true" label-width="68px">
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <ContentWrap>
+ <div class="clearfix public-account-management" v-loading="loading">
+ <!--宸﹁竟閰嶇疆鑿滃崟-->
+ <div class="left">
+ <div class="weixin-hd">
+ <div class="weixin-title">{{ accountName }}</div>
+ </div>
+ <div class="clearfix weixin-menu">
+ <MenuPreviewer
+ v-model="menuList"
+ :account-id="accountId"
+ :active-index="activeIndex"
+ :parent-index="parentIndex"
+ @menu-clicked="(parent, x) => menuClicked(parent, x)"
+ @submenu-clicked="(child, x, y) => subMenuClicked(child, x, y)"
+ />
+ </div>
+ <div class="save_div">
+ <el-button class="save_btn" type="success" @click="onSave" v-hasPermi="['mp:menu:save']"
+ >淇濆瓨骞跺彂甯冭彍鍗�</el-button
+ >
+ <el-button class="save_btn" type="danger" @click="onClear" v-hasPermi="['mp:menu:delete']"
+ >娓呯┖鑿滃崟</el-button
+ >
+ </div>
+ </div>
+ <!--鍙宠竟閰嶇疆-->
+ <div class="right" v-if="showRightPanel">
+ <MenuEditor
+ :account-id="accountId"
+ :is-parent="isParent"
+ v-model="activeMenu"
+ @delete="onDeleteMenu"
+ />
+ </div>
+ <!-- 涓�杩涢〉闈㈠氨鏄剧ず鐨勯粯璁ら〉闈紝褰撶偣鍑诲乏杈规寜閽殑鏃跺�欙紝灏变笉鏄剧ず浜�-->
+ <div v-else class="right">
+ <p>璇烽�夋嫨鑿滃崟閰嶇疆</p>
+ </div>
+ </div>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import MenuEditor from './components/MenuEditor.vue'
+import MenuPreviewer from './components/MenuPreviewer.vue'
+import * as MpMenuApi from '@/api/mp/menu'
+import * as UtilsTree from '@/utils/tree'
+import { RawMenu, Menu } from './components/types'
+
+defineOptions({ name: 'MpMenu' })
+
+const message = useMessage() // 娑堟伅
+const MENU_NOT_SELECTED = '__MENU_NOT_SELECTED__'
+
+// ======================== 鍒楄〃鏌ヨ ========================
+const loading = ref(false) // 閬僵灞�
+const accountId = ref(-1)
+const accountName = ref<string>('')
+const menuList = ref<Menu[]>([])
+
+// ======================== 鑿滃崟鎿嶄綔 ========================
+// 褰撳墠閫変腑鑿滃崟缂栫爜锛�
+// * 涓�绾э紙'x'锛�
+// * 浜岀骇锛�'x-y'锛�
+// * 鏈�変腑锛圡ENU_NOT_SELECTED锛�
+const activeIndex = ref<string>(MENU_NOT_SELECTED)
+// 浜岀骇鑿滃崟鏄剧ず鏍囧織: 褰掑睘鐨勪竴绾ц彍鍗昳ndex
+// * 鏈垵濮嬪寲锛�-1
+// * 鍒濆鍖栵細x
+const parentIndex = ref(-1)
+
+// ======================== 鑿滃崟缂栬緫 ========================
+const showRightPanel = ref(false) // 鍙宠竟閰嶇疆鏄剧ず榛樿璇︽儏杩樻槸閰嶇疆璇︽儏
+const isParent = ref<boolean>(true) // 鏄惁涓�绾ц彍鍗曪紝鎺у埗MenuEditor涓璶ame瀛楁闀垮害
+const activeMenu = ref<Menu>({}) // 閫変腑鑿滃崟锛孧enuEditor鐨刴odelValue
+
+// 涓�浜涗复鏃跺�兼斁鍦ㄨ繖閲岃繘琛屽垽鏂紝濡傛灉鏀惧湪 activeMenu锛岀敱浜庡紩鐢ㄥ叧绯伙紝menu 涔熶細澶氫簡澶氫綑鐨勫弬鏁�
+enum Level {
+ Undefined = '0',
+ Parent = '1',
+ Child = '2'
+}
+const tempSelfObj = ref<{
+ grand: Level
+ x: number
+ y: number
+}>({
+ grand: Level.Undefined,
+ x: 0,
+ y: 0
+})
+const dialogNewsVisible = ref(false) // 璺宠浆鍥炬枃鏃剁殑绱犳潗閫夋嫨寮圭獥
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number, name: string) => {
+ accountId.value = id
+ accountName.value = name
+ getList()
+}
+
+/** 鏌ヨ骞惰浆鎹㈣彍鍗� **/
+const getList = async () => {
+ loading.value = false
+ try {
+ const data = await MpMenuApi.getMenuList(accountId.value)
+ const menuData = menuListToFrontend(data)
+ menuList.value = UtilsTree.handleTree(menuData, 'id')
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ resetForm()
+ getList()
+}
+
+// 灏嗗悗绔繑鍥炵殑 menuList锛岃浆鎹㈡垚鍓嶇鐨� menuList
+const menuListToFrontend = (list: any[]) => {
+ if (!list) return []
+
+ const result: RawMenu[] = []
+ list.forEach((item: RawMenu) => {
+ const menu: any = {
+ ...item
+ }
+ menu.reply = {
+ type: item.replyMessageType,
+ accountId: item.accountId,
+ content: item.replyContent,
+ mediaId: item.replyMediaId,
+ url: item.replyMediaUrl,
+ title: item.replyTitle,
+ description: item.replyDescription,
+ thumbMediaId: item.replyThumbMediaId,
+ thumbMediaUrl: item.replyThumbMediaUrl,
+ articles: item.replyArticles,
+ musicUrl: item.replyMusicUrl,
+ hqMusicUrl: item.replyHqMusicUrl
+ }
+ result.push(menu as RawMenu)
+ })
+ return result
+}
+
+// 閲嶇疆琛ㄥ崟锛屾竻绌鸿〃鍗曟暟鎹�
+const resetForm = () => {
+ // 鑿滃崟鎿嶄綔
+ activeIndex.value = MENU_NOT_SELECTED
+ parentIndex.value = -1
+
+ // 鑿滃崟缂栬緫
+ showRightPanel.value = false
+ activeMenu.value = {}
+ tempSelfObj.value = { grand: Level.Undefined, x: 0, y: 0 }
+ dialogNewsVisible.value = false
+}
+
+// ======================== 鑿滃崟鎿嶄綔 ========================
+// 涓�绾ц彍鍗曠偣鍑讳簨浠�
+const menuClicked = (parent: Menu, x: number) => {
+ // 鍙充晶鐨勮〃鍗曠浉鍏�
+ showRightPanel.value = true // 鍙宠竟鑿滃崟
+ activeMenu.value = parent // 杩欎釜濡傛灉鏀惧湪椤堕儴锛宖lag 浼氭病鏈夈�傚洜涓洪噸鏂拌祴鍊间簡銆�
+ tempSelfObj.value.grand = Level.Parent // 琛ㄧず涓�绾ц彍鍗�
+ tempSelfObj.value.x = x // 琛ㄧず涓�绾ц彍鍗曠储寮�
+ isParent.value = true
+
+ // 宸︿晶鐨勯�変腑
+ activeIndex.value = `${x}` // 鑿滃崟閫変腑鏍峰紡
+ parentIndex.value = x // 浜岀骇鑿滃崟鏄剧ず鏍囧織
+}
+
+// 浜岀骇鑿滃崟鐐瑰嚮浜嬩欢
+const subMenuClicked = (child: Menu, x: number, y: number) => {
+ // 鍙充晶鐨勮〃鍗曠浉鍏�
+ showRightPanel.value = true // 鍙宠竟鑿滃崟
+ activeMenu.value = child // 灏嗙偣鍑荤殑鏁版嵁鏀惧埌涓存椂鍙橀噺锛屽璞℃湁寮曠敤浣滅敤
+ tempSelfObj.value.grand = Level.Child // 琛ㄧず浜岀骇鑿滃崟
+ tempSelfObj.value.x = x // 琛ㄧず涓�绾ц彍鍗曠储寮�
+ tempSelfObj.value.y = y // 琛ㄧず浜岀骇鑿滃崟绱㈠紩
+ isParent.value = false
+
+ // 宸︿晶鐨勯�変腑
+ activeIndex.value = `${x}-${y}`
+}
+
+// 鍒犻櫎褰撳墠鑿滃崟
+const onDeleteMenu = async () => {
+ try {
+ await message.confirm('纭畾瑕佸垹闄ゅ悧?')
+ if (tempSelfObj.value.grand === Level.Parent) {
+ // 涓�绾ц彍鍗曠殑鍒犻櫎鏂规硶
+ menuList.value.splice(tempSelfObj.value.x, 1)
+ } else if (tempSelfObj.value.grand === Level.Child) {
+ // 浜岀骇鑿滃崟鐨勫垹闄ゆ柟娉�
+ menuList.value[tempSelfObj.value.x].children?.splice(tempSelfObj.value.y, 1)
+ }
+ // 鎻愮ず
+ message.notifySuccess('鍒犻櫎鎴愬姛')
+
+ // 澶勭悊鑿滃崟鐨勯�変腑
+ activeMenu.value = {}
+ showRightPanel.value = false
+ activeIndex.value = MENU_NOT_SELECTED
+ } catch {
+ //
+ }
+}
+
+// ======================== 鑿滃崟缂栬緫 ========================
+const onSave = async () => {
+ try {
+ await message.confirm('纭畾瑕佷繚瀛樺悧?')
+ loading.value = true
+ await MpMenuApi.saveMenu(accountId.value, menuListToBackend())
+ getList()
+ message.notifySuccess('鍙戝竷鎴愬姛')
+ } finally {
+ loading.value = false
+ }
+}
+
+const onClear = async () => {
+ try {
+ await message.confirm('纭畾瑕佸垹闄ゅ悧?')
+ loading.value = true
+ await MpMenuApi.deleteMenu(accountId.value)
+ handleQuery()
+ message.notifySuccess('娓呯┖鎴愬姛')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 灏嗗墠绔殑 menuList锛岃浆鎹㈡垚鍚庣鎺ユ敹鐨� menuList
+const menuListToBackend = () => {
+ const result: any[] = []
+ menuList.value.forEach((item) => {
+ const menu = menuToBackend(item)
+ result.push(menu)
+
+ // 澶勭悊瀛愯彍鍗�
+ if (!item.children || item.children.length <= 0) {
+ return
+ }
+ menu.children = []
+ item.children.forEach((subItem) => {
+ menu.children.push(menuToBackend(subItem))
+ })
+ })
+ return result
+}
+
+// 灏嗗墠绔殑 menu锛岃浆鎹㈡垚鍚庣鎺ユ敹鐨� menu
+// TODO: @鑺嬭壙锛岄渶瑕佹牴鎹悗鍙癆PI鍒犻櫎涓嶉渶瑕佺殑瀛楁
+const menuToBackend = (menu: any) => {
+ const result = {
+ ...menu,
+ children: undefined, // 涓嶅鐞嗗瓙鑺傜偣
+ reply: undefined // 绋嶅悗澶嶅埗
+ }
+ result.replyMessageType = menu.reply.type
+ result.replyContent = menu.reply.content
+ result.replyMediaId = menu.reply.mediaId
+ result.replyMediaUrl = menu.reply.url
+ result.replyTitle = menu.reply.title
+ result.replyDescription = menu.reply.description
+ result.replyThumbMediaId = menu.reply.thumbMediaId
+ result.replyThumbMediaUrl = menu.reply.thumbMediaUrl
+ result.replyArticles = menu.reply.articles
+ result.replyMusicUrl = menu.reply.musicUrl
+ result.replyHqMusicUrl = menu.reply.hqMusicUrl
+
+ return result
+}
+</script>
+
+<!--鏈粍浠舵牱寮�-->
+<style lang="scss" scoped="scoped">
+/* 鍏叡棰滆壊鍙橀噺 */
+.clearfix {
+ *zoom: 1;
+}
+
+.clearfix::after {
+ display: table;
+ clear: both;
+ content: '';
+}
+
+div {
+ text-align: left;
+}
+
+.weixin-hd {
+ position: relative;
+ bottom: 426px;
+ left: 0;
+ width: 300px;
+ height: 64px;
+ color: #fff;
+ text-align: center;
+ background: transparent url('./assets/menu_head.png') no-repeat 0 0;
+ background-position: 0 0;
+ background-size: 100%;
+}
+
+.weixin-title {
+ position: absolute;
+ top: 33px;
+ left: 0;
+ width: 100%;
+ font-size: 14px;
+ color: #fff;
+ text-align: center;
+}
+
+.weixin-menu {
+ padding-left: 43px;
+ font-size: 12px;
+ background: transparent url('./assets/menu_foot.png') no-repeat 0 0;
+}
+
+.public-account-management {
+ width: 1200px;
+ // min-width: 1200px;
+ margin: 0 auto;
+
+ .left {
+ position: relative;
+ display: block;
+ float: left;
+ width: 350px;
+ height: 715px;
+ padding: 518px 25px 88px;
+ background: url('./assets/iphone_backImg.png') no-repeat;
+ background-size: 100% auto;
+ box-sizing: border-box;
+
+ .save_div {
+ margin-top: 15px;
+ text-align: center;
+
+ .save_btn {
+ bottom: 20px;
+ left: 100px;
+ }
+ }
+ }
+
+ /* 鍙宠竟鑿滃崟鍐呭 */
+ .right {
+ float: left;
+ width: 63%;
+ padding: 20px;
+ margin-left: 20px;
+ background-color: #e8e7e7;
+ box-sizing: border-box;
+ }
+}
+</style>
+<!--绱犳潗鏍峰紡-->
+<style lang="scss" scoped>
+.pagination {
+ margin-right: 25px;
+ text-align: right;
+}
+
+.select-item {
+ width: 280px;
+ padding: 10px;
+ margin: 0 auto 10px;
+ border: 1px solid #eaeaea;
+}
+
+.ope-row {
+ padding-top: 10px;
+ text-align: center;
+}
+
+.item-name {
+ overflow: hidden;
+ font-size: 12px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+</style>
diff --git a/src/views/mp/message/MessageTable.vue b/src/views/mp/message/MessageTable.vue
new file mode 100644
index 0000000..185bdec
--- /dev/null
+++ b/src/views/mp/message/MessageTable.vue
@@ -0,0 +1,148 @@
+<template>
+ <div>
+ <el-table v-loading="props.loading" :data="props.list">
+ <el-table-column
+ label="鍙戦�佹椂闂�"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="娑堟伅绫诲瀷" align="center" prop="type" width="80" />
+ <el-table-column label="鍙戦�佹柟" align="center" prop="sendFrom" width="80">
+ <template #default="scope">
+ <el-tag v-if="scope.row.sendFrom === 1" type="success">绮変笣</el-tag>
+ <el-tag v-else type="info">鍏紬鍙�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛鏍囪瘑" align="center" prop="openid" width="300" />
+ <el-table-column label="鍐呭" prop="content">
+ <template #default="scope">
+ <!-- 銆愪簨浠躲�戝尯鍩� -->
+ <div v-if="scope.row.type === MsgType.Event && scope.row.event === 'subscribe'">
+ <el-tag type="success">鍏虫敞</el-tag>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'unsubscribe'">
+ <el-tag type="danger">鍙栨秷鍏虫敞</el-tag>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'CLICK'">
+ <el-tag>鐐瑰嚮鑿滃崟</el-tag>
+ 銆恵{ scope.row.eventKey }}銆�
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'VIEW'">
+ <el-tag>鐐瑰嚮鑿滃崟閾炬帴</el-tag>
+ 銆恵{ scope.row.eventKey }}銆�
+ </div>
+ <div
+ v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_waitmsg'"
+ >
+ <el-tag>鎵爜缁撴灉</el-tag>
+ 銆恵{ scope.row.eventKey }}銆�
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'scancode_push'">
+ <el-tag>鎵爜缁撴灉</el-tag>
+ 銆恵{ scope.row.eventKey }}銆�
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_sysphoto'">
+ <el-tag>绯荤粺鎷嶇収鍙戝浘</el-tag>
+ </div>
+ <div
+ v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_photo_or_album'"
+ >
+ <el-tag>鎷嶇収鎴栬�呯浉鍐�</el-tag>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'pic_weixin'">
+ <el-tag>寰俊鐩稿唽</el-tag>
+ </div>
+ <div
+ v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'location_select'"
+ >
+ <el-tag>閫夋嫨鍦扮悊浣嶇疆</el-tag>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event && scope.row.event === 'SCAN'">
+ <el-tag type="success">鎵爜</el-tag>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Event">
+ <el-tag type="danger">鏈煡浜嬩欢绫诲瀷</el-tag>
+ </div>
+ <!-- 銆愭秷鎭�戝尯鍩� -->
+ <div v-else-if="scope.row.type === MsgType.Text">{{ scope.row.content }}</div>
+ <div v-else-if="scope.row.type === MsgType.Voice">
+ <wx-voice-player :url="scope.row.mediaUrl" :content="scope.row.recognition" />
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Image">
+ <a target="_blank" :href="scope.row.mediaUrl">
+ <img :src="scope.row.mediaUrl" style="width: 100px" />
+ </a>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Video || scope.row.type === 'shortvideo'">
+ <wx-video-player :url="scope.row.mediaUrl" style="margin-top: 10px" />
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Link">
+ <el-tag>閾炬帴</el-tag>
+ 锛�
+ <a :href="scope.row.url" target="_blank">{{ scope.row.title }}</a>
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Location">
+ <WxLocation
+ :label="scope.row.label"
+ :location-y="scope.row.locationY"
+ :location-x="scope.row.locationX"
+ />
+ </div>
+ <div v-else-if="scope.row.type === MsgType.Music">
+ <WxMusic
+ :title="scope.row.title"
+ :description="scope.row.description"
+ :thumb-media-url="scope.row.thumbMediaUrl"
+ :music-url="scope.row.musicUrl"
+ :hq-music-url="scope.row.hqMusicUrl"
+ />
+ </div>
+ <div v-else-if="scope.row.type === MsgType.News">
+ <WxNews :articles="scope.row.articles" />
+ </div>
+ <div v-else>
+ <el-tag type="danger">鏈煡娑堟伅绫诲瀷</el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="emit('send', scope.row.userId)"
+ v-hasPermi="['mp:message:send']"
+ >
+ 娑堟伅
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ </div>
+</template>
+
+<script lang="ts" setup>
+import WxVideoPlayer from '@/views/mp/components/wx-video-play'
+import WxVoicePlayer from '@/views/mp/components/wx-voice-play'
+import WxLocation from '@/views/mp/components/wx-location'
+import WxMusic from '@/views/mp/components/wx-music'
+import WxNews from '@/views/mp/components/wx-news'
+import { dateFormatter } from '@/utils/formatTime'
+import { MsgType } from '@/views/mp/components/wx-msg/types'
+
+const props = defineProps({
+ list: {
+ type: Array,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ required: true
+ }
+})
+
+const emit = defineEmits<{ (e: 'send', v: number) }>()
+</script>
diff --git a/src/views/mp/message/index.vue b/src/views/mp/message/index.vue
new file mode 100644
index 0000000..adceec5
--- /dev/null
+++ b/src/views/mp/message/index.vue
@@ -0,0 +1,152 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ <el-form-item label="娑堟伅绫诲瀷" prop="type">
+ <el-select v-model="queryParams.type" placeholder="璇烽�夋嫨娑堟伅绫诲瀷" class="!w-240px">
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.MP_MESSAGE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏍囪瘑" prop="openid">
+ <el-input
+ v-model="queryParams.openid"
+ placeholder="璇疯緭鍏ョ敤鎴锋爣璇�"
+ clearable
+ :v-on="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ style="width: 240px"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="['00:00:00', '23:59:59']"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <MessageTable :list="list" :loading="loading" @send="handleSend" />
+ <Pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 鍙戦�佹秷鎭殑寮圭獥 -->
+ <el-dialog
+ title="绮変笣娑堟伅鍒楄〃"
+ v-model="messageBox.show"
+ @click="messageBox.show = true"
+ width="50%"
+ destroy-on-close
+ >
+ <WxMsg :user-id="messageBox.userId" />
+ </el-dialog>
+</template>
+<script lang="ts" setup>
+import * as MpMessageApi from '@/api/mp/message'
+import WxMsg from '@/views/mp/components/wx-msg'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import MessageTable from './MessageTable.vue'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { MsgType } from '@/views/mp/components/wx-msg/types'
+import type { FormInstance } from 'element-plus'
+
+defineOptions({ name: 'MpMessage' })
+
+const loading = ref(false)
+const total = ref(0) // 鏁版嵁鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 褰撳墠椤电殑鍒楄〃鏁版嵁
+
+// 鎼滅储鍙傛暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ openid: '',
+ accountId: -1,
+ type: MsgType.Text,
+ createTime: []
+})
+const queryFormRef = ref<FormInstance | null>(null) // 鎼滅储鐨勮〃鍗�
+
+// 娑堟伅瀵硅瘽妗�
+const messageBox = reactive({
+ show: false,
+ userId: 0
+})
+
+/** 渚﹀惉accountId */
+const onAccountChanged = (id: number) => {
+ queryParams.accountId = id
+ queryParams.pageNo = 1
+ handleQuery()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+const getList = async () => {
+ try {
+ loading.value = true
+ const data = await MpMessageApi.getMessagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = async () => {
+ // 鏆傚瓨 accountId锛屽苟鍦� reset 鍚庢仮澶�
+ const accountId = queryParams.accountId
+ queryFormRef.value?.resetFields()
+ queryParams.accountId = accountId
+ handleQuery()
+}
+
+/** 鎵撳紑娑堟伅鍙戦�佺獥鍙� */
+const handleSend = async (userId: number) => {
+ messageBox.userId = userId
+ messageBox.show = true
+}
+</script>
diff --git a/src/views/mp/messageTemplate/MessageTemplateSendForm.vue b/src/views/mp/messageTemplate/MessageTemplateSendForm.vue
new file mode 100644
index 0000000..1c31742
--- /dev/null
+++ b/src/views/mp/messageTemplate/MessageTemplateSendForm.vue
@@ -0,0 +1,163 @@
+<template>
+ <Dialog title="鍙戦�佹秷鎭ā鏉�" v-model="dialogVisible" width="600px">
+ <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
+ <el-form-item label="妯℃澘缂栧彿">
+ <el-input v-model="formData.id" disabled />
+ </el-form-item>
+ <el-form-item label="妯℃澘鏍囬">
+ <el-input v-model="templateTitle" disabled />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛" prop="userId">
+ <el-select
+ v-model="formData.userId"
+ filterable
+ remote
+ reserve-keyword
+ placeholder="璇疯緭鍏ョ敤鎴锋樀绉版悳绱�"
+ :remote-method="searchUser"
+ :loading="userLoading"
+ class="!w-full"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname || user.openid"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘鏁版嵁" prop="data">
+ <el-input
+ v-model="formData.data"
+ type="textarea"
+ :rows="4"
+ placeholder='璇疯緭鍏ユā鏉挎暟鎹紙JSON 鏍煎紡锛夛紝渚嬪锛歿"keyword1": {"value": "娴嬭瘯鍐呭"}}'
+ />
+ </el-form-item>
+ <el-form-item label="璺宠浆閾炬帴" prop="url">
+ <el-input v-model="formData.url" placeholder="璇疯緭鍏ヨ烦杞摼鎺�" />
+ </el-form-item>
+ <el-form-item label="灏忕▼搴� appId" prop="miniProgramAppId">
+ <el-input v-model="formData.miniProgramAppId" placeholder="璇疯緭鍏ュ皬绋嬪簭 appId" />
+ </el-form-item>
+ <el-form-item label="灏忕▼搴忛〉闈㈣矾寰�" prop="miniProgramPagePath">
+ <el-input v-model="formData.miniProgramPagePath" placeholder="璇疯緭鍏ュ皬绋嬪簭椤甸潰璺緞" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" @click="submitForm" :loading="loading">鍙� 閫�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script setup lang="ts">
+import { MessageTemplateApi, MsgTemplateVO, MsgTemplateSendVO } from '@/api/mp/messageTemplate'
+import * as MpUserApi from '@/api/mp/user'
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鏄惁灞曠ず
+const loading = ref(false) // 鎻愪氦鍔犺浇涓�
+const templateTitle = ref('') // 妯℃澘鏍囬
+const accountId = ref<number>() // 鍏紬鍙疯处鍙风紪鍙�
+
+const formRef = ref() // 琛ㄥ崟 Ref
+const formData = ref<MsgTemplateSendVO>({
+ id: undefined!,
+ userId: undefined!,
+ data: '',
+ url: '',
+ miniProgramAppId: '',
+ miniProgramPagePath: ''
+})
+const formRules = reactive({
+ userId: [{ required: true, message: '璇烽�夋嫨鐢ㄦ埛', trigger: 'change' }]
+})
+
+// 鐢ㄦ埛鎼滅储鐩稿叧
+const userLoading = ref(false)
+const userList = ref<any[]>([])
+
+/** 鎼滅储鐢ㄦ埛 */
+const searchUser = async (query: string) => {
+ if (!accountId.value) {
+ return
+ }
+ userLoading.value = true
+ try {
+ const data = await MpUserApi.getUserPage({
+ pageNo: 1,
+ pageSize: 20,
+ accountId: accountId.value,
+ nickname: query || undefined
+ })
+ userList.value = data.list || []
+ } finally {
+ userLoading.value = false
+ }
+}
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: MsgTemplateVO) => {
+ resetForm()
+ dialogVisible.value = true
+ // 璁剧疆琛ㄥ崟鏁版嵁
+ formData.value.id = row.id
+ accountId.value = row.accountId
+ templateTitle.value = row.title
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ await searchUser('')
+}
+defineExpose({ open }) // 鏆撮湶 open 鏂规硶
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef.value) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ loading.value = true
+ try {
+ const sendData: MsgTemplateSendVO = {
+ ...formData.value
+ }
+ // 濡傛灉濉啓浜嗗皬绋嬪簭淇℃伅锛岄渶瑕佹嫾鎺ユ垚 miniprogram 瀛楁
+ if (sendData.miniProgramAppId && sendData.miniProgramPagePath) {
+ sendData.miniprogram = JSON.stringify({
+ appid: sendData.miniProgramAppId,
+ pagepath: sendData.miniProgramPagePath
+ })
+ }
+ // 濡傛灉濉啓浜� data 瀛楁
+ if (sendData.data) {
+ try {
+ sendData.data = JSON.parse(sendData.data)
+ } catch (e) {
+ message.error('妯℃澘鏁版嵁鏍煎紡涓嶆纭紝璇疯緭鍏ユ湁鏁堢殑 JSON 鏍煎紡')
+ return
+ }
+ }
+ await MessageTemplateApi.sendMessageTemplate(sendData)
+ message.success('鍙戦�佹垚鍔�')
+ dialogVisible.value = false
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined!,
+ userId: undefined!,
+ data: '',
+ url: '',
+ miniProgramAppId: '',
+ miniProgramPagePath: ''
+ }
+ userList.value = []
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mp/messageTemplate/index.vue b/src/views/mp/messageTemplate/index.vue
new file mode 100644
index 0000000..d163db7
--- /dev/null
+++ b/src/views/mp/messageTemplate/index.vue
@@ -0,0 +1,144 @@
+<template>
+ <doc-alert title="妯$増娑堟伅" url="https://doc.iocoder.cn/mp/message-template/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ type="success"
+ plain
+ @click="handleSync"
+ :loading="syncLoading"
+ v-hasPermi="['mp:message-template:sync']"
+ :disabled="queryParams.accountId === -1"
+ >
+ <Icon icon="ep:refresh" class="mr-5px" /> 鍚屾
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="鍏紬鍙锋ā鏉� ID" align="center" prop="templateId" width="200px" />
+ <el-table-column label="鏍囬" align="center" prop="title" width="150px" />
+ <el-table-column label="妯℃澘鍐呭" align="center" prop="content" />
+ <el-table-column label="妯℃澘绀轰緥" align="center" prop="example" width="200px" />
+ <el-table-column label="涓�绾ц涓�" align="center" prop="primaryIndustry" width="120px" />
+ <el-table-column label="浜岀骇琛屼笟" align="center" prop="deputyIndustry" width="120px" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="160">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="handleSend(scope.row)"
+ v-hasPermi="['mp:message-template:send']"
+ >
+ 鍙戦��
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['mp:message-template:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氬彂閫佹秷鎭� -->
+ <MessageTemplateSendForm ref="sendFormRef" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { MessageTemplateApi, MsgTemplateVO } from '@/api/mp/messageTemplate'
+import MessageTemplateSendForm from './MessageTemplateSendForm.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+
+/** 鍏紬鍙锋秷鎭ā鏉垮垪琛� */
+defineOptions({ name: 'MpMessageTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<MsgTemplateVO[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ accountId: -1
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const syncLoading = ref(false) // 鍚屾妯℃澘鐨勫姞杞戒腑
+
+/** 鍏紬鍙烽�夋嫨鍙樺寲 */
+const onAccountChanged = (accountId: number) => {
+ queryParams.accountId = accountId
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MessageTemplateApi.getMessageTemplateList(queryParams)
+ if (data) {
+ list.value = data
+ }
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍚屾鎿嶄綔 */
+const handleSync = async () => {
+ try {
+ await message.confirm('鏄惁纭鍚屾娑堟伅妯℃澘锛�')
+ syncLoading.value = true
+ await MessageTemplateApi.syncMessageTemplate(queryParams.accountId)
+ message.success('鍚屾娑堟伅妯℃澘鎴愬姛')
+ await getList()
+ } finally {
+ syncLoading.value = false
+ }
+}
+
+/** 鍙戦�佹秷鎭搷浣� */
+const sendFormRef = ref()
+const handleSend = (row: MsgTemplateVO) => {
+ sendFormRef.value.open(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await MessageTemplateApi.deleteMessageTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+</script>
diff --git a/src/views/mp/statistics/index.vue b/src/views/mp/statistics/index.vue
new file mode 100644
index 0000000..7b29a69
--- /dev/null
+++ b/src/views/mp/statistics/index.vue
@@ -0,0 +1,357 @@
+<template>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form class="-mb-15px" ref="queryForm" :inline="true" label-width="68px">
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ <el-form-item label="鏃堕棿鑼冨洿" prop="dateRange">
+ <el-date-picker
+ v-model="dateRange"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ @change="getSummary"
+ class="!w-240px"
+ />
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍥捐〃 -->
+ <ContentWrap>
+ <el-row>
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header>
+ <div>
+ <span>鐢ㄦ埛澧炲噺鏁版嵁</span>
+ </div>
+ </template>
+ <Echart :options="userSummaryOption" :height="420" />
+ </el-card>
+ </el-col>
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header>
+ <div>
+ <span>绱鐢ㄦ埛鏁版嵁</span>
+ </div>
+ </template>
+ <Echart :options="userCumulateOption" :height="420" />
+ </el-card>
+ </el-col>
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header>
+ <div>
+ <span>娑堟伅姒傚喌鏁版嵁</span>
+ </div>
+ </template>
+ <Echart :options="upstreamMessageOption" :height="420" />
+ </el-card>
+ </el-col>
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header>
+ <div>
+ <span>鎺ュ彛鍒嗘瀽鏁版嵁</span>
+ </div>
+ </template>
+ <Echart :options="interfaceSummaryOption" :height="420" />
+ </el-card>
+ </el-col>
+ </el-row>
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { formatDate, addTime, betweenDay, beginOfDay, endOfDay } from '@/utils/formatTime'
+import * as StatisticsApi from '@/api/mp/statistics'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+
+defineOptions({ name: 'MpStatistics' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 榛樿寮�濮嬫椂闂存槸褰撳墠鏃ユ湡-7锛岀粨鏉熸椂闂存槸褰撳墠鏃ユ湡-1
+const dateRange = ref([
+ beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)),
+ endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
+])
+const accountId = ref(-1) // 閫変腑鐨勫叕浼楀彿缂栧彿
+
+const xAxisDate = ref([] as any[]) // X 杞寸殑鏃ユ湡鑼冨洿
+// 鐢ㄦ埛澧炲噺鏁版嵁鍥捐〃閰嶇疆椤�
+const userSummaryOption = reactive({
+ color: ['#67C23A', '#E5323E'],
+ legend: {
+ data: ['鏂板鐢ㄦ埛', '鍙栨秷鍏虫敞鐨勭敤鎴�']
+ },
+ tooltip: {},
+ xAxis: {
+ data: [] as any[] // X 杞寸殑鏃ユ湡鑼冨洿
+ },
+ yAxis: {
+ minInterval: 1
+ },
+ series: [
+ {
+ name: '鏂板鐢ㄦ埛',
+ type: 'bar' as const,
+ label: {
+ show: true
+ },
+ barGap: 0,
+ data: [] as any[] // 鏂板鐢ㄦ埛鐨勬暟鎹�
+ },
+ {
+ name: '鍙栨秷鍏虫敞鐨勭敤鎴�',
+ type: 'bar' as const,
+ label: {
+ show: true
+ },
+ data: [] as any[] // 鍙栨秷鍏虫敞鐨勭敤鎴风殑鏁版嵁
+ }
+ ]
+})
+// 绱鐢ㄦ埛鏁版嵁鍥捐〃閰嶇疆椤�
+const userCumulateOption = reactive({
+ legend: {
+ data: ['绱鐢ㄦ埛閲�']
+ },
+ xAxis: {
+ type: 'category' as const,
+ data: [] as any[]
+ },
+ yAxis: {
+ minInterval: 1
+ },
+ series: [
+ {
+ name: '绱鐢ㄦ埛閲�',
+ data: [] as any[], // 绱鐢ㄦ埛閲忕殑鏁版嵁
+ type: 'line' as const,
+ smooth: true,
+ label: {
+ show: true
+ }
+ }
+ ]
+})
+// 娑堟伅鍙戦�佹鍐垫暟鎹浘琛ㄩ厤缃」
+const upstreamMessageOption = reactive({
+ color: ['#67C23A', '#E5323E'],
+ legend: {
+ data: ['鐢ㄦ埛鍙戦�佷汉鏁�', '鐢ㄦ埛鍙戦�佹潯鏁�']
+ },
+ tooltip: {},
+ xAxis: {
+ data: [] as any[] // X 杞寸殑鏃ユ湡鑼冨洿
+ },
+ yAxis: {
+ minInterval: 1
+ },
+ series: [
+ {
+ name: '鐢ㄦ埛鍙戦�佷汉鏁�',
+ type: 'line' as const,
+ smooth: true,
+ label: {
+ show: true
+ },
+ data: [] as any[] // 鐢ㄦ埛鍙戦�佷汉鏁扮殑鏁版嵁
+ },
+ {
+ name: '鐢ㄦ埛鍙戦�佹潯鏁�',
+ type: 'line' as const,
+ smooth: true,
+ label: {
+ show: true
+ },
+ data: [] as any[] // 鐢ㄦ埛鍙戦�佹潯鏁扮殑鏁版嵁
+ }
+ ]
+})
+// 鎺ュ彛鍒嗘瀽鍐垫暟鎹浘琛ㄩ厤缃」
+const interfaceSummaryOption = reactive({
+ color: ['#67C23A', '#E5323E', '#E6A23C', '#409EFF'],
+ legend: {
+ data: ['琚姩鍥炲鐢ㄦ埛娑堟伅鐨勬鏁�', '澶辫触娆℃暟', '鏈�澶ц�楁椂', '鎬昏�楁椂']
+ },
+ tooltip: {},
+ xAxis: {
+ data: [] as any[] // X 杞寸殑鏃ユ湡鑼冨洿
+ },
+ yAxis: {},
+ series: [
+ {
+ name: '琚姩鍥炲鐢ㄦ埛娑堟伅鐨勬鏁�',
+ type: 'bar' as const,
+ label: {
+ show: true
+ },
+ barGap: 0,
+ data: [] as any[] // 琚姩鍥炲鐢ㄦ埛娑堟伅鐨勬鏁扮殑鏁版嵁
+ },
+ {
+ name: '澶辫触娆℃暟',
+ type: 'bar' as const,
+ label: {
+ show: true
+ },
+ data: [] as any[] // 澶辫触娆℃暟鐨勬暟鎹�
+ },
+ {
+ name: '鏈�澶ц�楁椂',
+ type: 'bar' as const,
+ label: {
+ show: true
+ },
+ data: [] as any[] // 鏈�澶ц�楁椂鐨勬暟鎹�
+ },
+ {
+ name: '鎬昏�楁椂',
+ type: 'bar' as const,
+ label: {
+ show: true
+ },
+ data: [] as any[] // 鎬昏�楁椂鐨勬暟鎹�
+ }
+ ]
+})
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number) => {
+ accountId.value = id
+ getSummary()
+}
+
+/** 鍔犺浇鏁版嵁 */
+const getSummary = () => {
+ // 濡傛灉娌℃湁閫変腑鍏紬鍙疯处鍙凤紝鍒欒繘琛屾彁绀恒��
+ if (!accountId) {
+ message.error('鏈�変腑鍏紬鍙凤紝鏃犳硶缁熻鏁版嵁')
+ return false
+ }
+ // 蹇呴』閫夋嫨 7 澶╁唴锛屽洜涓哄叕浼楀彿鏈夋椂闂磋法搴﹂檺鍒朵负 7
+ if (betweenDay(dateRange.value[0], dateRange.value[1]) >= 7) {
+ message.error('鏃堕棿闂撮殧 7 澶╀互鍐咃紝璇烽噸鏂伴�夋嫨')
+ return false
+ }
+ // 娓呯┖妯潗鏍囨棩鏈�
+ xAxisDate.value = []
+ // 妯潗鏍囧姞杞芥棩鏈熸暟鎹�
+ const days = betweenDay(dateRange.value[0], dateRange.value[1]) // 鐩稿樊澶╂暟
+ for (let i = 0; i <= days; i++) {
+ xAxisDate.value.push(
+ formatDate(addTime(dateRange.value[0], 3600 * 1000 * 24 * i), 'YYYY-MM-DD')
+ )
+ }
+ // 鍒濆鍖栧浘琛�
+ initUserSummaryChart()
+ initUserCumulateChart()
+ initUpstreamMessageChart()
+ interfaceSummaryChart()
+}
+
+/** 鐢ㄦ埛澧炲噺鏁版嵁 */
+const initUserSummaryChart = async () => {
+ userSummaryOption.xAxis.data = []
+ userSummaryOption.series[0].data = []
+ userSummaryOption.series[1].data = []
+ try {
+ // 鐢ㄦ埛澧炲噺鏁版嵁
+ const data = await StatisticsApi.getUserSummary({
+ accountId: accountId.value,
+ date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
+ })
+ // 妯潗鏍�
+ userSummaryOption.xAxis.data = xAxisDate.value
+ // 澶勭悊鏁版嵁
+ xAxisDate.value.forEach((date, index) => {
+ data.forEach((item) => {
+ // 鍖归厤鏃ユ湡
+ const refDate = formatDate(new Date(item.refDate), 'YYYY-MM-DD')
+ if (refDate.indexOf(date) === -1) {
+ return
+ }
+ // 璁剧疆鏁版嵁鍒板搴旂殑浣嶇疆
+ userSummaryOption.series[0].data[index] = item.newUser
+ userSummaryOption.series[1].data[index] = item.cancelUser
+ })
+ })
+ } catch {
+ //
+ }
+}
+
+/** 绱鐢ㄦ埛鏁版嵁 */
+const initUserCumulateChart = async () => {
+ userCumulateOption.xAxis.data = []
+ userCumulateOption.series[0].data = []
+ // 鍙戣捣璇锋眰
+ try {
+ const data = await StatisticsApi.getUserCumulate({
+ accountId: accountId.value,
+ date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
+ })
+ userCumulateOption.xAxis.data = xAxisDate.value
+ // 澶勭悊鏁版嵁
+ data.forEach((item, index) => {
+ userCumulateOption.series[0].data[index] = item.cumulateUser
+ })
+ } catch {
+ //
+ }
+}
+
+/** 娑堟伅姒傚喌鏁版嵁 */
+const initUpstreamMessageChart = async () => {
+ upstreamMessageOption.xAxis.data = []
+ upstreamMessageOption.series[0].data = []
+ upstreamMessageOption.series[1].data = []
+ // 鍙戣捣璇锋眰
+ try {
+ const data = await StatisticsApi.getUpstreamMessage({
+ accountId: accountId.value,
+ date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
+ })
+ upstreamMessageOption.xAxis.data = xAxisDate.value
+ // 澶勭悊鏁版嵁
+ data.forEach((item, index) => {
+ upstreamMessageOption.series[0].data[index] = item.messageUser
+ upstreamMessageOption.series[1].data[index] = item.messageCount
+ })
+ } catch {
+ //
+ }
+}
+
+/** 鎺ュ彛鍒嗘瀽鏁版嵁 */
+const interfaceSummaryChart = async () => {
+ interfaceSummaryOption.xAxis.data = []
+ interfaceSummaryOption.series[0].data = []
+ interfaceSummaryOption.series[1].data = []
+ interfaceSummaryOption.series[2].data = []
+ interfaceSummaryOption.series[3].data = []
+ // 鍙戣捣璇锋眰
+ try {
+ const data = await StatisticsApi.getInterfaceSummary({
+ accountId: accountId.value,
+ date: [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])]
+ })
+ interfaceSummaryOption.xAxis.data = xAxisDate.value
+ // 澶勭悊鏁版嵁
+ data.forEach((item, index) => {
+ interfaceSummaryOption.series[0].data[index] = item.callbackCount
+ interfaceSummaryOption.series[1].data[index] = item.failCount
+ interfaceSummaryOption.series[2].data[index] = item.maxTimeCost
+ interfaceSummaryOption.series[3].data[index] = item.totalTimeCost
+ })
+ } catch {
+ //
+ }
+}
+</script>
diff --git a/src/views/mp/tag/TagForm.vue b/src/views/mp/tag/TagForm.vue
new file mode 100644
index 0000000..9a85bec
--- /dev/null
+++ b/src/views/mp/tag/TagForm.vue
@@ -0,0 +1,98 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鏍囩鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユ爣绛惧悕绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as MpTagApi from '@/api/mp/tag'
+import type { FormInstance, FormRules } from 'element-plus'
+
+defineOptions({ name: 'MpTagForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref<'create' | 'update' | ''>('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ accountId: -1,
+ name: ''
+})
+const formRules: FormRules = {
+ name: [{ required: true, message: '璇疯緭鍏ユ爣绛惧悕绉�', trigger: 'blur' }]
+}
+const formRef = ref<FormInstance | null>(null) // 琛ㄥ崟 Ref
+
+const emit = defineEmits<{
+ (e: 'success'): void
+}>()
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: 'create' | 'update', accountId: number, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ formData.value.accountId = accountId
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await MpTagApi.getTag(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value?.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as MpTagApi.TagVO
+ if (formType.value === 'create') {
+ await MpTagApi.createTag(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await MpTagApi.updateTag(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ accountId: -1,
+ name: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mp/tag/index.vue b/src/views/mp/tag/index.vue
new file mode 100644
index 0000000..8720a26
--- /dev/null
+++ b/src/views/mp/tag/index.vue
@@ -0,0 +1,158 @@
+<template>
+ <doc-alert title="鍏紬鍙锋爣绛�" url="https://doc.iocoder.cn/mp/tag/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" />
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['mp:tag:create']"
+ :disabled="queryParams.accountId === 0"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleSync"
+ v-hasPermi="['mp:tag:sync']"
+ :disabled="queryParams.accountId === 0"
+ >
+ <Icon icon="ep:refresh" class="mr-5px" /> 鍚屾
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鏍囩鍚嶇О" align="center" prop="name" />
+ <el-table-column label="绮変笣鏁�" align="center" prop="count" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['mp:tag:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['mp:tag:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <TagForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as MpTagApi from '@/api/mp/tag'
+import TagForm from './TagForm.vue'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+
+defineOptions({ name: 'MpTag' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: -1
+})
+
+const formRef = ref<InstanceType<typeof TagForm> | null>(null)
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number) => {
+ queryParams.accountId = id
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ try {
+ loading.value = true
+ const data = await MpTagApi.getTagPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const openForm = (type: 'create' | 'update', id?: number) => {
+ formRef.value?.open(type, queryParams.accountId, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await MpTagApi.deleteTag(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ //
+ }
+}
+
+/** 鍚屾鎿嶄綔 */
+const handleSync = async () => {
+ try {
+ await message.confirm('鏄惁纭鍚屾鏍囩锛�')
+ await MpTagApi.syncTag(queryParams.accountId as number)
+ message.success('鍚屾鏍囩鎴愬姛')
+ await getList()
+ } catch {
+ //
+ }
+}
+</script>
diff --git a/src/views/mp/user/UserForm.vue b/src/views/mp/user/UserForm.vue
new file mode 100644
index 0000000..818fdd8
--- /dev/null
+++ b/src/views/mp/user/UserForm.vue
@@ -0,0 +1,102 @@
+<template>
+ <Dialog v-model="dialogVisible" title="淇敼">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鏄电О" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ユ樀绉�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ <el-form-item label="鏍囩" prop="tagIds">
+ <el-select v-model="formData.tagIds" clearable multiple placeholder="璇烽�夋嫨鏍囩">
+ <el-option
+ v-for="item in tagList"
+ :key="item.tagId"
+ :label="item.name"
+ :value="item.tagId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as MpTagApi from '@/api/mp/tag'
+import * as MpUserApi from '@/api/mp/user'
+
+defineOptions({ name: 'MpUserForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref({
+ id: undefined,
+ nickname: undefined,
+ remark: undefined,
+ tagIds: []
+})
+const formRules = reactive({}) // 琛ㄥ崟鐨勬牎楠�
+const formRef = ref() // 琛ㄥ崟 Ref
+const tagList = ref([]) // 鍏紬鍙锋爣绛惧垪琛�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await MpUserApi.getUser(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇鏍囩
+ tagList.value = await MpTagApi.getSimpleTagList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await MpUserApi.updateUser(formData.value)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ nickname: undefined,
+ remark: undefined,
+ tagIds: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/mp/user/index.vue b/src/views/mp/user/index.vue
new file mode 100644
index 0000000..431e1c6
--- /dev/null
+++ b/src/views/mp/user/index.vue
@@ -0,0 +1,225 @@
+<template>
+ <doc-alert title="鍏紬鍙风矇涓�" url="https://doc.iocoder.cn/mp/user/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏紬鍙�" prop="accountId">
+ <WxAccountSelect @change="onAccountChanged" :modelValue="queryParams.accountId" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏍囪瘑" prop="openid">
+ <el-input
+ v-model="queryParams.openid"
+ placeholder="璇疯緭鍏ョ敤鎴锋爣璇�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏄电О" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ placeholder="璇疯緭鍏ユ樀绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"> <Icon icon="ep:search" />鎼滅储 </el-button>
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" />閲嶇疆 </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleSync"
+ v-hasPermi="['mp:user:sync']"
+ :disabled="queryParams.accountId === 0"
+ >
+ <Icon icon="ep:refresh" class="mr-5px" /> 鍚屾
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" v-if="isDialog"/>
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐢ㄦ埛鏍囪瘑" align="center" prop="openid" width="260" />
+ <el-table-column label="鐢ㄦ埛澶村儚" min-width="80px" prop="headImageUrl">
+ <template #default="scope">
+ <el-avatar :src="scope.row.headImageUrl"/>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄电О" align="center" prop="nickname" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鏍囩" align="center" prop="tagIds" width="200">
+ <template #default="scope">
+ <span v-for="(tagId, index) in scope.row.tagIds" :key="index">
+ <el-tag>{{ tagList.find((tag) => tag.tagId === tagId)?.name }} </el-tag>
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="璁㈤槄鐘舵��" align="center" prop="subscribeStatus">
+ <template #default="scope">
+ <el-tag v-if="scope.row.subscribeStatus === 0" type="success">宸茶闃�</el-tag>
+ <el-tag v-else type="danger">鏈闃�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="璁㈤槄鏃堕棿"
+ align="center"
+ prop="subscribeTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="openForm(scope.row.id)"
+ v-hasPermi="['mp:user:update']"
+ >
+ 淇敼
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氫慨鏀� -->
+ <UserForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import {dateFormatter} from '@/utils/formatTime'
+import * as MpUserApi from '@/api/mp/user'
+import * as MpTagApi from '@/api/mp/tag'
+import WxAccountSelect from '@/views/mp/components/wx-account-select'
+import type {FormInstance} from 'element-plus'
+import UserForm from './UserForm.vue'
+import {ref} from "vue";
+
+defineOptions({ name: 'MpUser' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅
+
+const isDialog = ref(false) // 鏄笉鏄脊绐楄皟鐢�
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const multipleSelection = ref<String[]>([])
+
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ accountId: -1,
+ openid: '',
+ nickname: ''
+})
+const queryFormRef = ref<FormInstance | null>(null) // 鎼滅储鐨勮〃鍗�
+const tagList = ref<any[]>([]) // 鍏紬鍙锋爣绛惧垪琛�
+
+/** 渚﹀惉鍏紬鍙峰彉鍖� **/
+const onAccountChanged = (id: number) => {
+ queryParams.accountId = id
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ try {
+ multipleSelection.value = []
+ loading.value = true
+ const data = await MpUserApi.getUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+ if(isDialog.value){
+ emitChange()
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ const accountId = queryParams.accountId
+ queryFormRef.value?.resetFields()
+ queryParams.accountId = accountId
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref<InstanceType<typeof UserForm> | null>(null)
+const openForm = (id: number) => {
+ formRef.value?.open(id)
+}
+
+/** 鍚屾鏍囩 */
+const handleSync = async () => {
+ try {
+ await message.confirm('鏄惁纭鍚屾绮変笣锛�')
+ await MpUserApi.syncUser(queryParams.accountId)
+ message.success('寮�濮嬩粠寰俊鍏紬鍙峰悓姝ョ矇涓濅俊鎭紝鍚屾闇�瑕佷竴娈垫椂闂达紝寤鸿绋嶅悗鍐嶆煡璇�')
+ await getList()
+ } catch {}
+}
+
+/** Expose*/
+defineExpose({
+ open: (accountId: number) => {
+ onAccountChanged(accountId)
+ isDialog.value = true
+ }
+});
+
+/** Emits*/
+interface Emits {
+ (e: 'change', data: {
+ multipleSelection: any[]
+ total: number
+ queryParams: object
+ }): void
+ // (e: 'select', user: any): void
+ // (e: 'cancel'): void
+}
+const emit = defineEmits<Emits>()
+const emitChange = () => {
+ emit('change', {multipleSelection: multipleSelection.value, total: total.value, queryParams})
+}
+
+const handleSelectionChange = (val: any[]) => {
+ multipleSelection.value = val
+ if (isDialog.value) {
+ emitChange()
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ tagList.value = await MpTagApi.getSimpleTagList()
+})
+</script>
diff --git a/src/views/pay/app/components/AppForm.vue b/src/views/pay/app/components/AppForm.vue
new file mode 100644
index 0000000..edac374
--- /dev/null
+++ b/src/views/pay/app/components/AppForm.vue
@@ -0,0 +1,138 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="160px"
+ >
+ <el-form-item label="搴旂敤鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ簲鐢ㄥ悕" />
+ </el-form-item>
+ <el-form-item label="搴旂敤鏍囪瘑" prop="appKey">
+ <el-input v-model="formData.appKey" placeholder="璇疯緭鍏ュ簲鐢ㄦ爣璇�" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏀粯缁撴灉鐨勫洖璋冨湴鍧�" prop="orderNotifyUrl">
+ <el-input v-model="formData.orderNotifyUrl" placeholder="璇疯緭鍏ユ敮浠樼粨鏋滅殑鍥炶皟鍦板潃" />
+ </el-form-item>
+ <el-form-item label="閫�娆剧粨鏋滅殑鍥炶皟鍦板潃" prop="refundNotifyUrl">
+ <el-input v-model="formData.refundNotifyUrl" placeholder="璇疯緭鍏ラ��娆剧粨鏋滅殑鍥炶皟鍦板潃" />
+ </el-form-item>
+ <el-form-item label="杞处缁撴灉鐨勫洖璋冨湴鍧�" prop="transferNotifyUrl">
+ <el-input v-model="formData.transferNotifyUrl" placeholder="璇疯緭鍏ヨ浆璐︾粨鏋滅殑鍥炶皟鍦板潃" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as AppApi from '@/api/pay/app'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'PayAppForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ appKey: undefined,
+ status: CommonStatusEnum.ENABLE,
+ remark: undefined,
+ orderNotifyUrl: undefined,
+ refundNotifyUrl: undefined,
+ transferNotifyUrl: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '搴旂敤鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ appKey: [{ required: true, message: '搴旂敤鏍囪瘑涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }],
+ orderNotifyUrl: [{ required: true, message: '鏀粯缁撴灉鐨勫洖璋冨湴鍧�涓嶈兘涓虹┖', trigger: 'blur' }],
+ refundNotifyUrl: [{ required: true, message: '閫�娆剧粨鏋滅殑鍥炶皟鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await AppApi.getApp(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as AppApi.AppVO
+ if (formType.value === 'create') {
+ await AppApi.createApp(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await AppApi.updateApp(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ status: CommonStatusEnum.ENABLE,
+ remark: undefined,
+ orderNotifyUrl: undefined,
+ refundNotifyUrl: undefined,
+ transferNotifyUrl: undefined,
+ appKey: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/pay/app/components/channel/AlipayChannelForm.vue b/src/views/pay/app/components/channel/AlipayChannelForm.vue
new file mode 100644
index 0000000..89a141e
--- /dev/null
+++ b/src/views/pay/app/components/channel/AlipayChannelForm.vue
@@ -0,0 +1,351 @@
+<template>
+ <div>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="830px" @closed="close">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="娓犻亾璐圭巼" label-width="180px" prop="feeRate">
+ <el-input v-model="formData.feeRate" clearable placeholder="璇疯緭鍏ユ笭閬撹垂鐜�">
+ <template #append>%</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="寮�鏀惧钩鍙� APPID" label-width="180px" prop="config.appId">
+ <el-input v-model="formData.config.appId" clearable placeholder="璇疯緭鍏ュ紑鏀惧钩鍙� APPID" />
+ </el-form-item>
+ <el-form-item label="娓犻亾鐘舵��" label-width="180px" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="parseInt(dict.value)"
+ :value="parseInt(dict.value)"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="缃戝叧鍦板潃" label-width="180px" prop="config.serverUrl">
+ <el-radio-group v-model="formData.config.serverUrl">
+ <el-radio value="https://openapi.alipay.com/gateway.do">绾夸笂鐜</el-radio>
+ <el-radio value="https://openapi-sandbox.dl.alipaydev.com/gateway.do">
+ 娌欑鐜
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="绠楁硶绫诲瀷" label-width="180px" prop="config.signType">
+ <el-radio-group v-model="formData.config.signType">
+ <el-radio key="RSA2" value="RSA2">RSA2</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍏挜绫诲瀷" label-width="180px" prop="config.mode">
+ <el-radio-group v-model="formData.config.mode">
+ <el-radio key="鍏挜妯″紡" :value="1">鍏挜妯″紡</el-radio>
+ <el-radio key="璇佷功妯″紡" :value="2">璇佷功妯″紡</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <div v-if="formData.config.mode === 1">
+ <el-form-item label="搴旂敤绉侀挜" label-width="180px" prop="config.privateKey">
+ <el-input
+ v-model="formData.config.privateKey"
+ :autosize="{ minRows: 8, maxRows: 8 }"
+ :style="{ width: '100%' }"
+ clearable
+ placeholder="璇疯緭鍏ュ簲鐢ㄧ閽�"
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="鏀粯瀹濆叕閽�" label-width="180px" prop="config.alipayPublicKey">
+ <el-input
+ v-model="formData.config.alipayPublicKey"
+ :autosize="{ minRows: 8, maxRows: 8 }"
+ :style="{ width: '100%' }"
+ clearable
+ placeholder="璇疯緭鍏ユ敮浠樺疂鍏挜"
+ type="textarea"
+ />
+ </el-form-item>
+ </div>
+ <div v-if="formData.config.mode === 2">
+ <el-form-item label="搴旂敤绉侀挜" label-width="180px" prop="config.privateKey">
+ <el-input
+ v-model="formData.config.privateKey"
+ :autosize="{ minRows: 8, maxRows: 8 }"
+ :style="{ width: '100%' }"
+ clearable
+ placeholder="璇疯緭鍏ュ簲鐢ㄧ閽�"
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗘埛鍏挜搴旂敤璇佷功" label-width="180px" prop="config.appCertContent">
+ <el-input
+ v-model="formData.config.appCertContent"
+ :autosize="{ minRows: 8, maxRows: 8 }"
+ :style="{ width: '100%' }"
+ placeholder="璇蜂笂浼犲晢鎴峰叕閽ュ簲鐢ㄨ瘉涔�"
+ readonly
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="" label-width="180px">
+ <el-upload
+ ref="privateKeyContentFile"
+ :accept="fileAccept"
+ :before-upload="fileBeforeUpload"
+ :http-request="appCertUpload"
+ :limit="1"
+ action=""
+ >
+ <el-button type="primary">
+ <Icon class="mr-5px" icon="ep:upload" />
+ 鐐瑰嚮涓婁紶
+ </el-button>
+ </el-upload>
+ </el-form-item>
+ <el-form-item
+ label="鏀粯瀹濆叕閽ヨ瘉涔�"
+ label-width="180px"
+ prop="config.alipayPublicCertContent"
+ >
+ <el-input
+ v-model="formData.config.alipayPublicCertContent"
+ :autosize="{ minRows: 8, maxRows: 8 }"
+ :style="{ width: '100%' }"
+ placeholder="璇蜂笂浼犳敮浠樺疂鍏挜璇佷功"
+ readonly
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="" label-width="180px">
+ <el-upload
+ ref="privateCertContentFile"
+ :accept="fileAccept"
+ :before-upload="fileBeforeUpload"
+ :http-request="alipayPublicCertUpload"
+ :limit="1"
+ action=""
+ >
+ <el-button type="primary">
+ <Icon class="mr-5px" icon="ep:upload" />
+ 鐐瑰嚮涓婁紶
+ </el-button>
+ </el-upload>
+ </el-form-item>
+ <el-form-item label="鏍硅瘉涔�" label-width="180px" prop="config.rootCertContent">
+ <el-input
+ v-model="formData.config.rootCertContent"
+ :autosize="{ minRows: 8, maxRows: 8 }"
+ :style="{ width: '100%' }"
+ placeholder="璇蜂笂浼犳牴璇佷功"
+ readonly
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="" label-width="180px">
+ <el-upload
+ ref="privateCertContentFile"
+ :accept="fileAccept"
+ :before-upload="fileBeforeUpload"
+ :http-request="rootCertUpload"
+ :limit="1"
+ action=""
+ >
+ <el-button type="primary">
+ <Icon class="mr-5px" icon="ep:upload" />
+ 鐐瑰嚮涓婁紶
+ </el-button>
+ </el-upload>
+ </el-form-item>
+ </div>
+
+ <el-form-item label="鎺ュ彛鍐呭鍔犲瘑鏂瑰紡" label-width="180px" prop="config.encryptType">
+ <el-radio-group v-model="formData.config.encryptType">
+ <el-radio key="NONE" label="">鏃犲姞瀵�</el-radio>
+ <el-radio key="AES" label="AES">AES</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <div v-if="formData.config.encryptType === 'AES'">
+ <el-form-item label="鎺ュ彛鍐呭鍔犲瘑瀵嗛挜" label-width="180px" prop="config.encryptKey">
+ <el-input
+ v-model="formData.config.encryptKey"
+ clearable
+ placeholder="璇疯緭鍏ユ帴鍙e唴瀹瑰姞瀵嗗瘑閽�"
+ />
+ </el-form-item>
+ </div>
+
+ <el-form-item label="澶囨敞" label-width="180px" prop="remark">
+ <el-input v-model="formData.remark" :style="{ width: '100%' }" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ </div>
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ChannelApi from '@/api/pay/channel'
+
+defineOptions({ name: 'AlipayChannelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref<any>({
+ appId: '',
+ code: '',
+ status: undefined,
+ feeRate: undefined,
+ remark: '',
+ config: {
+ appId: '',
+ serverUrl: null,
+ signType: '',
+ mode: null,
+ privateKey: '',
+ alipayPublicKey: '',
+ appCertContent: '',
+ alipayPublicCertContent: '',
+ rootCertContent: '',
+ encryptType: '',
+ encryptKey: ''
+ }
+})
+const formRules = {
+ feeRate: [{ required: true, message: '璇疯緭鍏ユ笭閬撹垂鐜�', trigger: 'blur' }],
+ status: [{ required: true, message: '娓犻亾鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ 'config.appId': [{ required: true, message: '璇疯緭鍏ュ紑鏀惧钩鍙颁笂鍒涘缓鐨勫簲鐢ㄧ殑 ID', trigger: 'blur' }],
+ 'config.serverUrl': [{ required: true, message: '璇蜂紶鍏ョ綉鍏冲湴鍧�', trigger: 'blur' }],
+ 'config.signType': [{ required: true, message: '璇蜂紶鍏ョ鍚嶇畻娉曠被鍨�', trigger: 'blur' }],
+ 'config.mode': [{ required: true, message: '鍏挜绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.privateKey': [{ required: true, message: '璇疯緭鍏ュ晢鎴风閽�', trigger: 'blur' }],
+ 'config.alipayPublicKey': [
+ { required: true, message: '璇疯緭鍏ユ敮浠樺疂鍏挜瀛楃涓�', trigger: 'blur' }
+ ],
+ 'config.appCertContent': [{ required: true, message: '璇蜂笂浼犲晢鎴峰叕閽ュ簲鐢ㄨ瘉涔�', trigger: 'blur' }],
+ 'config.alipayPublicCertContent': [
+ { required: true, message: '璇蜂笂浼犳敮浠樺疂鍏挜璇佷功', trigger: 'blur' }
+ ],
+ 'config.rootCertContent': [{ required: true, message: '璇蜂笂浼犳寚瀹氭牴璇佷功', trigger: 'blur' }],
+ 'config.encryptKey': [{ required: true, message: '璇疯緭鍏ユ帴鍙e唴瀹瑰姞瀵嗗瘑閽�', trigger: 'blur' }]
+}
+const fileAccept = '.crt'
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (appId, code) => {
+ dialogVisible.value = true
+ formLoading.value = true
+ resetForm(appId, code)
+ // 鍔犺浇鏁版嵁
+ try {
+ const data = await ChannelApi.getChannel(appId, code)
+ if (data && data.id) {
+ formData.value = data
+ formData.value.config = JSON.parse(data.config)
+ }
+ dialogTitle.value = !formData.value.id ? '鍒涘缓鏀粯娓犻亾' : '缂栬緫鏀粯娓犻亾'
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
+ data.config = JSON.stringify(formData.value.config)
+ if (!data.id) {
+ await ChannelApi.createChannel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ChannelApi.updateChannel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = (appId, code) => {
+ formData.value = {
+ appId: appId,
+ code: code,
+ status: CommonStatusEnum.ENABLE,
+ remark: '',
+ feeRate: null,
+ config: {
+ appId: '',
+ serverUrl: null,
+ signType: 'RSA2',
+ mode: null,
+ privateKey: '',
+ alipayPublicKey: '',
+ appCertContent: '',
+ alipayPublicCertContent: '',
+ rootCertContent: '',
+ encryptType: '',
+ encryptKey: ''
+ }
+ }
+ formRef.value?.resetFields()
+}
+
+const fileBeforeUpload = (file) => {
+ let format = '.' + file.name.split('.')[1]
+ if (format !== fileAccept) {
+ message.error(`璇蜂笂浼犳寚瀹氭牸寮�"${fileAccept}"鏂囦欢`)
+ return false
+ }
+ let isRightSize = file.size / 1024 / 1024 < 2
+ if (!isRightSize) {
+ message.error('鏂囦欢澶у皬瓒呰繃 2MB')
+ }
+ return isRightSize
+}
+
+const appCertUpload = (event) => {
+ const readFile = new FileReader()
+ readFile.onload = (e: any) => {
+ formData.value.config.appCertContent = e.target.result
+ }
+ readFile.readAsText(event.file)
+}
+
+const alipayPublicCertUpload = (event) => {
+ const readFile = new FileReader()
+ readFile.onload = (e: any) => {
+ formData.value.config.alipayPublicCertContent = e.target.result
+ }
+ readFile.readAsText(event.file)
+}
+
+const rootCertUpload = (event) => {
+ const readFile = new FileReader()
+ readFile.onload = (e: any) => {
+ formData.value.config.rootCertContent = e.target.result
+ }
+ readFile.readAsText(event.file)
+}
+</script>
diff --git a/src/views/pay/app/components/channel/MockChannelForm.vue b/src/views/pay/app/components/channel/MockChannelForm.vue
new file mode 100644
index 0000000..f7ebe7f
--- /dev/null
+++ b/src/views/pay/app/components/channel/MockChannelForm.vue
@@ -0,0 +1,122 @@
+<template>
+ <div>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="娓犻亾鐘舵��" label-width="180px" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="parseInt(dict.value)"
+ :value="parseInt(dict.value)"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" label-width="180px" prop="remark">
+ <el-input v-model="formData.remark" :style="{ width: '100%' }" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ </div>
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ChannelApi from '@/api/pay/channel'
+
+defineOptions({ name: 'MockChannelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref<any>({
+ appId: '',
+ code: '',
+ status: undefined,
+ feeRate: 0,
+ remark: '',
+ config: {
+ name: 'mock-conf'
+ }
+})
+const formRules = {
+ status: [{ required: true, message: '娓犻亾鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+}
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (appId, code) => {
+ dialogVisible.value = true
+ formLoading.value = true
+ resetForm(appId, code)
+ // 鍔犺浇鏁版嵁
+ try {
+ const data = await ChannelApi.getChannel(appId, code)
+
+ if (data && data.id) {
+ formData.value = data
+ formData.value.config = JSON.parse(data.config)
+ }
+ dialogTitle.value = !formData.value.id ? '鍒涘缓鏀粯娓犻亾' : '缂栬緫鏀粯娓犻亾'
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
+ data.config = JSON.stringify(formData.value.config)
+ if (!data.id) {
+ await ChannelApi.createChannel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ChannelApi.updateChannel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = (appId, code) => {
+ formData.value = {
+ appId: appId,
+ code: code,
+ status: CommonStatusEnum.ENABLE,
+ remark: '',
+ feeRate: 0,
+ config: {
+ name: 'mock-conf'
+ }
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/pay/app/components/channel/WalletChannelForm.vue b/src/views/pay/app/components/channel/WalletChannelForm.vue
new file mode 100644
index 0000000..2f636f4
--- /dev/null
+++ b/src/views/pay/app/components/channel/WalletChannelForm.vue
@@ -0,0 +1,122 @@
+<template>
+ <div>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="娓犻亾鐘舵��" label-width="180px" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="parseInt(dict.value)"
+ :value="parseInt(dict.value)"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" label-width="180px" prop="remark">
+ <el-input v-model="formData.remark" :style="{ width: '100%' }" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ </div>
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ChannelApi from '@/api/pay/channel'
+
+defineOptions({ name: 'WalletChannelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref<any>({
+ appId: '',
+ code: '',
+ status: undefined,
+ feeRate: 0,
+ remark: '',
+ config: {
+ name: 'wallet-conf'
+ }
+})
+const formRules = {
+ status: [{ required: true, message: '娓犻亾鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+}
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (appId, code) => {
+ dialogVisible.value = true
+ formLoading.value = true
+ resetForm(appId, code)
+ // 鍔犺浇鏁版嵁
+ try {
+ const data = await ChannelApi.getChannel(appId, code)
+
+ if (data && data.id) {
+ formData.value = data
+ formData.value.config = JSON.parse(data.config)
+ }
+ dialogTitle.value = !formData.value.id ? '鍒涘缓鏀粯娓犻亾' : '缂栬緫鏀粯娓犻亾'
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
+ data.config = JSON.stringify(formData.value.config)
+ if (!data.id) {
+ await ChannelApi.createChannel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ChannelApi.updateChannel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = (appId, code) => {
+ formData.value = {
+ appId: appId,
+ code: code,
+ status: CommonStatusEnum.ENABLE,
+ remark: '',
+ feeRate: 0,
+ config: {
+ name: 'wallet-conf'
+ }
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/pay/app/components/channel/WeixinChannelForm.vue b/src/views/pay/app/components/channel/WeixinChannelForm.vue
new file mode 100644
index 0000000..fd91554
--- /dev/null
+++ b/src/views/pay/app/components/channel/WeixinChannelForm.vue
@@ -0,0 +1,377 @@
+<template>
+ <div>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ >
+ <el-form-item label="娓犻亾璐圭巼" label-width="180px" prop="feeRate">
+ <el-input
+ v-model="formData.feeRate"
+ :style="{ width: '100%' }"
+ clearable
+ placeholder="璇疯緭鍏ユ笭閬撹垂鐜�"
+ >
+ <template #append>%</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="寰俊 APPID" label-width="180px" prop="config.appId">
+ <el-input
+ v-model="formData.config.appId"
+ :style="{ width: '100%' }"
+ clearable
+ placeholder="璇疯緭鍏ュ井淇� APPID"
+ />
+ </el-form-item>
+ <el-form-item label-width="180px">
+ <a
+ href="https://pay.weixin.qq.com/index.php/extend/merchant_appid/mapay_platform/account_manage"
+ target="_blank"
+ >
+ 鍓嶅線寰俊鍟嗘埛骞冲彴鏌ョ湅 APPID
+ </a>
+ </el-form-item>
+ <el-form-item label="鍟嗘埛鍙�" label-width="180px" prop="config.mchId">
+ <el-input v-model="formData.config.mchId" :style="{ width: '100%' }" />
+ </el-form-item>
+
+ <el-form-item label-width="180px">
+ <a href="https://pay.weixin.qq.com/index.php/extend/pay_setting" target="_blank">
+ 鍓嶅線寰俊鍟嗘埛骞冲彴鏌ョ湅鍟嗘埛鍙�
+ </a>
+ </el-form-item>
+ <el-form-item label="娓犻亾鐘舵��" label-width="180px" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="parseInt(dict.value)"
+ :value="parseInt(dict.value)"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="API 鐗堟湰" label-width="180px" prop="config.apiVersion">
+ <el-radio-group v-model="formData.config.apiVersion">
+ <el-radio value="v2">v2</el-radio>
+ <el-radio value="v3">v3</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <div v-if="formData.config.apiVersion === 'v2'">
+ <el-form-item label="鍟嗘埛瀵嗛挜" label-width="180px" prop="config.mchKey">
+ <el-input v-model="formData.config.mchKey" clearable placeholder="璇疯緭鍏ュ晢鎴峰瘑閽�" />
+ </el-form-item>
+ <el-form-item
+ label="apiclient_cert.p12 璇佷功"
+ label-width="180px"
+ prop="config.keyContent"
+ >
+ <el-input
+ v-model="formData.config.keyContent"
+ :autosize="{ minRows: 2, maxRows: 4 }"
+ :style="{ width: '100%' }"
+ placeholder="璇蜂笂浼� apiclient_cert.p12 璇佷功"
+ readonly
+ type="textarea"
+ :rows="2"
+ />
+ </el-form-item>
+ <el-form-item label="" label-width="180px">
+ <el-upload
+ :before-upload="p12FileBeforeUpload"
+ :http-request="keyContentUpload"
+ :limit="1"
+ accept=".p12"
+ action=""
+ >
+ <el-button type="primary">
+ <Icon class="mr-5px" icon="ep:upload" />
+ 鐐瑰嚮涓婁紶
+ </el-button>
+ </el-upload>
+ </el-form-item>
+ </div>
+ <div v-if="formData.config.apiVersion === 'v3'">
+ <el-form-item label="API V3 瀵嗛挜" label-width="180px" prop="config.apiV3Key">
+ <el-input
+ v-model="formData.config.apiV3Key"
+ clearable
+ placeholder="璇疯緭鍏� API V3 瀵嗛挜"
+ />
+ </el-form-item>
+ <el-form-item
+ label="apiclient_key.pem 璇佷功"
+ label-width="180px"
+ prop="config.privateKeyContent"
+ >
+ <el-input
+ v-model="formData.config.privateKeyContent"
+ :autosize="{ minRows: 2, maxRows: 4 }"
+ :style="{ width: '100%' }"
+ placeholder="璇蜂笂浼� apiclient_key.pem 璇佷功"
+ readonly
+ type="textarea"
+ :rows="2"
+ />
+ </el-form-item>
+ <el-form-item label="" label-width="180px" prop="privateKeyContentFile">
+ <el-upload
+ ref="privateKeyContentFile"
+ :before-upload="pemFileBeforeUpload"
+ :http-request="privateKeyContentUpload"
+ :limit="1"
+ accept=".pem"
+ action=""
+ >
+ <el-button type="primary">
+ <Icon class="mr-5px" icon="ep:upload" />
+ 鐐瑰嚮涓婁紶
+ </el-button>
+ </el-upload>
+ </el-form-item>
+ <el-form-item label="璇佷功搴忓垪鍙�" label-width="180px" prop="config.certSerialNo">
+ <el-input
+ v-model="formData.config.certSerialNo"
+ clearable
+ placeholder="璇疯緭鍏ヨ瘉涔﹀簭鍒楀彿"
+ />
+ </el-form-item>
+ <el-form-item label-width="180px">
+ <a
+ href="https://pay.weixin.qq.com/index.php/core/cert/api_cert#/api-cert-manage"
+ target="_blank"
+ >
+ 鍓嶅線寰俊鍟嗘埛骞冲彴鏌ョ湅璇佷功搴忓垪鍙�
+ </a>
+ </el-form-item>
+ <el-form-item
+ label="public_key.pem 璇佷功"
+ label-width="180px"
+ prop="config.publicKeyContent"
+ >
+ <el-input
+ v-model="formData.config.publicKeyContent"
+ :autosize="{ minRows: 2, maxRows: 4 }"
+ :style="{ width: '100%' }"
+ placeholder="璇蜂笂浼� public_key.pem 璇佷功"
+ readonly
+ type="textarea"
+ :rows="2"
+ />
+ </el-form-item>
+ <el-form-item label="" label-width="180px" prop="publicKeyContentFile">
+ <el-upload
+ ref="publicKeyContentFile"
+ :before-upload="pemFileBeforeUpload"
+ :http-request="publicKeyContentUpload"
+ :limit="1"
+ accept=".pem"
+ action=""
+ >
+ <el-button type="primary">
+ <Icon class="mr-5px" icon="ep:upload" />
+ 鐐瑰嚮涓婁紶
+ </el-button>
+ </el-upload>
+ </el-form-item>
+ <el-form-item label="鍏挜 ID" label-width="180px" prop="config.publicKeyId">
+ <el-input v-model="formData.config.publicKeyId" clearable placeholder="璇疯緭鍏ュ叕閽� ID" />
+ </el-form-item>
+ <el-form-item label-width="180px">
+ <a href="https://pay.weixin.qq.com/doc/v3/merchant/4012153196" target="_blank">
+ 寰俊鏀粯鍏挜浜у搧绠�浠嬪強浣跨敤璇存槑
+ </a>
+ </el-form-item>
+ </div>
+ <el-form-item label="澶囨敞" label-width="180px" prop="remark">
+ <el-input v-model="formData.remark" :style="{ width: '100%' }" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+ </div>
+</template>
+<script lang="ts" setup>
+import { CommonStatusEnum } from '@/utils/constants'
+import { DICT_TYPE, getDictOptions } from '@/utils/dict'
+import * as ChannelApi from '@/api/pay/channel'
+
+defineOptions({ name: 'WeixinChannelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref<any>({
+ appId: '',
+ code: '',
+ status: undefined,
+ feeRate: undefined,
+ remark: '',
+ config: {
+ appId: '',
+ mchId: '',
+ apiVersion: '',
+ mchKey: '',
+ keyContent: '',
+ privateKeyContent: '',
+ certSerialNo: '',
+ apiV3Key: '',
+ publicKeyContent: '',
+ publicKeyId: ''
+ }
+})
+const formRules = {
+ feeRate: [{ required: true, message: '璇疯緭鍏ユ笭閬撹垂鐜�', trigger: 'blur' }],
+ status: [{ required: true, message: '娓犻亾鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ 'config.mchId': [{ required: true, message: '璇蜂紶鍏ュ晢鎴峰彿', trigger: 'blur' }],
+ 'config.appId': [{ required: true, message: '璇疯緭鍏ュ叕浼楀彿APPID', trigger: 'blur' }],
+ 'config.apiVersion': [{ required: true, message: 'API鐗堟湰涓嶈兘涓虹┖', trigger: 'blur' }],
+ 'config.mchKey': [{ required: true, message: '璇疯緭鍏ュ晢鎴峰瘑閽�', trigger: 'blur' }],
+ 'config.keyContent': [
+ { required: true, message: '璇蜂笂浼� apiclient_cert.p12 璇佷功', trigger: 'blur' }
+ ],
+ 'config.privateKeyContent': [
+ { required: true, message: '璇蜂笂浼� apiclient_key.pem 璇佷功', trigger: 'blur' }
+ ],
+ 'config.certSerialNo': [{ required: true, message: '璇疯緭鍏ヨ瘉涔﹀簭鍒楀彿', trigger: 'blur' }],
+ 'config.publicKeyId': [{ required: true, message: '璇疯緭鍏ュ叕閽� ID', trigger: 'blur' }],
+ 'config.apiV3Key': [{ required: true, message: '璇蜂笂浼� api V3 瀵嗛挜鍊�', trigger: 'blur' }]
+}
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (appId, code) => {
+ dialogVisible.value = true
+ formLoading.value = true
+ resetForm(appId, code)
+ // 鍔犺浇鏁版嵁
+ try {
+ const data = await ChannelApi.getChannel(appId, code)
+ if (data && data.id) {
+ formData.value = data
+ formData.value.config = JSON.parse(data.config)
+ }
+ dialogTitle.value = !formData.value.id ? '鍒涘缓鏀粯娓犻亾' : '缂栬緫鏀粯娓犻亾'
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value } as unknown as ChannelApi.ChannelVO
+ data.config = JSON.stringify(formData.value.config)
+ if (!data.id) {
+ await ChannelApi.createChannel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ChannelApi.updateChannel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = (appId, code) => {
+ formData.value = {
+ appId: appId,
+ code: code,
+ status: CommonStatusEnum.ENABLE,
+ feeRate: undefined,
+ remark: '',
+ config: {
+ appId: '',
+ mchId: '',
+ apiVersion: '',
+ mchKey: '',
+ keyContent: '',
+ privateKeyContent: '',
+ certSerialNo: '',
+ apiV3Key: '',
+ publicKeyContent: '',
+ publicKeyId: ''
+ }
+ }
+ formRef.value?.resetFields()
+}
+
+/**
+ * apiclient_cert.p12銆乤piclient_key.pem 涓婁紶鍓嶇殑鏍¢獙
+ */
+const fileBeforeUpload = (file, fileAccept) => {
+ let format = '.' + file.name.split('.')[1]
+ if (format !== fileAccept) {
+ message.error('璇蜂笂浼犳寚瀹氭牸寮�"' + fileAccept + '"鏂囦欢')
+ return false
+ }
+ let isRightSize = file.size / 1024 / 1024 < 2
+ if (!isRightSize) {
+ message.error('鏂囦欢澶у皬瓒呰繃 2MB')
+ }
+ return isRightSize
+}
+
+const p12FileBeforeUpload = (file) => {
+ fileBeforeUpload(file, '.p12')
+}
+
+const pemFileBeforeUpload = (file) => {
+ fileBeforeUpload(file, '.pem')
+}
+
+/**
+ * 璇诲彇 apiclient_key.pem 鍒� privateKeyContent 瀛楁
+ */
+const privateKeyContentUpload = async (event) => {
+ const readFile = new FileReader()
+ readFile.onload = (e: any) => {
+ formData.value.config.privateKeyContent = e.target.result
+ }
+ readFile.readAsText(event.file)
+}
+
+/**
+ * 璇诲彇 apiclient_cert.p12 鍒� keyContent 瀛楁
+ */
+const keyContentUpload = async (event) => {
+ const readFile = new FileReader()
+ readFile.onload = (e: any) => {
+ formData.value.config.keyContent = e.target.result.split(',')[1]
+ }
+ readFile.readAsDataURL(event.file) // 璇绘垚 base64
+}
+
+/**
+ * 璇诲彇 public_key.pem 鍒� publicKeyContent 瀛楁
+ */
+const publicKeyContentUpload = async (event) => {
+ const readFile = new FileReader()
+ readFile.onload = (e: any) => {
+ formData.value.config.publicKeyContent = e.target.result
+ }
+ readFile.readAsText(event.file)
+}
+</script>
diff --git a/src/views/pay/app/index.vue b/src/views/pay/app/index.vue
new file mode 100644
index 0000000..37604ed
--- /dev/null
+++ b/src/views/pay/app/index.vue
@@ -0,0 +1,372 @@
+<template>
+ <doc-alert title="鏀粯鍔熻兘寮�鍚�" url="https://doc.iocoder.cn/pay/build/" />
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="搴旂敤鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ簲鐢ㄥ悕"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨寮�鍚姸鎬�"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button v-hasPermi="['pay:app:create']" plain type="primary" @click="openForm('create')">
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column align="center" label="搴旂敤鏍囪瘑" prop="appKey" />
+ <el-table-column align="center" label="搴旂敤鍚�" min-width="90" prop="name" />
+ <el-table-column align="center" label="寮�鍚姸鎬�" prop="status">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鏀粯瀹濋厤缃�">
+ <el-table-column
+ v-for="channel in alipayChannels"
+ :key="channel.code"
+ :label="channel.name.replace('鏀粯瀹�', '')"
+ align="center"
+ >
+ <template #default="scope">
+ <el-button
+ v-if="isChannelExists(scope.row.channelCodes, channel.code)"
+ circle
+ size="small"
+ type="success"
+ @click="openChannelForm(scope.row, channel.code)"
+ >
+ <Icon icon="ep:check" />
+ </el-button>
+ <el-button
+ v-else
+ circle
+ size="small"
+ type="danger"
+ @click="openChannelForm(scope.row, channel.code)"
+ >
+ <Icon icon="ep:close" />
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table-column>
+ <el-table-column align="center" label="寰俊閰嶇疆">
+ <el-table-column
+ v-for="channel in wxChannels"
+ :key="channel.code"
+ :label="channel.name.replace('寰俊', '')"
+ align="center"
+ >
+ <template #default="scope">
+ <el-button
+ v-if="isChannelExists(scope.row.channelCodes, channel.code)"
+ circle
+ size="small"
+ type="success"
+ @click="openChannelForm(scope.row, channel.code)"
+ >
+ <Icon icon="ep:check" />
+ </el-button>
+ <el-button
+ v-else
+ circle
+ size="small"
+ type="danger"
+ @click="openChannelForm(scope.row, channel.code)"
+ >
+ <Icon icon="ep:close" />
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table-column>
+ <el-table-column align="center" label="閽卞寘鏀粯閰嶇疆">
+ <el-table-column :label="PayChannelEnum.WALLET.name" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.WALLET.code)"
+ circle
+ size="small"
+ type="success"
+ @click="openChannelForm(scope.row, PayChannelEnum.WALLET.code)"
+ >
+ <Icon icon="ep:check" />
+ </el-button>
+ <el-button
+ v-else
+ circle
+ size="small"
+ type="danger"
+ @click="openChannelForm(scope.row, PayChannelEnum.WALLET.code)"
+ >
+ <Icon icon="ep:close" />
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table-column>
+ <el-table-column align="center" label="妯℃嫙鏀粯閰嶇疆">
+ <el-table-column :label="PayChannelEnum.MOCK.name" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="isChannelExists(scope.row.channelCodes, PayChannelEnum.MOCK.code)"
+ circle
+ size="small"
+ type="success"
+ @click="openChannelForm(scope.row, PayChannelEnum.MOCK.code)"
+ >
+ <Icon icon="ep:check" />
+ </el-button>
+ <el-button
+ v-else
+ circle
+ size="small"
+ type="danger"
+ @click="openChannelForm(scope.row, PayChannelEnum.MOCK.code)"
+ >
+ <Icon icon="ep:close" />
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table-column>
+ <el-table-column align="center" fixed="right" label="鎿嶄綔" min-width="110">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['pay:app:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['pay:app:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <AppForm ref="formRef" @success="getList" />
+ <AlipayChannelForm ref="alipayFormRef" @success="getList" />
+ <WeixinChannelForm ref="weixinFormRef" @success="getList" />
+ <MockChannelForm ref="mockFormRef" @success="getList" />
+ <WalletChannelForm ref="walletFormRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as AppApi from '@/api/pay/app'
+import AppForm from './components/AppForm.vue'
+import { CommonStatusEnum, PayChannelEnum } from '@/utils/constants'
+import AlipayChannelForm from './components/channel/AlipayChannelForm.vue'
+import WeixinChannelForm from './components/channel/WeixinChannelForm.vue'
+import MockChannelForm from './components/channel/MockChannelForm.vue'
+import WalletChannelForm from './components/channel/WalletChannelForm.vue'
+
+defineOptions({ name: 'PayApp' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ remark: undefined,
+ payNotifyUrl: undefined,
+ refundNotifyUrl: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+const alipayChannels = [
+ PayChannelEnum.ALIPAY_APP,
+ PayChannelEnum.ALIPAY_PC,
+ PayChannelEnum.ALIPAY_WAP,
+ PayChannelEnum.ALIPAY_QR,
+ PayChannelEnum.ALIPAY_BAR
+]
+
+const wxChannels = [
+ PayChannelEnum.WX_LITE,
+ PayChannelEnum.WX_PUB,
+ PayChannelEnum.WX_APP,
+ PayChannelEnum.WX_NATIVE,
+ PayChannelEnum.WX_WAP,
+ PayChannelEnum.WX_BAR
+]
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await AppApi.getAppPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 搴旂敤鐘舵�佷慨鏀� */
+const handleStatusChange = async (row: any) => {
+ let text = row.status === CommonStatusEnum.ENABLE ? '鍚敤' : '鍋滅敤'
+ try {
+ await message.confirm('纭瑕�"' + text + '""' + row.name + '"搴旂敤鍚�?')
+ await AppApi.changeAppStatus({ id: row.id, status: row.status })
+ message.success(text + '鎴愬姛')
+ } catch {
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await AppApi.deleteApp(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/**
+ * 鏍规嵁娓犻亾缂栫爜鍒ゆ柇娓犻亾鍒楄〃涓槸鍚﹀瓨鍦�
+ *
+ * @param channels 娓犻亾鍒楄〃
+ * @param channelCode 娓犻亾缂栫爜
+ */
+const isChannelExists = (channels, channelCode) => {
+ if (!channels) {
+ return false
+ }
+ return channels.indexOf(channelCode) !== -1
+}
+
+/**
+ * 鏂板鏀粯娓犻亾淇℃伅
+ */
+const alipayFormRef = ref()
+const weixinFormRef = ref()
+const mockFormRef = ref()
+const walletFormRef = ref()
+const channelParam = reactive({
+ appId: null, // 搴旂敤 ID
+ payCode: null // 娓犻亾缂栫爜
+})
+const openChannelForm = async (row, payCode) => {
+ channelParam.appId = row.id
+ channelParam.payCode = payCode
+ if (payCode.indexOf('alipay_') === 0) {
+ alipayFormRef.value.open(row.id, payCode)
+ return
+ }
+ if (payCode.indexOf('wx_') === 0) {
+ weixinFormRef.value.open(row.id, payCode)
+ return
+ }
+ if (payCode.indexOf('mock') === 0) {
+ mockFormRef.value.open(row.id, payCode)
+ }
+ if (payCode.indexOf('wallet') === 0) {
+ walletFormRef.value.open(row.id, payCode)
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+})
+</script>
diff --git a/src/views/pay/cashier/index.vue b/src/views/pay/cashier/index.vue
new file mode 100644
index 0000000..f07536f
--- /dev/null
+++ b/src/views/pay/cashier/index.vue
@@ -0,0 +1,494 @@
+<template>
+ <!-- 鏀粯淇℃伅 -->
+ <el-card v-loading="loading">
+ <el-descriptions title="鏀粯淇℃伅" :column="3" border>
+ <el-descriptions-item label="鏀粯鍗曞彿">{{ payOrder.id }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗗搧鏍囬">{{ payOrder.subject }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗗搧鍐呭">{{ payOrder.body }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯閲戦">
+ 锟{ (payOrder.price / 100.0).toFixed(2) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(payOrder.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="杩囨湡鏃堕棿">
+ {{ formatDate(payOrder.expireTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+
+ <!-- 鏀粯閫夋嫨妗� -->
+ <el-card style="margin-top: 10px" v-loading="submitLoading" element-loading-text="鎻愪氦鏀粯涓�...">
+ <!-- 鏀粯瀹� -->
+ <el-descriptions title="閫夋嫨鏀粯瀹濇敮浠�" />
+ <div class="pay-channel-container">
+ <div
+ class="box"
+ v-for="channel in channelsAlipay"
+ :key="channel.code"
+ @click="submit(channel.code)"
+ >
+ <img :src="channel.icon" />
+ <div class="title">{{ channel.name }}</div>
+ </div>
+ </div>
+ <!-- 寰俊鏀粯 -->
+ <el-descriptions title="閫夋嫨寰俊鏀粯" style="margin-top: 20px" />
+ <div class="pay-channel-container">
+ <div
+ class="box"
+ v-for="channel in channelsWechat"
+ :key="channel.code"
+ @click="submit(channel.code)"
+ >
+ <img :src="channel.icon" />
+ <div class="title">{{ channel.name }}</div>
+ </div>
+ </div>
+ <!-- 鍏跺畠鏀粯 -->
+ <el-descriptions title="閫夋嫨鍏跺畠鏀粯" style="margin-top: 20px" />
+ <div class="pay-channel-container">
+ <div
+ class="box"
+ v-for="channel in channelsMock"
+ :key="channel.code"
+ @click="submit(channel.code)"
+ >
+ <img :src="channel.icon" />
+ <div class="title">{{ channel.name }}</div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 灞曠ず褰㈠紡锛氫簩缁寸爜 URL -->
+ <Dialog
+ :title="qrCode.title"
+ v-model="qrCode.visible"
+ width="350px"
+ append-to-body
+ :close-on-press-escape="false"
+ >
+ <Qrcode :text="qrCode.url" :width="310" />
+ </Dialog>
+
+ <!-- 灞曠ず褰㈠紡锛欱arCode 鏉″舰鐮� -->
+ <Dialog
+ :title="barCode.title"
+ v-model="barCode.visible"
+ width="500px"
+ append-to-body
+ :close-on-press-escape="false"
+ >
+ <el-form ref="form" label-width="80px">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鏉″舰鐮�" prop="name">
+ <el-input v-model="barCode.value" placeholder="璇疯緭鍏ユ潯褰㈢爜" required />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <div style="text-align: right">
+ 鎴栦娇鐢�
+ <el-link
+ type="danger"
+ target="_blank"
+ href="https://baike.baidu.com/item/鏉$爜鏀粯/10711903"
+ >
+ (鎵爜鏋�/鎵爜鐩�)
+ </el-link>
+ 鎵爜
+ </div>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button
+ type="primary"
+ @click="submit0(barCode.channelCode)"
+ :disabled="barCode.value.length === 0"
+ >
+ 纭鏀粯
+ </el-button>
+ <el-button @click="barCode.visible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { Qrcode } from '@/components/Qrcode'
+import * as PayOrderApi from '@/api/pay/order'
+import { PayChannelEnum, PayDisplayModeEnum, PayOrderStatusEnum } from '@/utils/constants'
+import { formatDate } from '@/utils/formatTime'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 瀵煎叆鍥炬爣
+import svg_alipay_pc from '@/assets/svgs/pay/icon/alipay_pc.svg'
+import svg_alipay_wap from '@/assets/svgs/pay/icon/alipay_wap.svg'
+import svg_alipay_app from '@/assets/svgs/pay/icon/alipay_app.svg'
+import svg_alipay_qr from '@/assets/svgs/pay/icon/alipay_qr.svg'
+import svg_alipay_bar from '@/assets/svgs/pay/icon/alipay_bar.svg'
+import svg_wx_pub from '@/assets/svgs/pay/icon/wx_pub.svg'
+import svg_wx_lite from '@/assets/svgs/pay/icon/wx_lite.svg'
+import svg_wx_app from '@/assets/svgs/pay/icon/wx_app.svg'
+import svg_wx_native from '@/assets/svgs/pay/icon/wx_native.svg'
+import svg_wx_bar from '@/assets/svgs/pay/icon/wx_bar.svg'
+import svg_wallet from '@/assets/svgs/pay/icon/wallet.svg'
+import svg_mock from '@/assets/svgs/pay/icon/mock.svg'
+
+defineOptions({ name: 'PayCashier' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const route = useRoute() // 璺敱
+const { push, currentRoute } = useRouter() // 璺敱
+const { delView } = useTagsViewStore() // 瑙嗗浘鎿嶄綔
+
+const id = ref(undefined) // 鏀粯鍗曞彿
+const returnUrl = ref<string | undefined>(undefined) // 鏀粯瀹岀殑鍥炶皟鍦板潃
+const loading = ref(false) // 鏀粯淇℃伅鐨� loading
+const payOrder = ref({}) // 鏀粯淇℃伅
+const channelsAlipay = [
+ {
+ name: '鏀粯瀹� PC 缃戠珯鏀粯',
+ icon: svg_alipay_pc,
+ code: 'alipay_pc'
+ },
+ {
+ name: '鏀粯瀹� Wap 缃戠珯鏀粯',
+ icon: svg_alipay_wap,
+ code: 'alipay_wap'
+ },
+ {
+ name: '鏀粯瀹� App 缃戠珯鏀粯',
+ icon: svg_alipay_app,
+ code: 'alipay_app'
+ },
+ {
+ name: '鏀粯瀹濇壂鐮佹敮浠�',
+ icon: svg_alipay_qr,
+ code: 'alipay_qr'
+ },
+ {
+ name: '鏀粯瀹濇潯鐮佹敮浠�',
+ icon: svg_alipay_bar,
+ code: 'alipay_bar'
+ }
+]
+const channelsWechat = [
+ {
+ name: '寰俊鍏紬鍙锋敮浠�',
+ icon: svg_wx_pub,
+ code: 'wx_pub'
+ },
+ {
+ name: '寰俊灏忕▼搴忔敮浠�',
+ icon: svg_wx_lite,
+ code: 'wx_lite'
+ },
+ {
+ name: '寰俊 App 鏀粯',
+ icon: svg_wx_app,
+ code: 'wx_app'
+ },
+ {
+ name: '寰俊鎵爜鏀粯',
+ icon: svg_wx_native,
+ code: 'wx_native'
+ },
+ {
+ name: '寰俊鏉$爜鏀粯',
+ icon: svg_wx_bar,
+ code: 'wx_bar'
+ }
+]
+const channelsMock = [
+ {
+ name: '閽卞寘鏀粯',
+ icon: svg_wallet,
+ code: 'wallet'
+ },
+ {
+ name: '妯℃嫙鏀粯',
+ icon: svg_mock,
+ code: 'mock'
+ }
+]
+
+const submitLoading = ref(false) // 鎻愪氦鏀粯鐨� loading
+const interval = ref<any>(undefined) // 瀹氭椂浠诲姟锛岃疆璇㈡槸鍚﹀畬鎴愭敮浠�
+const qrCode = ref({
+ // 灞曠ず褰㈠紡锛氫簩缁寸爜
+ url: '',
+ title: '',
+ visible: false
+})
+const barCode = ref({
+ // 灞曠ず褰㈠紡锛氭潯褰㈢爜
+ channelCode: '',
+ value: '',
+ title: '',
+ visible: false
+})
+
+/** 鑾峰緱鏀粯淇℃伅 */
+const getDetail = async () => {
+ // 1.1 鏈紶閫掕鍗曠紪鍙�
+ if (!id.value) {
+ message.error('鏈紶閫掓敮浠樺崟鍙凤紝鏃犳硶鏌ョ湅瀵瑰簲鐨勬敮浠樹俊鎭�')
+ goReturnUrl('cancel')
+ return
+ }
+ const data = await PayOrderApi.getOrder(id.value, true)
+ // 1.2 鏃犳硶鏌ヨ鍒版敮浠樹俊鎭�
+ if (!data) {
+ message.error('鏀粯璁㈠崟涓嶅瓨鍦紝璇锋鏌ワ紒')
+ goReturnUrl('cancel')
+ return
+ }
+ // 1.3 濡傛灉宸叉敮浠樸�佹垨鑰呭凡鍏抽棴锛屽垯鐩存帴璺宠浆
+ if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+ message.success('鏀粯鎴愬姛')
+ goReturnUrl('success')
+ return
+ } else if (data.status === PayOrderStatusEnum.CLOSED.status) {
+ message.error('鏃犳硶鏀粯锛屽師鍥狅細璁㈠崟宸插叧闂�')
+ goReturnUrl('close')
+ return
+ }
+ // 2. 姝e父灞曠ず鏀粯淇℃伅
+ payOrder.value = data
+}
+
+/** 鎻愪氦鏀粯 */
+const submit = (channelCode) => {
+ // 鏉″舰鐮佹敮浠橈紝闇�瑕佺壒娈婂鐞�
+ if (channelCode === PayChannelEnum.ALIPAY_BAR.code) {
+ barCode.value = {
+ channelCode: channelCode,
+ value: '',
+ title: '鈥滄敮浠樺疂鈥濇潯鐮佹敮浠�',
+ visible: true
+ }
+ return
+ }
+ if (channelCode === PayChannelEnum.WX_BAR.code) {
+ barCode.value = {
+ channelCode: channelCode,
+ value: '',
+ title: '鈥滃井淇♀�濇潯鐮佹敮浠�',
+ visible: true
+ }
+ return
+ }
+
+ // 寰俊鍏紬鍙枫�佸皬绋嬪簭鏀粯锛屾棤娉曞湪 PC 缃戦〉涓繘琛�
+ if (channelCode === PayChannelEnum.WX_PUB.code) {
+ message.error('寰俊鍏紬鍙锋敮浠橈細涓嶆敮鎸� PC 缃戠珯')
+ return
+ }
+ if (channelCode === PayChannelEnum.WX_LITE.code) {
+ message.error('寰俊灏忕▼搴忥細涓嶆敮鎸� PC 缃戠珯')
+ return
+ }
+
+ // 榛樿鐨勬彁浜ゅ鐞�
+ submit0(channelCode)
+}
+
+const submit0 = async (channelCode) => {
+ submitLoading.value = true
+ try {
+ const formData = {
+ id: id.value,
+ channelCode: channelCode,
+ returnUrl: location.href, // 鏀粯鎴愬姛鍚庯紝鏀粯娓犻亾璺宠浆鍥炲綋鍓嶉〉锛涘啀鐢卞綋鍓嶉〉锛岃烦杞洖 {@link returnUrl} 瀵瑰簲鐨勫湴鍧�
+ ...buildSubmitParam(channelCode)
+ }
+ const data = await PayOrderApi.submitOrder(formData)
+ // 鐩存帴杩斿洖宸叉敮浠樼殑鎯呭喌锛屼緥濡傝鎵爜鏀粯
+ if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+ clearQueryInterval()
+ message.success('鏀粯鎴愬姛锛�')
+ goReturnUrl('success')
+ return
+ }
+
+ // 灞曠ず瀵瑰簲鐨勭晫闈�
+ if (data.displayMode === PayDisplayModeEnum.URL.mode) {
+ displayUrl(channelCode, data)
+ } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
+ displayQrCode(channelCode, data)
+ } else if (data.displayMode === PayDisplayModeEnum.APP.mode) {
+ displayApp(channelCode)
+ }
+
+ // 鎵撳紑杞浠诲姟
+ createQueryInterval()
+ } finally {
+ submitLoading.value = false
+ }
+}
+
+/** 鏋勫缓鎻愪氦鏀粯鐨勯澶栧弬鏁� */
+const buildSubmitParam = (channelCode) => {
+ // 鈶� 鏀粯瀹� BarCode 鏀粯鏃讹紝闇�瑕佷紶閫� authCode 鏉″舰鐮�
+ if (channelCode === PayChannelEnum.ALIPAY_BAR.code) {
+ return {
+ channelExtras: {
+ auth_code: barCode.value.value
+ }
+ }
+ }
+ // 鈶� 寰俊 BarCode 鏀粯鏃讹紝闇�瑕佷紶閫� authCode 鏉″舰鐮�
+ if (channelCode === PayChannelEnum.WX_BAR.code) {
+ return {
+ channelExtras: {
+ authCode: barCode.value.value
+ }
+ }
+ }
+ return {}
+}
+
+/** 鎻愪氦鏀粯鍚庯紝URL 鐨勫睍绀哄舰寮� */
+const displayUrl = (_channelCode, data) => {
+ location.href = data.displayContent
+ submitLoading.value = false
+}
+
+/** 鎻愪氦鏀粯鍚庯紙鎵爜鏀粯锛� */
+const displayQrCode = (channelCode, data) => {
+ let title = '璇蜂娇鐢ㄦ墜鏈烘祻瑙堝櫒鈥滄壂涓�鎵��'
+ if (channelCode === PayChannelEnum.ALIPAY_WAP.code) {
+ // 鑰冭檻鍒� WAP 娴嬭瘯锛屾墍浠ュ紩瀵兼墜鏈烘祻瑙堝櫒鎼�
+ } else if (channelCode.indexOf('alipay_') === 0) {
+ title = '璇蜂娇鐢ㄦ敮浠樺疂鈥滄壂涓�鎵�濇壂鐮佹敮浠�'
+ } else if (channelCode.indexOf('wx_') === 0) {
+ title = '璇蜂娇鐢ㄥ井淇♀�滄壂涓�鎵�濇壂鐮佹敮浠�'
+ }
+ qrCode.value = {
+ title: title,
+ url: data.displayContent,
+ visible: true
+ }
+ submitLoading.value = false
+}
+
+/** 鎻愪氦鏀粯鍚庯紙App锛� */
+const displayApp = (channelCode) => {
+ if (channelCode === PayChannelEnum.ALIPAY_APP.code) {
+ message.error('鏀粯瀹� App 鏀粯锛氭棤娉曞湪缃戦〉鏀粯锛�')
+ }
+ if (channelCode === PayChannelEnum.WX_APP.code) {
+ message.error('寰俊 App 鏀粯锛氭棤娉曞湪缃戦〉鏀粯锛�')
+ }
+ submitLoading.value = false
+}
+
+/** 杞鏌ヨ浠诲姟 */
+const createQueryInterval = () => {
+ if (interval.value) {
+ return
+ }
+ interval.value = setInterval(async () => {
+ const data = await PayOrderApi.getOrder(id.value)
+ // 宸叉敮浠�
+ if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+ clearQueryInterval()
+ message.success('鏀粯鎴愬姛锛�')
+ goReturnUrl('success')
+ }
+ // 宸插彇娑�
+ if (data.status === PayOrderStatusEnum.CLOSED.status) {
+ clearQueryInterval()
+ message.error('鏀粯宸插叧闂紒')
+ goReturnUrl('close')
+ }
+ }, 1000 * 2)
+}
+
+/** 娓呯┖鏌ヨ浠诲姟 */
+const clearQueryInterval = () => {
+ // 娓呯┖鍚勭寮圭獥
+ qrCode.value = {
+ title: '',
+ url: '',
+ visible: false
+ }
+ // 娓呯┖浠诲姟
+ clearInterval(interval.value)
+ interval.value = undefined
+}
+
+/**
+ * 鍥炲埌涓氬姟鐨� URL
+ *
+ * @param payResult 鏀粯缁撴灉
+ * 鈶� success锛氭敮浠樻垚鍔�
+ * 鈶� cancel锛氬彇娑堟敮浠�
+ * 鈶� close锛氭敮浠樺凡鍏抽棴
+ */
+const goReturnUrl = (payResult) => {
+ // 娓呯悊浠诲姟
+ clearQueryInterval()
+
+ // 鏈厤缃殑鎯呭喌涓嬶紝鍙兘鍏抽棴
+ if (!returnUrl.value) {
+ delView(unref(currentRoute))
+ return
+ }
+
+ const url =
+ returnUrl.value.indexOf('?') >= 0
+ ? returnUrl.value + '&payResult=' + payResult
+ : returnUrl.value + '?payResult=' + payResult
+ // 濡傛灉鏈夐厤缃紝涓旀槸 http 寮�澶达紝鍒欐祻瑙堝櫒璺宠浆
+ if (returnUrl.value.indexOf('http') === 0) {
+ location.href = url
+ } else {
+ delView(unref(currentRoute))
+ push({
+ path: url
+ })
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ id.value = route.query.id
+ if (route.query.returnUrl) {
+ returnUrl.value = decodeURIComponent(route.query.returnUrl)
+ }
+ getDetail()
+})
+
+/** 閿�姣� */
+onBeforeUnmount(() => {
+ clearQueryInterval()
+})
+</script>
+
+<style lang="scss" scoped>
+.pay-channel-container {
+ display: flex;
+ margin-top: -10px;
+
+ .box {
+ width: 160px;
+ padding-top: 10px;
+ padding-bottom: 5px;
+ margin-right: 10px;
+ text-align: center;
+ cursor: pointer;
+ border: 1px solid #e6ebf5;
+
+ img {
+ width: 40px;
+ height: 40px;
+ }
+
+ .title {
+ padding-top: 5px;
+ }
+ }
+}
+</style>
diff --git a/src/views/pay/demo/order/index.vue b/src/views/pay/demo/order/index.vue
new file mode 100644
index 0000000..09f7477
--- /dev/null
+++ b/src/views/pay/demo/order/index.vue
@@ -0,0 +1,240 @@
+<template>
+ <doc-alert title="鏀粯瀹濇敮浠樻帴鍏�" url="https://doc.iocoder.cn/pay/alipay-pay-demo/" />
+ <doc-alert title="鏀粯瀹濄�佸井淇¢��娆炬帴鍏�" url="https://doc.iocoder.cn/pay/refund-demo/" />
+ <doc-alert title="寰俊鍏紬鍙锋敮浠樻帴鍏�" url="https://doc.iocoder.cn/pay/wx-pub-pay-demo/" />
+ <doc-alert title="寰俊灏忕▼搴忔敮浠樻帴鍏�" url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/" />
+
+ <!-- 鎿嶄綔宸ュ叿鏍� -->
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain @click="openForm"><Icon icon="ep:plus" />鍙戣捣璁㈠崟</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="璁㈠崟缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" />
+ <el-table-column label="鍟嗗搧鍚嶅瓧" align="center" prop="spuName" />
+ <el-table-column label="鏀粯浠锋牸" align="center" prop="price">
+ <template #default="scope">
+ <span>锟{ (scope.row.price / 100.0).toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�娆鹃噾棰�" align="center" prop="refundPrice">
+ <template #default="scope">
+ <span>锟{ (scope.row.refundPrice / 100.0).toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鏀粯鍗曞彿" align="center" prop="payOrderId" />
+ <el-table-column label="鏄惁鏀粯" align="center" prop="payStatus">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.payStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鏀粯鏃堕棿"
+ align="center"
+ prop="payTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="閫�娆炬椂闂�" align="center" prop="refundTime" width="180">
+ <template #default="scope">
+ <span v-if="scope.row.refundTime">{{ formatDate(scope.row.refundTime) }}</span>
+ <span v-else-if="scope.row.payRefundId">閫�娆句腑锛岀瓑寰呴��娆剧粨鏋�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" @click="handlePay(scope.row)" v-if="!scope.row.payStatus">
+ 鍓嶅線鏀粯
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleRefund(scope.row)"
+ v-if="scope.row.payStatus && !scope.row.payRefundId"
+ >
+ 鍙戣捣閫�娆�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 瀵硅瘽妗�(娣诲姞 / 淇敼) -->
+ <Dialog title="鍙戣捣璁㈠崟" v-model="dialogVisible" width="500px">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鍟嗗搧" prop="spuId">
+ <el-select
+ v-model="formData.spuId"
+ placeholder="璇疯緭鍏ヤ笅鍗曞晢鍝�"
+ clearable
+ style="width: 380px"
+ >
+ <el-option v-for="item in spus" :key="item.id" :label="item.name" :value="item.id">
+ <span style="float: left">{{ item.name }}</span>
+ <span style="float: right; font-size: 13px; color: #8492a6">
+ 锟{ (item.price / 100.0).toFixed(2) }}
+ </span>
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup name="PayDemoOrder">
+import * as PayDemoApi from '@/api/pay/demo/order'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import { DICT_TYPE } from '@/utils/dict'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const router = useRouter() // 璺敱瀵硅薄
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+// 鏌ヨ鏉′欢
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+
+const formRef = ref()
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PayDemoApi.getDemoOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鏀粯鎸夐挳鎿嶄綔 */
+const handlePay = (row: any) => {
+ router.push({
+ name: 'PayCashier',
+ query: {
+ id: row.payOrderId,
+ returnUrl: encodeURIComponent('/pay/demo/order?id=' + row.id)
+ }
+ })
+}
+
+/** 閫�娆炬寜閽搷浣� */
+const handleRefund = async (row: any) => {
+ const id = row.id
+ try {
+ await message.confirm('鏄惁纭閫�娆剧紪鍙蜂负"' + id + '"鐨勭ず渚嬭鍗�?')
+ await PayDemoApi.refundDemoOrder(id)
+ await getList()
+ message.success('鍙戣捣閫�娆炬垚鍔燂紒')
+ } catch {}
+}
+
+// ========== 寮圭獥 ==========
+
+// 鍟嗗搧鏁扮粍
+const spus = ref([
+ {
+ id: 1,
+ name: '鍗庝负鎵嬫満',
+ price: 1
+ },
+ {
+ id: 2,
+ name: '灏忕背鐢佃',
+ price: 10
+ },
+ {
+ id: 3,
+ name: '鑻规灉鎵嬭〃',
+ price: 100
+ },
+ {
+ id: 4,
+ name: '鍗庣绗旇鏈�',
+ price: 1000
+ },
+ {
+ id: 5,
+ name: '钄氭潵姹借溅',
+ price: 200000
+ }
+])
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const formData = ref<any>({}) // 琛ㄥ崟鏁版嵁
+const formRules = {
+ spuId: [{ required: true, message: '鍟嗗搧缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }]
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+const reset = () => {
+ formData.value = {
+ spuId: undefined
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+const openForm = () => {
+ reset()
+ dialogVisible.value = true
+}
+
+/** 鎻愪氦鎸夐挳 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await PayDemoApi.createDemoOrder(formData.value)
+ message.success(t('common.createSuccess'))
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ getList()
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/pay/demo/withdraw/DemoWithdrawForm.vue b/src/views/pay/demo/withdraw/DemoWithdrawForm.vue
new file mode 100644
index 0000000..81dedb8
--- /dev/null
+++ b/src/views/pay/demo/withdraw/DemoWithdrawForm.vue
@@ -0,0 +1,129 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="800px">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="鎻愮幇鏍囬" prop="subject">
+ <el-input v-model="formData.subject" placeholder="璇疯緭鍏ユ彁鐜版爣棰�" />
+ </el-form-item>
+ <el-form-item label="鎻愮幇绫诲瀷" prop="type">
+ <el-radio-group v-model="formData.type">
+ <el-radio :value="1">鏀粯瀹�</el-radio>
+ <el-radio :value="2">寰俊浣欓</el-radio>
+ <el-radio :value="3">閽卞寘</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鎻愮幇閲戦" prop="price">
+ <el-input-number
+ v-model="formData.price"
+ :min="0.01"
+ :precision="2"
+ :step="0.01"
+ placeholder="璇疯緭鍏ユ彁鐜伴噾棰�"
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item label="鏀舵浜鸿处鍙�" prop="userAccount">
+ <el-input v-model="formData.userAccount" :placeholder="getAccountPlaceholder()" />
+ </el-form-item>
+ <el-form-item label="鏀舵浜哄鍚�" prop="userName">
+ <el-input v-model="formData.userName" placeholder="璇疯緭鍏ユ敹娆句汉濮撳悕" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as DemoWithdrawApi from '@/api/pay/demo/withdraw/index'
+import { yuanToFen } from '@/utils'
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref<DemoWithdrawApi.PayDemoWithdrawVO>({
+ subject: '',
+ price: 0,
+ type: 1,
+ userName: '',
+ userAccount: ''
+})
+const formRules = reactive({
+ subject: [{ required: true, message: '鎻愮幇鏍囬涓嶈兘涓虹┖', trigger: 'blur' }],
+ price: [{ required: true, message: '鎻愮幇閲戦涓嶈兘涓虹┖', trigger: 'blur' }],
+ type: [{ required: true, message: '鎻愮幇绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ userAccount: [{ required: true, message: '鏀舵浜鸿处鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+}
+/** 鍏抽棴寮圭獥 */
+const close = async () => {
+ dialogVisible.value = false
+ resetForm()
+}
+defineExpose({ open, close }) // 鎻愪緵 open锛� close 鏂规硶锛岀敤浜庢墦寮�, 鍏抽棴寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value }
+ data.price = yuanToFen(data.price)
+ if (formType.value === 'create') {
+ await DemoWithdrawApi.createDemoWithdraw(data)
+ message.success(t('common.createSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ subject: '',
+ price: 0,
+ type: 1,
+ userName: '',
+ userAccount: ''
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鏍规嵁鎻愮幇绫诲瀷鑾峰彇璐﹀彿杈撳叆妗嗙殑鍗犱綅绗︽枃鏈� */
+const getAccountPlaceholder = () => {
+ if (formData.value.type === 1) {
+ return '璇疯緭鍏ユ敮浠樺疂璐﹀彿'
+ } else if (formData.value.type === 2) {
+ return '璇疯緭鍏ュ井淇� openid'
+ } else if (formData.value.type === 3) {
+ return '璇疯緭鍏ラ挶鍖呯紪鍙�'
+ }
+ return '璇疯緭鍏ユ敹娆句汉璐﹀彿'
+}
+</script>
diff --git a/src/views/pay/demo/withdraw/index.vue b/src/views/pay/demo/withdraw/index.vue
new file mode 100644
index 0000000..7629a33
--- /dev/null
+++ b/src/views/pay/demo/withdraw/index.vue
@@ -0,0 +1,172 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button type="primary" plain @click="openForm('create')">
+ <Icon icon="ep:plus" />鍒涘缓绀轰緥鎻愮幇鍗�
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true">
+ <el-table-column label="鎿嶄綔" align="center" width="100">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.status === 0 && !scope.row.payTransferId"
+ type="primary"
+ link
+ @click="handleTransfer(scope.row.id)"
+ >
+ 鍙戣捣杞处
+ </el-button>
+ <el-button
+ v-else-if="scope.row.status === 20"
+ type="warning"
+ link
+ @click="handleTransfer(scope.row.id)"
+ >
+ 閲嶆柊杞处
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮幇鍗曠紪鍙�" align="center" prop="id" width="100" />
+ <el-table-column label="鎻愮幇鏍囬" align="center" prop="subject" min-width="120" />
+ <el-table-column label="鎻愮幇绫诲瀷" align="center" prop="type" min-width="90">
+ <template #default="scope">
+ <el-tag v-if="scope.row.type === 1">鏀粯瀹�</el-tag>
+ <el-tag v-else-if="scope.row.type === 2">寰俊浣欓</el-tag>
+ <el-tag v-else-if="scope.row.type === 3">閽卞寘浣欓</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻愮幇閲戦" align="center" prop="price" width="120">
+ <template #default="scope">
+ <span>锟{ (scope.row.price / 100.0).toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀舵浜哄鍚�" align="center" prop="userName" min-width="150" />
+ <el-table-column label="鏀舵浜鸿处鍙�" align="center" prop="userAccount" min-width="250" />
+ <el-table-column label="鎻愮幇鐘舵��" align="center" prop="status" width="100">
+ <template #default="scope">
+ <el-tag v-if="scope.row.status === 0 && !scope.row.payTransferId" type="warning">
+ 绛夊緟杞处
+ </el-tag>
+ <el-tag v-else-if="scope.row.status === 0 && scope.row.payTransferId" type="info">
+ 杞处涓�
+ </el-tag>
+ <el-tag v-else-if="scope.row.status === 10" type="success">杞处鎴愬姛</el-tag>
+ <el-tag v-else-if="scope.row.status === 20" type="danger">杞处澶辫触</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="杞处鍗曞彿" align="center" prop="payTransferId" min-width="120" />
+ <el-table-column label="杞处娓犻亾" align="center" prop="transferChannelCode" min-width="180">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="scope.row.transferChannelCode" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="杞处鏃堕棿"
+ align="center"
+ prop="transferTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column
+ label="杞处澶辫触鍘熷洜"
+ align="center"
+ prop="transferErrorMsg"
+ min-width="200"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DemoWithdrawForm ref="demoFormRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as DemoWithdrawApi from '@/api/pay/demo/withdraw'
+import DemoWithdrawForm from './DemoWithdrawForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { useMessage } from '@/hooks/web/useMessage'
+
+const message = useMessage()
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DemoWithdrawApi.getDemoWithdrawPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 鍒涘缓绀轰緥鎻愮幇鍗曟搷浣� */
+const demoFormRef = ref()
+const openForm = (type: string) => {
+ demoFormRef.value.open(type)
+}
+
+/** 澶勭悊杞处鎿嶄綔 */
+const handleTransfer = async (id: number) => {
+ try {
+ // 杞处鎿嶄綔鐨勪簩娆$‘璁�
+ await message.confirm('纭瑕佹墽琛岃浆璐︽搷浣滃悧?')
+ // 鍙戣捣杞处
+ loading.value = true
+ const payTransferId = await DemoWithdrawApi.transferDemoWithdraw(id)
+ message.success('杞处鎻愪氦鎴愬姛锛岃浆璐﹀崟鍙凤細' + payTransferId)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/pay/notify/NotifyDetail.vue b/src/views/pay/notify/NotifyDetail.vue
new file mode 100644
index 0000000..c3fdb7c
--- /dev/null
+++ b/src/views/pay/notify/NotifyDetail.vue
@@ -0,0 +1,92 @@
+<template>
+ <Dialog v-model="dialogVisible" title="閫氱煡璇︽儏" width="50%">
+ <el-descriptions :column="2">
+ <el-descriptions-item label="閫氱煡鐘舵��" :span="2">
+ <dict-tag :type="DICT_TYPE.PAY_NOTIFY_STATUS" :value="detailData.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘埛璁㈠崟缂栧彿" :span="2">
+ <el-tag>{{ detailData.merchantOrderId }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘埛閫�娆剧紪鍙�" :span="2" v-if="detailData.merchantRefundId">
+ <el-tag>{{ detailData.merchantRefundId }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘埛杞处缂栧彿" :span="2" v-if="detailData.merchantTransferId">
+ <el-tag>{{ detailData.merchantTransferId }}</el-tag>
+ </el-descriptions-item>
+
+ <el-descriptions-item label="搴旂敤缂栧彿">{{ detailData.appId }}</el-descriptions-item>
+ <el-descriptions-item label="搴旂敤鍚嶇О">{{ detailData.appName }}</el-descriptions-item>
+
+ <el-descriptions-item label="鍏宠仈缂栧彿">{{ detailData.dataId }}</el-descriptions-item>
+ <el-descriptions-item label="閫氱煡绫诲瀷">
+ <dict-tag :type="DICT_TYPE.PAY_NOTIFY_TYPE" :value="detailData.type" />
+ </el-descriptions-item>
+
+ <el-descriptions-item label="閫氱煡娆℃暟">{{ detailData.notifyTimes }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�澶ч�氱煡娆℃暟">
+ {{ detailData.maxNotifyTimes }}
+ </el-descriptions-item>
+
+ <el-descriptions-item label="鏈�鍚庨�氱煡鏃堕棿">
+ {{ formatDate(detailData.lastExecuteTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬫閫氱煡鏃堕棿">
+ {{ formatDate(detailData.nextNotifyTime) }}
+ </el-descriptions-item>
+
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(detailData.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <!-- 鍒嗗壊绾� -->
+ <el-divider />
+
+ <el-descriptions :column="1" direction="vertical" border>
+ <el-descriptions-item label="鍥炶皟鏃ュ織">
+ <el-table :data="detailData.logs">
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="id" />
+ <el-table-column label="閫氱煡鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_NOTIFY_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="閫氱煡娆℃暟" align="center" prop="notifyTimes" />
+ <el-table-column label="閫氱煡鏃堕棿" align="center" prop="lastExecuteTime" width="180">
+ <template #default="scope">
+ <span>{{ formatDate(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍝嶅簲缁撴灉" align="center" prop="response" />
+ </el-table>
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as PayNotifyApi from '@/api/pay/notify'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'PayNotifyDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = await PayNotifyApi.getNotifyTaskDetail(id)
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/pay/notify/index.vue b/src/views/pay/notify/index.vue
new file mode 100644
index 0000000..e48345c
--- /dev/null
+++ b/src/views/pay/notify/index.vue
@@ -0,0 +1,250 @@
+<template>
+ <doc-alert title="鏀粯鍔熻兘寮�鍚�" url="https://doc.iocoder.cn/pay/build/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="搴旂敤缂栧彿" prop="appId">
+ <el-select
+ v-model="queryParams.appId"
+ placeholder="璇烽�夋嫨搴旂敤淇℃伅"
+ clearable
+ filterable
+ class="!w-240px"
+ >
+ <el-option v-for="item in appList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫氱煡绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ placeholder="璇烽�夋嫨閫氱煡绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PAY_NOTIFY_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈缂栧彿" prop="dataId">
+ <el-input
+ v-model="queryParams.dataId"
+ placeholder="璇疯緭鍏ュ叧鑱旂紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閫氱煡鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨閫氱煡鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PAY_NOTIFY_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍟嗘埛璁㈠崟缂栧彿" prop="merchantOrderId">
+ <el-input
+ v-model="queryParams.merchantOrderId"
+ placeholder="璇疯緭鍏ュ晢鎴疯鍗曠紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗘埛閫�娆剧紪鍙�" prop="merchantRefundId">
+ <el-input
+ v-model="queryParams.merchantRefundId"
+ placeholder="璇疯緭鍏ュ晢鎴烽��娆剧紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗘埛杞处缂栧彿" prop="merchantTransferId">
+ <el-input
+ v-model="queryParams.merchantTransferId"
+ placeholder="璇疯緭鍏ュ晢鎴疯浆璐︾紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ style="width: 240px"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="浠诲姟缂栧彿" align="center" prop="id" />
+ <el-table-column label="搴旂敤缂栧彿" align="center" prop="appName" />
+ <el-table-column label="鍟嗘埛鍗曚俊鎭�" align="center" prop="merchant">
+ <template #default="scope">
+ <div v-if="scope.row.merchantOrderId">
+ <div>鍟嗘埛璁㈠崟缂栧彿锛歿{ scope.row.merchantOrderId }}</div>
+ </div>
+ <div v-if="scope.row.merchantRefundId">
+ <div>鍟嗘埛閫�娆剧紪鍙凤細{{ scope.row.merchantRefundId }}</div>
+ </div>
+ <div v-if="scope.row.merchantTransferId">
+ <div>鍟嗘埛杞处缂栧彿锛歿{ scope.row.merchantTransferId }}</div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="閫氱煡绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_NOTIFY_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏宠仈缂栧彿" align="center" prop="dataId" />
+ <el-table-column label="閫氱煡鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_NOTIFY_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鏈�鍚庨�氱煡鏃堕棿"
+ align="center"
+ prop="lastExecuteTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ label="涓嬫閫氱煡鏃堕棿"
+ align="center"
+ prop="nextNotifyTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="閫氱煡娆℃暟" align="center" prop="notifyTimes">
+ <template #default="scope">
+ <el-tag size="small" type="success">
+ {{ scope.row.notifyTimes }} / {{ scope.row.maxNotifyTimes }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row.id)"
+ v-hasPermi="['pay:notify:query']"
+ >
+ 鏌ョ湅璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉缁勪欢 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氶瑙� -->
+ <NotifyDetail ref="detailRef" />
+</template>
+
+<script lang="ts" setup>
+import * as PayNotifyApi from '@/api/pay/notify'
+import * as PayAppApi from '@/api/pay/app'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import NotifyDetail from './NotifyDetail.vue'
+
+defineOptions({ name: 'PayNotify' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref() // 鍒楄〃鐨勬暟鎹�
+const queryParams = ref({
+ pageNo: 1,
+ pageSize: 10,
+ appId: null,
+ type: null,
+ dataId: null,
+ status: null,
+ merchantOrderId: null,
+ merchantRefundId: null,
+ merchantTransferId: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const appList = ref([]) // 鏀粯搴旂敤鍒楄〃闆嗗悎
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.value.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PayNotifyApi.getNotifyTaskPage(queryParams.value)
+ list.value = data.list
+ total.value = data.total
+ loading.value = false
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 璇︽儏鎸夐挳鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+ detailRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱绛涢�夐」
+ appList.value = await PayAppApi.getAppList()
+})
+</script>
diff --git a/src/views/pay/order/OrderDetail.vue b/src/views/pay/order/OrderDetail.vue
new file mode 100644
index 0000000..90d4ccb
--- /dev/null
+++ b/src/views/pay/order/OrderDetail.vue
@@ -0,0 +1,113 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璁㈠崟璇︽儏" width="700px">
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="鍟嗘埛鍗曞彿">
+ <el-tag size="small">{{ detailData.merchantOrderId }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀粯鍗曞彿">
+ <el-tag type="warning" size="small" v-if="detailData.no">{{ detailData.no }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="搴旂敤缂栧彿">{{ detailData.appId }}</el-descriptions-item>
+ <el-descriptions-item label="搴旂敤鍚嶇О">{{ detailData.appName }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯鐘舵��">
+ <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="detailData.status" size="small" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀粯閲戦">
+ <el-tag type="success" size="small">锟{ (detailData.price / 100.0).toFixed(2) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵嬬画璐�">
+ <el-tag type="warning" size="small">
+ 锟{ (detailData.channelFeePrice / 100.0).toFixed(2) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵嬬画璐规瘮渚�">
+ {{ detailData.channelFeeRate.toFixed(2) }}%
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀粯鏃堕棿">
+ {{ formatDate(detailData.successTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="澶辨晥鏃堕棿">
+ {{ formatDate(detailData.expireTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(detailData.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <!-- 鍒嗗壊绾� -->
+ <el-divider />
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="鍟嗗搧鏍囬">{{ detailData.subject }}</el-descriptions-item>
+ <el-descriptions-item label="鍟嗗搧鎻忚堪">{{ detailData.body }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯娓犻亾">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="detailData.channelCode" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀粯 IP">{{ detailData.userIp }}</el-descriptions-item>
+ <el-descriptions-item label="娓犻亾鍗曞彿">
+ <el-tag size="mini" type="success" v-if="detailData.channelOrderNo">
+ {{ detailData.channelOrderNo }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="娓犻亾鐢ㄦ埛">{{ detailData.channelUserId }}</el-descriptions-item>
+ <el-descriptions-item label="閫�娆鹃噾棰�">
+ <el-tag size="mini" type="danger">
+ 锟{ (detailData.refundPrice / 100.0).toFixed(2) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫氱煡 URL">{{ detailData.notifyUrl }}</el-descriptions-item>
+ </el-descriptions>
+ <!-- 鍒嗗壊绾� -->
+ <el-divider />
+ <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
+ <el-descriptions-item label="鏀粯閫氶亾寮傛鍥炶皟鍐呭">
+ <el-text style="white-space: pre-wrap; word-break: break-word">
+ {{ detailData.extension.channelNotifyData }}
+ </el-text>
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as OrderApi from '@/api/pay/order'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'PayOrderDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({
+ extension: {}
+})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = await OrderApi.getOrderDetail(id)
+ if (!detailData.value.extension) {
+ detailData.value.extension = {}
+ }
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
+<style>
+.tag-purple {
+ color: #722ed1;
+ background: #f9f0ff;
+ border-color: #d3adf7;
+}
+
+.tag-pink {
+ color: #eb2f96;
+ background: #fff0f6;
+ border-color: #ffadd2;
+}
+</style>
diff --git a/src/views/pay/order/index.vue b/src/views/pay/order/index.vue
new file mode 100644
index 0000000..9ba1922
--- /dev/null
+++ b/src/views/pay/order/index.vue
@@ -0,0 +1,275 @@
+<template>
+ <doc-alert title="鏀粯瀹濇敮浠樻帴鍏�" url="https://doc.iocoder.cn/pay/alipay-pay-demo/" />
+ <doc-alert title="寰俊鍏紬鍙锋敮浠樻帴鍏�" url="https://doc.iocoder.cn/pay/wx-pub-pay-demo/" />
+ <doc-alert title="寰俊灏忕▼搴忔敮浠樻帴鍏�" url="https://doc.iocoder.cn/pay/wx-lite-pay-demo/" />
+
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="搴旂敤缂栧彿" prop="appId">
+ <el-select
+ clearable
+ v-model="queryParams.appId"
+ placeholder="璇烽�夋嫨搴旂敤淇℃伅"
+ class="!w-240px"
+ >
+ <el-option v-for="item in appList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀粯娓犻亾" prop="channelCode">
+ <el-select
+ v-model="queryParams.channelCode"
+ placeholder="璇烽�夋嫨鏀粯娓犻亾"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍟嗘埛鍗曞彿" prop="merchantOrderId">
+ <el-input
+ v-model="queryParams.merchantOrderId"
+ placeholder="璇疯緭鍏ュ晢鎴峰崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏀粯鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ユ敮浠樺崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娓犻亾鍗曞彿" prop="channelOrderNo">
+ <el-input
+ v-model="queryParams.channelOrderNo"
+ placeholder="璇疯緭鍏ユ笭閬撳崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏀粯鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鏀粯鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PAY_ORDER_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['pay:order:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" width="80" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鏀粯閲戦" align="center" prop="price" width="100">
+ <template #default="scope"> 锟{ parseFloat(scope.row.price / 100).toFixed(2) }} </template>
+ </el-table-column>
+ <el-table-column label="閫�娆鹃噾棰�" align="center" prop="refundPrice" width="100">
+ <template #default="scope">
+ 锟{ parseFloat(scope.row.refundPrice / 100).toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵嬬画閲戦" align="center" prop="channelFeePrice" width="100">
+ <template #default="scope">
+ 锟{ parseFloat(scope.row.channelFeePrice / 100).toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="璁㈠崟鍙�" align="left" width="300">
+ <template #default="scope">
+ <p class="order-font">
+ <el-tag size="small"> 鍟嗘埛</el-tag> {{ scope.row.merchantOrderId }}
+ </p>
+ <p class="order-font" v-if="scope.row.no">
+ <el-tag size="small" type="warning">鏀粯</el-tag> {{ scope.row.no }}
+ </p>
+ <p class="order-font" v-if="scope.row.channelOrderNo">
+ <el-tag size="small" type="success">娓犻亾</el-tag> {{ scope.row.channelOrderNo }}
+ </p>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀粯鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_ORDER_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀粯娓犻亾" align="center" prop="channelCode" width="140">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="scope.row.channelCode" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鏀粯鏃堕棿"
+ align="center"
+ prop="successTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鏀粯搴旂敤" align="center" prop="appName" width="100" />
+ <el-table-column label="鍟嗗搧鏍囬" align="center" prop="subject" width="180" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="openDetail(scope.row.id)"
+ v-hasPermi="['pay:order:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氶瑙� -->
+ <OrderDetail ref="detailRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as OrderApi from '@/api/pay/order'
+import OrderDetail from './OrderDetail.vue'
+import download from '@/utils/download'
+import { getAppList } from '@/api/pay/app'
+
+defineOptions({ name: 'PayOrder' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ appId: null,
+ channelCode: null,
+ merchantOrderId: null,
+ channelOrderNo: null,
+ no: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭绛夊緟
+const appList = ref([]) // 鏀粯搴旂敤鍒楄〃闆嗗悎
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await OrderApi.getOrderPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await OrderApi.exportOrder(queryParams)
+ download.excel(data, '鏀粯璁㈠崟.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 棰勮璇︽儏 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+ detailRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ appList.value = await getAppList()
+})
+</script>
+<style>
+.order-font {
+ padding: 2px 0;
+ font-size: 12px;
+}
+</style>
diff --git a/src/views/pay/refund/RefundDetail.vue b/src/views/pay/refund/RefundDetail.vue
new file mode 100644
index 0000000..20a1654
--- /dev/null
+++ b/src/views/pay/refund/RefundDetail.vue
@@ -0,0 +1,95 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璇︽儏" width="700px">
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="鍟嗘埛閫�娆惧崟鍙�">
+ <el-tag size="small">{{ refundDetail.merchantRefundId }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="娓犻亾閫�娆惧崟鍙�">
+ <el-tag type="success" size="small" v-if="refundDetail.channelRefundNo">{{
+ refundDetail.channelRefundNo
+ }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍟嗘埛鏀粯鍗曞彿">
+ <el-tag size="small">{{ refundDetail.merchantOrderId }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="娓犻亾鏀粯鍗曞彿">
+ <el-tag type="success" size="small">{{ refundDetail.channelOrderNo }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="搴旂敤缂栧彿">{{ refundDetail.appId }}</el-descriptions-item>
+ <el-descriptions-item label="搴旂敤鍚嶇О">{{ refundDetail.appName }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯閲戦">
+ <el-tag type="success" size="small">
+ 锟{ (refundDetail.payPrice / 100.0).toFixed(2) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫�娆鹃噾棰�">
+ <el-tag size="mini" type="danger">
+ 锟{ (refundDetail.refundPrice / 100.0).toFixed(2) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫�娆剧姸鎬�">
+ <dict-tag :type="DICT_TYPE.PAY_REFUND_STATUS" :value="refundDetail.status" />
+ </el-descriptions-item>
+ <el-descriptions-item label="閫�娆炬椂闂�">
+ {{ formatDate(refundDetail.successTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(refundDetail.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏇存柊鏃堕棿">
+ {{ formatDate(refundDetail.updateTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <!-- 鍒嗗壊绾� -->
+ <el-divider />
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="閫�娆炬笭閬�">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="refundDetail.channelCode" />
+ </el-descriptions-item>
+ <el-descriptions-item label="閫�娆惧師鍥�">{{ refundDetail.reason }}</el-descriptions-item>
+ <el-descriptions-item label="閫�娆� IP">{{ refundDetail.userIp }}</el-descriptions-item>
+ <el-descriptions-item label="閫氱煡 URL">{{ refundDetail.notifyUrl }}</el-descriptions-item>
+ </el-descriptions>
+ <!-- 鍒嗗壊绾� -->
+ <el-divider />
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="娓犻亾閿欒鐮�">
+ {{ refundDetail.channelErrorCode }}
+ </el-descriptions-item>
+ <el-descriptions-item label="娓犻亾閿欒鐮佹弿杩�">
+ {{ refundDetail.channelErrorMsg }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
+ <el-descriptions-item label="鏀粯閫氶亾寮傛鍥炶皟鍐呭">
+ <el-text style="white-space: pre-wrap; word-break: break-word">
+ {{ refundDetail.channelNotifyData }}
+ </el-text>
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as RefundApi from '@/api/pay/refund'
+
+defineOptions({ name: 'PayRefundDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const refundDetail = ref({})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ refundDetail.value = await RefundApi.getRefund(id)
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/pay/refund/index.vue b/src/views/pay/refund/index.vue
new file mode 100644
index 0000000..2a98568
--- /dev/null
+++ b/src/views/pay/refund/index.vue
@@ -0,0 +1,298 @@
+<template>
+ <doc-alert title="鏀粯瀹濄�佸井淇¢��娆炬帴鍏�" url="https://doc.iocoder.cn/pay/refund-demo/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="120px"
+ >
+ <el-form-item label="搴旂敤缂栧彿" prop="appId">
+ <el-select
+ v-model="queryParams.appId"
+ clearable
+ placeholder="璇烽�夋嫨搴旂敤淇℃伅"
+ class="!w-240px"
+ >
+ <el-option v-for="item in appList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�娆炬笭閬�" prop="channelCode">
+ <el-select
+ v-model="queryParams.channelCode"
+ placeholder="璇烽�夋嫨閫�娆炬笭閬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍟嗘埛鏀粯鍗曞彿" prop="merchantOrderId">
+ <el-input
+ v-model="queryParams.merchantOrderId"
+ placeholder="璇疯緭鍏ュ晢鎴锋敮浠樺崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍟嗘埛閫�娆惧崟鍙�" prop="merchantRefundId">
+ <el-input
+ v-model="queryParams.merchantRefundId"
+ placeholder="璇疯緭鍏ュ晢鎴烽��娆惧崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娓犻亾鏀粯鍗曞彿" prop="channelOrderNo">
+ <el-input
+ v-model="queryParams.channelOrderNo"
+ placeholder="璇疯緭鍏ユ笭閬撴敮浠樺崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娓犻亾閫�娆惧崟鍙�" prop="channelRefundNo">
+ <el-input
+ v-model="queryParams.channelRefundNo"
+ placeholder="璇疯緭鍏ユ笭閬撻��娆惧崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閫�娆剧姸鎬�" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨閫�娆剧姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.PAY_REFUND_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" /> 鎼滅储 </el-button>
+ <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆 </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:tenant:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="170"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鏀粯閲戦" align="center" prop="payPrice" width="100">
+ <template #default="scope">
+ 锟{ parseFloat(scope.row.payPrice / 100).toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�娆鹃噾棰�" align="center" prop="refundPrice" width="100">
+ <template #default="scope">
+ 锟{ parseFloat(scope.row.refundPrice / 100).toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�娆捐鍗曞彿" align="left" width="300">
+ <template #default="scope">
+ <p class="order-font">
+ <el-tag size="small">鍟嗘埛</el-tag> {{ scope.row.merchantRefundId }}
+ </p>
+ <p class="order-font">
+ <el-tag size="small" type="warning">閫�娆�</el-tag> {{ scope.row.no }}
+ </p>
+ <p class="order-font" v-if="scope.row.channelRefundNo">
+ <el-tag size="small" type="success">娓犻亾</el-tag> {{ scope.row.channelRefundNo }}
+ </p>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀粯璁㈠崟鍙�" align="left" width="300">
+ <template #default="scope">
+ <p class="order-font">
+ <el-tag size="small">鍟嗘埛</el-tag> {{ scope.row.merchantOrderId }}
+ </p>
+ <p class="order-font">
+ <el-tag size="small" type="success">娓犻亾</el-tag> {{ scope.row.channelOrderNo }}
+ </p>
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�娆剧姸鎬�" align="center" prop="status" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_REFUND_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�娆炬笭閬�" align="center" width="140">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="scope.row.channelCode" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鎴愬姛鏃堕棿"
+ align="center"
+ prop="successTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鏀粯搴旂敤" align="center" prop="successTime" width="100">
+ <template #default="scope">
+ <span>{{ scope.row.appName }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ @click="openDetail(scope.row.id)"
+ v-hasPermi="['pay:order:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氶瑙� -->
+ <RefundDetail ref="detailRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as RefundApi from '@/api/pay/refund'
+import * as AppApi from '@/api/pay/app'
+import RefundDetail from './RefundDetail.vue'
+import download from '@/utils/download'
+
+defineOptions({ name: 'PayRefund' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(false) // 鍒楄〃閬僵灞�
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ merchantId: undefined,
+ appId: undefined,
+ channelCode: undefined,
+ merchantOrderId: undefined,
+ merchantRefundId: undefined,
+ status: undefined,
+ payPrice: undefined,
+ refundPrice: undefined,
+ channelOrderNo: undefined,
+ channelRefundNo: undefined,
+ createTime: [],
+ successTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭绛夊緟
+const appList = ref([]) // 鏀粯搴旂敤鍒楄〃闆嗗悎
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await RefundApi.getRefundPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await RefundApi.exportRefund(queryParams)
+ download.excel(data, '鏀粯璁㈠崟.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 棰勮璇︽儏 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+ detailRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ appList.value = await AppApi.getAppList()
+})
+</script>
+<style>
+.order-font {
+ padding: 2px 0;
+ font-size: 12px;
+}
+</style>
diff --git a/src/views/pay/transfer/TransferDetail.vue b/src/views/pay/transfer/TransferDetail.vue
new file mode 100644
index 0000000..3fe0620
--- /dev/null
+++ b/src/views/pay/transfer/TransferDetail.vue
@@ -0,0 +1,80 @@
+<template>
+ <Dialog v-model="dialogVisible" title="杞处鍗曡鎯�" width="700px">
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="鍟嗘埛鍗曞彿">
+ <el-tag size="small">{{ detailData.merchantTransferId }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="杞处鍗曞彿">
+ <el-tag type="warning" size="small" v-if="detailData.no">{{ detailData.no }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="搴旂敤缂栧彿">{{ detailData.appId }}</el-descriptions-item>
+ <el-descriptions-item label="杞处鐘舵��">
+ <dict-tag :type="DICT_TYPE.PAY_TRANSFER_STATUS" :value="detailData.status" size="small" />
+ </el-descriptions-item>
+ <el-descriptions-item label="杞处閲戦">
+ <el-tag type="success" size="small">锟{ (detailData.price / 100.0).toFixed(2) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="杞处鏃堕棿">
+ {{ formatDate(detailData.successTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <!-- 鍒嗗壊绾� -->
+ <el-divider />
+ <el-descriptions :column="2" label-class-name="desc-label">
+ <el-descriptions-item label="鏀舵浜哄鍚�">{{ detailData.userName }}</el-descriptions-item>
+ <el-descriptions-item label="鏀舵浜鸿处鍙�">{{ detailData.userAccount }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯娓犻亾">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="detailData.channelCode" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏀粯 IP">{{ detailData.userIp }}</el-descriptions-item>
+ <el-descriptions-item label="娓犻亾鍗曞彿">
+ <el-tag size="mini" type="success" v-if="detailData.channelTransferNo">
+ {{ detailData.channelTransferNo }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫氱煡 URL">{{ detailData.notifyUrl }}</el-descriptions-item>
+ </el-descriptions>
+ <el-divider />
+ <el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
+ <el-descriptions-item label="杞处娓犻亾閫氱煡鍐呭">
+ <el-text style="white-space: pre-wrap; word-break: break-word">
+ {{ detailData.channelNotifyData }}
+ </el-text>
+ </el-descriptions-item>
+ </el-descriptions>
+ <el-divider />
+ <div style="text-align: right">
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </div>
+ </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as TransferApi from '@/api/pay/transfer'
+import { formatDate } from '@/utils/formatTime'
+
+defineOptions({ name: 'PayTransferDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({})
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = await TransferApi.getTransfer(id)
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
+
+<style scoped></style>
diff --git a/src/views/pay/transfer/index.vue b/src/views/pay/transfer/index.vue
new file mode 100644
index 0000000..2c6f856
--- /dev/null
+++ b/src/views/pay/transfer/index.vue
@@ -0,0 +1,283 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="杞处鍗曞彿" prop="no">
+ <el-input
+ v-model="queryParams.no"
+ placeholder="璇疯緭鍏ヨ浆璐﹀崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="杞处娓犻亾" prop="channelCode">
+ <el-select
+ v-model="queryParams.channelCode"
+ placeholder="璇烽�夋嫨鏀粯娓犻亾"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍟嗘埛鍗曞彿" prop="merchantTransferId">
+ <el-input
+ v-model="queryParams.merchantTransferId"
+ placeholder="璇疯緭鍏ュ晢鎴峰崟鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="type">
+ <el-select v-model="queryParams.type" placeholder="璇烽�夋嫨绫诲瀷" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_TRANSFER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="杞处鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨杞处鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.PAY_TRANSFER_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀舵浜哄鍚�" prop="userName">
+ <el-input
+ v-model="queryParams.userName"
+ placeholder="璇疯緭鍏ユ敹娆句汉濮撳悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鏀舵浜鸿处鍙�" prop="accountNo">
+ <el-input
+ v-model="queryParams.accountNo"
+ placeholder="璇疯緭鍏ユ敹娆句汉璐﹀彿"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="娓犻亾鍗曞彿" prop="channelTransferNo">
+ <el-input
+ v-model="queryParams.channelTransferNo"
+ placeholder="娓犻亾鍗曞彿"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['pay:transfer:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鏀粯搴旂敤" align="center" prop="appName" min-width="100" />
+ <el-table-column label="杞处閲戦" align="center" prop="price">
+ <template #default="scope">
+ <span>锟{ (scope.row.price / 100.0).toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="杞处鐘舵��" align="center" prop="status" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_TRANSFER_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁㈠崟鍙�" align="left" width="300">
+ <template #default="scope">
+ <p class="transfer-font">
+ <el-tag size="small"> 鍟嗘埛</el-tag>
+ {{ scope.row.merchantTransferId }}
+ </p>
+ <p class="transfer-font" v-if="scope.row.no">
+ <el-tag size="small" type="warning">杞处</el-tag>
+ {{ scope.row.no }}
+ </p>
+ <p class="transfer-font" v-if="scope.row.channelTransferNo">
+ <el-tag size="small" type="success">娓犻亾</el-tag>
+ {{ scope.row.channelTransferNo }}
+ </p>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀舵浜哄鍚�" align="center" prop="userName" width="120" />
+ <el-table-column label="鏀舵璐﹀彿" align="left" prop="userAccount" width="200" />
+ <el-table-column label="杞处鏍囬" align="center" prop="subject" width="120" />
+ <el-table-column label="杞处娓犻亾" align="center" prop="channelCode">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="scope.row.channelCode" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="杞处鎴愬姛鏃堕棿"
+ align="center"
+ prop="successTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button link type="primary" @click="openDetail(scope.row.id)"> 璇︽儏 </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ <TransferDetail ref="detailRef" />
+ </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as TransferApi from '@/api/pay/transfer'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import TransferDetail from './TransferDetail.vue'
+import download from '@/utils/download'
+
+defineOptions({ name: 'PayTransfer' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ no: null,
+ appId: null,
+ channelId: null,
+ channelCode: null,
+ merchantTransferId: null,
+ type: null,
+ status: null,
+ successTime: [],
+ price: null,
+ subject: null,
+ userName: null,
+ userAccount: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TransferApi.getTransferPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await TransferApi.exportTransfer(queryParams)
+ download.excel(data, '杞处鍗�.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+ detailRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped>
+.transfer-font {
+ padding: 2px 0;
+ font-size: 12px;
+}
+</style>
diff --git a/src/views/pay/wallet/balance/WalletForm.vue b/src/views/pay/wallet/balance/WalletForm.vue
new file mode 100644
index 0000000..8173e12
--- /dev/null
+++ b/src/views/pay/wallet/balance/WalletForm.vue
@@ -0,0 +1,22 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible" width="800">
+ <WalletTransactionList :wallet-id="walletId" />
+ <template #footer>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import WalletTransactionList from '../transaction/WalletTransactionList.vue'
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const walletId = ref(0)
+/** 鎵撳紑寮圭獥 */
+const open = async (theWalletId: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = '閽卞寘浣欓鏄庣粏'
+ walletId.value = theWalletId
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/pay/wallet/balance/index.vue b/src/views/pay/wallet/balance/index.vue
new file mode 100644
index 0000000..1a37eec
--- /dev/null
+++ b/src/views/pay/wallet/balance/index.vue
@@ -0,0 +1,156 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" />
+ <el-table-column label="鐢ㄦ埛绫诲瀷" align="center" prop="userType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓" align="center" prop="balance">
+ <template #default="{ row }"> {{ fenToYuan(row.balance) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column label="绱鏀嚭" align="center" prop="totalExpense">
+ <template #default="{ row }"> {{ fenToYuan(row.totalExpense) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column label="绱鍏呭��" align="center" prop="totalRecharge">
+ <template #default="{ row }"> {{ fenToYuan(row.totalRecharge) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column label="鍐荤粨閲戦" align="center" prop="freezePrice">
+ <template #default="{ row }"> {{ fenToYuan(row.freezePrice) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="openForm(scope.row.id)">璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 寮圭獥 -->
+ <WalletForm ref="formRef" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { fenToYuan } from '@/utils'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import WalletForm from './WalletForm.vue'
+
+defineOptions({ name: 'WalletBalance' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ userType: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await WalletApi.getWalletPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (id?: number) => {
+ formRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue b/src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue
new file mode 100644
index 0000000..e7a7328
--- /dev/null
+++ b/src/views/pay/wallet/rechargePackage/WalletRechargePackageForm.vue
@@ -0,0 +1,122 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="150px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="濂楅鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ椁愬悕" />
+ </el-form-item>
+ <el-form-item label="鏀粯閲戦(鍏�)" prop="payPrice">
+ <el-input-number v-model="formData.payPrice" :min="0" :precision="2" :step="0.01" />
+ </el-form-item>
+ <el-form-item label="璧犻�侀噾棰�(鍏�)" prop="bonusPrice">
+ <el-input-number v-model="formData.bonusPrice" :min="0" :precision="2" :step="0.01" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script setup lang="ts">
+import * as WalletRechargePackageApi from '@/api/pay/wallet/rechargePackage'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { fenToYuan, yuanToFen } from '@/utils'
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ payPrice: undefined,
+ bonusPrice: undefined,
+ status: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '濂楅鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ payPrice: [{ required: true, message: '鏀粯閲戦涓嶈兘涓虹┖', trigger: 'blur' }],
+ bonusPrice: [{ required: true, message: '璧犻�侀噾棰濅笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await WalletRechargePackageApi.getWalletRechargePackage(id)
+ formData.value.payPrice = fenToYuan(formData.value.payPrice)
+ formData.value.bonusPrice = fenToYuan(formData.value.bonusPrice)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = { ...formData.value }
+ data.payPrice = yuanToFen(data.payPrice)
+ data.bonusPrice = yuanToFen(data.bonusPrice)
+ if (formType.value === 'create') {
+ await WalletRechargePackageApi.createWalletRechargePackage(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await WalletRechargePackageApi.updateWalletRechargePackage(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ payPrice: undefined,
+ bonusPrice: undefined,
+ status: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/pay/wallet/rechargePackage/index.vue b/src/views/pay/wallet/rechargePackage/index.vue
new file mode 100644
index 0000000..f097577
--- /dev/null
+++ b/src/views/pay/wallet/rechargePackage/index.vue
@@ -0,0 +1,185 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="濂楅鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ椁愬悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['pay:wallet-recharge-package:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="濂楅鍚�" align="center" prop="name" />
+ <el-table-column label="鏀粯閲戦" align="center" prop="payPrice">
+ <template #default="{ row }"> {{ fenToYuan(row.payPrice) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column label="璧犻�侀噾棰�" align="center" prop="bonusPrice">
+ <template #default="{ row }"> {{ fenToYuan(row.bonusPrice) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180px"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['pay:wallet-recharge-package:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['pay:wallet-recharge-package:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <WalletRechargePackageForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as WalletRechargePackageApi from '@/api/pay/wallet/rechargePackage'
+import WalletRechargePackageForm from './WalletRechargePackageForm.vue'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'WalletRechargePackage' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ payPrice: null,
+ bonusPrice: null,
+ status: null,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await WalletRechargePackageApi.getWalletRechargePackagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await WalletRechargePackageApi.deleteWalletRechargePackage(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/pay/wallet/transaction/WalletTransactionList.vue b/src/views/pay/wallet/transaction/WalletTransactionList.vue
new file mode 100644
index 0000000..49c5d97
--- /dev/null
+++ b/src/views/pay/wallet/transaction/WalletTransactionList.vue
@@ -0,0 +1,79 @@
+<template>
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="缂栧彿" prop="id" />
+ <el-table-column align="center" label="閽卞寘缂栧彿" prop="walletId" />
+ <el-table-column align="center" label="鍏宠仈涓氬姟鏍囬" prop="title" />
+ <el-table-column align="center" label="浜ゆ槗閲戦" prop="price">
+ <template #default="{ row }"> {{ fenToYuan(row.price) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column align="center" label="閽卞寘浣欓" prop="balance">
+ <template #default="{ row }"> {{ fenToYuan(row.balance) }} 鍏�</template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="浜ゆ槗鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as WalletTransactionApi from '@/api/pay/wallet/transaction'
+import * as WalletApi from '@/api/pay/wallet/balance'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'WalletTransactionList' })
+const props = defineProps({
+ walletId: {
+ type: Number,
+ required: false
+ },
+ userId: {
+ type: Number,
+ required: false
+ }
+})
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ walletId: null
+})
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ if (props.userId) {
+ const wallet = await WalletApi.getWallet({ userId: props.userId })
+ queryParams.walletId = wallet.id as any
+ } else {
+ queryParams.walletId = props.walletId as any
+ }
+ const data = await WalletTransactionApi.getWalletTransactionPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
+<style lang="scss" scoped></style>
diff --git a/src/views/report/goview/index.vue b/src/views/report/goview/index.vue
new file mode 100644
index 0000000..1ec8c29
--- /dev/null
+++ b/src/views/report/goview/index.vue
@@ -0,0 +1,16 @@
+<template>
+ <doc-alert title="澶у睆璁捐鍣�" url="https://doc.iocoder.cn/report/screen/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame :src="src" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { getAccessToken, getRefreshToken } from '@/utils/auth'
+
+defineOptions({ name: 'GoView' })
+
+const src = ref(
+ `${import.meta.env.VITE_GOVIEW_URL}?accessToken=${getAccessToken()}&refreshToken=${getRefreshToken()}`
+)
+</script>
diff --git a/src/views/report/jmreport/bi.vue b/src/views/report/jmreport/bi.vue
new file mode 100644
index 0000000..c773b4a
--- /dev/null
+++ b/src/views/report/jmreport/bi.vue
@@ -0,0 +1,15 @@
+<template>
+ <doc-alert title="澶у睆璁捐鍣�" url="https://doc.iocoder.cn/screen/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame :src="src" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { getRefreshToken } from '@/utils/auth'
+
+defineOptions({ name: 'JimuBI' })
+
+// 浣跨敤 getRefreshToken() 鏂规硶锛岃�屼笉浣跨敤 getAccessToken() 鏂规硶鐨勫師鍥狅細绉湪鎶ヨ〃鏃犳硶鏂逛究鐨勫埛鏂拌闂护鐗�
+const src = ref(import.meta.env.VITE_BASE_URL + '/drag/list?token=' + getRefreshToken())
+</script>
diff --git a/src/views/report/jmreport/index.vue b/src/views/report/jmreport/index.vue
new file mode 100644
index 0000000..3786151
--- /dev/null
+++ b/src/views/report/jmreport/index.vue
@@ -0,0 +1,15 @@
+<template>
+ <doc-alert title="鎶ヨ〃璁捐鍣�" url="https://doc.iocoder.cn/report/" />
+
+ <ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
+ <IFrame :src="src" />
+ </ContentWrap>
+</template>
+<script lang="ts" setup>
+import { getRefreshToken } from '@/utils/auth'
+
+defineOptions({ name: 'JimuReport' })
+
+// 浣跨敤 getRefreshToken() 鏂规硶锛岃�屼笉浣跨敤 getAccessToken() 鏂规硶鐨勫師鍥狅細绉湪鎶ヨ〃鏃犳硶鏂逛究鐨勫埛鏂拌闂护鐗�
+const src = ref(import.meta.env.VITE_BASE_URL + '/jmreport/list?token=' + getRefreshToken())
+</script>
diff --git a/src/views/system/area/AreaForm.vue b/src/views/system/area/AreaForm.vue
new file mode 100644
index 0000000..47dfd1d
--- /dev/null
+++ b/src/views/system/area/AreaForm.vue
@@ -0,0 +1,72 @@
+<template>
+ <Dialog v-model="dialogVisible" title="IP 鏌ヨ">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="IP" prop="ip">
+ <el-input v-model="formData.ip" placeholder="璇疯緭鍏� IP 鍦板潃" />
+ </el-form-item>
+ <el-form-item label="鍦板潃" prop="result">
+ <el-input v-model="formData.result" placeholder="灞曠ず鏌ヨ IP 缁撴灉" readonly />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as AreaApi from '@/api/system/area'
+
+defineOptions({ name: 'SystemAreaForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛氭彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ ip: '',
+ result: undefined
+})
+const formRules = reactive({
+ ip: [{ required: true, message: 'IP 鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async () => {
+ dialogVisible.value = true
+ resetForm()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ formData.value.result = await AreaApi.getAreaByIp(formData.value.ip!.trim())
+ message.success('鏌ヨ鎴愬姛')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ ip: '',
+ result: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/area/index.vue b/src/views/system/area/index.vue
new file mode 100644
index 0000000..339ecef
--- /dev/null
+++ b/src/views/system/area/index.vue
@@ -0,0 +1,79 @@
+<template>
+ <doc-alert title="鍦板尯 & IP" url="https://doc.iocoder.cn/area-and-ip/" />
+
+ <!-- 鎿嶄綔鏍� -->
+ <ContentWrap>
+ <el-button type="primary" plain @click="openForm()">
+ <Icon icon="ep:plus" class="mr-5px" /> IP 鏌ヨ
+ </el-button>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <div style="width: 100%; height: 700px">
+ <!-- AutoResizer 鑷姩璋冭妭澶у皬 -->
+ <el-auto-resizer>
+ <template #default="{ height, width }">
+ <!-- Virtualized Table 铏氭嫙鍖栬〃鏍硷細楂樻�ц兘锛岃В鍐宠〃鏍煎湪澶ф暟鎹噺涓嬬殑鍗¢】闂 -->
+ <el-table-v2
+ v-loading="loading"
+ :columns="columns"
+ :data="list"
+ :width="width"
+ :height="height"
+ expand-column-key="id"
+ />
+ </template>
+ </el-auto-resizer>
+ </div>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <AreaForm ref="formRef" />
+</template>
+<script setup lang="tsx">
+import { Column } from 'element-plus'
+import AreaForm from './AreaForm.vue'
+import * as AreaApi from '@/api/system/area'
+
+defineOptions({ name: 'SystemArea' })
+
+// 琛ㄦ牸鐨� column 瀛楁
+const columns: Column[] = [
+ {
+ dataKey: 'id', // 闇�瑕佹覆鏌撳綋鍓嶅垪鐨勬暟鎹瓧娈�
+ title: '缂栧彿', // 鏄剧ず鍦ㄥ崟鍏冩牸琛ㄥご鐨勬枃鏈�
+ width: 400, // 褰撳墠鍒楃殑瀹藉害锛屽繀椤昏缃�
+ fixed: true, // 鏄惁鍥哄畾鍒�
+ key: 'id' // 鏍戝舰灞曞紑瀵瑰簲鐨� key
+ },
+ {
+ dataKey: 'name',
+ title: '鍦板悕',
+ width: 200
+ }
+]
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref([]) // 琛ㄦ牸鐨勬暟鎹�
+
+/** 鑾峰緱鏁版嵁鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ list.value = await AreaApi.getAreaTree()
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = () => {
+ formRef.value.open()
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/dept/DeptForm.vue b/src/views/system/dept/DeptForm.vue
new file mode 100644
index 0000000..d6333f7
--- /dev/null
+++ b/src/views/system/dept/DeptForm.vue
@@ -0,0 +1,172 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="涓婄骇閮ㄩ棬" prop="parentId">
+ <el-tree-select
+ v-model="formData.parentId"
+ :data="deptTree"
+ :props="defaultProps"
+ check-strictly
+ default-expand-all
+ placeholder="璇烽�夋嫨涓婄骇閮ㄩ棬"
+ value-key="deptId"
+ />
+ </el-form-item>
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+ </el-form-item>
+ <el-form-item label="璐熻矗浜�" prop="leaderUserId">
+ <el-select v-model="formData.leaderUserId" clearable placeholder="璇疯緭鍏ヨ礋璐d汉">
+ <el-option
+ v-for="item in userList"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="phone">
+ <el-input v-model="formData.phone" maxlength="11" placeholder="璇疯緭鍏ヨ仈绯荤數璇�" />
+ </el-form-item>
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="formData.email" maxlength="50" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="formData.status" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { CommonStatusEnum } from '@/utils/constants'
+import { FormRules } from 'element-plus'
+
+defineOptions({ name: 'SystemDeptForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ title: '',
+ parentId: undefined,
+ name: undefined,
+ sort: undefined,
+ leaderUserId: undefined,
+ phone: undefined,
+ email: undefined,
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive<FormRules>({
+ parentId: [{ required: true, message: '涓婄骇閮ㄩ棬涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '閮ㄩ棬鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鏄剧ず鎺掑簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ email: [{ type: 'email', message: '璇疯緭鍏ユ纭殑閭鍦板潃', trigger: ['blur', 'change'] }],
+ phone: [{ pattern: /^1[3-9]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙风爜', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const deptTree = ref() // 鏍戝舰缁撴瀯
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DeptApi.getDept(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+ // 鑾峰緱閮ㄩ棬鏍�
+ await getTree()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as DeptApi.DeptVO
+ if (formType.value === 'create') {
+ await DeptApi.createDept(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DeptApi.updateDept(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ title: '',
+ parentId: undefined,
+ name: undefined,
+ sort: undefined,
+ leaderUserId: undefined,
+ phone: undefined,
+ email: undefined,
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鑾峰緱閮ㄩ棬鏍� */
+const getTree = async () => {
+ deptTree.value = []
+ const data = await DeptApi.getSimpleDeptList()
+ let dept: Tree = { id: 0, name: '椤剁骇閮ㄩ棬', children: [] }
+ dept.children = handleTree(data)
+ deptTree.value.push(dept)
+}
+</script>
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
new file mode 100644
index 0000000..0aceacf
--- /dev/null
+++ b/src/views/system/dept/index.vue
@@ -0,0 +1,220 @@
+<template>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閮ㄩ棬鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨閮ㄩ棬鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:dept:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button type="danger" plain @click="toggleExpandAll">
+ <Icon icon="ep:sort" class="mr-5px" /> 灞曞紑/鎶樺彔
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:dept:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ row-key="id"
+ :default-expand-all="isExpandAll"
+ v-if="refreshTable"
+ @selection-change="handleRowCheckboxChange"
+ >
+ <el-table-column type="selection" width="55" />
+ <el-table-column prop="name" label="閮ㄩ棬鍚嶇О" />
+ <el-table-column prop="leader" label="璐熻矗浜�">
+ <template #default="scope">
+ {{ userList.find((user) => user.id === scope.row.leaderUserId)?.nickname }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="sort" label="鎺掑簭" />
+ <el-table-column prop="status" label="鐘舵��">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:dept:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:dept:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DeptForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import { handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import DeptForm from './DeptForm.vue'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'SystemDept' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref() // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 100,
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const isExpandAll = ref(true) // 鏄惁灞曞紑锛岄粯璁ゅ叏閮ㄥ睍寮�
+const refreshTable = ref(true) // 閲嶆柊娓叉煋琛ㄦ牸鐘舵��
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+/** 鏌ヨ閮ㄩ棬鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DeptApi.getDeptList(queryParams)
+ list.value = handleTree(data)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+const toggleExpandAll = () => {
+ refreshTable.value = false
+ isExpandAll.value = !isExpandAll.value
+ nextTick(() => {
+ refreshTable.value = true
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryParams.pageNo = 1
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DeptApi.deleteDept(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: DeptApi.DeptVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await DeptApi.deleteDeptList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/system/dict/DictTypeForm.vue b/src/views/system/dict/DictTypeForm.vue
new file mode 100644
index 0000000..b9159b5
--- /dev/null
+++ b/src/views/system/dict/DictTypeForm.vue
@@ -0,0 +1,124 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="瀛楀吀鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ瓧鍏稿悕绉�" />
+ </el-form-item>
+ <el-form-item label="瀛楀吀绫诲瀷" prop="type">
+ <el-input
+ v-model="formData.type"
+ :disabled="typeof formData.id !== 'undefined'"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as DictTypeApi from '@/api/system/dict/dict.type'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemDictTypeForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ type: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '瀛楀吀鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ type: [{ required: true, message: '瀛楀吀绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DictTypeApi.getDictType(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as DictTypeApi.DictTypeVO
+ if (formType.value === 'create') {
+ await DictTypeApi.createDictType(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DictTypeApi.updateDictType(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ type: '',
+ name: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/dict/data/DictDataForm.vue b/src/views/system/dict/data/DictDataForm.vue
new file mode 100644
index 0000000..bab18db
--- /dev/null
+++ b/src/views/system/dict/data/DictDataForm.vue
@@ -0,0 +1,183 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="瀛楀吀绫诲瀷" prop="type">
+ <el-input
+ v-model="formData.dictType"
+ :disabled="typeof formData.id !== 'undefined'"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ />
+ </el-form-item>
+ <el-form-item label="鏁版嵁鏍囩" prop="label">
+ <el-input v-model="formData.label" placeholder="璇疯緭鍏ユ暟鎹爣绛�" />
+ </el-form-item>
+ <el-form-item label="鏁版嵁閿��" prop="value">
+ <el-input v-model="formData.value" placeholder="璇疯緭鍏ユ暟鎹敭鍊�" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" controls-position="right" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="棰滆壊绫诲瀷" prop="colorType">
+ <el-select v-model="formData.colorType">
+ <el-option
+ v-for="item in colorTypeOptions"
+ :key="item.value"
+ :label="item.label + '(' + item.value + ')'"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="CSS Class" prop="cssClass">
+ <el-input v-model="formData.cssClass" placeholder="璇疯緭鍏� CSS Class" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as DictDataApi from '@/api/system/dict/dict.data'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemDictDataForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ sort: undefined,
+ label: '',
+ value: '',
+ dictType: '',
+ status: CommonStatusEnum.ENABLE,
+ colorType: '',
+ cssClass: '',
+ remark: ''
+})
+const formRules = reactive({
+ label: [{ required: true, message: '鏁版嵁鏍囩涓嶈兘涓虹┖', trigger: 'blur' }],
+ value: [{ required: true, message: '鏁版嵁閿�间笉鑳戒负绌�', trigger: 'blur' }],
+ sort: [{ required: true, message: '鏁版嵁椤哄簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+// 鏁版嵁鏍囩鍥炴樉鏍峰紡
+const colorTypeOptions = readonly([
+ {
+ value: 'default',
+ label: '榛樿'
+ },
+ {
+ value: 'primary',
+ label: '涓昏'
+ },
+ {
+ value: 'success',
+ label: '鎴愬姛'
+ },
+ {
+ value: 'info',
+ label: '淇℃伅'
+ },
+ {
+ value: 'warning',
+ label: '璀﹀憡'
+ },
+ {
+ value: 'danger',
+ label: '鍗遍櫓'
+ }
+])
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, dictType?: string) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ if (dictType) {
+ formData.value.dictType = dictType
+ }
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await DictDataApi.getDictData(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as DictDataApi.DictDataVO
+ if (formType.value === 'create') {
+ await DictDataApi.createDictData(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await DictDataApi.updateDictData(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ sort: undefined,
+ label: '',
+ value: '',
+ dictType: '',
+ status: CommonStatusEnum.ENABLE,
+ colorType: '',
+ cssClass: '',
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/dict/data/index.vue b/src/views/system/dict/data/index.vue
new file mode 100644
index 0000000..49936c2
--- /dev/null
+++ b/src/views/system/dict/data/index.vue
@@ -0,0 +1,245 @@
+<template>
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="瀛楀吀鍚嶇О" prop="dictType">
+ <el-select v-model="queryParams.dictType" class="!w-240px" @change="dictChange">
+ <el-option
+ v-for="item in dictTypeList"
+ :key="item.type"
+ :label="item.name"
+ :value="item.type"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀛楀吀鏍囩" prop="label">
+ <el-input
+ v-model="queryParams.label"
+ placeholder="璇疯緭鍏ュ瓧鍏告爣绛�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="鏁版嵁鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:dict:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:dict:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:dict:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="瀛楀吀缂栫爜" align="center" prop="id" />
+ <el-table-column label="瀛楀吀鏍囩" align="center" prop="label" />
+ <el-table-column label="瀛楀吀閿��" align="center" prop="value" />
+ <el-table-column label="瀛楀吀鎺掑簭" align="center" prop="sort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="棰滆壊绫诲瀷" align="center" prop="colorType" />
+ <el-table-column label="CSS Class" align="center" prop="cssClass" />
+ <el-table-column label="澶囨敞" align="center" prop="remark" show-overflow-tooltip />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:dict:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:dict:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DictDataForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as DictDataApi from '@/api/system/dict/dict.data'
+import * as DictTypeApi from '@/api/system/dict/dict.type'
+import DictDataForm from './DictDataForm.vue'
+
+defineOptions({ name: 'SystemDictData' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+const route = useRoute() // 璺敱
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ label: '',
+ status: undefined,
+ dictType: route.params.dictType
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const dictTypeList = ref<DictTypeApi.DictTypeVO[]>() // 瀛楀吀绫诲瀷鐨勫垪琛�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DictDataApi.getDictDataPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 瀛楀吀绫诲瀷鏇存敼鍚屾椂鏇存柊鍒楄〃鏁版嵁 */
+const dictChange = (v) => {
+ queryParams.dictType = v
+ handleQuery()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id, queryParams.dictType)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DictDataApi.deleteDictData(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: DictDataApi.DictDataVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await DictDataApi.deleteDictDataList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await DictDataApi.exportDictData(queryParams)
+ download.excel(data, '瀛楀吀鏁版嵁.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鏌ヨ瀛楀吀锛堢簿绠�)鍒楄〃
+ dictTypeList.value = await DictTypeApi.getSimpleDictTypeList()
+})
+</script>
diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue
new file mode 100644
index 0000000..c66a1a8
--- /dev/null
+++ b/src/views/system/dict/index.vue
@@ -0,0 +1,261 @@
+<template>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="瀛楀吀鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ瓧鍏稿悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="瀛楀吀绫诲瀷" prop="type">
+ <el-input
+ v-model="queryParams.type"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ瓧鍏哥被鍨�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨瀛楀吀鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['system:dict:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['system:dict:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ <el-button
+ v-hasPermi="['system:dict:delete']"
+ :disabled="checkedIds.length === 0"
+ plain
+ type="danger"
+ @click="handleDeleteBatch"
+ >
+ <Icon class="mr-5px" icon="ep:delete" />
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column align="center" label="瀛楀吀缂栧彿" prop="id" />
+ <el-table-column align="center" label="瀛楀吀鍚嶇О" prop="name" show-overflow-tooltip />
+ <el-table-column align="center" label="瀛楀吀绫诲瀷" prop="type" width="300" />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['system:dict:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 淇敼
+ </el-button>
+ <router-link :to="'/dict/type/data/' + scope.row.type">
+ <el-button link type="primary">鏁版嵁</el-button>
+ </router-link>
+ <el-button
+ v-hasPermi="['system:dict:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <DictTypeForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as DictTypeApi from '@/api/system/dict/dict.type'
+import DictTypeForm from './DictTypeForm.vue'
+import download from '@/utils/download'
+
+defineOptions({ name: 'SystemDictType' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 瀛楀吀琛ㄦ牸鏁版嵁
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: '',
+ type: '',
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ瀛楀吀绫诲瀷鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await DictTypeApi.getDictTypePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await DictTypeApi.deleteDictType(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: DictTypeApi.DictTypeVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await DictTypeApi.deleteDictTypeList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await DictTypeApi.exportDictType(queryParams)
+ download.excel(data, '瀛楀吀绫诲瀷.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/loginlog/LoginLogDetail.vue b/src/views/system/loginlog/LoginLogDetail.vue
new file mode 100644
index 0000000..c6deb11
--- /dev/null
+++ b/src/views/system/loginlog/LoginLogDetail.vue
@@ -0,0 +1,51 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織缂栧彿" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐧诲綍绫诲瀷">
+ <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="detailData.logType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛鍚嶇О">
+ {{ detailData.username }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐧诲綍鍦板潃">
+ {{ detailData.userIp }}
+ </el-descriptions-item>
+ <el-descriptions-item label="娴忚鍣�">
+ {{ detailData.userAgent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐧婚檰缁撴灉">
+ <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="detailData.result" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐧诲綍鏃ユ湡">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as LoginLogApi from '@/api/system/loginLog'
+
+defineOptions({ name: 'SystemLoginLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as LoginLogApi.LoginLogVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: LoginLogApi.LoginLogVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/loginlog/index.vue b/src/views/system/loginlog/index.vue
new file mode 100644
index 0000000..a22ebc0
--- /dev/null
+++ b/src/views/system/loginlog/index.vue
@@ -0,0 +1,180 @@
+<template>
+ <doc-alert title="绯荤粺鏃ュ織" url="https://doc.iocoder.cn/system-log/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="username">
+ <el-input
+ v-model="queryParams.username"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐧诲綍鍦板潃" prop="userIp">
+ <el-input
+ v-model="queryParams.userIp"
+ placeholder="璇疯緭鍏ョ櫥褰曞湴鍧�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐧诲綍鏃ユ湡" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:login-log:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐧诲綍绫诲瀷" align="center" prop="logType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="scope.row.logType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛鍚嶇О" align="center" prop="username" width="180" />
+ <el-table-column label="鐧诲綍鍦板潃" align="center" prop="userIp" width="180" />
+ <el-table-column label="娴忚鍣�" align="center" prop="userAgent" />
+ <el-table-column label="鐧婚檰缁撴灉" align="center" prop="result">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="scope.row.result" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鐧诲綍鏃ユ湡"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['system:login-log:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <LoginLogDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as LoginLogApi from '@/api/system/loginLog'
+import LoginLogDetail from './LoginLogDetail.vue'
+
+defineOptions({ name: 'SystemLoginLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ username: undefined,
+ userIp: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await LoginLogApi.getLoginLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: LoginLogApi.LoginLogVO) => {
+ detailRef.value.open(data)
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await LoginLogApi.exportLoginLog(queryParams)
+ download.excel(data, '鐧诲綍鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/mail/account/MailAccountForm.vue b/src/views/system/mail/account/MailAccountForm.vue
new file mode 100644
index 0000000..e8d273f
--- /dev/null
+++ b/src/views/system/mail/account/MailAccountForm.vue
@@ -0,0 +1,159 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="150px"
+ >
+ <el-form-item label="閭" prop="mail">
+ <el-input v-model="formData.mail" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚�" prop="username">
+ <el-input v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
+ </el-form-item>
+ <el-form-item label="瀵嗙爜" prop="password">
+ <el-input
+ v-model="formData.password"
+ placeholder="璇疯緭鍏ュ瘑鐮�"
+ type="password"
+ show-password
+ />
+ </el-form-item>
+ <el-form-item label="SMTP 鏈嶅姟鍣ㄥ煙鍚�" prop="host">
+ <el-input v-model="formData.host" placeholder="璇疯緭鍏� SMTP 鏈嶅姟鍣ㄥ煙鍚�" />
+ </el-form-item>
+ <el-form-item label="SMTP 鏈嶅姟鍣ㄧ鍙�" prop="port">
+ <el-input-number
+ v-model="formData.port"
+ placeholder="璇疯緭鍏� SMTP 鏈嶅姟鍣ㄧ鍙�"
+ :min="1"
+ :max="65535"
+ />
+ </el-form-item>
+ <el-form-item label="鏄惁寮�鍚� SSL">
+ <el-radio-group v-model="formData.sslEnable">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏄惁寮�鍚� STARTTLS">
+ <el-radio-group v-model="formData.starttlsEnable">
+ <el-radio
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
+import * as MailAccountApi from '@/api/system/mail/account'
+
+defineOptions({ name: 'SystemMailAccountForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ mail: '',
+ username: '',
+ password: '',
+ host: '',
+ port: 465,
+ sslEnable: true,
+ starttlsEnable: false
+})
+const formRules = reactive({
+ mail: [
+ { required: true, message: '閭涓嶈兘涓虹┖', trigger: 'blur' },
+ { type: 'email', message: '璇疯緭鍏ユ纭殑閭鏍煎紡', trigger: ['blur', 'change'] }
+ ],
+ username: [{ required: true, message: '鐢ㄦ埛鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ password: [{ required: true, message: '瀵嗙爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ host: [{ required: true, message: 'SMTP 鏈嶅姟鍣ㄥ煙鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ port: [{ required: true, message: 'SMTP 鏈嶅姟鍣ㄧ鍙d笉鑳戒负绌�', trigger: 'blur' }],
+ sslEnable: [{ required: true, message: '鏄惁寮�鍚� SSL 涓嶈兘涓虹┖', trigger: 'blur' }],
+ starttlsEnable: [{ required: true, message: '鏄惁寮�鍚� STARTTLS 涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await MailAccountApi.getMailAccount(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as MailAccountApi.MailAccountVO
+ if (formType.value === 'create') {
+ await MailAccountApi.createMailAccount(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await MailAccountApi.updateMailAccount(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ mail: '',
+ username: '',
+ password: '',
+ host: '',
+ port: 465,
+ sslEnable: true,
+ starttlsEnable: false
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/mail/account/index.vue b/src/views/system/mail/account/index.vue
new file mode 100644
index 0000000..51c07ed
--- /dev/null
+++ b/src/views/system/mail/account/index.vue
@@ -0,0 +1,213 @@
+<template>
+ <doc-alert title="閭欢閰嶇疆" url="https://doc.iocoder.cn/mail" />
+
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="閭" prop="mail">
+ <el-input
+ v-model="queryParams.mail"
+ placeholder="璇疯緭鍏ラ偖绠�"
+ clearable
+ class="!w-240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚�" prop="username">
+ <el-input
+ v-model="queryParams.username"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕"
+ clearable
+ class="!w-240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:mail-account:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板</el-button
+ >
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:mail-account:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="閭" align="center" prop="mail" />
+ <el-table-column label="鐢ㄦ埛鍚�" align="center" prop="username" />
+ <el-table-column label="SMTP 鏈嶅姟鍣ㄥ煙鍚�" align="center" prop="host" />
+ <el-table-column label="SMTP 鏈嶅姟鍣ㄧ鍙�" align="center" prop="port" />
+ <el-table-column label="鏄惁寮�鍚� SSL" align="center" prop="sslEnable">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.sslEnable" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁寮�鍚� STARTTLS" align="center" prop="starttlsEnable">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.starttlsEnable" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:mail-account:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:mail-account:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <MailAccountForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as MailAccountApi from '@/api/system/mail/account'
+import MailAccountForm from './MailAccountForm.vue'
+
+defineOptions({ name: 'SystemMailAccount' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ mail: '',
+ username: '',
+ createTime: []
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MailAccountApi.getMailAccountPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await MailAccountApi.deleteMailAccount(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: MailAccountApi.MailAccountVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await MailAccountApi.deleteMailAccountList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/mail/log/MailLogDetail.vue b/src/views/system/mail/log/MailLogDetail.vue
new file mode 100644
index 0000000..37d757a
--- /dev/null
+++ b/src/views/system/mail/log/MailLogDetail.vue
@@ -0,0 +1,99 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織涓婚敭" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閭璐﹀彿">
+ {{ accountList.find((account) => account.id === detailData.accountId)?.mail }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閭欢妯℃澘">
+ {{ detailData.templateId }} | {{ detailData.templateCode }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯$増鍙戦�佷汉鍚嶇О">
+ {{ detailData.templateNickname }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎺ユ敹鐢ㄦ埛">
+ <span v-if="detailData.userType && detailData.userId">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+ ({{ detailData.userId }})
+ </span>
+ <span v-else>鏃�</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎺ユ敹淇℃伅">
+ <div>
+ <div v-if="detailData.toMails && detailData.toMails.length > 0">
+ 鏀朵欢锛�
+ <span v-for="(mail, index) in detailData.toMails" :key="mail">
+ {{ mail }}<span v-if="index < detailData.toMails.length - 1">銆�</span>
+ </span>
+ </div>
+ <div v-if="detailData.ccMails && detailData.ccMails.length > 0">
+ 鎶勯�侊細
+ <span v-for="(mail, index) in detailData.ccMails" :key="mail">
+ {{ mail }}<span v-if="index < detailData.ccMails.length - 1">銆�</span>
+ </span>
+ </div>
+ <div v-if="detailData.bccMails && detailData.bccMails.length > 0">
+ 瀵嗛�侊細
+ <span v-for="(mail, index) in detailData.bccMails" :key="mail">
+ {{ mail }}<span v-if="index < detailData.bccMails.length - 1">銆�</span>
+ </span>
+ </div>
+ </div>
+ </el-descriptions-item>
+ <el-descriptions-item label="閭欢鏍囬">
+ {{ detailData.templateTitle }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閭欢鍐呭">
+ <div v-dompurify-html="detailData.templateContent"></div>
+ </el-descriptions-item>
+ <el-descriptions-item label="閭欢鍙傛暟">
+ {{ detailData.templateParams }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佺姸鎬�">
+ <dict-tag :type="DICT_TYPE.SYSTEM_MAIL_SEND_STATUS" :value="detailData.sendStatus" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佹椂闂�">
+ {{ formatDate(detailData.sendTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佽繑鍥炵殑娑堟伅缂栧彿">
+ {{ detailData.sendMessageId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佸紓甯�">
+ {{ detailData.sendException }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as MailLogApi from '@/api/system/mail/log'
+import * as MailAccountApi from '@/api/system/mail/account'
+
+defineOptions({ name: 'SystemMailLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref() // 璇︽儏鏁版嵁
+const accountList = ref<MailAccountApi.MailAccountVO[]>([]) // 閭璐﹀彿鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: MailLogApi.MailLogVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+ // 鍔犺浇閭璐﹀彿鍒楄〃
+ accountList.value = await MailAccountApi.getSimpleMailAccountList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/mail/log/index.vue b/src/views/system/mail/log/index.vue
new file mode 100644
index 0000000..1bd794f
--- /dev/null
+++ b/src/views/system/mail/log/index.vue
@@ -0,0 +1,279 @@
+<template>
+ <doc-alert title="閭欢閰嶇疆" url="https://doc.iocoder.cn/mail" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="鎺ユ敹閭" prop="toMail">
+ <el-input
+ v-model="queryParams.toMail"
+ placeholder="璇疯緭鍏ユ帴鏀堕偖绠�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閭璐﹀彿" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ placeholder="璇烽�夋嫨閭璐﹀彿"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="account in accountList"
+ :key="account.id"
+ :value="account.id"
+ :label="account.mail"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栧彿" prop="templateId">
+ <el-input
+ v-model="queryParams.templateId"
+ placeholder="璇疯緭鍏ユā鏉跨紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍙戦�佺姸鎬�" prop="sendStatus">
+ <el-select
+ v-model="queryParams.sendStatus"
+ placeholder="璇烽�夋嫨鍙戦�佺姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戦�佹椂闂�" prop="sendTime">
+ <el-date-picker
+ v-model="queryParams.sendTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:mail-log:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="鍙戦�佹椂闂�"
+ align="center"
+ prop="sendTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎺ユ敹鐢ㄦ埛" align="center" width="150">
+ <template #default="scope">
+ <div v-if="scope.row.userType && scope.row.userId">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ <div>{{ '(' + scope.row.userId + ')' }}</div>
+ </div>
+ <div v-else>-</div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺ユ敹淇℃伅" align="center" width="300">
+ <template #default="scope">
+ <div class="text-left">
+ <div v-if="scope.row.toMails && scope.row.toMails.length > 0">
+ 鏀朵欢锛�
+ <span v-for="(mail, index) in scope.row.toMails" :key="mail">
+ {{ mail }}<span v-if="index < scope.row.toMails.length - 1">銆�</span>
+ </span>
+ </div>
+ <div v-if="scope.row.ccMails && scope.row.ccMails.length > 0">
+ 鎶勯�侊細
+ <span v-for="(mail, index) in scope.row.ccMails" :key="mail">
+ {{ mail }}<span v-if="index < scope.row.ccMails.length - 1">銆�</span>
+ </span>
+ </div>
+ <div v-if="scope.row.bccMails && scope.row.bccMails.length > 0">
+ 瀵嗛�侊細
+ <span v-for="(mail, index) in scope.row.bccMails" :key="mail">
+ {{ mail }}<span v-if="index < scope.row.bccMails.length - 1">銆�</span>
+ </span>
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="閭欢鏍囬" align="center" prop="templateTitle" width="200" />
+ <el-table-column label="鍙戦�佺姸鎬�" align="center" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_MAIL_SEND_STATUS" :value="scope.row.sendStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column label="閭璐﹀彿" align="center" width="200">
+ <template #default="scope">
+ {{ getAccountMail(scope.row.accountId) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="妯℃澘缂栧彿" align="center" prop="templateId" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" class-name="fixed-width">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['system:mail-log:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <MailLogDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as MailAccountApi from '@/api/system/mail/account'
+import * as MailLogApi from '@/api/system/mail/log'
+import MailLogDetail from './MailLogDetail.vue'
+
+defineOptions({ name: 'SystemMailLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ toMail: '',
+ accountId: undefined,
+ templateId: undefined,
+ sendStatus: undefined,
+ userId: undefined,
+ userType: undefined,
+ sendTime: []
+})
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const accountList = ref<MailAccountApi.MailAccountVO[]>([]) // 閭璐﹀彿鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MailLogApi.getMailLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await MailLogApi.exportMailLog(queryParams)
+ download.excel(data, '閭欢鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: MailLogApi.MailLogVO) => {
+ detailRef.value.open(data)
+}
+
+/** 鑾峰彇閭璐﹀彿鍚嶇О */
+const getAccountMail = (accountId: number) => {
+ const account = accountList.value.find((account) => account.id === accountId)
+ return account?.mail || ''
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇閭璐﹀彿鍒楄〃
+ accountList.value = await MailAccountApi.getSimpleMailAccountList()
+})
+</script>
diff --git a/src/views/system/mail/template/MailTemplateForm.vue b/src/views/system/mail/template/MailTemplateForm.vue
new file mode 100644
index 0000000..cae5f9d
--- /dev/null
+++ b/src/views/system/mail/template/MailTemplateForm.vue
@@ -0,0 +1,147 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" :width="800">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ >
+ <el-form-item label="閭璐﹀彿" prop="accountId">
+ <el-select v-model="formData.accountId" placeholder="璇烽�夋嫨閭璐﹀彿">
+ <el-option
+ v-for="account in accountList"
+ :key="account.id"
+ :label="account.mail"
+ :value="account.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栫爜" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ユā鏉跨紪鐮�" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユā鏉垮悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍙戦�佷汉鍚嶇О" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ュ彂閫佷汉鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鏍囬" prop="title">
+ <el-input v-model="formData.title" placeholder="璇疯緭鍏ユā鏉挎爣棰�" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍐呭" prop="content">
+ <Editor v-model="formData.content" height="200px" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as MailTemplateApi from '@/api/system/mail/template'
+import * as MailAccountApi from '@/api/system/mail/account'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemMailTemplateForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ code: '',
+ accountId: undefined,
+ nickname: '',
+ title: '',
+ content: '',
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ accountId: [{ required: true, message: '閭璐﹀彿涓嶈兘涓虹┖', trigger: 'change' }],
+ code: [{ required: true, message: '妯℃澘缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '妯℃澘鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ title: [{ required: true, message: '妯℃澘鏍囬涓嶈兘涓虹┖', trigger: 'blur' }],
+ content: [{ required: true, message: '妯℃澘鍐呭涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const accountList = ref<MailAccountApi.MailAccountVO[]>([]) // 閭璐﹀彿鍒楄〃
+
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await MailTemplateApi.getMailTemplate(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇閭璐﹀彿鍒楄〃
+ accountList.value = await MailAccountApi.getSimpleMailAccountList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as MailTemplateApi.MailTemplateVO
+ if (formType.value === 'create') {
+ await MailTemplateApi.createMailTemplate(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await MailTemplateApi.updateMailTemplate(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ code: '',
+ accountId: undefined,
+ nickname: '',
+ title: '',
+ content: '',
+ status: CommonStatusEnum.ENABLE
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/mail/template/MailTemplateSendForm.vue b/src/views/system/mail/template/MailTemplateSendForm.vue
new file mode 100644
index 0000000..3decb44
--- /dev/null
+++ b/src/views/system/mail/template/MailTemplateSendForm.vue
@@ -0,0 +1,136 @@
+<template>
+ <Dialog v-model="dialogVisible" title="娴嬭瘯">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ >
+ <el-form-item label="妯℃澘鍐呭" prop="content">
+ <Editor :model-value="formData.content" height="150px" readonly />
+ </el-form-item>
+ <el-form-item label="鏀朵欢閭" prop="toMails">
+ <el-input-tag
+ v-model="formData.toMails"
+ placeholder="璇疯緭鍏ユ敹浠堕偖绠憋紝澶氫釜閭鐢ㄥ洖杞﹀垎闅�"
+ class="!w-full"
+ />
+ </el-form-item>
+ <el-form-item label="鎶勯�侀偖绠�" prop="ccMails">
+ <el-input-tag
+ v-model="formData.ccMails"
+ placeholder="璇疯緭鍏ユ妱閫侀偖绠憋紝澶氫釜閭鐢ㄥ洖杞﹀垎闅�"
+ class="!w-full"
+ />
+ </el-form-item>
+ <el-form-item label="瀵嗛�侀偖绠�" prop="bccMails">
+ <el-input-tag
+ v-model="formData.bccMails"
+ placeholder="璇疯緭鍏ュ瘑閫侀偖绠憋紝澶氫釜閭鐢ㄥ洖杞﹀垎闅�"
+ class="!w-full"
+ />
+ </el-form-item>
+ <el-form-item
+ v-for="param in formData.params"
+ :key="param"
+ :label="'鍙傛暟 {' + param + '}'"
+ :prop="'templateParams.' + param"
+ >
+ <el-input
+ v-model="formData.templateParams[param]"
+ :placeholder="'璇疯緭鍏� ' + param + ' 鍙傛暟'"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as MailTemplateApi from '@/api/system/mail/template'
+
+defineOptions({ name: 'SystemMailTemplateSendForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ content: '',
+ params: {},
+ toMails: [],
+ ccMails: [],
+ bccMails: [],
+ templateCode: '',
+ templateParams: new Map()
+})
+const formRules = reactive({
+ templateCode: [{ required: true, message: '妯$増缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ templateParams: {}
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formLoading.value = true
+ try {
+ const data = await MailTemplateApi.getMailTemplate(id)
+ // 璁剧疆鍔ㄦ�佽〃鍗�
+ formData.value.content = data.content
+ formData.value.params = data.params
+ formData.value.templateCode = data.code
+ formData.value.templateParams = data.params.reduce((obj, item) => {
+ obj[item] = '' // 缁欐瘡涓姩鎬佸睘鎬ц祴鍊硷紝閬垮厤鏃犳硶璇诲彇
+ return obj
+ }, {})
+ formRules.templateParams = data.params.reduce((obj, item) => {
+ obj[item] = { required: true, message: '鍙傛暟 ' + item + ' 涓嶈兘涓虹┖', trigger: 'blur' }
+ return obj
+ }, {})
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as MailTemplateApi.MailSendReqVO
+ const logId = await MailTemplateApi.sendMail(data)
+ if (logId) {
+ message.success('鎻愪氦鍙戦�佹垚鍔燂紒鍙戦�佺粨鏋滐紝瑙佸彂閫佹棩蹇楃紪鍙凤細' + logId)
+ }
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ content: '',
+ params: {},
+ toMails: [],
+ ccMails: [],
+ bccMails: [],
+ templateCode: '',
+ templateParams: new Map()
+ }
+ formRules.templateParams = {}
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/mail/template/index.vue b/src/views/system/mail/template/index.vue
new file mode 100644
index 0000000..5913f30
--- /dev/null
+++ b/src/views/system/mail/template/index.vue
@@ -0,0 +1,302 @@
+<template>
+ <doc-alert title="閭欢閰嶇疆" url="https://doc.iocoder.cn/mail" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="150px"
+ >
+ <el-form-item label="妯℃澘缂栫爜" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ユā鏉跨紪鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="閭璐﹀彿" prop="accountId">
+ <el-select
+ v-model="queryParams.accountId"
+ placeholder="璇烽�夋嫨閭璐﹀彿"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="account in accountList"
+ :key="account.id"
+ :value="account.id"
+ :label="account.mail"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨寮�鍚姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:mail-template:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:mail-template:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" />鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column
+ label="妯℃澘缂栫爜"
+ align="center"
+ prop="code"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="妯℃澘鍚嶇О"
+ align="center"
+ prop="name"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="妯℃澘鏍囬"
+ align="center"
+ prop="title"
+ width="150"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="妯℃澘鍐呭"
+ align="center"
+ prop="content"
+ min-width="200"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="閭璐﹀彿" align="center" prop="accountId" width="200">
+ <template #default="scope">
+ {{ getAccountMail(scope.row.accountId) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戦�佷汉鍚嶇О"
+ align="center"
+ prop="nickname"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="寮�鍚姸鎬�" align="center" prop="status" width="80">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="210" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:mail-template:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openSendForm(scope.row.id)"
+ v-hasPermi="['system:mail-template:send-mail']"
+ >
+ 娴嬭瘯
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:mail-template:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <MailTemplateForm ref="formRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氭祴璇曞彂閫� -->
+ <MailTemplateSendForm ref="sendFormRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as MailTemplateApi from '@/api/system/mail/template'
+import * as MailAccountApi from '@/api/system/mail/account'
+import MailTemplateForm from './MailTemplateForm.vue'
+import MailTemplateSendForm from './MailTemplateSendForm.vue'
+
+defineOptions({ name: 'SystemMailTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ code: '',
+ name: '',
+ accountId: undefined,
+ status: undefined,
+ createTime: []
+})
+const accountList = ref<MailAccountApi.MailAccountVO[]>([]) // 閭璐﹀彿鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MailTemplateApi.getMailTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍙戦�侀偖浠舵寜閽� */
+const sendFormRef = ref()
+const openSendForm = (id: number) => {
+ sendFormRef.value.open(id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await MailTemplateApi.deleteMailTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: MailTemplateApi.MailTemplateVO[]) => {
+ checkedIds.value = rows.map((row) => row.id!)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await MailTemplateApi.deleteMailTemplateList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鑾峰彇閭璐﹀彿鍚嶇О */
+const getAccountMail = (accountId: number) => {
+ const account = accountList.value.find((account) => account.id === accountId)
+ return account?.mail || ''
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇閭璐﹀彿鍒楄〃
+ accountList.value = await MailAccountApi.getSimpleMailAccountList()
+})
+</script>
diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue
new file mode 100644
index 0000000..ddc3d3e
--- /dev/null
+++ b/src/views/system/menu/MenuForm.vue
@@ -0,0 +1,257 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="100px"
+ >
+ <el-form-item label="涓婄骇鑿滃崟">
+ <el-tree-select
+ v-model="formData.parentId"
+ :data="menuTree"
+ :default-expanded-keys="[0]"
+ :props="defaultProps"
+ check-strictly
+ node-key="id"
+ />
+ </el-form-item>
+ <el-form-item label="鑿滃崟鍚嶇О" prop="name">
+ <el-input v-model="formData.name" clearable placeholder="璇疯緭鍏ヨ彍鍗曞悕绉�" />
+ </el-form-item>
+ <el-form-item label="鑿滃崟绫诲瀷" prop="type">
+ <el-radio-group v-model="formData.type">
+ <el-radio-button
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)"
+ :key="dict.label"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="formData.type !== 3" label="鑿滃崟鍥炬爣">
+ <IconSelect v-model="formData.icon" clearable />
+ </el-form-item>
+ <el-form-item v-if="formData.type !== 3" label="璺敱鍦板潃" prop="path">
+ <template #label>
+ <Tooltip
+ message="璁块棶鐨勮矾鐢卞湴鍧�锛屽锛歚user`銆傚闇�澶栫綉鍦板潃鏃讹紝鍒欎互 `http(s)://` 寮�澶�"
+ title="璺敱鍦板潃"
+ />
+ </template>
+ <el-input v-model="formData.path" clearable placeholder="璇疯緭鍏ヨ矾鐢卞湴鍧�" />
+ </el-form-item>
+ <el-form-item v-if="formData.type === 2" label="缁勪欢鍦板潃" prop="component">
+ <el-input v-model="formData.component" clearable placeholder="渚嬪璇达細system/user/index" />
+ </el-form-item>
+ <el-form-item v-if="formData.type === 2" label="缁勪欢鍚嶅瓧" prop="componentName">
+ <el-input v-model="formData.componentName" clearable placeholder="渚嬪璇达細SystemUser" />
+ </el-form-item>
+ <el-form-item v-if="formData.type !== 1" label="鏉冮檺鏍囪瘑" prop="permission">
+ <template #label>
+ <Tooltip
+ message="Controller 鏂规硶涓婄殑鏉冮檺瀛楃锛屽锛欯PreAuthorize(`@ss.hasPermission('system:user:list')`)"
+ title="鏉冮檺鏍囪瘑"
+ />
+ </template>
+ <el-input v-model="formData.permission" clearable placeholder="璇疯緭鍏ユ潈闄愭爣璇�" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず鎺掑簭" prop="sort">
+ <el-input-number v-model="formData.sort" :min="0" clearable controls-position="right" />
+ </el-form-item>
+ <el-form-item label="鑿滃崟鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.label"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="formData.type !== 3" label="鏄剧ず鐘舵��" prop="visible">
+ <template #label>
+ <Tooltip message="閫夋嫨闅愯棌鏃讹紝璺敱灏嗕笉浼氬嚭鐜板湪渚ц竟鏍忥紝浣嗕粛鐒跺彲浠ヨ闂�" title="鏄剧ず鐘舵��" />
+ </template>
+ <el-radio-group v-model="formData.visible">
+ <el-radio key="true" :value="true" border>鏄剧ず</el-radio>
+ <el-radio key="false" :value="false" border>闅愯棌</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="formData.type !== 3" label="鎬绘槸鏄剧ず" prop="alwaysShow">
+ <template #label>
+ <Tooltip
+ message="閫夋嫨涓嶆槸鏃讹紝褰撹鑿滃崟鍙湁涓�涓瓙鑿滃崟鏃讹紝涓嶅睍绀鸿嚜宸憋紝鐩存帴灞曠ず瀛愯彍鍗�"
+ title="鎬绘槸鏄剧ず"
+ />
+ </template>
+ <el-radio-group v-model="formData.alwaysShow">
+ <el-radio key="true" :value="true" border>鎬绘槸</el-radio>
+ <el-radio key="false" :value="false" border>涓嶆槸</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="formData.type === 2" label="缂撳瓨鐘舵��" prop="keepAlive">
+ <template #label>
+ <Tooltip
+ message="閫夋嫨缂撳瓨鏃讹紝鍒欎細琚� `keep-alive` 缂撳瓨锛屽繀椤诲~鍐欍�岀粍浠跺悕绉般�嶅瓧娈�"
+ title="缂撳瓨鐘舵��"
+ />
+ </template>
+ <el-radio-group v-model="formData.keepAlive">
+ <el-radio key="true" :value="true" border>缂撳瓨</el-radio>
+ <el-radio key="false" :value="false" border>涓嶇紦瀛�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as MenuApi from '@/api/system/menu'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+import { CommonStatusEnum, SystemMenuTypeEnum } from '@/utils/constants'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+defineOptions({ name: 'SystemMenuForm' })
+
+const { wsCache } = useCache()
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ permission: '',
+ type: SystemMenuTypeEnum.DIR,
+ sort: Number(undefined),
+ parentId: 0,
+ path: '',
+ icon: '',
+ component: '',
+ componentName: '',
+ status: CommonStatusEnum.ENABLE,
+ visible: true,
+ keepAlive: true,
+ alwaysShow: true
+})
+const formRules = reactive({
+ name: [{ required: true, message: '鑿滃崟鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ type: [{ required: true, message: '鑿滃崟绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ sort: [{ required: true, message: '鑿滃崟椤哄簭涓嶈兘涓虹┖', trigger: 'blur' }],
+ path: [{ required: true, message: '璺敱鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number, parentId?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ if (parentId) {
+ formData.value.parentId = parentId
+ }
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await MenuApi.getMenu(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鑾峰緱鑿滃崟鍒楄〃
+ await getTree()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ if (
+ formData.value.type === SystemMenuTypeEnum.DIR ||
+ formData.value.type === SystemMenuTypeEnum.MENU
+ ) {
+ if (!isExternal(formData.value.path)) {
+ if (formData.value.parentId === 0 && formData.value.path.charAt(0) !== '/') {
+ message.error('璺緞蹇呴』浠� / 寮�澶�')
+ return
+ } else if (formData.value.parentId !== 0 && formData.value.path.charAt(0) === '/') {
+ message.error('璺緞涓嶈兘浠� / 寮�澶�')
+ return
+ }
+ }
+ }
+ const data = formData.value as unknown as MenuApi.MenuVO
+ if (formType.value === 'create') {
+ await MenuApi.createMenu(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await MenuApi.updateMenu(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ // 娓呯┖锛屼粠鑰岃Е鍙戝埛鏂�
+ wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+ }
+}
+
+/** 鑾峰彇涓嬫媺妗哰涓婄骇鑿滃崟]鐨勬暟鎹� */
+const menuTree = ref<Tree[]>([]) // 鏍戝舰缁撴瀯
+const getTree = async () => {
+ menuTree.value = []
+ const res = await MenuApi.getSimpleMenusList()
+ let menu: Tree = { id: 0, name: '涓荤被鐩�', children: [] }
+ menu.children = handleTree(res)
+ menuTree.value.push(menu)
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ permission: '',
+ type: SystemMenuTypeEnum.DIR,
+ sort: Number(undefined),
+ parentId: 0,
+ path: '',
+ icon: '',
+ component: '',
+ componentName: '',
+ status: CommonStatusEnum.ENABLE,
+ visible: true,
+ keepAlive: true,
+ alwaysShow: true
+ }
+ formRef.value?.resetFields()
+}
+
+/** 鍒ゆ柇 path 鏄笉鏄閮ㄧ殑 HTTP 绛夐摼鎺� */
+const isExternal = (path: string) => {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+</script>
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
new file mode 100644
index 0000000..af3e0ad
--- /dev/null
+++ b/src/views/system/menu/index.vue
@@ -0,0 +1,318 @@
+<template>
+ <doc-alert title="鍔熻兘鏉冮檺" url="https://doc.iocoder.cn/resource-permission" />
+ <doc-alert title="鑿滃崟璺敱" url="https://doc.iocoder.cn/vue3/route/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="鑿滃崟鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ彍鍗曞悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鑿滃崟鐘舵��"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['system:menu:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button plain type="danger" @click="toggleExpandAll">
+ <Icon class="mr-5px" icon="ep:sort" />
+ 灞曞紑/鎶樺彔
+ </el-button>
+ <el-button plain @click="refreshMenu">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 鍒锋柊鑿滃崟缂撳瓨
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-auto-resizer>
+ <template #default="{ width }">
+ <el-table-v2
+ v-model:expanded-row-keys="expandedRowKeys"
+ :columns="columns"
+ :data="list"
+ :expand-column-key="columns[0].key"
+ :height="1000"
+ :width="width"
+ fixed
+ row-key="id"
+ />
+ </template>
+ </el-auto-resizer>
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <MenuForm ref="formRef" @success="getList" />
+</template>
+<script lang="tsx" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { handleTree } from '@/utils/tree'
+import * as MenuApi from '@/api/system/menu'
+import { MenuVO } from '@/api/system/menu'
+import MenuForm from './MenuForm.vue'
+import DictTag from '@/components/DictTag/src/DictTag.vue'
+import { Icon } from '@/components/Icon'
+import { ElButton, TableV2FixedDir, ElSwitch } from 'element-plus'
+import { checkPermi } from '@/utils/permission'
+import { CommonStatusEnum } from '@/utils/constants'
+import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
+
+defineOptions({ name: 'SystemMenu' })
+
+// 铏氭嫙鍒楄〃琛ㄦ牸
+const columns = [
+ {
+ key: 'name',
+ title: '鑿滃崟鍚嶇О',
+ dataKey: 'name',
+ width: 250,
+ fixed: TableV2FixedDir.LEFT
+ },
+ {
+ key: 'icon',
+ title: '鍥炬爣',
+ dataKey: 'icon',
+ width: 100,
+ align: 'center',
+ cellRenderer: ({ cellData: icon }) => <Icon icon={icon} />
+ },
+ {
+ key: 'sort',
+ title: '鎺掑簭',
+ dataKey: 'sort',
+ width: 60
+ },
+ {
+ key: 'permission',
+ title: '鏉冮檺鏍囪瘑',
+ dataKey: 'permission',
+ width: 300
+ },
+ {
+ key: 'component',
+ title: '缁勪欢璺緞',
+ dataKey: 'component',
+ width: 500
+ },
+ {
+ key: 'componentName',
+ title: '缁勪欢鍚嶇О',
+ dataKey: 'componentName',
+ width: 200
+ },
+ {
+ key: 'status',
+ title: '鐘舵��',
+ dataKey: 'status',
+ width: 60,
+ fixed: TableV2FixedDir.RIGHT,
+ cellRenderer: ({ rowData }) => {
+ // 妫�鏌ユ潈闄�
+ if (!checkPermi(['system:menu:update'])) {
+ return <DictTag type={DICT_TYPE.COMMON_STATUS} value={rowData.status} />
+ }
+
+ // 濡傛灉鏈夋潈闄愶紝娓叉煋 ElSwitch
+ return (
+ <ElSwitch
+ v-model={rowData.status}
+ active-value={CommonStatusEnum.ENABLE}
+ inactive-value={CommonStatusEnum.DISABLE}
+ loading={menuStatusUpdating[rowData.id]}
+ class="ml-4px"
+ onChange={(val) => handleStatusChanged(rowData, val)}
+ />
+ )
+ }
+ },
+ {
+ key: 'operations',
+ title: '鎿嶄綔',
+ align: 'center',
+ width: 160,
+ fixed: TableV2FixedDir.RIGHT,
+ cellRenderer: ({ rowData }) => {
+ // 瀹氫箟鎸夐挳鍒楄〃
+ const buttons: InstanceType<typeof ElButton>[] = []
+
+ // 妫�鏌ユ潈闄愬苟娣诲姞鎸夐挳
+ if (checkPermi(['system:menu:update'])) {
+ buttons.push(
+ <ElButton key="edit" link type="primary" onClick={() => openForm('update', rowData.id)}>
+ 淇敼
+ </ElButton>
+ )
+ }
+ if (checkPermi(['system:menu:create'])) {
+ buttons.push(
+ <ElButton
+ key="create"
+ link
+ type="primary"
+ onClick={() => openForm('create', undefined, rowData.id)}
+ >
+ 鏂板
+ </ElButton>
+ )
+ }
+ if (checkPermi(['system:menu:delete'])) {
+ buttons.push(
+ <ElButton key="delete" link type="danger" onClick={() => handleDelete(rowData.id)}>
+ 鍒犻櫎
+ </ElButton>
+ )
+ }
+ // 濡傛灉娌℃湁鏉冮檺锛岃繑鍥� null
+ if (buttons.length === 0) {
+ return null
+ }
+ // 娓叉煋鎸夐挳鍒楄〃
+ return <>{buttons}</>
+ }
+ }
+]
+
+const { wsCache } = useCache()
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const list = ref<any[]>([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ name: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const isExpandAll = ref(false) // 鏄惁灞曞紑锛岄粯璁ゅ叏閮ㄦ姌鍙�
+const refreshTable = ref(true) // 閲嶆柊娓叉煋琛ㄦ牸鐘舵��
+
+// 娣诲姞灞曞紑琛屾帶鍒�
+const expandedRowKeys = ref<number[]>([])
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await MenuApi.getMenuList(queryParams)
+ list.value = handleTree(data)
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number, parentId?: number) => {
+ formRef.value.open(type, id, parentId)
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+const toggleExpandAll = () => {
+ if (!isExpandAll.value) {
+ // 灞曞紑鎵�鏈�
+ expandedRowKeys.value = list.value.map((item) => item.id)
+ } else {
+ // 鎶樺彔鎵�鏈�
+ expandedRowKeys.value = []
+ }
+ isExpandAll.value = !isExpandAll.value
+}
+
+/** 鍒锋柊鑿滃崟缂撳瓨鎸夐挳鎿嶄綔 */
+const refreshMenu = async () => {
+ try {
+ await message.confirm('鍗冲皢鏇存柊缂撳瓨鍒锋柊娴忚鍣紒', '鍒锋柊鑿滃崟缂撳瓨')
+ // 娓呯┖锛屼粠鑰岃Е鍙戝埛鏂�
+ wsCache.delete(CACHE_KEY.USER)
+ wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
+ // 鍒锋柊娴忚鍣�
+ location.reload()
+ } catch {}
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await MenuApi.deleteMenu(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 寮�鍚�/鍏抽棴鑿滃崟鐨勭姸鎬� */
+const menuStatusUpdating = ref({}) // 鑿滃崟鐘舵�佹洿鏂颁腑鐨� menu 鏄犲皠銆俴ey锛氳彍鍗曠紪鍙凤紝value锛氭槸鍚︽洿鏂颁腑
+const handleStatusChanged = async (menu: MenuVO, val: number) => {
+ // 1. 鏍囪 menu.id 鏇存柊涓�
+ menuStatusUpdating.value[menu.id] = true
+ try {
+ // 2. 鍙戣捣鏇存柊鐘舵��
+ menu.status = val
+ await MenuApi.updateMenu(menu)
+ } finally {
+ // 3. 鏍囪 menu.id 鏇存柊瀹屾垚
+ menuStatusUpdating.value[menu.id] = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/notice/NoticeForm.vue b/src/views/system/notice/NoticeForm.vue
new file mode 100644
index 0000000..5c18d80
--- /dev/null
+++ b/src/views/system/notice/NoticeForm.vue
@@ -0,0 +1,132 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="鍏憡鏍囬" prop="title">
+ <el-input v-model="formData.title" placeholder="璇疯緭鍏ュ叕鍛婃爣棰�" />
+ </el-form-item>
+ <el-form-item label="鍏憡鍐呭" prop="content">
+ <Editor v-model="formData.content" height="150px" />
+ </el-form-item>
+ <el-form-item label="鍏憡绫诲瀷" prop="type">
+ <el-select v-model="formData.type" clearable placeholder="璇烽�夋嫨鍏憡绫诲瀷">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE)"
+ :key="parseInt(dict.value as any)"
+ :label="dict.label"
+ :value="parseInt(dict.value as any)"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="formData.status" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="parseInt(dict.value as any)"
+ :label="dict.label"
+ :value="parseInt(dict.value as any)"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭澶囨敞" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as NoticeApi from '@/api/system/notice'
+
+defineOptions({ name: 'SystemNoticeForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ title: '',
+ type: undefined,
+ content: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+})
+const formRules = reactive({
+ title: [{ required: true, message: '鍏憡鏍囬涓嶈兘涓虹┖', trigger: 'blur' }],
+ type: [{ required: true, message: '鍏憡绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }],
+ content: [{ required: true, message: '鍏憡鍐呭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await NoticeApi.getNotice(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as NoticeApi.NoticeVO
+ if (formType.value === 'create') {
+ await NoticeApi.createNotice(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await NoticeApi.updateNotice(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ title: '',
+ type: undefined,
+ content: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue
new file mode 100644
index 0000000..5fecbbd
--- /dev/null
+++ b/src/views/system/notice/index.vue
@@ -0,0 +1,218 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鍏憡鏍囬" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ュ叕鍛婃爣棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍏憡鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鍏憡鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:notice:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:notice:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="鍏憡缂栧彿" align="center" prop="id" />
+ <el-table-column label="鍏憡鏍囬" align="center" prop="title" />
+ <el-table-column label="鍏憡绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:notice:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:notice:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ <el-button link @click="handlePush(scope.row.id)" v-hasPermi="['system:notice:update']">
+ 鎺ㄩ��
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <NoticeForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as NoticeApi from '@/api/system/notice'
+import NoticeForm from './NoticeForm.vue'
+
+defineOptions({ name: 'SystemNotice' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ title: '',
+ type: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍏憡鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await NoticeApi.getNoticePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await NoticeApi.deleteNotice(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: NoticeApi.NoticeVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await NoticeApi.deleteNoticeList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎺ㄩ�佹寜閽搷浣� */
+const handlePush = async (id: number) => {
+ try {
+ // 鎺ㄩ�佺殑浜屾纭
+ await message.confirm('鏄惁鎺ㄩ�佹墍閫変腑閫氱煡锛�')
+ // 鍙戣捣鎺ㄩ��
+ await NoticeApi.pushNotice(id)
+ message.success(t('鎺ㄩ�佹垚鍔�'))
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/notify/message/NotifyMessageDetail.vue b/src/views/system/notify/message/NotifyMessageDetail.vue
new file mode 100644
index 0000000..8472351
--- /dev/null
+++ b/src/views/system/notify/message/NotifyMessageDetail.vue
@@ -0,0 +1,66 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="璇︽儏">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="缂栧彿" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛绫诲瀷">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛缂栧彿">
+ {{ detailData.userId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯$増缂栧彿">
+ {{ detailData.templateId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯℃澘缂栫爜">
+ {{ detailData.templateCode }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佷汉鍚嶇О">
+ {{ detailData.templateNickname }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯$増鍐呭">
+ {{ detailData.templateContent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯$増鍙傛暟">
+ {{ detailData.templateParams }}
+ </el-descriptions-item>
+ <el-descriptions-item label="妯$増绫诲瀷">
+ <dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="detailData.templateType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄惁宸茶">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="detailData.readStatus" />
+ </el-descriptions-item>
+ <el-descriptions-item label="闃呰鏃堕棿">
+ {{ formatDate(detailData.readTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as NotifyMessageApi from '@/api/system/notify/message'
+
+defineOptions({ name: 'SystemNotifyMessageDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as NotifyMessageApi.NotifyMessageVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: NotifyMessageApi.NotifyMessageVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/notify/message/index.vue b/src/views/system/notify/message/index.vue
new file mode 100644
index 0000000..9484411
--- /dev/null
+++ b/src/views/system/notify/message/index.vue
@@ -0,0 +1,212 @@
+<template>
+ <doc-alert title="绔欏唴淇¢厤缃�" url="https://doc.iocoder.cn/notify/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栫爜" prop="templateCode">
+ <el-input
+ v-model="queryParams.templateCode"
+ placeholder="璇疯緭鍏ユā鏉跨紪鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="妯$増绫诲瀷" prop="templateType">
+ <el-select
+ v-model="queryParams.templateType"
+ placeholder="璇烽�夋嫨妯$増绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐢ㄦ埛绫诲瀷" align="center" prop="userType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" width="80" />
+ <el-table-column label="妯℃澘缂栫爜" align="center" prop="templateCode" width="80" />
+ <el-table-column label="鍙戦�佷汉鍚嶇О" align="center" prop="templateNickname" width="180" />
+ <el-table-column
+ label="妯$増鍐呭"
+ align="center"
+ prop="templateContent"
+ width="200"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="妯$増鍙傛暟"
+ align="center"
+ prop="templateParams"
+ width="180"
+ show-overflow-tooltip
+ >
+ <template #default="scope"> {{ scope.row.templateParams }}</template>
+ </el-table-column>
+ <el-table-column label="妯$増绫诲瀷" align="center" prop="templateType" width="120">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.templateType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁宸茶" align="center" prop="readStatus" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="闃呰鏃堕棿"
+ align="center"
+ prop="readTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['system:notify-message:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <NotifyMessageDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as NotifyMessageApi from '@/api/system/notify/message'
+import NotifyMessageDetail from './NotifyMessageDetail.vue'
+
+defineOptions({ name: 'SystemNotifyMessage' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userType: undefined,
+ userId: undefined,
+ templateCode: undefined,
+ templateType: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await NotifyMessageApi.getNotifyMessagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: NotifyMessageApi.NotifyMessageVO) => {
+ detailRef.value.open(data)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/notify/my/MyNotifyMessageDetail.vue b/src/views/system/notify/my/MyNotifyMessageDetail.vue
new file mode 100644
index 0000000..0bfa30c
--- /dev/null
+++ b/src/views/system/notify/my/MyNotifyMessageDetail.vue
@@ -0,0 +1,48 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="娑堟伅璇︽儏">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鍙戦�佷汉">
+ {{ detailData.templateNickname }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佹椂闂�">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="娑堟伅绫诲瀷">
+ <dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="detailData.templateType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏄惁宸茶">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="detailData.readStatus" />
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detailData.readStatus" label="闃呰鏃堕棿">
+ {{ formatDate(detailData.readTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍐呭">
+ {{ detailData.templateContent }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as NotifyMessageApi from '@/api/system/notify/message'
+
+defineOptions({ name: 'MyNotifyMessageDetailDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as NotifyMessageApi.NotifyMessageVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: NotifyMessageApi.NotifyMessageVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/notify/my/index.vue b/src/views/system/notify/my/index.vue
new file mode 100644
index 0000000..ae4b9c5
--- /dev/null
+++ b/src/views/system/notify/my/index.vue
@@ -0,0 +1,218 @@
+<template>
+ <doc-alert title="绔欏唴淇¢厤缃�" url="https://doc.iocoder.cn/notify/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鏄惁宸茶" prop="readStatus">
+ <el-select
+ v-model="queryParams.readStatus"
+ placeholder="璇烽�夋嫨鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戦�佹椂闂�" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button @click="handleUpdateList">
+ <Icon icon="ep:reading" class="mr-5px" /> 鏍囪宸茶
+ </el-button>
+ <el-button @click="handleUpdateAll">
+ <Icon icon="ep:reading" class="mr-5px" /> 鍏ㄩ儴宸茶
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table
+ v-loading="loading"
+ :data="list"
+ ref="tableRef"
+ row-key="id"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" :selectable="selectable" :reserve-selection="true" />
+ <el-table-column label="鍙戦�佷汉" align="center" prop="templateNickname" width="180" />
+ <el-table-column
+ label="鍙戦�佹椂闂�"
+ align="center"
+ prop="createTime"
+ width="200"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="绫诲瀷" align="center" prop="templateType" width="180">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.templateType" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="娑堟伅鍐呭"
+ align="center"
+ prop="templateContent"
+ show-overflow-tooltip
+ />
+ <el-table-column label="鏄惁宸茶" align="center" prop="readStatus" width="160">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.readStatus" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="闃呰鏃堕棿"
+ align="center"
+ prop="readTime"
+ width="200"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="160">
+ <template #default="scope">
+ <el-button
+ link
+ :type="scope.row.readStatus ? 'primary' : 'warning'"
+ @click="openDetail(scope.row)"
+ >
+ {{ scope.row.readStatus ? '璇︽儏' : '宸茶' }}
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <MyNotifyMessageDetail ref="detailRef" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as NotifyMessageApi from '@/api/system/notify/message'
+import MyNotifyMessageDetail from './MyNotifyMessageDetail.vue'
+
+defineOptions({ name: 'SystemMyNotify' })
+
+const message = useMessage() // 娑堟伅
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ readStatus: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const tableRef = ref() // 琛ㄦ牸鐨� Ref
+const selectedIds = ref<number[]>([]) // 琛ㄦ牸鐨勯�変腑 ID 鏁扮粍
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await NotifyMessageApi.getMyNotifyMessagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ tableRef.value.clearSelection()
+ handleQuery()
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: NotifyMessageApi.NotifyMessageVO) => {
+ if (!data.readStatus) {
+ handleReadOne(data.id)
+ }
+ detailRef.value.open(data)
+}
+
+/** 鏍囪涓�鏉$珯鍐呬俊宸茶 */
+const handleReadOne = async (id) => {
+ await NotifyMessageApi.updateNotifyMessageRead(id)
+ await getList()
+}
+
+/** 鏍囪鍏ㄩ儴绔欏唴淇″凡璇� **/
+const handleUpdateAll = async () => {
+ await NotifyMessageApi.updateAllNotifyMessageRead()
+ message.success('鍏ㄩ儴宸茶鎴愬姛锛�')
+ tableRef.value.clearSelection()
+ await getList()
+}
+
+/** 鏍囪涓�浜涚珯鍐呬俊宸茶 **/
+const handleUpdateList = async () => {
+ if (selectedIds.value.length === 0) {
+ return
+ }
+ await NotifyMessageApi.updateNotifyMessageRead(selectedIds.value)
+ message.success('鎵归噺宸茶鎴愬姛锛�')
+ tableRef.value.clearSelection()
+ await getList()
+}
+
+/** 鏌愪竴琛岋紝鏄惁鍏佽閫変腑 */
+const selectable = (row) => {
+ return !row.readStatus
+}
+
+/** 褰撹〃鏍奸�夋嫨椤瑰彂鐢熷彉鍖栨椂浼氳Е鍙戣浜嬩欢 */
+const handleSelectionChange = (array: NotifyMessageApi.NotifyMessageVO[]) => {
+ selectedIds.value = []
+ if (!array) {
+ return
+ }
+ array.forEach((row) => selectedIds.value.push(row.id))
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/notify/template/NotifyTemplateForm.vue b/src/views/system/notify/template/NotifyTemplateForm.vue
new file mode 100644
index 0000000..beb2863
--- /dev/null
+++ b/src/views/system/notify/template/NotifyTemplateForm.vue
@@ -0,0 +1,141 @@
+<template>
+ <Dialog :title="dialogTitle" v-model="dialogVisible">
+ <el-form
+ ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ v-loading="formLoading"
+ >
+ <el-form-item label="妯$増缂栫爜" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ユā鐗堢紪鐮�" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユā鐗堝悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍙戜欢浜哄悕绉�" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ュ彂浠朵汉鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍐呭" prop="content">
+ <el-input type="textarea" v-model="formData.content" placeholder="璇疯緭鍏ユā鏉垮唴瀹�" />
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="type">
+ <el-select v-model="formData.type" placeholder="璇烽�夋嫨绫诲瀷">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="submitForm" type="primary" :disabled="formLoading">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as NotifyTemplateApi from '@/api/system/notify/template'
+import { CommonStatusEnum } from '@/utils/constants'
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨�
+const formData = ref<NotifyTemplateApi.NotifyTemplateVO>({
+ id: undefined,
+ name: '',
+ nickname: '',
+ code: '',
+ content: '',
+ type: undefined,
+ params: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+})
+const formRules = reactive({
+ type: [{ required: true, message: '娑堟伅绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }],
+ code: [{ required: true, message: '妯℃澘缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '妯℃澘鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ nickname: [{ required: true, message: '鍙戜欢浜哄鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ content: [{ required: true, message: '妯℃澘鍐呭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = type
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await NotifyTemplateApi.getNotifyTemplate(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as NotifyTemplateApi.NotifyTemplateVO
+ if (formType.value === 'create') {
+ await NotifyTemplateApi.createNotifyTemplate(data)
+ message.success('鏂板鎴愬姛')
+ } else {
+ await NotifyTemplateApi.updateNotifyTemplate(data)
+ message.success('淇敼鎴愬姛')
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ nickname: '',
+ code: '',
+ content: '',
+ type: undefined,
+ params: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/notify/template/NotifyTemplateSendForm.vue b/src/views/system/notify/template/NotifyTemplateSendForm.vue
new file mode 100644
index 0000000..4c3e9c4
--- /dev/null
+++ b/src/views/system/notify/template/NotifyTemplateSendForm.vue
@@ -0,0 +1,146 @@
+<template>
+ <Dialog v-model="dialogVisible" title="娴嬭瘯鍙戦��" :max-height="500">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ >
+ <el-form-item label="妯℃澘鍐呭" prop="content">
+ <el-input
+ v-model="formData.content"
+ placeholder="璇疯緭鍏ユā鏉垮唴瀹�"
+ readonly
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-radio-group v-model="formData.userType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-show="formData.userType === 1" label="鎺ユ敹浜篒D" prop="userId">
+ <el-input v-model="formData.userId" style="width: 160px" />
+ </el-form-item>
+ <el-form-item v-show="formData.userType === 2" label="鎺ユ敹浜�" prop="userId">
+ <el-select v-model="formData.userId" placeholder="璇烽�夋嫨鎺ユ敹浜�">
+ <el-option
+ v-for="item in userOption"
+ :key="item.id"
+ :label="item.nickname"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item
+ v-for="param in formData.params"
+ :key="param"
+ :label="'鍙傛暟 {' + param + '}'"
+ :prop="'templateParams.' + param"
+ >
+ <el-input
+ v-model="formData.templateParams[param]"
+ :placeholder="'璇疯緭鍏� ' + param + ' 鍙傛暟'"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/system/user'
+import * as NotifyTemplateApi from '@/api/system/notify/template'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+defineOptions({ name: 'SystemNotifyTemplateSendForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ content: '',
+ params: {},
+ userId: undefined,
+ userType: 1,
+ templateCode: '',
+ templateParams: new Map()
+})
+const formRules = reactive({
+ userId: [{ required: true, message: '鐢ㄦ埛缂栧彿涓嶈兘涓虹┖', trigger: 'change' }],
+ templateCode: [{ required: true, message: '妯$増缂栧彿涓嶈兘涓虹┖', trigger: 'blur' }],
+ templateParams: {}
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const userOption = ref<UserApi.UserVO[]>([])
+
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formLoading.value = true
+ try {
+ const data = await NotifyTemplateApi.getNotifyTemplate(id)
+ // 璁剧疆鍔ㄦ�佽〃鍗�
+ formData.value.content = data.content
+ formData.value.params = data.params
+ formData.value.templateCode = data.code
+ formData.value.templateParams = data.params.reduce((obj, item) => {
+ obj[item] = '' // 缁欐瘡涓姩鎬佸睘鎬ц祴鍊硷紝閬垮厤鏃犳硶璇诲彇
+ return obj
+ }, {})
+ formRules.templateParams = data.params.reduce((obj, item) => {
+ obj[item] = { required: true, message: '鍙傛暟 ' + item + ' 涓嶈兘涓虹┖', trigger: 'blur' }
+ return obj
+ }, {})
+ } finally {
+ formLoading.value = false
+ }
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ userOption.value = await UserApi.getSimpleUserList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as NotifyTemplateApi.NotifySendReqVO
+ const logId = await NotifyTemplateApi.sendNotify(data)
+ if (logId) {
+ message.success('鎻愪氦鍙戦�佹垚鍔燂紒鍙戦�佺粨鏋滐紝瑙佸彂閫佹棩蹇楃紪鍙凤細' + logId)
+ }
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ content: '',
+ params: {},
+ mobile: '',
+ templateCode: '',
+ templateParams: new Map(),
+ userType: 1
+ } as any
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/notify/template/index.vue b/src/views/system/notify/template/index.vue
new file mode 100644
index 0000000..086be9c
--- /dev/null
+++ b/src/views/system/notify/template/index.vue
@@ -0,0 +1,265 @@
+<template>
+ <doc-alert title="绔欏唴淇¢厤缃�" url="https://doc.iocoder.cn/notify/" />
+
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ユā鏉垮悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栧彿" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ユā鐗堢紪鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨寮�鍚姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:notify-template:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:notify-template:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" />鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column
+ label="妯℃澘缂栫爜"
+ align="center"
+ prop="code"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="妯℃澘鍚嶇О"
+ align="center"
+ prop="name"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙戦�佷汉鍚嶇О" align="center" prop="nickname" />
+ <el-table-column
+ label="妯℃澘鍐呭"
+ align="center"
+ prop="content"
+ width="200"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="寮�鍚姸鎬�" align="center" prop="status" width="80">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="210" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:notify-template:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openSendForm(scope.row)"
+ v-hasPermi="['system:notify-template:send-notify']"
+ >
+ 娴嬭瘯
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:notify-template:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <NotifyTemplateForm ref="formRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氭祴璇曞彂閫� -->
+ <NotifyTemplateSendForm ref="sendFormRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as NotifyTemplateApi from '@/api/system/notify/template'
+import NotifyTemplateForm from './NotifyTemplateForm.vue'
+import NotifyTemplateSendForm from './NotifyTemplateSendForm.vue'
+
+defineOptions({ name: 'NotifySmsTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ code: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await NotifyTemplateApi.getNotifyTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await NotifyTemplateApi.deleteNotifyTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: NotifyTemplateApi.NotifyTemplateVO[]) => {
+ checkedIds.value = rows.map((row) => row.id!)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await NotifyTemplateApi.deleteNotifyTemplateList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍙戦�佺珯鍐呬俊鎸夐挳 */
+const sendFormRef = ref() // 琛ㄥ崟 Ref
+const openSendForm = (row: NotifyTemplateApi.NotifyTemplateVO) => {
+ sendFormRef.value.open(row.id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/oauth2/client/ClientForm.vue b/src/views/system/oauth2/client/ClientForm.vue
new file mode 100644
index 0000000..563682a
--- /dev/null
+++ b/src/views/system/oauth2/client/ClientForm.vue
@@ -0,0 +1,261 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" max-height="500px" scroll>
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="160px"
+ >
+ <el-form-item label="瀹㈡埛绔紪鍙�" prop="secret">
+ <el-input v-model="formData.clientId" placeholder="璇疯緭鍏ュ鎴风缂栧彿" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绔瘑閽�" prop="secret">
+ <el-input v-model="formData.secret" placeholder="璇疯緭鍏ュ鎴风瀵嗛挜" />
+ </el-form-item>
+ <el-form-item label="搴旂敤鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ簲鐢ㄥ悕" />
+ </el-form-item>
+ <el-form-item label="搴旂敤鍥炬爣">
+ <UploadImg v-model="formData.logo" :limit="1" />
+ </el-form-item>
+ <el-form-item label="搴旂敤鎻忚堪">
+ <el-input v-model="formData.description" placeholder="璇疯緭鍏ュ簲鐢ㄥ悕" type="textarea" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="璁块棶浠ょ墝鐨勬湁鏁堟湡" prop="accessTokenValiditySeconds">
+ <el-input-number v-model="formData.accessTokenValiditySeconds" placeholder="鍗曚綅锛氱" />
+ </el-form-item>
+ <el-form-item label="鍒锋柊浠ょ墝鐨勬湁鏁堟湡" prop="refreshTokenValiditySeconds">
+ <el-input-number v-model="formData.refreshTokenValiditySeconds" placeholder="鍗曚綅锛氱" />
+ </el-form-item>
+ <el-form-item label="鎺堟潈绫诲瀷" prop="authorizedGrantTypes">
+ <el-select
+ v-model="formData.authorizedGrantTypes"
+ filterable
+ multiple
+ placeholder="璇疯緭鍏ユ巿鏉冪被鍨�"
+ style="width: 500px"
+ >
+ <el-option
+ v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_OAUTH2_GRANT_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎺堟潈鑼冨洿" prop="scopes">
+ <el-select
+ v-model="formData.scopes"
+ filterable
+ multiple
+ allow-create
+ placeholder="璇疯緭鍏ユ巿鏉冭寖鍥�"
+ style="width: 500px"
+ >
+ <el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑷姩鎺堟潈鑼冨洿" prop="autoApproveScopes">
+ <el-select
+ v-model="formData.autoApproveScopes"
+ filterable
+ multiple
+ placeholder="璇疯緭鍏ユ巿鏉冭寖鍥�"
+ style="width: 500px"
+ >
+ <el-option v-for="scope in formData.scopes" :key="scope" :label="scope" :value="scope" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙噸瀹氬悜鐨� URI 鍦板潃" prop="redirectUris">
+ <el-select
+ v-model="formData.redirectUris"
+ allow-create
+ filterable
+ multiple
+ placeholder="璇疯緭鍏ュ彲閲嶅畾鍚戠殑 URI 鍦板潃"
+ style="width: 500px"
+ >
+ <el-option
+ v-for="redirectUri in formData.redirectUris"
+ :key="redirectUri"
+ :label="redirectUri"
+ :value="redirectUri"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏉冮檺" prop="authorities">
+ <el-select
+ v-model="formData.authorities"
+ allow-create
+ filterable
+ multiple
+ placeholder="璇疯緭鍏ユ潈闄�"
+ style="width: 500px"
+ >
+ <el-option
+ v-for="authority in formData.authorities"
+ :key="authority"
+ :label="authority"
+ :value="authority"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璧勬簮" prop="resourceIds">
+ <el-select
+ v-model="formData.resourceIds"
+ allow-create
+ filterable
+ multiple
+ placeholder="璇疯緭鍏ヨ祫婧�"
+ style="width: 500px"
+ >
+ <el-option
+ v-for="resourceId in formData.resourceIds"
+ :key="resourceId"
+ :label="resourceId"
+ :value="resourceId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="闄勫姞淇℃伅" prop="additionalInformation">
+ <el-input
+ v-model="formData.additionalInformation"
+ placeholder="璇疯緭鍏ラ檮鍔犱俊鎭紝JSON 鏍煎紡鏁版嵁"
+ type="textarea"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getDictOptions, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as ClientApi from '@/api/system/oauth2/client'
+
+defineOptions({ name: 'SystemOAuth2ClientForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ clientId: undefined,
+ secret: undefined,
+ name: undefined,
+ logo: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE,
+ accessTokenValiditySeconds: 30 * 60,
+ refreshTokenValiditySeconds: 30 * 24 * 60,
+ redirectUris: [],
+ authorizedGrantTypes: [],
+ scopes: [],
+ autoApproveScopes: [],
+ authorities: [],
+ resourceIds: [],
+ additionalInformation: undefined
+})
+const formRules = reactive({
+ clientId: [{ required: true, message: '瀹㈡埛绔紪鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+ secret: [{ required: true, message: '瀹㈡埛绔瘑閽ヤ笉鑳戒负绌�', trigger: 'blur' }],
+ name: [{ required: true, message: '搴旂敤鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ logo: [{ required: true, message: '搴旂敤鍥炬爣涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ accessTokenValiditySeconds: [
+ { required: true, message: '璁块棶浠ょ墝鐨勬湁鏁堟湡涓嶈兘涓虹┖', trigger: 'blur' }
+ ],
+ refreshTokenValiditySeconds: [
+ { required: true, message: '鍒锋柊浠ょ墝鐨勬湁鏁堟湡涓嶈兘涓虹┖', trigger: 'blur' }
+ ],
+ redirectUris: [{ required: true, message: '鍙噸瀹氬悜鐨� URI 鍦板潃涓嶈兘涓虹┖', trigger: 'blur' }],
+ authorizedGrantTypes: [{ required: true, message: '鎺堟潈绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await ClientApi.getOAuth2Client(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as ClientApi.OAuth2ClientVO
+ if (formType.value === 'create') {
+ await ClientApi.createOAuth2Client(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await ClientApi.updateOAuth2Client(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ clientId: undefined,
+ secret: undefined,
+ name: undefined,
+ logo: undefined,
+ description: undefined,
+ status: CommonStatusEnum.ENABLE,
+ accessTokenValiditySeconds: 30 * 60,
+ refreshTokenValiditySeconds: 30 * 24 * 60,
+ redirectUris: [],
+ authorizedGrantTypes: [],
+ scopes: [],
+ autoApproveScopes: [],
+ authorities: [],
+ resourceIds: [],
+ additionalInformation: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/oauth2/client/index.vue b/src/views/system/oauth2/client/index.vue
new file mode 100644
index 0000000..c75475b
--- /dev/null
+++ b/src/views/system/oauth2/client/index.vue
@@ -0,0 +1,220 @@
+<template>
+ <doc-alert title="OAuth 2.0锛圫SO 鍗曠偣鐧诲綍)" url="https://doc.iocoder.cn/oauth2/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="搴旂敤鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ簲鐢ㄥ悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ plain
+ type="primary"
+ @click="openForm('create')"
+ v-hasPermi="['system:oauth2-client:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ plain
+ type="danger"
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:oauth2-client:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="瀹㈡埛绔紪鍙�" align="center" prop="clientId" />
+ <el-table-column label="瀹㈡埛绔瘑閽�" align="center" prop="secret" />
+ <el-table-column label="搴旂敤鍚�" align="center" prop="name" />
+ <el-table-column label="搴旂敤鍥炬爣" align="center" prop="logo">
+ <template #default="scope">
+ <img width="40px" height="40px" :src="scope.row.logo" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁块棶浠ょ墝鐨勬湁鏁堟湡" align="center" prop="accessTokenValiditySeconds">
+ <template #default="scope">{{ scope.row.accessTokenValiditySeconds }} 绉�</template>
+ </el-table-column>
+ <el-table-column label="鍒锋柊浠ょ墝鐨勬湁鏁堟湡" align="center" prop="refreshTokenValiditySeconds">
+ <template #default="scope">{{ scope.row.refreshTokenValiditySeconds }} 绉�</template>
+ </el-table-column>
+ <el-table-column label="鎺堟潈绫诲瀷" align="center" prop="authorizedGrantTypes">
+ <template #default="scope">
+ <el-tag
+ :disable-transitions="true"
+ :key="index"
+ v-for="(authorizedGrantType, index) in scope.row.authorizedGrantTypes"
+ :index="index"
+ class="mr-5px"
+ >
+ {{ authorizedGrantType }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:oauth2-client:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:oauth2-client:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <ClientForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ClientApi from '@/api/system/oauth2/client'
+import ClientForm from './ClientForm.vue'
+
+defineOptions({ name: 'SystemOAuth2Client' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: null,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await ClientApi.getOAuth2ClientPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await ClientApi.deleteOAuth2Client(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: ClientApi.OAuth2ClientVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await ClientApi.deleteOAuth2ClientList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/oauth2/token/index.vue b/src/views/system/oauth2/token/index.vue
new file mode 100644
index 0000000..2a94f8e
--- /dev/null
+++ b/src/views/system/oauth2/token/index.vue
@@ -0,0 +1,164 @@
+<template>
+ <doc-alert title="OAuth 2.0锛圫SO 鍗曠偣鐧诲綍)" url="https://doc.iocoder.cn/oauth2/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="90px"
+ >
+ <el-form-item label="鐢ㄦ埛缂栧彿" prop="userId">
+ <el-input
+ v-model="queryParams.userId"
+ placeholder="璇疯緭鍏ョ敤鎴风紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绔紪鍙�" prop="clientId">
+ <el-input
+ v-model="queryParams.clientId"
+ placeholder="璇疯緭鍏ュ鎴风缂栧彿"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="璁块棶浠ょ墝" align="center" prop="accessToken" width="300" />
+ <el-table-column label="鍒锋柊浠ょ墝" align="center" prop="refreshToken" width="300" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" prop="userId" />
+ <el-table-column label="鐢ㄦ埛绫诲瀷" align="center" prop="userType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="杩囨湡鏃堕棿"
+ align="center"
+ prop="expiresTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ @click="handleForceLogout(scope.row.accessToken)"
+ v-hasPermi="['system:oauth2-token:delete']"
+ >
+ 寮洪��
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as OAuth2AccessTokenApi from '@/api/system/oauth2/token'
+
+defineOptions({ name: 'SystemTokenClient' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: null,
+ userType: undefined,
+ clientId: null
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await OAuth2AccessTokenApi.getAccessTokenPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 寮哄埗閫�鍑烘搷浣� */
+const handleForceLogout = async (accessToken: string) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.confirm('鏄惁瑕佸己鍒堕��鍑虹敤鎴�')
+ // 鍙戣捣鍒犻櫎
+ await OAuth2AccessTokenApi.deleteAccessToken(accessToken)
+ message.success(t('common.success'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/operatelog/OperateLogDetail.vue b/src/views/system/operatelog/OperateLogDetail.vue
new file mode 100644
index 0000000..150edc8
--- /dev/null
+++ b/src/views/system/operatelog/OperateLogDetail.vue
@@ -0,0 +1,68 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織涓婚敭" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閾捐矾杩借釜" v-if="detailData.traceId">
+ {{ detailData.traceId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔浜虹紪鍙�">
+ {{ detailData.userId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔浜哄悕瀛�">
+ {{ detailData.userName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔浜� IP">
+ {{ detailData.userIp }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔浜� UA">
+ {{ detailData.userAgent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔妯″潡">
+ {{ detailData.type }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔鍚�">
+ {{ detailData.subType }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔鍐呭">
+ {{ detailData.action }}
+ </el-descriptions-item>
+ <el-descriptions-item v-if="detailData.extra" label="鎿嶄綔鎷撳睍鍙傛暟">
+ {{ detailData.extra }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇锋眰 URL">
+ {{ detailData.requestMethod }} {{ detailData.requestUrl }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎿嶄綔鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓氬姟缂栧彿">
+ {{ detailData.bizId }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { formatDate } from '@/utils/formatTime'
+import * as OperateLogApi from '@/api/system/operatelog'
+
+defineOptions({ name: 'SystemOperateLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as OperateLogApi.OperateLogVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: OperateLogApi.OperateLogVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/operatelog/index.vue b/src/views/system/operatelog/index.vue
new file mode 100644
index 0000000..ccf2b91
--- /dev/null
+++ b/src/views/system/operatelog/index.vue
@@ -0,0 +1,213 @@
+<template>
+ <doc-alert title="绯荤粺鏃ュ織" url="https://doc.iocoder.cn/system-log/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鎿嶄綔浜�" prop="userId">
+ <el-select
+ v-model="queryParams.userId"
+ clearable
+ filterable
+ placeholder="璇疯緭鍏ユ搷浣滀汉鍛�"
+ class="!w-240px"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.id"
+ :label="user.nickname"
+ :value="user.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎿嶄綔妯″潡" prop="type">
+ <el-input
+ v-model="queryParams.type"
+ placeholder="璇疯緭鍏ユ搷浣滄ā鍧�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔鍚�" prop="subType">
+ <el-input
+ v-model="queryParams.subType"
+ placeholder="璇疯緭鍏ユ搷浣滃悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔鍐呭" prop="action">
+ <el-input
+ v-model="queryParams.action"
+ placeholder="璇疯緭鍏ユ搷浣滃悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="涓氬姟缂栧彿" prop="bizId">
+ <el-input
+ v-model="queryParams.bizId"
+ placeholder="璇疯緭鍏ヤ笟鍔$紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:operate-log:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="id" width="100" />
+ <el-table-column label="鎿嶄綔浜�" align="center" prop="userName" width="120" />
+ <el-table-column label="鎿嶄綔妯″潡" align="center" prop="type" width="120" />
+ <el-table-column label="鎿嶄綔鍚�" align="center" prop="subType" width="160" />
+ <el-table-column label="鎿嶄綔鍐呭" align="center" prop="action" />
+ <el-table-column
+ label="鎿嶄綔鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="涓氬姟缂栧彿" align="center" prop="bizId" width="120" />
+ <el-table-column label="鎿嶄綔 IP" align="center" prop="userIp" width="120" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="60">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['system:operate-log:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <OperateLogDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as OperateLogApi from '@/api/system/operatelog'
+import OperateLogDetail from './OperateLogDetail.vue'
+import * as UserApi from '@/api/system/user'
+const userList = ref<UserApi.UserVO[]>([]) // 鐢ㄦ埛鍒楄〃
+
+defineOptions({ name: 'SystemOperateLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ userId: undefined,
+ type: undefined,
+ subType: undefined,
+ action: undefined,
+ createTime: [],
+ bizId: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await OperateLogApi.getOperateLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: OperateLogApi.OperateLogVO) => {
+ detailRef.value.open(data)
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await OperateLogApi.exportOperateLog(queryParams)
+ download.excel(data, '鎿嶄綔鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰緱鐢ㄦ埛鍒楄〃
+ userList.value = await UserApi.getSimpleUserList()
+})
+</script>
diff --git a/src/views/system/post/PostForm.vue b/src/views/system/post/PostForm.vue
new file mode 100644
index 0000000..1894e0c
--- /dev/null
+++ b/src/views/system/post/PostForm.vue
@@ -0,0 +1,125 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="800">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="宀椾綅鏍囬" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ矖浣嶆爣棰�" />
+ </el-form-item>
+ <el-form-item label="宀椾綅缂栫爜" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ュ矖浣嶇紪鐮�" />
+ </el-form-item>
+ <el-form-item label="宀椾綅椤哄簭" prop="sort">
+ <el-input v-model="formData.sort" placeholder="璇疯緭鍏ュ矖浣嶉『搴�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="formData.status" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭澶囨敞" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as PostApi from '@/api/system/post'
+
+defineOptions({ name: 'SystemPostForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ code: '',
+ sort: 0,
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '宀椾綅鏍囬涓嶈兘涓虹┖', trigger: 'blur' }],
+ code: [{ required: true, message: '宀椾綅缂栫爜涓嶈兘涓虹┖', trigger: 'change' }],
+ status: [{ required: true, message: '宀椾綅鐘舵�佷笉鑳戒负绌�', trigger: 'change' }],
+ remark: [{ required: false, message: '宀椾綅鍐呭涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await PostApi.getPost(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as PostApi.PostVO
+ if (formType.value === 'create') {
+ await PostApi.createPost(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await PostApi.updatePost(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ code: '',
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+ } as any
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue
new file mode 100644
index 0000000..291d21e
--- /dev/null
+++ b/src/views/system/post/index.vue
@@ -0,0 +1,232 @@
+<template>
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="宀椾綅鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ矖浣嶅悕绉�"
+ clearable
+ class="!w-240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="宀椾綅缂栫爜" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ュ矖浣嶇紪鐮�"
+ clearable
+ class="!w-240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:post:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:post:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:post:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="宀椾綅缂栧彿" align="center" prop="id" />
+ <el-table-column label="宀椾綅鍚嶇О" align="center" prop="name" />
+ <el-table-column label="宀椾綅缂栫爜" align="center" prop="code" />
+ <el-table-column label="宀椾綅椤哄簭" align="center" prop="sort" />
+ <el-table-column label="宀椾綅澶囨敞" align="center" prop="remark" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:post:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:post:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <PostForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as PostApi from '@/api/system/post'
+import PostForm from './PostForm.vue'
+
+defineOptions({ name: 'SystemPost' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ code: '',
+ name: '',
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ宀椾綅鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await PostApi.getPostPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await PostApi.deletePost(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: PostApi.PostVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await PostApi.deletePostList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await PostApi.exportPost(queryParams)
+ download.excel(data, '宀椾綅鍒楄〃.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/role/RoleAssignMenuForm.vue b/src/views/system/role/RoleAssignMenuForm.vue
new file mode 100644
index 0000000..7c81ef7
--- /dev/null
+++ b/src/views/system/role/RoleAssignMenuForm.vue
@@ -0,0 +1,153 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鑿滃崟鏉冮檺">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="瑙掕壊鍚嶇О">
+ <el-tag>{{ formData.name }}</el-tag>
+ </el-form-item>
+ <el-form-item label="瑙掕壊鏍囪瘑">
+ <el-tag>{{ formData.code }}</el-tag>
+ </el-form-item>
+ <el-form-item label="鑿滃崟鏉冮檺">
+ <el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
+ <template #header>
+ 鍏ㄩ��/鍏ㄤ笉閫�:
+ <el-switch
+ v-model="treeNodeAll"
+ active-text="鏄�"
+ inactive-text="鍚�"
+ inline-prompt
+ @change="handleCheckedTreeNodeAll"
+ />
+ 鍏ㄩ儴灞曞紑/鎶樺彔:
+ <el-switch
+ v-model="menuExpand"
+ active-text="灞曞紑"
+ inactive-text="鎶樺彔"
+ inline-prompt
+ @change="handleCheckedTreeExpand"
+ />
+ </template>
+ <el-tree
+ ref="treeRef"
+ :data="menuOptions"
+ :props="defaultProps"
+ empty-text="鍔犺浇涓紝璇风◢鍊�"
+ node-key="id"
+ show-checkbox
+ />
+ </el-card>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as RoleApi from '@/api/system/role'
+import * as MenuApi from '@/api/system/menu'
+import * as PermissionApi from '@/api/system/permission'
+
+defineOptions({ name: 'SystemRoleAssignMenuForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = reactive({
+ id: undefined,
+ name: '',
+ code: '',
+ menuIds: []
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const menuOptions = ref<any[]>([]) // 鑿滃崟鏍戝舰缁撴瀯
+const menuExpand = ref(false) // 灞曞紑/鎶樺彔
+const treeRef = ref() // 鑿滃崟鏍戠粍浠� Ref
+const treeNodeAll = ref(false) // 鍏ㄩ��/鍏ㄤ笉閫�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: RoleApi.RoleVO) => {
+ dialogVisible.value = true
+ resetForm()
+ // 鍔犺浇 Menu 鍒楄〃銆傛敞鎰忥紝蹇呴』鏀惧湪鍓嶉潰锛屼笉鐒朵笅闈� setChecked 娌℃暟鎹妭鐐�
+ menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
+ // 璁剧疆鏁版嵁
+ formData.id = row.id
+ formData.name = row.name
+ formData.code = row.code
+ formLoading.value = true
+ try {
+ formData.value.menuIds = await PermissionApi.getRoleMenuList(row.id)
+ // 璁剧疆閫変腑
+ formData.value.menuIds.forEach((menuId: number) => {
+ treeRef.value.setChecked(menuId, true, false)
+ })
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = {
+ roleId: formData.id,
+ menuIds: [
+ ...(treeRef.value.getCheckedKeys(false) as unknown as Array<number>), // 鑾峰緱褰撳墠閫変腑鑺傜偣
+ ...(treeRef.value.getHalfCheckedKeys() as unknown as Array<number>) // 鑾峰緱鍗婇�変腑鐨勭埗鑺傜偣
+ ]
+ }
+ await PermissionApi.assignRoleMenu(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ // 閲嶇疆閫夐」
+ treeNodeAll.value = false
+ menuExpand.value = false
+ // 閲嶇疆琛ㄥ崟
+ formData.value = {
+ id: undefined,
+ name: '',
+ code: '',
+ menuIds: []
+ }
+ treeRef.value?.setCheckedNodes([])
+ formRef.value?.resetFields()
+}
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckedTreeNodeAll = () => {
+ treeRef.value.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
+}
+
+/** 灞曞紑/鎶樺彔鍏ㄩ儴 */
+const handleCheckedTreeExpand = () => {
+ const nodes = treeRef.value?.store.nodesMap
+ for (let node in nodes) {
+ if (nodes[node].expanded === menuExpand.value) {
+ continue
+ }
+ nodes[node].expanded = menuExpand.value
+ }
+}
+</script>
diff --git a/src/views/system/role/RoleDataPermissionForm.vue b/src/views/system/role/RoleDataPermissionForm.vue
new file mode 100644
index 0000000..1243435
--- /dev/null
+++ b/src/views/system/role/RoleDataPermissionForm.vue
@@ -0,0 +1,169 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鏁版嵁鏉冮檺" width="800">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="瑙掕壊鍚嶇О">
+ <el-tag>{{ formData.name }}</el-tag>
+ </el-form-item>
+ <el-form-item label="瑙掕壊鏍囪瘑">
+ <el-tag>{{ formData.code }}</el-tag>
+ </el-form-item>
+ <el-form-item label="鏉冮檺鑼冨洿">
+ <el-select v-model="formData.dataScope">
+ <el-option
+ v-for="item in getIntDictOptions(DICT_TYPE.SYSTEM_DATA_SCOPE)"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <el-form-item
+ v-if="formData.dataScope === SystemDataScopeEnum.DEPT_CUSTOM"
+ label="閮ㄩ棬鑼冨洿"
+ label-width="80px"
+ >
+ <el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
+ <template #header>
+ 鍏ㄩ��/鍏ㄤ笉閫�:
+ <el-switch
+ v-model="treeNodeAll"
+ active-text="鏄�"
+ inactive-text="鍚�"
+ inline-prompt
+ @change="handleCheckedTreeNodeAll()"
+ />
+ 鍏ㄩ儴灞曞紑/鎶樺彔:
+ <el-switch
+ v-model="deptExpand"
+ active-text="灞曞紑"
+ inactive-text="鎶樺彔"
+ inline-prompt
+ @change="handleCheckedTreeExpand"
+ />
+ 鐖跺瓙鑱斿姩(閫変腑鐖惰妭鐐癸紝鑷姩閫夋嫨瀛愯妭鐐�):
+ <el-switch v-model="checkStrictly" active-text="鏄�" inactive-text="鍚�" inline-prompt />
+ </template>
+ <el-tree
+ ref="treeRef"
+ :check-strictly="!checkStrictly"
+ :data="deptOptions"
+ :props="defaultProps"
+ default-expand-all
+ empty-text="鍔犺浇涓紝璇风◢鍚�"
+ node-key="id"
+ show-checkbox
+ />
+ </el-card>
+ </el-form-item>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { defaultProps, handleTree } from '@/utils/tree'
+import { SystemDataScopeEnum } from '@/utils/constants'
+import * as RoleApi from '@/api/system/role'
+import * as DeptApi from '@/api/system/dept'
+import * as PermissionApi from '@/api/system/permission'
+
+defineOptions({ name: 'SystemRoleDataPermissionForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = reactive({
+ id: undefined,
+ name: '',
+ code: '',
+ dataScope: undefined,
+ dataScopeDeptIds: []
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const deptOptions = ref<any[]>([]) // 閮ㄩ棬鏍戝舰缁撴瀯
+const deptExpand = ref(true) // 灞曞紑/鎶樺彔
+const treeRef = ref() // 鑿滃崟鏍戠粍浠� Ref
+const treeNodeAll = ref(false) // 鍏ㄩ��/鍏ㄤ笉閫�
+const checkStrictly = ref(true) // 鏄惁涓ユ牸妯″紡锛屽嵆鐖跺瓙涓嶅叧鑱�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: RoleApi.RoleVO) => {
+ dialogVisible.value = true
+ resetForm()
+ // 鍔犺浇 Dept 鍒楄〃銆傛敞鎰忥紝蹇呴』鏀惧湪鍓嶉潰锛屼笉鐒朵笅闈� setChecked 娌℃暟鎹妭鐐�
+ deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
+ // 璁剧疆鏁版嵁
+ formData.id = row.id
+ formData.name = row.name
+ formData.code = row.code
+ formData.dataScope = row.dataScope
+ await nextTick()
+ // 闇�瑕佸湪 DOM 娓叉煋瀹屾垚鍚庯紝鍐嶈缃�変腑鐘舵��
+ row.dataScopeDeptIds?.forEach((deptId: number): void => {
+ treeRef.value.setChecked(deptId, true, false)
+ })
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ formLoading.value = true
+ try {
+ const data = {
+ roleId: formData.id,
+ dataScope: formData.dataScope,
+ dataScopeDeptIds:
+ formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM
+ ? []
+ : treeRef.value.getCheckedKeys(false)
+ }
+ await PermissionApi.assignRoleDataScope(data)
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ // 閲嶇疆閫夐」
+ treeNodeAll.value = false
+ deptExpand.value = true
+ checkStrictly.value = true
+ // 閲嶇疆琛ㄥ崟
+ formData.value = {
+ id: undefined,
+ name: '',
+ code: '',
+ dataScope: undefined,
+ dataScopeDeptIds: []
+ }
+ treeRef.value?.setCheckedNodes([])
+ formRef.value?.resetFields()
+}
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckedTreeNodeAll = () => {
+ treeRef.value.setCheckedNodes(treeNodeAll.value ? deptOptions.value : [])
+}
+
+/** 灞曞紑/鎶樺彔鍏ㄩ儴 */
+const handleCheckedTreeExpand = () => {
+ const nodes = treeRef.value?.store.nodesMap
+ for (let node in nodes) {
+ if (nodes[node].expanded === deptExpand.value) {
+ continue
+ }
+ nodes[node].expanded = deptExpand.value
+ }
+}
+</script>
diff --git a/src/views/system/role/RoleForm.vue b/src/views/system/role/RoleForm.vue
new file mode 100644
index 0000000..161b757
--- /dev/null
+++ b/src/views/system/role/RoleForm.vue
@@ -0,0 +1,126 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="瑙掕壊鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ヨ鑹插悕绉�" />
+ </el-form-item>
+ <el-form-item label="瑙掕壊鏍囪瘑" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ヨ鑹叉爣璇�" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず椤哄簭" prop="sort">
+ <el-input v-model="formData.sort" placeholder="璇疯緭鍏ユ樉绀洪『搴�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="formData.status" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭澶囨敞" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as RoleApi from '@/api/system/role'
+
+defineOptions({ name: 'SystemRoleForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: '',
+ code: '',
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+})
+const formRules = reactive({
+ name: [{ required: true, message: '瑙掕壊鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ code: [{ required: true, message: '瑙掕壊鏍囪瘑涓嶈兘涓虹┖', trigger: 'change' }],
+ sort: [{ required: true, message: '鏄剧ず椤哄簭涓嶈兘涓虹┖', trigger: 'change' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'change' }],
+ remark: [{ required: false, message: '澶囨敞涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await RoleApi.getRole(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: '',
+ code: '',
+ sort: undefined,
+ status: CommonStatusEnum.ENABLE,
+ remark: ''
+ }
+ formRef.value?.resetFields()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as RoleApi.RoleVO
+ if (formType.value === 'create') {
+ await RoleApi.createRole(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await RoleApi.updateRole(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+</script>
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
new file mode 100644
index 0000000..91c68e3
--- /dev/null
+++ b/src/views/system/role/index.vue
@@ -0,0 +1,299 @@
+<template>
+ <doc-alert title="鍔熻兘鏉冮檺" url="https://doc.iocoder.cn/resource-permission" />
+ <doc-alert title="鏁版嵁鏉冮檺" url="https://doc.iocoder.cn/data-permission" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="68px"
+ >
+ <el-form-item label="瑙掕壊鍚嶇О" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ鑹插悕绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="瑙掕壊鏍囪瘑" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ヨ鑹叉爣璇�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['system:role:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ <el-button
+ v-hasPermi="['system:role:export']"
+ :loading="exportLoading"
+ plain
+ type="success"
+ @click="handleExport"
+ >
+ <Icon class="mr-5px" icon="ep:download" />
+ 瀵煎嚭
+ </el-button>
+ <el-button
+ v-hasPermi="['system:role:delete']"
+ :disabled="checkedIds.length === 0"
+ plain
+ type="danger"
+ @click="handleDeleteBatch"
+ >
+ <Icon class="mr-5px" icon="ep:delete" />
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column align="center" label="瑙掕壊缂栧彿" prop="id" />
+ <el-table-column align="center" label="瑙掕壊鍚嶇О" prop="name" />
+ <el-table-column label="瑙掕壊绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_ROLE_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瑙掕壊鏍囪瘑" prop="code" />
+ <el-table-column align="center" label="鏄剧ず椤哄簭" prop="sort" />
+ <el-table-column align="center" label="澶囨敞" prop="remark" />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180"
+ />
+ <el-table-column :width="300" align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['system:role:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['system:permission:assign-role-menu']"
+ link
+ preIcon="ep:basketball"
+ title="鑿滃崟鏉冮檺"
+ type="primary"
+ @click="openAssignMenuForm(scope.row)"
+ >
+ 鑿滃崟鏉冮檺
+ </el-button>
+ <el-button
+ v-hasPermi="['system:permission:assign-role-data-scope']"
+ link
+ preIcon="ep:coin"
+ title="鏁版嵁鏉冮檺"
+ type="primary"
+ @click="openDataPermissionForm(scope.row)"
+ >
+ 鏁版嵁鏉冮檺
+ </el-button>
+ <el-button
+ v-hasPermi="['system:role:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <RoleForm ref="formRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氳彍鍗曟潈闄� -->
+ <RoleAssignMenuForm ref="assignMenuFormRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氭暟鎹潈闄� -->
+ <RoleDataPermissionForm ref="dataPermissionFormRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as RoleApi from '@/api/system/role'
+import RoleForm from './RoleForm.vue'
+import RoleAssignMenuForm from './RoleAssignMenuForm.vue'
+import RoleDataPermissionForm from './RoleDataPermissionForm.vue'
+
+defineOptions({ name: 'SystemRole' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ code: '',
+ name: '',
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+
+/** 鏌ヨ瑙掕壊鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await RoleApi.getRolePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鏁版嵁鏉冮檺鎿嶄綔 */
+const dataPermissionFormRef = ref()
+const openDataPermissionForm = async (row: RoleApi.RoleVO) => {
+ dataPermissionFormRef.value.open(row)
+}
+
+/** 鑿滃崟鏉冮檺鎿嶄綔 */
+const assignMenuFormRef = ref()
+const openAssignMenuForm = async (row: RoleApi.RoleVO) => {
+ assignMenuFormRef.value.open(row)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await RoleApi.deleteRole(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: RoleApi.RoleVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await RoleApi.deleteRoleList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await RoleApi.exportRole(queryParams)
+ download.excel(data, '瑙掕壊鏁版嵁.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/sms/channel/SmsChannelForm.vue b/src/views/system/sms/channel/SmsChannelForm.vue
new file mode 100644
index 0000000..926c28f
--- /dev/null
+++ b/src/views/system/sms/channel/SmsChannelForm.vue
@@ -0,0 +1,144 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="130px"
+ >
+ <el-form-item label="鐭俊绛惧悕" prop="signature">
+ <el-input v-model="formData.signature" placeholder="璇疯緭鍏ョ煭淇$鍚�" />
+ </el-form-item>
+ <el-form-item label="娓犻亾缂栫爜" prop="code">
+ <el-select v-model="formData.code" clearable placeholder="璇烽�夋嫨娓犻亾缂栫爜">
+ <el-option
+ v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍚敤鐘舵��">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ <el-form-item label="鐭俊 API 鐨勮处鍙�" prop="apiKey">
+ <el-input v-model="formData.apiKey" placeholder="璇疯緭鍏ョ煭淇� API 鐨勮处鍙�" />
+ </el-form-item>
+ <el-form-item label="鐭俊 API 鐨勫瘑閽�" prop="apiSecret">
+ <el-input v-model="formData.apiSecret" placeholder="璇疯緭鍏ョ煭淇� API 鐨勫瘑閽�" />
+ </el-form-item>
+ <el-form-item label="鐭俊鍙戦�佸洖璋� URL" prop="callbackUrl">
+ <el-input v-model="formData.callbackUrl" placeholder="璇疯緭鍏ョ煭淇″彂閫佸洖璋� URL" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import * as SmsChannelApi from '@/api/system/sms/smsChannel'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemSmsChannelForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ signature: '',
+ code: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: '',
+ apiKey: '',
+ apiSecret: '',
+ callbackUrl: ''
+})
+const formRules = reactive({
+ signature: [{ required: true, message: '鐭俊绛惧悕涓嶈兘涓虹┖', trigger: 'blur' }],
+ code: [{ required: true, message: '娓犻亾缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ status: [{ required: true, message: '鍚敤鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ apiKey: [{ required: true, message: '鐭俊 API 鐨勮处鍙蜂笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SmsChannelApi.getSmsChannel(id)
+ console.log(formData)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SmsChannelApi.SmsChannelVO
+ if (formType.value === 'create') {
+ await SmsChannelApi.createSmsChannel(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SmsChannelApi.updateSmsChannel(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ signature: '',
+ code: '',
+ status: CommonStatusEnum.ENABLE,
+ remark: '',
+ apiKey: '',
+ apiSecret: '',
+ callbackUrl: ''
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/sms/channel/index.vue b/src/views/system/sms/channel/index.vue
new file mode 100644
index 0000000..929c3a7
--- /dev/null
+++ b/src/views/system/sms/channel/index.vue
@@ -0,0 +1,238 @@
+<template>
+ <doc-alert title="鐭俊閰嶇疆" url="https://doc.iocoder.cn/sms/" />
+
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐭俊绛惧悕" prop="signature">
+ <el-input
+ v-model="queryParams.signature"
+ placeholder="璇疯緭鍏ョ煭淇$鍚�"
+ clearable
+ class="!w-240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍚敤鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鍚敤鐘舵��"
+ class="!w-240px"
+ clearable
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:sms-channel:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" /> 鏂板</el-button
+ >
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:sms-channel:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" /> 鎵归噺鍒犻櫎</el-button
+ >
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column label="鐭俊绛惧悕" align="center" prop="signature" />
+ <el-table-column label="娓犻亾缂栫爜" align="center" prop="code">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.code" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚敤鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column
+ label="鐭俊 API 鐨勮处鍙�"
+ align="center"
+ prop="apiKey"
+ :show-overflow-tooltip="true"
+ width="180"
+ />
+ <el-table-column
+ label="鐭俊 API 鐨勫瘑閽�"
+ align="center"
+ prop="apiSecret"
+ :show-overflow-tooltip="true"
+ width="180"
+ />
+ <el-table-column
+ label="鐭俊鍙戦�佸洖璋� URL"
+ align="center"
+ prop="callbackUrl"
+ :show-overflow-tooltip="true"
+ width="180"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:sms-channel:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:sms-channel:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SmsChannelForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as SmsChannelApi from '@/api/system/sms/smsChannel'
+import SmsChannelForm from './SmsChannelForm.vue'
+
+defineOptions({ name: 'SystemSmsChannel' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ signature: undefined,
+ status: undefined,
+ createTime: []
+})
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SmsChannelApi.getSmsChannelPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SmsChannelApi.deleteSmsChannel(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: SmsChannelApi.SmsChannelVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await SmsChannelApi.deleteSmsChannelList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/sms/log/SmsLogDetail.vue b/src/views/system/sms/log/SmsLogDetail.vue
new file mode 100644
index 0000000..b0d22c2
--- /dev/null
+++ b/src/views/system/sms/log/SmsLogDetail.vue
@@ -0,0 +1,86 @@
+<template>
+ <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鏃ュ織涓婚敭" min-width="120">
+ {{ detailData.id }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐭俊娓犻亾">
+ {{ channelList.find((channel) => channel.id === detailData.channelId)?.signature }}
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="detailData.channelCode" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐭俊妯℃澘">
+ {{ detailData.templateId }} | {{ detailData.templateCode }}
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="detailData.templateType" />
+ </el-descriptions-item>
+ <el-descriptions-item label="API 鐨勬ā鏉跨紪鍙�">
+ {{ detailData.apiTemplateId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛淇℃伅">
+ {{ detailData.mobile }}
+ <span v-if="detailData.userType && detailData.userId">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="detailData.userType" />
+ ({{ detailData.userId }})
+ </span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐭俊鍐呭">
+ {{ detailData.templateContent }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐭俊鍙傛暟">
+ {{ detailData.templateParams }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ formatDate(detailData.createTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佺姸鎬�">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="detailData.sendStatus" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戦�佹椂闂�">
+ {{ formatDate(detailData.sendTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="API 鍙戦�佺粨鏋�">
+ {{ detailData.apiSendCode }} | {{ detailData.apiSendMsg }}
+ </el-descriptions-item>
+ <el-descriptions-item label="API 鐭俊缂栧彿">
+ {{ detailData.apiSerialNo }}
+ </el-descriptions-item>
+ <el-descriptions-item label="API 璇锋眰缂栧彿">
+ {{ detailData.apiRequestId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="API 鎺ユ敹鐘舵��">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="detailData.receiveStatus" />
+ {{ formatDate(detailData.receiveTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="API 鎺ユ敹缁撴灉">
+ {{ detailData.apiReceiveCode }} | {{ detailData.apiReceiveMsg }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as SmsLogApi from '@/api/system/sms/smsLog'
+import * as SmsChannelApi from '@/api/system/sms/smsChannel'
+
+defineOptions({ name: 'SystemSmsLogDetail' })
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref() // 璇︽儏鏁版嵁
+const channelList = ref([]) // 鐭俊娓犻亾鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (data: SmsLogApi.SmsLogVO) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ detailLoading.value = true
+ try {
+ detailData.value = data
+ } finally {
+ detailLoading.value = false
+ }
+ // 鍔犺浇娓犻亾鍒楄〃
+ channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/sms/log/index.vue b/src/views/system/sms/log/index.vue
new file mode 100644
index 0000000..e1a5a23
--- /dev/null
+++ b/src/views/system/sms/log/index.vue
@@ -0,0 +1,268 @@
+<template>
+ <doc-alert title="鐭俊閰嶇疆" url="https://doc.iocoder.cn/sms/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="100px"
+ >
+ <el-form-item label="鎵嬫満鍙�" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐭俊娓犻亾" prop="channelId">
+ <el-select
+ v-model="queryParams.channelId"
+ placeholder="璇烽�夋嫨鐭俊娓犻亾"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="channel in channelList"
+ :key="channel.id"
+ :value="channel.id"
+ :label="
+ channel.signature +
+ `銆� ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}銆慲
+ "
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栧彿" prop="templateId">
+ <el-input
+ v-model="queryParams.templateId"
+ placeholder="璇疯緭鍏ユā鏉跨紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鍙戦�佺姸鎬�" prop="sendStatus">
+ <el-select
+ v-model="queryParams.sendStatus"
+ placeholder="璇烽�夋嫨鍙戦�佺姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戦�佹椂闂�" prop="sendTime">
+ <el-date-picker
+ v-model="queryParams.sendTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎺ユ敹鐘舵��" prop="receiveStatus">
+ <el-select
+ v-model="queryParams.receiveStatus"
+ placeholder="璇烽�夋嫨鎺ユ敹鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎺ユ敹鏃堕棿" prop="receiveTime">
+ <el-date-picker
+ v-model="queryParams.receiveTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:sms-log:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list">
+ <el-table-column label="缂栧彿" align="center" prop="id" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎵嬫満鍙�" align="center" prop="mobile" width="120">
+ <template #default="scope">
+ <div>{{ scope.row.mobile }}</div>
+ <div v-if="scope.row.userType && scope.row.userId">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ {{ '(' + scope.row.userId + ')' }}
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐭俊鍐呭" align="center" prop="templateContent" width="300" />
+ <el-table-column label="鍙戦�佺姸鎬�" align="center" width="180">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="scope.row.sendStatus" />
+ <div>{{ formatDate(scope.row.sendTime) }}</div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺ユ敹鐘舵��" align="center" width="180">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="scope.row.receiveStatus" />
+ <div>{{ formatDate(scope.row.receiveTime) }}</div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐭俊娓犻亾" align="center" width="120">
+ <template #default="scope">
+ <div>
+ {{ channelList.find((channel) => channel.id === scope.row.channelId)?.signature }}
+ </div>
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.channelCode" />
+ </template>
+ </el-table-column>
+ <el-table-column label="妯℃澘缂栧彿" align="center" prop="templateId" />
+ <el-table-column label="鐭俊绫诲瀷" align="center" prop="templateType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="scope.row.templateType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" class-name="fixed-width">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openDetail(scope.row)"
+ v-hasPermi="['system:sms-log:query']"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <SmsLogDetail ref="detailRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
+import { dateFormatter, formatDate } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as SmsChannelApi from '@/api/system/sms/smsChannel'
+import * as SmsLogApi from '@/api/system/sms/smsLog'
+import SmsLogDetail from './SmsLogDetail.vue'
+
+defineOptions({ name: 'SystemSmsLog' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ channelId: null,
+ templateId: null,
+ mobile: '',
+ sendStatus: null,
+ receiveStatus: null,
+ sendTime: [],
+ receiveTime: []
+})
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const channelList = ref([]) // 鐭俊娓犻亾鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SmsLogApi.getSmsLogPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await SmsLogApi.exportSmsLog(queryParams)
+ download.excel(data, '鐭俊鏃ュ織.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (data: SmsLogApi.SmsLogVO) => {
+ detailRef.value.open(data)
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇娓犻亾鍒楄〃
+ channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
+})
+</script>
diff --git a/src/views/system/sms/template/SmsTemplateForm.vue b/src/views/system/sms/template/SmsTemplateForm.vue
new file mode 100644
index 0000000..f339136
--- /dev/null
+++ b/src/views/system/sms/template/SmsTemplateForm.vue
@@ -0,0 +1,163 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ >
+ <el-form-item label="鐭俊娓犻亾缂栧彿" prop="channelId">
+ <el-select v-model="formData.channelId" placeholder="璇烽�夋嫨鐭俊娓犻亾缂栧彿">
+ <el-option
+ v-for="channel in channelList"
+ :key="channel.id"
+ :label="
+ channel.signature +
+ `銆� ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}銆慲
+ "
+ :value="channel.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐭俊绫诲瀷" prop="type">
+ <el-select v-model="formData.type" placeholder="璇烽�夋嫨鐭俊绫诲瀷">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栧彿" prop="code">
+ <el-input v-model="formData.code" placeholder="璇疯緭鍏ユā鏉跨紪鍙�" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍚嶇О" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ユā鏉垮悕绉�" />
+ </el-form-item>
+ <el-form-item label="妯℃澘鍐呭" prop="content">
+ <el-input v-model="formData.content" placeholder="璇疯緭鍏ユā鏉垮唴瀹�" type="textarea" />
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鐭俊 API 妯℃澘缂栧彿" prop="apiTemplateId">
+ <el-input v-model="formData.apiTemplateId" placeholder="璇疯緭鍏ョ煭淇� API 鐨勬ā鏉跨紪鍙�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getDictLabel, getIntDictOptions } from '@/utils/dict'
+import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
+import * as SmsChannelApi from '@/api/system/sms/smsChannel'
+import { CommonStatusEnum } from '@/utils/constants'
+
+defineOptions({ name: 'SystemSmsTemplateForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨�
+const formData = ref<SmsTemplateApi.SmsTemplateVO>({
+ id: undefined,
+ type: undefined,
+ status: CommonStatusEnum.ENABLE,
+ code: '',
+ name: '',
+ content: '',
+ remark: '',
+ apiTemplateId: '',
+ channelId: undefined
+})
+const formRules = reactive({
+ type: [{ required: true, message: '鐭俊绫诲瀷涓嶈兘涓虹┖', trigger: 'change' }],
+ status: [{ required: true, message: '寮�鍚姸鎬佷笉鑳戒负绌�', trigger: 'blur' }],
+ code: [{ required: true, message: '妯℃澘缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ name: [{ required: true, message: '妯℃澘鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ content: [{ required: true, message: '妯℃澘鍐呭涓嶈兘涓虹┖', trigger: 'blur' }],
+ apiTemplateId: [{ required: true, message: '鐭俊 API 鐨勬ā鏉跨紪鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+ channelId: [{ required: true, message: '鐭俊娓犻亾缂栧彿涓嶈兘涓虹┖', trigger: 'change' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const channelList = ref<SmsChannelApi.SmsChannelVO[]>([]) // 鐭俊娓犻亾鍒楄〃
+
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SmsTemplateApi.getSmsTemplate(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇娓犻亾鍒楄〃
+ channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ formLoading.value = true
+ try {
+ const data = formData.value as SmsTemplateApi.SmsTemplateVO
+ if (formType.value === 'create') {
+ await SmsTemplateApi.createSmsTemplate(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SmsTemplateApi.updateSmsTemplate(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ type: undefined,
+ status: CommonStatusEnum.ENABLE,
+ code: '',
+ name: '',
+ content: '',
+ remark: '',
+ apiTemplateId: '',
+ channelId: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/sms/template/SmsTemplateSendForm.vue b/src/views/system/sms/template/SmsTemplateSendForm.vue
new file mode 100644
index 0000000..b73ec41
--- /dev/null
+++ b/src/views/system/sms/template/SmsTemplateSendForm.vue
@@ -0,0 +1,120 @@
+<template>
+ <Dialog v-model="dialogVisible" title="娴嬭瘯">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="140px"
+ >
+ <el-form-item label="妯℃澘鍐呭" prop="content">
+ <el-input
+ v-model="formData.content"
+ placeholder="璇疯緭鍏ユā鏉垮唴瀹�"
+ readonly
+ type="textarea"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙�" prop="mobile">
+ <el-input v-model="formData.mobile" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
+ </el-form-item>
+ <el-form-item
+ v-for="param in formData.params"
+ :key="param"
+ :label="'鍙傛暟 {' + param + '}'"
+ :prop="'templateParams.' + param"
+ >
+ <el-input
+ v-model="formData.templateParams[param]"
+ :placeholder="'璇疯緭鍏� ' + param + ' 鍙傛暟'"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
+
+defineOptions({ name: 'SystemSmsTemplateSendForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+
+// 鍙戦�佺煭淇¤〃鍗曠浉鍏�
+const formData = ref({
+ content: '',
+ params: {},
+ mobile: '',
+ templateCode: '',
+ templateParams: new Map()
+})
+const formRules = reactive({
+ mobile: [{ required: true, message: '鎵嬫満涓嶈兘涓虹┖', trigger: 'blur' }],
+ templateCode: [{ required: true, message: '妯$増缂栫爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ templateParams: {}
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+const open = async (id: number) => {
+ dialogVisible.value = true
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formLoading.value = true
+ try {
+ const data = await SmsTemplateApi.getSmsTemplate(id)
+ // 璁剧疆鍔ㄦ�佽〃鍗�
+ formData.value.content = data.content
+ formData.value.params = data.params
+ formData.value.templateCode = data.code
+ formData.value.templateParams = data.params.reduce((obj, item) => {
+ obj[item] = '' // 缁欐瘡涓姩鎬佸睘鎬ц祴鍊硷紝閬垮厤鏃犳硶璇诲彇
+ return obj
+ }, {})
+ formRules.templateParams = data.params.reduce((obj, item) => {
+ obj[item] = { required: true, message: '鍙傛暟 ' + item + ' 涓嶈兘涓虹┖', trigger: 'blur' }
+ return obj
+ }, {})
+ } finally {
+ formLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as SmsTemplateApi.SendSmsReqVO
+ const logId = await SmsTemplateApi.sendSms(data)
+ if (logId) {
+ message.success('鎻愪氦鍙戦�佹垚鍔燂紒鍙戦�佺粨鏋滐紝瑙佸彂閫佹棩蹇楃紪鍙凤細' + logId)
+ }
+ dialogVisible.value = false
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ content: '',
+ params: {},
+ mobile: '',
+ templateCode: '',
+ templateParams: new Map()
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/sms/template/index.vue b/src/views/system/sms/template/index.vue
new file mode 100644
index 0000000..b582ed8
--- /dev/null
+++ b/src/views/system/sms/template/index.vue
@@ -0,0 +1,345 @@
+<template>
+ <doc-alert title="鐭俊閰嶇疆" url="https://doc.iocoder.cn/sms/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="150px"
+ >
+ <el-form-item label="鐭俊绫诲瀷" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ placeholder="璇烽�夋嫨鐭俊绫诲瀷"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�鍚姸鎬�" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨寮�鍚姸鎬�"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="妯℃澘缂栫爜" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ユā鏉跨紪鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐭俊 API 鐨勬ā鏉跨紪鍙�" prop="apiTemplateId">
+ <el-input
+ v-model="queryParams.apiTemplateId"
+ placeholder="璇疯緭鍏ョ煭淇� API 鐨勬ā鏉跨紪鍙�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐭俊娓犻亾" prop="channelId">
+ <el-select
+ v-model="queryParams.channelId"
+ placeholder="璇烽�夋嫨鐭俊娓犻亾"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="channel in channelList"
+ :key="channel.id"
+ :value="channel.id"
+ :label="
+ channel.signature +
+ `銆� ${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, channel.code)}銆慲
+ "
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ style="width: 240px"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:sms-template:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:sms-template:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" />鎵归噺鍒犻櫎
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:sms-template:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" /> 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column
+ label="妯℃澘缂栫爜"
+ align="center"
+ prop="code"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="妯℃澘鍚嶇О"
+ align="center"
+ prop="name"
+ width="120"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="妯℃澘鍐呭"
+ align="center"
+ prop="content"
+ width="200"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="鐭俊绫诲瀷" align="center" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status" width="80">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column
+ label="鐭俊 API 鐨勬ā鏉跨紪鍙�"
+ align="center"
+ prop="apiTemplateId"
+ width="200"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="鐭俊娓犻亾" align="center" width="120">
+ <template #default="scope">
+ <div>
+ {{ channelList.find((channel) => channel.id === scope.row.channelId)?.signature }}
+ </div>
+ <dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="scope.row.channelCode" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="210" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:sms-template:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openSendForm(scope.row.id)"
+ v-hasPermi="['system:sms-template:send-sms']"
+ >
+ 娴嬭瘯
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:sms-template:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SmsTemplateForm ref="formRef" @success="getList" />
+ <!-- 琛ㄥ崟寮圭獥锛氭祴璇曞彂閫� -->
+ <SmsTemplateSendForm ref="sendFormRef" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions, getDictLabel } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as SmsTemplateApi from '@/api/system/sms/smsTemplate'
+import * as SmsChannelApi from '@/api/system/sms/smsChannel'
+import download from '@/utils/download'
+import SmsTemplateForm from './SmsTemplateForm.vue'
+import SmsTemplateSendForm from './SmsTemplateSendForm.vue'
+
+defineOptions({ name: 'SystemSmsTemplate' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(false) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: undefined,
+ status: undefined,
+ code: '',
+ content: '',
+ apiTemplateId: '',
+ channelId: undefined,
+ createTime: []
+})
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const channelList = ref<SmsChannelApi.SmsChannelVO[]>([]) // 鐭俊娓犻亾鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SmsTemplateApi.getSmsTemplatePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍙戦�佺煭淇℃寜閽� */
+const sendFormRef = ref()
+const openSendForm = (id: number) => {
+ sendFormRef.value.open(id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SmsTemplateApi.deleteSmsTemplate(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: SmsTemplateApi.SmsTemplateVO[]) => {
+ checkedIds.value = rows.map((row) => row.id!)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await SmsTemplateApi.deleteSmsTemplateList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await SmsTemplateApi.exportSmsTemplate(queryParams)
+ download.excel(data, '鐭俊妯℃澘.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鍔犺浇娓犻亾鍒楄〃
+ channelList.value = await SmsChannelApi.getSimpleSmsChannelList()
+})
+</script>
diff --git a/src/views/system/social/client/SocialClientForm.vue b/src/views/system/social/client/SocialClientForm.vue
new file mode 100644
index 0000000..dd83bb4
--- /dev/null
+++ b/src/views/system/social/client/SocialClientForm.vue
@@ -0,0 +1,159 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px"
+ >
+ <el-form-item label="搴旂敤鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ簲鐢ㄥ悕" />
+ </el-form-item>
+ <el-form-item label="绀句氦骞冲彴" prop="socialType">
+ <el-radio-group v-model="formData.socialType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-radio-group v-model="formData.userType">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绔紪鍙�" prop="clientId">
+ <el-input v-model="formData.clientId" placeholder="璇疯緭鍏ュ鎴风缂栧彿,瀵瑰簲鍚勫钩鍙扮殑appKey" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绔瘑閽�" prop="clientSecret">
+ <el-input
+ v-model="formData.clientSecret"
+ placeholder="璇疯緭鍏ュ鎴风瀵嗛挜,瀵瑰簲鍚勫钩鍙扮殑appSecret"
+ />
+ </el-form-item>
+ <el-form-item label="agentId" prop="agentId" v-if="formData!.socialType === 30">
+ <el-input v-model="formData.agentId" placeholder="鎺堟潈鏂圭殑缃戦〉搴旂敤 ID锛屾湁鍒欏~" />
+ </el-form-item>
+ <el-form-item label="publicKey" prop="publicKey" v-if="formData!.socialType === 40">
+ <el-input v-model="formData.publicKey" placeholder="璇疯緭鍏� publicKey 鍏挜" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as SocialClientApi from '@/api/system/social/client'
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ socialType: undefined,
+ userType: undefined,
+ clientId: undefined,
+ clientSecret: undefined,
+ publicKey: undefined,
+ agentId: undefined,
+ status: 0
+})
+const formRules = reactive({
+ name: [{ required: true, message: '搴旂敤鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ socialType: [{ required: true, message: '绀句氦骞冲彴涓嶈兘涓虹┖', trigger: 'blur' }],
+ userType: [{ required: true, message: '鐢ㄦ埛绫诲瀷涓嶈兘涓虹┖', trigger: 'blur' }],
+ clientId: [{ required: true, message: '瀹㈡埛绔紪鍙蜂笉鑳戒负绌�', trigger: 'blur' }],
+ clientSecret: [{ required: true, message: '瀹㈡埛绔瘑閽ヤ笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await SocialClientApi.getSocialClient(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as SocialClientApi.SocialClientVO
+ if (formType.value === 'create') {
+ await SocialClientApi.createSocialClient(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await SocialClientApi.updateSocialClient(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ socialType: undefined,
+ userType: undefined,
+ clientId: undefined,
+ clientSecret: undefined,
+ publicKey: undefined,
+ agentId: undefined,
+ status: 0
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/social/client/index.vue b/src/views/system/social/client/index.vue
new file mode 100644
index 0000000..fa49018
--- /dev/null
+++ b/src/views/system/social/client/index.vue
@@ -0,0 +1,227 @@
+<template>
+ <doc-alert title="涓夋柟鐧诲綍" url="https://doc.iocoder.cn/social-user/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="130px"
+ >
+ <el-form-item label="搴旂敤鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ簲鐢ㄥ悕"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绀句氦骞冲彴" prop="socialType">
+ <el-select
+ v-model="queryParams.socialType"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨绀句氦骞冲彴"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <el-select
+ v-model="queryParams.userType"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨鐢ㄦ埛绫诲瀷"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛绔紪鍙�" prop="clientId">
+ <el-input
+ v-model="queryParams.clientId"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ュ鎴风缂栧彿"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ v-hasPermi="['system:social-client:create']"
+ plain
+ type="primary"
+ @click="openForm('create')"
+ >
+ <Icon class="mr-5px" icon="ep:plus" />
+ 鏂板
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="缂栧彿" prop="id" />
+ <el-table-column align="center" label="搴旂敤鍚�" prop="name" />
+ <el-table-column align="center" label="绀句氦骞冲彴" prop="socialType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="scope.row.socialType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="鐢ㄦ埛绫诲瀷" prop="userType">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="瀹㈡埛绔紪鍙�" prop="clientId" width="180px" />
+ <el-table-column align="center" label="鐘舵��" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column align="center" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['system:social-client:update']"
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-hasPermi="['system:social-client:delete']"
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <SocialClientForm ref="formRef" @success="getList" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as SocialClientApi from '@/api/system/social/client'
+import SocialClientForm from './SocialClientForm.vue'
+
+defineOptions({ name: 'SocialClient' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ socialType: undefined,
+ userType: undefined,
+ clientId: undefined,
+ status: undefined
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SocialClientApi.getSocialClientPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await SocialClientApi.deleteSocialClient(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/social/user/SocialUserDetail.vue b/src/views/system/social/user/SocialUserDetail.vue
new file mode 100644
index 0000000..aef9d45
--- /dev/null
+++ b/src/views/system/social/user/SocialUserDetail.vue
@@ -0,0 +1,60 @@
+<template>
+ <Dialog v-model="dialogVisible" title="璇︽儏" width="800">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="绀句氦骞冲彴" min-width="160">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="detailData.type" />
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢ㄦ埛鏄电О" min-width="120">
+ {{ detailData.nickname }}
+ </el-descriptions-item>
+ <el-descriptions label="鐢ㄦ埛澶村儚" min-width="120">
+ <el-image :src="detailData.avatar" class="h-30px w-30px" />
+ </el-descriptions>
+ <el-descriptions-item label="绀句氦 token" min-width="120">
+ {{ detailData.token }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍘熷 Token 鏁版嵁" min-width="120">
+ <el-input
+ v-model="detailData.rawTokenInfo"
+ :autosize="{ maxRows: 20 }"
+ :readonly="true"
+ type="textarea"
+ />
+ </el-descriptions-item>
+ <el-descriptions-item label="鍘熷 User 鏁版嵁" min-width="120">
+ <el-input
+ v-model="detailData.rawUserInfo"
+ :autosize="{ maxRows: 20 }"
+ :readonly="true"
+ type="textarea"
+ />
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庝竴娆$殑璁よ瘉 code" min-width="120">
+ {{ detailData.code }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏈�鍚庝竴娆$殑璁よ瘉 state" min-width="120">
+ {{ detailData.state }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import * as SocialUserApi from '@/api/system/social/user'
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const detailLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const detailData = ref({} as SocialUserApi.SocialUserVO) // 璇︽儏鏁版嵁
+
+/** 鎵撳紑寮圭獥 */
+const open = async (id: number) => {
+ dialogVisible.value = true
+ // 璁剧疆鏁版嵁
+ try {
+ detailData.value = await SocialUserApi.getSocialUser(id)
+ } finally {
+ detailLoading.value = false
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+</script>
diff --git a/src/views/system/social/user/index.vue b/src/views/system/social/user/index.vue
new file mode 100644
index 0000000..dda9eb8
--- /dev/null
+++ b/src/views/system/social/user/index.vue
@@ -0,0 +1,187 @@
+<template>
+ <doc-alert title="涓夋柟鐧诲綍" url="https://doc.iocoder.cn/social-user/" />
+
+ <ContentWrap>
+ <!-- 鎼滅储宸ヤ綔鏍� -->
+ <el-form
+ ref="queryFormRef"
+ :inline="true"
+ :model="queryParams"
+ class="-mb-15px"
+ label-width="120px"
+ >
+ <el-form-item label="绀句氦骞冲彴" prop="type">
+ <el-select
+ v-model="queryParams.type"
+ class="!w-240px"
+ clearable
+ placeholder="璇烽�夋嫨绀句氦骞冲彴"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input
+ v-model="queryParams.nickname"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ョ敤鎴锋樀绉�"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绀句氦 openid" prop="openid">
+ <el-input
+ v-model="queryParams.openid"
+ class="!w-240px"
+ clearable
+ placeholder="璇疯緭鍏ョぞ浜� openid"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ end-placeholder="缁撴潫鏃ユ湡"
+ start-placeholder="寮�濮嬫棩鏈�"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon class="mr-5px" icon="ep:search" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon class="mr-5px" icon="ep:refresh" />
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
+ <el-table-column align="center" label="绀句氦骞冲彴" prop="type">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="scope.row.type" />
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="绀句氦 openid" prop="openid" />
+ <el-table-column align="center" label="鐢ㄦ埛鏄电О" prop="nickname" />
+ <el-table-column align="center" label="鐢ㄦ埛澶村儚" prop="avatar">
+ <template #default="{ row }">
+ <el-image :src="row.avatar" class="h-30px w-30px" @click="imagePreview(row.avatar)" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鍒涘缓鏃堕棿"
+ prop="createTime"
+ width="180px"
+ />
+ <el-table-column
+ :formatter="dateFormatter"
+ align="center"
+ label="鏇存柊鏃堕棿"
+ prop="updateTime"
+ width="180px"
+ />
+ <el-table-column align="center" fixed="right" label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ v-hasPermi="['system:social-user:query']"
+ link
+ type="primary"
+ @click="openDetail(scope.row.id)"
+ >
+ 璇︽儏
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ v-model:limit="queryParams.pageSize"
+ v-model:page="queryParams.pageNo"
+ :total="total"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氳鎯� -->
+ <SocialUserDetail ref="detailRef" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as SocialUserApi from '@/api/system/social/user'
+import SocialUserDetail from './SocialUserDetail.vue'
+import { createImageViewer } from '@/components/ImageViewer'
+
+defineOptions({ name: 'SocialUser' })
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ type: undefined,
+ openid: undefined,
+ nickname: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await SocialUserApi.getSocialUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+const imagePreview = (imgUrl: string) => {
+ createImageViewer({
+ urlList: [imgUrl]
+ })
+}
+
+/** 璇︽儏鎿嶄綔 */
+const detailRef = ref()
+const openDetail = (id: number) => {
+ detailRef.value.open(id)
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/tenant/TenantForm.vue b/src/views/system/tenant/TenantForm.vue
new file mode 100644
index 0000000..913d171
--- /dev/null
+++ b/src/views/system/tenant/TenantForm.vue
@@ -0,0 +1,186 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle" width="50%">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="绉熸埛鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ョ鎴峰悕" />
+ </el-form-item>
+ <el-form-item label="绉熸埛濂楅" prop="packageId">
+ <el-select v-model="formData.packageId" clearable placeholder="璇烽�夋嫨绉熸埛濂楅">
+ <el-option
+ v-for="item in packageList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑱旂郴浜�" prop="contactName">
+ <el-input v-model="formData.contactName" placeholder="璇疯緭鍏ヨ仈绯讳汉" />
+ </el-form-item>
+ <el-form-item label="鑱旂郴鎵嬫満" prop="contactMobile">
+ <el-input v-model="formData.contactMobile" placeholder="璇疯緭鍏ヨ仈绯绘墜鏈�" />
+ </el-form-item>
+ <el-form-item v-if="formData.id === undefined" label="鐢ㄦ埛鍚嶇О" prop="username">
+ <el-input v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕绉�" />
+ </el-form-item>
+ <el-form-item v-if="formData.id === undefined" label="鐢ㄦ埛瀵嗙爜" prop="password">
+ <el-input
+ v-model="formData.password"
+ placeholder="璇疯緭鍏ョ敤鎴峰瘑鐮�"
+ show-password
+ type="password"
+ />
+ </el-form-item>
+ <el-form-item label="璐﹀彿棰濆害" prop="accountCount">
+ <el-input-number
+ v-model="formData.accountCount"
+ :min="0"
+ controls-position="right"
+ placeholder="璇疯緭鍏ヨ处鍙烽搴�"
+ />
+ </el-form-item>
+ <el-form-item label="杩囨湡鏃堕棿" prop="expireTime">
+ <el-date-picker
+ v-model="formData.expireTime"
+ clearable
+ placeholder="璇烽�夋嫨杩囨湡鏃堕棿"
+ type="date"
+ value-format="x"
+ />
+ </el-form-item>
+ <el-form-item label="缁戝畾鍩熷悕" prop="websites">
+ <el-input-tag
+ v-model="formData.websites"
+ placeholder="璇疯緭鍏ョ粦瀹氬煙鍚嶏紝鎸夊洖杞︽坊鍔�"
+ class="w-full"
+ />
+ </el-form-item>
+ <el-form-item label="绉熸埛鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as TenantApi from '@/api/system/tenant'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as TenantPackageApi from '@/api/system/tenantPackage'
+
+defineOptions({ name: 'SystemTenantForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: undefined,
+ name: undefined,
+ packageId: undefined,
+ contactName: undefined,
+ contactMobile: undefined,
+ accountCount: undefined,
+ expireTime: undefined,
+ websites: [],
+ status: CommonStatusEnum.ENABLE,
+ // 鏂板涓撳睘
+ username: undefined,
+ password: undefined
+})
+const formRules = reactive({
+ name: [{ required: true, message: '绉熸埛鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ packageId: [{ required: true, message: '绉熸埛濂楅涓嶈兘涓虹┖', trigger: 'blur' }],
+ contactName: [{ required: true, message: '鑱旂郴浜轰笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '绉熸埛鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ accountCount: [{ required: true, message: '璐﹀彿棰濆害涓嶈兘涓虹┖', trigger: 'blur' }],
+ expireTime: [{ required: true, message: '杩囨湡鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }],
+ username: [{ required: true, message: '鐢ㄦ埛鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ password: [{ required: true, message: '鐢ㄦ埛瀵嗙爜涓嶈兘涓虹┖', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) // 绉熸埛濂楅
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await TenantApi.getTenant(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇濂楅鍒楄〃
+ packageList.value = await TenantPackageApi.getTenantPackageList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as TenantApi.TenantVO
+ if (formType.value === 'create') {
+ await TenantApi.createTenant(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await TenantApi.updateTenant(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: undefined,
+ name: undefined,
+ packageId: undefined,
+ contactName: undefined,
+ contactMobile: undefined,
+ accountCount: undefined,
+ expireTime: undefined,
+ websites: [],
+ status: CommonStatusEnum.ENABLE,
+ username: undefined,
+ password: undefined
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/tenant/index.vue b/src/views/system/tenant/index.vue
new file mode 100644
index 0000000..59b3867
--- /dev/null
+++ b/src/views/system/tenant/index.vue
@@ -0,0 +1,304 @@
+<template>
+ <doc-alert title="SaaS 澶氱鎴�" url="https://doc.iocoder.cn/saas-tenant/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="绉熸埛鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ョ鎴峰悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鑱旂郴浜�" prop="contactName">
+ <el-input
+ v-model="queryParams.contactName"
+ placeholder="璇疯緭鍏ヨ仈绯讳汉"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鑱旂郴鎵嬫満" prop="contactMobile">
+ <el-input
+ v-model="queryParams.contactMobile"
+ placeholder="璇疯緭鍏ヨ仈绯绘墜鏈�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="绉熸埛鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨绉熸埛鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+ class="!w-240px"
+ />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button @click="handleQuery">
+ <Icon icon="ep:search" class="mr-5px" />
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <Icon icon="ep:refresh" class="mr-5px" />
+ 閲嶇疆
+ </el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:tenant:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />
+ 鏂板
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:tenant:export']"
+ >
+ <Icon icon="ep:download" class="mr-5px" />
+ 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:tenant:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" />
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="绉熸埛缂栧彿" align="center" prop="id" />
+ <el-table-column label="绉熸埛鍚�" align="center" prop="name" />
+ <el-table-column label="绉熸埛濂楅" align="center" prop="packageId">
+ <template #default="scope">
+ <el-tag v-if="scope.row.packageId === 0" type="danger">绯荤粺绉熸埛</el-tag>
+ <template v-else v-for="item in packageList">
+ <el-tag type="success" :key="item.id" v-if="item.id === scope.row.packageId">
+ {{ item.name }}
+ </el-tag>
+ </template>
+ </template>
+ </el-table-column>
+ <el-table-column label="鑱旂郴浜�" align="center" prop="contactName" />
+ <el-table-column label="鑱旂郴鎵嬫満" align="center" prop="contactMobile" />
+ <el-table-column label="璐﹀彿棰濆害" align="center" prop="accountCount">
+ <template #default="scope">
+ <el-tag>{{ scope.row.accountCount }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="杩囨湡鏃堕棿"
+ align="center"
+ prop="expireTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="缁戝畾鍩熷悕" align="center" prop="websites" width="180">
+ <template #default="scope">
+ <el-tag v-for="website in scope.row.websites || []" :key="website" class="mr-1 mb-1">
+ {{ website }}
+ </el-tag>
+ <span v-if="!scope.row.websites || scope.row.websites.length === 0">-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="绉熸埛鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" min-width="110" fixed="right">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:tenant:update']"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:tenant:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <TenantForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as TenantApi from '@/api/system/tenant'
+import * as TenantPackageApi from '@/api/system/tenantPackage'
+import TenantForm from './TenantForm.vue'
+
+defineOptions({ name: 'SystemTenant' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ contactName: undefined,
+ contactMobile: undefined,
+ status: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+const exportLoading = ref(false) // 瀵煎嚭鐨勫姞杞戒腑
+const packageList = ref([] as TenantPackageApi.TenantPackageVO[]) //绉熸埛濂楅鍒楄〃
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TenantApi.getTenantPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value.resetFields()
+ handleQuery()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await TenantApi.deleteTenant(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: TenantApi.TenantVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await TenantApi.deleteTenantList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await TenantApi.exportTenant(queryParams)
+ download.excel(data, '绉熸埛鍒楄〃.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+ await getList()
+ // 鑾峰彇绉熸埛濂楅鍒楄〃
+ packageList.value = await TenantPackageApi.getTenantPackageList()
+})
+</script>
diff --git a/src/views/system/tenantPackage/TenantPackageForm.vue b/src/views/system/tenantPackage/TenantPackageForm.vue
new file mode 100644
index 0000000..2003c9c
--- /dev/null
+++ b/src/views/system/tenantPackage/TenantPackageForm.vue
@@ -0,0 +1,187 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-form-item label="濂楅鍚�" prop="name">
+ <el-input v-model="formData.name" placeholder="璇疯緭鍏ュ椁愬悕" />
+ </el-form-item>
+ <el-form-item label="鑿滃崟鏉冮檺">
+ <el-card class="w-full h-400px !overflow-y-scroll" shadow="never">
+ <template #header>
+ 鍏ㄩ��/鍏ㄤ笉閫�:
+ <el-switch
+ v-model="treeNodeAll"
+ active-text="鏄�"
+ inactive-text="鍚�"
+ inline-prompt
+ @change="handleCheckedTreeNodeAll"
+ />
+ 鍏ㄩ儴灞曞紑/鎶樺彔:
+ <el-switch
+ v-model="menuExpand"
+ active-text="灞曞紑"
+ inactive-text="鎶樺彔"
+ inline-prompt
+ @change="handleCheckedTreeExpand"
+ />
+ </template>
+ <el-tree
+ ref="treeRef"
+ :data="menuOptions"
+ :props="defaultProps"
+ empty-text="鍔犺浇涓紝璇风◢鍊�"
+ node-key="id"
+ show-checkbox
+ />
+ </el-card>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="formData.status">
+ <el-radio
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :value="dict.value"
+ >
+ {{ dict.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as TenantPackageApi from '@/api/system/tenantPackage'
+import * as MenuApi from '@/api/system/menu'
+import { ElTree } from 'element-plus'
+
+defineOptions({ name: 'SystemTenantPackageForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ id: null,
+ name: null,
+ remark: null,
+ menuIds: [],
+ status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+ name: [{ required: true, message: '濂楅鍚嶄笉鑳戒负绌�', trigger: 'blur' }],
+ status: [{ required: true, message: '鐘舵�佷笉鑳戒负绌�', trigger: 'blur' }],
+ menuIds: [{ required: true, message: '鍏宠仈鐨勮彍鍗曠紪鍙蜂笉鑳戒负绌�', trigger: 'blur' }]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const menuOptions = ref<any[]>([]) // 鏍戝舰缁撴瀯鏁版嵁
+const menuExpand = ref(false) // 灞曞紑/鎶樺彔
+const treeRef = ref<InstanceType<typeof ElTree>>() // 鏍戠粍浠� Ref
+const treeNodeAll = ref(false) // 鍏ㄩ��/鍏ㄤ笉閫�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 鍔犺浇 Menu 鍒楄〃銆傛敞鎰忥紝蹇呴』鏀惧湪鍓嶉潰锛屼笉鐒朵笅闈� setChecked 娌℃暟鎹妭鐐�
+ menuOptions.value = handleTree(await MenuApi.getSimpleMenusList())
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ const res = await TenantPackageApi.getTenantPackage(id)
+ // 璁剧疆閫変腑
+ formData.value = res
+ // 璁剧疆閫変腑
+ res.menuIds.forEach((menuId: number) => {
+ treeRef.value!.setChecked(menuId, true, false)
+ })
+ } finally {
+ formLoading.value = false
+ }
+ }
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as TenantPackageApi.TenantPackageVO
+ data.menuIds = [
+ ...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>), // 鑾峰緱褰撳墠閫変腑鑺傜偣
+ ...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>) // 鑾峰緱鍗婇�変腑鐨勭埗鑺傜偣
+ ]
+ if (formType.value === 'create') {
+ await TenantPackageApi.createTenantPackage(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await TenantPackageApi.updateTenantPackage(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ // 閲嶇疆閫夐」
+ treeNodeAll.value = false
+ menuExpand.value = false
+ // 閲嶇疆琛ㄥ崟
+ formData.value = {
+ id: null,
+ name: null,
+ remark: null,
+ menuIds: [],
+ status: CommonStatusEnum.ENABLE
+ }
+ treeRef.value?.setCheckedNodes([])
+ formRef.value?.resetFields()
+}
+
+/** 鍏ㄩ��/鍏ㄤ笉閫� */
+const handleCheckedTreeNodeAll = () => {
+ treeRef.value!.setCheckedNodes(treeNodeAll.value ? menuOptions.value : [])
+}
+
+/** 灞曞紑/鎶樺彔鍏ㄩ儴 */
+const handleCheckedTreeExpand = () => {
+ const nodes = treeRef.value?.store.nodesMap
+ for (let node in nodes) {
+ if (nodes[node].expanded === menuExpand.value) {
+ continue
+ }
+ nodes[node].expanded = menuExpand.value
+ }
+}
+</script>
diff --git a/src/views/system/tenantPackage/index.vue b/src/views/system/tenantPackage/index.vue
new file mode 100644
index 0000000..5691bd3
--- /dev/null
+++ b/src/views/system/tenantPackage/index.vue
@@ -0,0 +1,210 @@
+<template>
+ <doc-alert title="SaaS 澶氱鎴�" url="https://doc.iocoder.cn/saas-tenant/" />
+
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="濂楅鍚�" prop="name">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ椁愬悕"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable class="!w-240px">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ type="daterange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:tenant-package:create']"
+ >
+ <Icon icon="ep:plus" class="mr-5px" />
+ 鏂板
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:tenant-package:delete']"
+ >
+ <Icon icon="ep:delete" class="mr-5px" />
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+
+ <!-- 鍒楄〃 -->
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="濂楅缂栧彿" align="center" prop="id" width="120" />
+ <el-table-column label="濂楅鍚�" align="center" prop="name" />
+ <el-table-column label="鐘舵��" align="center" prop="status" width="100">
+ <template #default="scope">
+ <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="180"
+ :formatter="dateFormatter"
+ />
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:tenant-package:update']"
+ >
+ 淇敼
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ @click="handleDelete(scope.row.id)"
+ v-hasPermi="['system:tenant-package:delete']"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+
+ <!-- 琛ㄥ崟寮圭獥锛氭坊鍔�/淇敼 -->
+ <TenantPackageForm ref="formRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as TenantPackageApi from '@/api/system/tenantPackage'
+import TenantPackageForm from './TenantPackageForm.vue'
+
+defineOptions({ name: 'SystemTenantPackage' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟鎹�
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ name: undefined,
+ status: undefined,
+ remark: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await TenantPackageApi.getTenantPackagePage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ getList()
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await TenantPackageApi.deleteTenantPackage(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: TenantPackageApi.TenantPackageVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await TenantPackageApi.deleteTenantPackageList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/src/views/system/user/DeptTree.vue b/src/views/system/user/DeptTree.vue
new file mode 100644
index 0000000..71ed6cd
--- /dev/null
+++ b/src/views/system/user/DeptTree.vue
@@ -0,0 +1,79 @@
+<template>
+ <div class="head-container">
+ <el-input v-model="deptName" class="mb-20px" clearable placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�">
+ <template #prefix>
+ <Icon icon="ep:search" />
+ </template>
+ </el-input>
+ </div>
+ <div class="head-container">
+ <el-tree
+ ref="treeRef"
+ :data="deptList"
+ :expand-on-click-node="false"
+ :filter-node-method="filterNode"
+ :props="defaultProps"
+ default-expand-all
+ highlight-current
+ node-key="id"
+ @node-click="handleNodeClick"
+ />
+ </div>
+</template>
+
+<script lang="ts" setup>
+import { ElTree } from 'element-plus'
+import * as DeptApi from '@/api/system/dept'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+defineOptions({ name: 'SystemUserDeptTree' })
+
+const deptName = ref('')
+const deptList = ref<Tree[]>([]) // 鏍戝舰缁撴瀯
+const treeRef = ref<InstanceType<typeof ElTree>>()
+
+/** 鑾峰緱閮ㄩ棬鏍� */
+const getTree = async () => {
+ const res = await DeptApi.getSimpleDeptList()
+ deptList.value = []
+ deptList.value.push(...handleTree(res))
+}
+
+/** 鍩轰簬鍚嶅瓧杩囨护 */
+const filterNode = (name: string, data: Tree) => {
+ if (!name) return true
+ return data.name.includes(name)
+}
+
+/** 澶勭悊閮ㄩ棬琚偣鍑� */
+let currentNode: any = {}
+const handleNodeClick = async (row: { [key: string]: any }, treeNode: any) => {
+ // 鍒ゆ柇閫変腑鐘舵��
+ if (currentNode && currentNode.name === row.name) {
+ treeNode.checked = !treeNode.checked
+ } else {
+ treeNode.checked = true
+ }
+ if (treeNode.checked) {
+ // 閫変腑
+ currentNode = row
+ emits('node-click', row)
+ } else {
+ // 鍙栨秷閫変腑
+ treeRef.value!.setCurrentKey(undefined)
+ emits('node-click', undefined)
+ currentNode = null
+ }
+}
+const emits = defineEmits(['node-click'])
+
+/** 鐩戝惉deptName */
+watch(deptName, (val) => {
+ treeRef.value!.filter(val)
+})
+
+/** 鍒濆鍖� */
+onMounted(async () => {
+ await getTree()
+})
+</script>
diff --git a/src/views/system/user/UserAssignRoleForm.vue b/src/views/system/user/UserAssignRoleForm.vue
new file mode 100644
index 0000000..67a5ddb
--- /dev/null
+++ b/src/views/system/user/UserAssignRoleForm.vue
@@ -0,0 +1,96 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鍒嗛厤瑙掕壊">
+ <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+ <el-form-item label="鐢ㄦ埛鍚嶇О">
+ <el-input v-model="formData.username" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鏄电О">
+ <el-input v-model="formData.nickname" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="瑙掕壊">
+ <el-select v-model="formData.roleIds" multiple placeholder="璇烽�夋嫨瑙掕壊">
+ <el-option v-for="item in roleList" :key="item.id" :label="item.name" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as PermissionApi from '@/api/system/permission'
+import * as UserApi from '@/api/system/user'
+import * as RoleApi from '@/api/system/role'
+
+defineOptions({ name: 'SystemUserAssignRoleForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formData = ref({
+ id: -1,
+ nickname: '',
+ username: '',
+ roleIds: []
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const roleList = ref([] as RoleApi.RoleVO[]) // 瑙掕壊鐨勫垪琛�
+
+/** 鎵撳紑寮圭獥 */
+const open = async (row: UserApi.UserVO) => {
+ dialogVisible.value = true
+ resetForm()
+ // 璁剧疆鏁版嵁
+ formData.value.id = row.id
+ formData.value.username = row.username
+ formData.value.nickname = row.nickname
+ // 鑾峰緱瑙掕壊鎷ユ湁鐨勮彍鍗曢泦鍚�
+ formLoading.value = true
+ try {
+ formData.value.roleIds = await PermissionApi.getUserRoleList(row.id)
+ } finally {
+ formLoading.value = false
+ }
+ // 鑾峰緱瑙掕壊鍒楄〃
+ roleList.value = await RoleApi.getSimpleRoleList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ await PermissionApi.assignUserRole({
+ userId: formData.value.id,
+ roleIds: formData.value.roleIds
+ })
+ message.success(t('common.updateSuccess'))
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success', true)
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ id: -1,
+ nickname: '',
+ username: '',
+ roleIds: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/user/UserForm.vue b/src/views/system/user/UserForm.vue
new file mode 100644
index 0000000..89498e0
--- /dev/null
+++ b/src/views/system/user/UserForm.vue
@@ -0,0 +1,219 @@
+<template>
+ <Dialog v-model="dialogVisible" :title="dialogTitle">
+ <el-form
+ ref="formRef"
+ v-loading="formLoading"
+ :model="formData"
+ :rules="formRules"
+ label-width="80px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickname">
+ <el-input v-model="formData.nickname" placeholder="璇疯緭鍏ョ敤鎴锋樀绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select
+ v-model="formData.deptId"
+ :data="deptList"
+ :props="defaultProps"
+ check-strictly
+ node-key="id"
+ placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input v-model="formData.mobile" maxlength="11" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="formData.email" maxlength="50" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item v-if="formData.id === undefined" label="鐢ㄦ埛鍚嶇О" prop="username">
+ <el-input v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item v-if="formData.id === undefined" label="鐢ㄦ埛瀵嗙爜" prop="password">
+ <el-input
+ v-model="formData.password"
+ placeholder="璇疯緭鍏ョ敤鎴峰瘑鐮�"
+ show-password
+ type="password"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ埛鎬у埆">
+ <el-select v-model="formData.sex" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="宀椾綅">
+ <el-select v-model="formData.postIds" multiple placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in postList"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id!"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" placeholder="璇疯緭鍏ュ唴瀹�" type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as PostApi from '@/api/system/post'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+import { FormRules } from 'element-plus'
+
+defineOptions({ name: 'SystemUserForm' })
+
+const { t } = useI18n() // 鍥介檯鍖�
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const dialogTitle = ref('') // 寮圭獥鐨勬爣棰�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑锛�1锛変慨鏀规椂鐨勬暟鎹姞杞斤紱2锛夋彁浜ょ殑鎸夐挳绂佺敤
+const formType = ref('') // 琛ㄥ崟鐨勭被鍨嬶細create - 鏂板锛泆pdate - 淇敼
+const formData = ref({
+ nickname: '',
+ deptId: '',
+ mobile: '',
+ email: '',
+ id: undefined,
+ username: '',
+ password: '',
+ sex: undefined,
+ postIds: [],
+ remark: '',
+ status: CommonStatusEnum.ENABLE,
+ roleIds: []
+})
+const formRules = reactive<FormRules>({
+ username: [{ required: true, message: '鐢ㄦ埛鍚嶇О涓嶈兘涓虹┖', trigger: 'blur' }],
+ nickname: [{ required: true, message: '鐢ㄦ埛鏄电О涓嶈兘涓虹┖', trigger: 'blur' }],
+ password: [{ required: true, message: '鐢ㄦ埛瀵嗙爜涓嶈兘涓虹┖', trigger: 'blur' }],
+ email: [
+ {
+ type: 'email',
+ message: '璇疯緭鍏ユ纭殑閭鍦板潃',
+ trigger: ['blur', 'change']
+ }
+ ],
+ mobile: [
+ {
+ pattern: /^1[3-9]\d{9}$/,
+ message: '璇疯緭鍏ユ纭殑鎵嬫満鍙风爜',
+ trigger: 'blur'
+ }
+ ]
+})
+const formRef = ref() // 琛ㄥ崟 Ref
+const deptList = ref<Tree[]>([]) // 鏍戝舰缁撴瀯
+const postList = ref([] as PostApi.PostVO[]) // 宀椾綅鍒楄〃
+
+/** 鎵撳紑寮圭獥 */
+const open = async (type: string, id?: number) => {
+ dialogVisible.value = true
+ dialogTitle.value = t('action.' + type)
+ formType.value = type
+ resetForm()
+ // 淇敼鏃讹紝璁剧疆鏁版嵁
+ if (id) {
+ formLoading.value = true
+ try {
+ formData.value = await UserApi.getUser(id)
+ } finally {
+ formLoading.value = false
+ }
+ }
+ // 鍔犺浇閮ㄩ棬鏍�
+ deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+ // 鍔犺浇宀椾綅鍒楄〃
+ postList.value = await PostApi.getSimplePostList()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const emit = defineEmits(['success']) // 瀹氫箟 success 浜嬩欢锛岀敤浜庢搷浣滄垚鍔熷悗鐨勫洖璋�
+const submitForm = async () => {
+ // 鏍¢獙琛ㄥ崟
+ if (!formRef) return
+ const valid = await formRef.value.validate()
+ if (!valid) return
+ // 鎻愪氦璇锋眰
+ formLoading.value = true
+ try {
+ const data = formData.value as unknown as UserApi.UserVO
+ if (formType.value === 'create') {
+ await UserApi.createUser(data)
+ message.success(t('common.createSuccess'))
+ } else {
+ await UserApi.updateUser(data)
+ message.success(t('common.updateSuccess'))
+ }
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emit('success')
+ } finally {
+ formLoading.value = false
+ }
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = () => {
+ formData.value = {
+ nickname: '',
+ deptId: '',
+ mobile: '',
+ email: '',
+ id: undefined,
+ username: '',
+ password: '',
+ sex: undefined,
+ postIds: [],
+ remark: '',
+ status: CommonStatusEnum.ENABLE,
+ roleIds: []
+ }
+ formRef.value?.resetFields()
+}
+</script>
diff --git a/src/views/system/user/UserImportForm.vue b/src/views/system/user/UserImportForm.vue
new file mode 100644
index 0000000..5cf1129
--- /dev/null
+++ b/src/views/system/user/UserImportForm.vue
@@ -0,0 +1,138 @@
+<template>
+ <Dialog v-model="dialogVisible" title="鐢ㄦ埛瀵煎叆" width="400">
+ <el-upload
+ ref="uploadRef"
+ v-model:file-list="fileList"
+ :action="importUrl + '?updateSupport=' + updateSupport"
+ :auto-upload="false"
+ :disabled="formLoading"
+ :headers="uploadHeaders"
+ :limit="1"
+ :on-error="submitFormError"
+ :on-exceed="handleExceed"
+ :on-success="submitFormSuccess"
+ accept=".xlsx, .xls"
+ drag
+ >
+ <Icon icon="ep:upload" />
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <div class="el-upload__tip">
+ <el-checkbox v-model="updateSupport" />
+ 鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ </div>
+ <span>浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢銆�</span>
+ <el-link
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ type="primary"
+ @click="importTemplate"
+ >
+ 涓嬭浇妯℃澘
+ </el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <el-button :disabled="formLoading" type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </template>
+ </Dialog>
+</template>
+<script lang="ts" setup>
+import * as UserApi from '@/api/system/user'
+import { getAccessToken, getTenantId } from '@/utils/auth'
+import download from '@/utils/download'
+
+defineOptions({ name: 'SystemUserImportForm' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+
+const dialogVisible = ref(false) // 寮圭獥鐨勬槸鍚﹀睍绀�
+const formLoading = ref(false) // 琛ㄥ崟鐨勫姞杞戒腑
+const uploadRef = ref()
+const importUrl =
+ import.meta.env.VITE_BASE_URL + import.meta.env.VITE_API_URL + '/system/user/import'
+const uploadHeaders = ref() // 涓婁紶 Header 澶�
+const fileList = ref([]) // 鏂囦欢鍒楄〃
+const updateSupport = ref(0) // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+
+/** 鎵撳紑寮圭獥 */
+const open = () => {
+ dialogVisible.value = true
+ updateSupport.value = 0
+ fileList.value = []
+ resetForm()
+}
+defineExpose({ open }) // 鎻愪緵 open 鏂规硶锛岀敤浜庢墦寮�寮圭獥
+
+/** 鎻愪氦琛ㄥ崟 */
+const submitForm = async () => {
+ if (fileList.value.length == 0) {
+ message.error('璇蜂笂浼犳枃浠�')
+ return
+ }
+ // 鎻愪氦璇锋眰
+ uploadHeaders.value = {
+ Authorization: 'Bearer ' + getAccessToken(),
+ 'tenant-id': getTenantId()
+ }
+ formLoading.value = true
+ uploadRef.value!.submit()
+}
+
+/** 鏂囦欢涓婁紶鎴愬姛 */
+const emits = defineEmits(['success'])
+const submitFormSuccess = (response: any) => {
+ if (response.code !== 0) {
+ message.error(response.msg)
+ resetForm()
+ return
+ }
+ // 鎷兼帴鎻愮ず璇�
+ const data = response.data
+ let text = '涓婁紶鎴愬姛鏁伴噺锛�' + data.createUsernames.length + ';'
+ for (let username of data.createUsernames) {
+ text += '< ' + username + ' >'
+ }
+ text += '鏇存柊鎴愬姛鏁伴噺锛�' + data.updateUsernames.length + ';'
+ for (const username of data.updateUsernames) {
+ text += '< ' + username + ' >'
+ }
+ text += '鏇存柊澶辫触鏁伴噺锛�' + Object.keys(data.failureUsernames).length + ';'
+ for (const username in data.failureUsernames) {
+ text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
+ }
+ message.alert(text)
+ formLoading.value = false
+ dialogVisible.value = false
+ // 鍙戦�佹搷浣滄垚鍔熺殑浜嬩欢
+ emits('success')
+}
+
+/** 涓婁紶閿欒鎻愮ず */
+const submitFormError = (): void => {
+ message.error('涓婁紶澶辫触锛岃鎮ㄩ噸鏂颁笂浼狅紒')
+ formLoading.value = false
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+const resetForm = async (): Promise<void> => {
+ // 閲嶇疆涓婁紶鐘舵�佸拰鏂囦欢
+ formLoading.value = false
+ await nextTick()
+ uploadRef.value?.clearFiles()
+}
+
+/** 鏂囦欢鏁拌秴鍑烘彁绀� */
+const handleExceed = (): void => {
+ message.error('鏈�澶氬彧鑳戒笂浼犱竴涓枃浠讹紒')
+}
+
+/** 涓嬭浇妯℃澘鎿嶄綔 */
+const importTemplate = async () => {
+ const res = await UserApi.importUserTemplate()
+ download.excel(res, '鐢ㄦ埛瀵煎叆妯$増.xls')
+}
+</script>
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
new file mode 100644
index 0000000..5664517
--- /dev/null
+++ b/src/views/system/user/index.vue
@@ -0,0 +1,397 @@
+<template>
+ <doc-alert title="鐢ㄦ埛浣撶郴" url="https://doc.iocoder.cn/user-center/" />
+ <doc-alert title="涓夋柟鐧婚檰" url="https://doc.iocoder.cn/social-user/" />
+ <doc-alert title="Excel 瀵煎叆瀵煎嚭" url="https://doc.iocoder.cn/excel-import-and-export/" />
+
+ <el-row :gutter="20">
+ <!-- 宸︿晶閮ㄩ棬鏍� -->
+ <el-col :span="4" :xs="24">
+ <ContentWrap class="h-1/1">
+ <DeptTree @node-click="handleDeptNodeClick" />
+ </ContentWrap>
+ </el-col>
+ <el-col :span="20" :xs="24">
+ <!-- 鎼滅储 -->
+ <ContentWrap>
+ <el-form
+ class="-mb-15px"
+ :model="queryParams"
+ ref="queryFormRef"
+ :inline="true"
+ label-width="68px"
+ >
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="username">
+ <el-input
+ v-model="queryParams.username"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="mobile">
+ <el-input
+ v-model="queryParams.mobile"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable
+ @keyup.enter="handleQuery"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鐢ㄦ埛鐘舵��"
+ clearable
+ class="!w-240px"
+ >
+ <el-option
+ v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="queryParams.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="datetimerange"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ class="!w-240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button @click="handleQuery"><Icon icon="ep:search" />鎼滅储</el-button>
+ <el-button @click="resetQuery"><Icon icon="ep:refresh" />閲嶇疆</el-button>
+ <el-button
+ type="primary"
+ plain
+ @click="openForm('create')"
+ v-hasPermi="['system:user:create']"
+ >
+ <Icon icon="ep:plus" /> 鏂板
+ </el-button>
+ <el-button
+ type="warning"
+ plain
+ @click="handleImport"
+ v-hasPermi="['system:user:import']"
+ >
+ <Icon icon="ep:upload" /> 瀵煎叆
+ </el-button>
+ <el-button
+ type="success"
+ plain
+ @click="handleExport"
+ :loading="exportLoading"
+ v-hasPermi="['system:user:export']"
+ >
+ <Icon icon="ep:download" />瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ :disabled="checkedIds.length === 0"
+ @click="handleDeleteBatch"
+ v-hasPermi="['system:user:delete']"
+ >
+ <Icon icon="ep:delete" />鎵归噺鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </ContentWrap>
+ <ContentWrap>
+ <el-table v-loading="loading" :data="list" @selection-change="handleRowCheckboxChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" key="id" prop="id" />
+ <el-table-column
+ label="鐢ㄦ埛鍚嶇О"
+ align="center"
+ prop="username"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="鐢ㄦ埛鏄电О"
+ align="center"
+ prop="nickname"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="閮ㄩ棬"
+ align="center"
+ key="deptName"
+ prop="deptName"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" prop="mobile" width="120" />
+ <el-table-column label="鐘舵��" key="status">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ :disabled="!checkPermi(['system:user:update'])"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ :formatter="dateFormatter"
+ width="180"
+ />
+ <el-table-column label="鎿嶄綔" align="center" width="160">
+ <template #default="scope">
+ <div class="flex items-center justify-center">
+ <el-button
+ type="primary"
+ link
+ @click="openForm('update', scope.row.id)"
+ v-hasPermi="['system:user:update']"
+ >
+ <Icon icon="ep:edit" />淇敼
+ </el-button>
+ <el-dropdown
+ @command="(command) => handleCommand(command, scope.row)"
+ v-hasPermi="[
+ 'system:user:delete',
+ 'system:user:update-password',
+ 'system:permission:assign-user-role'
+ ]"
+ >
+ <el-button type="primary" link><Icon icon="ep:d-arrow-right" /> 鏇村</el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item
+ command="handleDelete"
+ v-if="checkPermi(['system:user:delete'])"
+ >
+ <Icon icon="ep:delete" />鍒犻櫎
+ </el-dropdown-item>
+ <el-dropdown-item
+ command="handleResetPwd"
+ v-if="checkPermi(['system:user:update-password'])"
+ >
+ <Icon icon="ep:key" />閲嶇疆瀵嗙爜
+ </el-dropdown-item>
+ <el-dropdown-item
+ command="handleRole"
+ v-if="checkPermi(['system:permission:assign-user-role'])"
+ >
+ <Icon icon="ep:circle-check" />鍒嗛厤瑙掕壊
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ <Pagination
+ :total="total"
+ v-model:page="queryParams.pageNo"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </ContentWrap>
+ </el-col>
+ </el-row>
+
+ <!-- 娣诲姞鎴栦慨鏀圭敤鎴峰璇濇 -->
+ <UserForm ref="formRef" @success="getList" />
+ <!-- 鐢ㄦ埛瀵煎叆瀵硅瘽妗� -->
+ <UserImportForm ref="importFormRef" @success="getList" />
+ <!-- 鍒嗛厤瑙掕壊 -->
+ <UserAssignRoleForm ref="assignRoleFormRef" @success="getList" />
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { checkPermi } from '@/utils/permission'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { CommonStatusEnum } from '@/utils/constants'
+import * as UserApi from '@/api/system/user'
+import UserForm from './UserForm.vue'
+import UserImportForm from './UserImportForm.vue'
+import UserAssignRoleForm from './UserAssignRoleForm.vue'
+import DeptTree from './DeptTree.vue'
+
+defineOptions({ name: 'SystemUser' })
+
+const message = useMessage() // 娑堟伅寮圭獥
+const { t } = useI18n() // 鍥介檯鍖�
+
+const loading = ref(true) // 鍒楄〃鐨勫姞杞戒腑
+const total = ref(0) // 鍒楄〃鐨勬�婚〉鏁�
+const list = ref([]) // 鍒楄〃鐨勬暟
+const queryParams = reactive({
+ pageNo: 1,
+ pageSize: 10,
+ username: undefined,
+ mobile: undefined,
+ status: undefined,
+ deptId: undefined,
+ createTime: []
+})
+const queryFormRef = ref() // 鎼滅储鐨勮〃鍗�
+
+/** 鏌ヨ鍒楄〃 */
+const getList = async () => {
+ loading.value = true
+ try {
+ const data = await UserApi.getUserPage(queryParams)
+ list.value = data.list
+ total.value = data.total
+ } finally {
+ loading.value = false
+ }
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ queryParams.pageNo = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+const resetQuery = () => {
+ queryFormRef.value?.resetFields()
+ handleQuery()
+}
+
+/** 澶勭悊閮ㄩ棬琚偣鍑� */
+const handleDeptNodeClick = async (row: any) => {
+ if (row === undefined) {
+ queryParams.deptId = undefined
+ await getList()
+ } else {
+ queryParams.deptId = row.id
+ await getList()
+ }
+}
+
+/** 娣诲姞/淇敼鎿嶄綔 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+ formRef.value.open(type, id)
+}
+
+/** 鐢ㄦ埛瀵煎叆 */
+const importFormRef = ref()
+const handleImport = () => {
+ importFormRef.value.open()
+}
+
+/** 淇敼鐢ㄦ埛鐘舵�� */
+const handleStatusChange = async (row: UserApi.UserVO) => {
+ try {
+ // 淇敼鐘舵�佺殑浜屾纭
+ const text = row.status === CommonStatusEnum.ENABLE ? '鍚敤' : '鍋滅敤'
+ await message.confirm('纭瑕�"' + text + '""' + row.username + '"鐢ㄦ埛鍚�?')
+ // 鍙戣捣淇敼鐘舵��
+ await UserApi.updateUserStatus(row.id, row.status)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {
+ // 鍙栨秷鍚庯紝杩涜鎭㈠鎸夐挳
+ row.status =
+ row.status === CommonStatusEnum.ENABLE ? CommonStatusEnum.DISABLE : CommonStatusEnum.ENABLE
+ }
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+const exportLoading = ref(false)
+const handleExport = async () => {
+ try {
+ // 瀵煎嚭鐨勪簩娆$‘璁�
+ await message.exportConfirm()
+ // 鍙戣捣瀵煎嚭
+ exportLoading.value = true
+ const data = await UserApi.exportUser(queryParams)
+ download.excel(data, '鐢ㄦ埛鏁版嵁.xls')
+ } catch {
+ } finally {
+ exportLoading.value = false
+ }
+}
+
+/** 鎿嶄綔鍒嗗彂 */
+const handleCommand = (command: string, row: UserApi.UserVO) => {
+ switch (command) {
+ case 'handleDelete':
+ handleDelete(row.id)
+ break
+ case 'handleResetPwd':
+ handleResetPwd(row)
+ break
+ case 'handleRole':
+ handleRole(row)
+ break
+ default:
+ break
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+const handleDelete = async (id: number) => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鍒犻櫎
+ await UserApi.deleteUser(id)
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 鎵归噺鍒犻櫎鎸夐挳鎿嶄綔 */
+const checkedIds = ref<number[]>([])
+const handleRowCheckboxChange = (rows: UserApi.UserVO[]) => {
+ checkedIds.value = rows.map((row) => row.id)
+}
+
+const handleDeleteBatch = async () => {
+ try {
+ // 鍒犻櫎鐨勪簩娆$‘璁�
+ await message.delConfirm()
+ // 鍙戣捣鎵归噺鍒犻櫎
+ await UserApi.deleteUserList(checkedIds.value)
+ checkedIds.value = []
+ message.success(t('common.delSuccess'))
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } catch {}
+}
+
+/** 閲嶇疆瀵嗙爜 */
+const handleResetPwd = async (row: UserApi.UserVO) => {
+ try {
+ // 閲嶇疆鐨勪簩娆$‘璁�
+ const result = await message.prompt(
+ '璇疯緭鍏�"' + row.username + '"鐨勬柊瀵嗙爜',
+ t('common.reminder')
+ )
+ const password = result.value
+ // 鍙戣捣閲嶇疆
+ await UserApi.resetUserPassword(row.id, password)
+ message.success('淇敼鎴愬姛锛屾柊瀵嗙爜鏄細' + password)
+ } catch {}
+}
+
+/** 鍒嗛厤瑙掕壊 */
+const assignRoleFormRef = ref()
+const handleRole = (row: UserApi.UserVO) => {
+ assignRoleFormRef.value.open(row)
+}
+
+/** 鍒濆鍖� */
+onMounted(() => {
+ getList()
+})
+</script>
diff --git a/stylelint.config.js b/stylelint.config.js
new file mode 100644
index 0000000..b336785
--- /dev/null
+++ b/stylelint.config.js
@@ -0,0 +1,235 @@
+module.exports = {
+ root: true,
+ plugins: ['stylelint-order'],
+ customSyntax: 'postcss-html',
+ extends: ['stylelint-config-standard'],
+ rules: {
+ 'selector-pseudo-class-no-unknown': [
+ true,
+ {
+ ignorePseudoClasses: ['global', 'deep']
+ }
+ ],
+ 'at-rule-no-unknown': [
+ true,
+ {
+ ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend']
+ }
+ ],
+ 'media-query-no-invalid': null,
+ 'function-no-unknown': null,
+ 'no-empty-source': null,
+ 'named-grid-areas-no-invalid': null,
+ // 'unicode-bom': 'never',
+ 'no-descending-specificity': null,
+ 'font-family-no-missing-generic-family-keyword': null,
+ // 'declaration-colon-space-after': 'always-single-line',
+ // 'declaration-colon-space-before': 'never',
+ // 'declaration-block-trailing-semicolon': null,
+ 'rule-empty-line-before': [
+ 'always',
+ {
+ ignore: ['after-comment', 'first-nested']
+ }
+ ],
+ 'unit-no-unknown': [
+ true,
+ {
+ ignoreUnits: ['rpx']
+ }
+ ],
+ 'order/order': [
+ [
+ 'dollar-variables',
+ 'custom-properties',
+ 'at-rules',
+ 'declarations',
+ {
+ type: 'at-rule',
+ name: 'supports'
+ },
+ {
+ type: 'at-rule',
+ name: 'media'
+ },
+ 'rules'
+ ],
+ {
+ severity: 'warning'
+ }
+ ],
+ // Specify the alphabetical order of the attributes in the declaration block
+ 'order/properties-order': [
+ 'position',
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+ 'z-index',
+ 'display',
+ 'float',
+ 'width',
+ 'height',
+ 'max-width',
+ 'max-height',
+ 'min-width',
+ 'min-height',
+ 'padding',
+ 'padding-top',
+ 'padding-right',
+ 'padding-bottom',
+ 'padding-left',
+ 'margin',
+ 'margin-top',
+ 'margin-right',
+ 'margin-bottom',
+ 'margin-left',
+ 'margin-collapse',
+ 'margin-top-collapse',
+ 'margin-right-collapse',
+ 'margin-bottom-collapse',
+ 'margin-left-collapse',
+ 'overflow',
+ 'overflow-x',
+ 'overflow-y',
+ 'clip',
+ 'clear',
+ 'font',
+ 'font-family',
+ 'font-size',
+ 'font-smoothing',
+ 'osx-font-smoothing',
+ 'font-style',
+ 'font-weight',
+ 'hyphens',
+ 'src',
+ 'line-height',
+ 'letter-spacing',
+ 'word-spacing',
+ 'color',
+ 'text-align',
+ 'text-decoration',
+ 'text-indent',
+ 'text-overflow',
+ 'text-rendering',
+ 'text-size-adjust',
+ 'text-shadow',
+ 'text-transform',
+ 'word-break',
+ 'word-wrap',
+ 'white-space',
+ 'vertical-align',
+ 'list-style',
+ 'list-style-type',
+ 'list-style-position',
+ 'list-style-image',
+ 'pointer-events',
+ 'cursor',
+ 'background',
+ 'background-attachment',
+ 'background-color',
+ 'background-image',
+ 'background-position',
+ 'background-repeat',
+ 'background-size',
+ 'border',
+ 'border-collapse',
+ 'border-top',
+ 'border-right',
+ 'border-bottom',
+ 'border-left',
+ 'border-color',
+ 'border-image',
+ 'border-top-color',
+ 'border-right-color',
+ 'border-bottom-color',
+ 'border-left-color',
+ 'border-spacing',
+ 'border-style',
+ 'border-top-style',
+ 'border-right-style',
+ 'border-bottom-style',
+ 'border-left-style',
+ 'border-width',
+ 'border-top-width',
+ 'border-right-width',
+ 'border-bottom-width',
+ 'border-left-width',
+ 'border-radius',
+ 'border-top-right-radius',
+ 'border-bottom-right-radius',
+ 'border-bottom-left-radius',
+ 'border-top-left-radius',
+ 'border-radius-topright',
+ 'border-radius-bottomright',
+ 'border-radius-bottomleft',
+ 'border-radius-topleft',
+ 'content',
+ 'quotes',
+ 'outline',
+ 'outline-offset',
+ 'opacity',
+ 'filter',
+ 'visibility',
+ 'size',
+ 'zoom',
+ 'transform',
+ 'box-align',
+ 'box-flex',
+ 'box-orient',
+ 'box-pack',
+ 'box-shadow',
+ 'box-sizing',
+ 'table-layout',
+ 'animation',
+ 'animation-delay',
+ 'animation-duration',
+ 'animation-iteration-count',
+ 'animation-name',
+ 'animation-play-state',
+ 'animation-timing-function',
+ 'animation-fill-mode',
+ 'transition',
+ 'transition-delay',
+ 'transition-duration',
+ 'transition-property',
+ 'transition-timing-function',
+ 'background-clip',
+ 'backface-visibility',
+ 'resize',
+ 'appearance',
+ 'user-select',
+ 'interpolation-mode',
+ 'direction',
+ 'marks',
+ 'page',
+ 'set-link-source',
+ 'unicode-bidi',
+ 'speak'
+ ]
+ },
+ ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
+ overrides: [
+ {
+ files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
+ extends: ['stylelint-config-recommended', 'stylelint-config-html'],
+ rules: {
+ 'keyframes-name-pattern': null,
+ 'selector-class-pattern': null,
+ 'no-duplicate-selectors': null,
+ 'selector-pseudo-class-no-unknown': [
+ true,
+ {
+ ignorePseudoClasses: ['deep', 'global']
+ }
+ ],
+ 'selector-pseudo-element-no-unknown': [
+ true,
+ {
+ ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted']
+ }
+ ]
+ }
+ }
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..38376ef
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,43 @@
+{
+ "compilerOptions": {
+ "target": "esnext",
+ "useDefineForClassFields": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "strict": true,
+ "jsx": "preserve",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "lib": ["esnext", "dom"],
+ "baseUrl": "./",
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "allowSyntheticDefaultImports": true,
+ "strictFunctionTypes": false,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "experimentalDecorators": true,
+ "noImplicitAny": false,
+ "skipLibCheck": true,
+ "paths": {
+ "@/*": ["src/*"]
+ },
+ "types": [
+ // "@intlify/unplugin-vue-i18n/types",
+ "vite/client"
+ // "element-plus/global",
+ // "@types/qrcode",
+ // "vite-plugin-svg-icons/client"
+ ],
+ "outDir": "target", // 璇蜂繚鐣欒繖涓睘鎬э紝闃叉tsconfig.json鏂囦欢鎶ラ敊
+ "typeRoots": ["./node_modules/@types/", "./types"]
+ },
+ "include": [
+ "src",
+ "types/**/*.d.ts",
+ "src/types/auto-imports.d.ts",
+ "src/types/auto-components.d.ts"
+ ],
+ "exclude": ["dist", "target", "node_modules"]
+}
diff --git a/types/components.d.ts b/types/components.d.ts
new file mode 100644
index 0000000..9d0ba09
--- /dev/null
+++ b/types/components.d.ts
@@ -0,0 +1,8 @@
+declare module 'vue' {
+ export interface GlobalComponents {
+ Icon: typeof import('@/components/Icon')['Icon']
+ DictTag: typeof import('@/components/DictTag')['DictTag']
+ }
+}
+
+export {}
diff --git a/types/env.d.ts b/types/env.d.ts
new file mode 100644
index 0000000..17535ea
--- /dev/null
+++ b/types/env.d.ts
@@ -0,0 +1,41 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+ import { DefineComponent } from 'vue'
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
+
+interface ImportMetaEnv {
+ readonly VITE_APP_TITLE: string
+ readonly VITE_PORT: number
+ readonly VITE_OPEN: string
+ readonly VITE_DEV: string
+ readonly VITE_APP_CAPTCHA_ENABLE: string
+ readonly VITE_APP_TENANT_ENABLE: string
+ readonly VITE_APP_DEFAULT_LOGIN_TENANT: string
+ readonly VITE_APP_DEFAULT_LOGIN_USERNAME: string
+ readonly VITE_APP_DEFAULT_LOGIN_PASSWORD: string
+ readonly VITE_APP_DOCALERT_ENABLE: string
+ readonly VITE_BASE_URL: string
+ readonly VITE_API_URL: string
+ readonly VITE_BASE_PATH: string
+ readonly VITE_DROP_DEBUGGER: string
+ readonly VITE_DROP_CONSOLE: string
+ readonly VITE_SOURCEMAP: string
+ readonly VITE_OUT_DIR: string
+ readonly VITE_GOVIEW_URL: string
+ // API 鍔犺В瀵嗙浉鍏抽厤缃�
+ readonly VITE_APP_API_ENCRYPT_ENABLE: string
+ readonly VITE_APP_API_ENCRYPT_HEADER: string
+ readonly VITE_APP_API_ENCRYPT_ALGORITHM: string
+ readonly VITE_APP_API_ENCRYPT_REQUEST_KEY: string
+ readonly VITE_APP_API_ENCRYPT_RESPONSE_KEY: string
+}
+
+declare global {
+ interface ImportMeta {
+ readonly env: ImportMetaEnv
+ }
+}
diff --git a/types/global.d.ts b/types/global.d.ts
new file mode 100644
index 0000000..eebe9bb
--- /dev/null
+++ b/types/global.d.ts
@@ -0,0 +1,58 @@
+export {}
+declare global {
+ interface Fn<T = any> {
+ (...arg: T[]): T
+ }
+
+ type Nullable<T> = T | null
+
+ type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>
+
+ type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>
+
+ type ComponentRef<T> = InstanceType<T>
+
+ type LocaleType = 'zh-CN' | 'en'
+
+ declare type TimeoutHandle = ReturnType<typeof setTimeout>
+ declare type IntervalHandle = ReturnType<typeof setInterval>
+
+ type AxiosHeaders =
+ | 'application/json'
+ | 'application/x-www-form-urlencoded'
+ | 'multipart/form-data'
+
+ type AxiosMethod = 'get' | 'post' | 'delete' | 'put' | 'GET' | 'POST' | 'DELETE' | 'PUT'
+
+ type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
+
+ interface AxiosConfig {
+ params?: any
+ data?: any
+ url?: string
+ method?: AxiosMethod
+ headersType?: string
+ responseType?: AxiosResponseType
+ }
+
+ interface IResponse<T = any> {
+ code: string
+ data: T extends any ? T : T & any
+ }
+
+ interface PageParam {
+ pageSize?: number
+ pageNo?: number
+ }
+
+ interface Tree {
+ id: number
+ name: string
+ children?: Tree[] | any[]
+ }
+ // 鍒嗛〉鏁版嵁鍏叡杩斿洖
+ interface PageResult<T> {
+ list: T // 鏁版嵁
+ total: number // 鎬婚噺
+ }
+}
diff --git a/types/router.d.ts b/types/router.d.ts
new file mode 100644
index 0000000..03e91f1
--- /dev/null
+++ b/types/router.d.ts
@@ -0,0 +1,84 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { defineComponent } from 'vue'
+
+/**
+* redirect: noredirect 褰撹缃� noredirect 鐨勬椂鍊欒璺敱鍦ㄩ潰鍖呭睉瀵艰埅涓笉鍙鐐瑰嚮
+* name:'router-name' 璁惧畾璺敱鐨勫悕瀛楋紝涓�瀹氳濉啓涓嶇劧浣跨敤<keep-alive>鏃朵細鍑虹幇鍚勭闂
+* meta : {
+ hidden: true 褰撹缃� true 鐨勬椂鍊欒璺敱涓嶄細鍐嶄晶杈规爮鍑虹幇 濡�404锛宭ogin绛夐〉闈�(榛樿 false)
+
+ alwaysShow: true 褰撲綘涓�涓矾鐢变笅闈㈢殑 children 澹版槑鐨勮矾鐢卞ぇ浜�1涓椂锛岃嚜鍔ㄤ細鍙樻垚宓屽鐨勬ā寮忥紝
+ 鍙湁涓�涓椂锛屼細灏嗛偅涓瓙璺敱褰撳仛鏍硅矾鐢辨樉绀哄湪渚ц竟鏍忥紝
+ 鑻ヤ綘鎯充笉绠¤矾鐢变笅闈㈢殑 children 澹版槑鐨勪釜鏁伴兘鏄剧ず浣犵殑鏍硅矾鐢憋紝
+ 浣犲彲浠ヨ缃� alwaysShow: true锛岃繖鏍峰畠灏变細蹇界暐涔嬪墠瀹氫箟鐨勮鍒欙紝
+ 涓�鐩存樉绀烘牴璺敱(榛樿 false)
+
+ title: 'title' 璁剧疆璇ヨ矾鐢卞湪渚ц竟鏍忓拰闈㈠寘灞戜腑灞曠ず鐨勫悕瀛�
+
+ titleSuffix: '2' 褰� path 鍜� title 閲嶅鏃剁殑鍚庣紑鎴栧娉�
+
+ icon: 'svg-name' 璁剧疆璇ヨ矾鐢辩殑鍥炬爣
+
+ noCache: true 濡傛灉璁剧疆涓簍rue锛屽垯涓嶄細琚� <keep-alive> 缂撳瓨(榛樿 false)
+
+ breadcrumb: false 濡傛灉璁剧疆涓篺alse锛屽垯涓嶄細鍦╞readcrumb闈㈠寘灞戜腑鏄剧ず(榛樿 true)
+
+ affix: true 濡傛灉璁剧疆涓簍rue锛屽垯浼氫竴鐩村浐瀹氬湪tag椤逛腑(榛樿 false)
+
+ noTagsView: true 濡傛灉璁剧疆涓簍rue锛屽垯涓嶄細鍑虹幇鍦╰ag涓�(榛樿 false)
+
+ activeMenu: '/home' 鏄剧ず楂樹寒鐨勮矾鐢辫矾寰�
+
+ followAuth: '/home' 璺熼殢鍝釜璺敱杩涜鏉冮檺杩囨护
+
+ canTo: true 璁剧疆涓簍rue鍗充娇hidden涓簍rue锛屼篃渚濈劧鍙互杩涜璺敱璺宠浆(榛樿 false)
+ }
+**/
+declare module 'vue-router' {
+ interface RouteMeta extends Record<string | number | symbol, unknown> {
+ hidden?: boolean
+ alwaysShow?: boolean
+ title?: string
+ titleSuffix?: string
+ icon?: string
+ noCache?: boolean
+ breadcrumb?: boolean
+ affix?: boolean
+ activeMenu?: string
+ noTagsView?: boolean
+ followAuth?: string
+ canTo?: boolean
+ }
+}
+
+type Component<T = any> =
+ | ReturnType<typeof defineComponent>
+ | (() => Promise<typeof import('*.vue')>)
+ | (() => Promise<T>)
+
+declare global {
+ interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
+ name: string
+ meta: RouteMeta
+ component?: Component | string
+ children?: AppRouteRecordRaw[]
+ props?: Recordable
+ fullPath?: string
+ keepAlive?: boolean
+ }
+
+ interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
+ icon: any
+ name: string
+ meta: RouteMeta
+ component: string
+ componentName?: string
+ path: string
+ redirect: string
+ children?: AppCustomRouteRecordRaw[]
+ keepAlive?: boolean
+ visible?: boolean
+ parentId?: number
+ alwaysShow?: boolean
+ }
+}
diff --git a/types/wangeditor-types.d.ts b/types/wangeditor-types.d.ts
new file mode 100644
index 0000000..2363f56
--- /dev/null
+++ b/types/wangeditor-types.d.ts
@@ -0,0 +1,27 @@
+import { SlateDescendant } from '@wangeditor-next/editor'
+
+declare module 'slate' {
+ interface CustomTypes {
+ // 鎵╁睍 text
+ Text: {
+ text: string
+ bold?: boolean
+ italic?: boolean
+ code?: boolean
+ through?: boolean
+ underline?: boolean
+ sup?: boolean
+ sub?: boolean
+ color?: string
+ bgColor?: string
+ fontSize?: string
+ fontFamily?: string
+ }
+
+ // 鎵╁睍 Element 鐨� type 灞炴��
+ Element: {
+ type: string
+ children: SlateDescendant[]
+ }
+ }
+}
diff --git a/uno.config.ts b/uno.config.ts
new file mode 100644
index 0000000..1b8c837
--- /dev/null
+++ b/uno.config.ts
@@ -0,0 +1,107 @@
+import { defineConfig, toEscapedSelector as e, presetUno } from 'unocss'
+// import transformerVariantGroup from '@unocss/transformer-variant-group'
+
+export default defineConfig({
+ // ...UnoCSS options
+ rules: [
+ [
+ /^custom-hover$/,
+ ([], { rawSelector }) => {
+ const selector = e(rawSelector)
+ return `
+${selector} {
+ display: flex;
+ height: 100%;
+ padding: 0 10px;
+ cursor: pointer;
+ align-items: center;
+ transition: background var(--transition-time-02);
+}
+/* you can have multiple rules */
+${selector}:hover {
+ background-color: var(--top-header-hover-color);
+}
+.dark ${selector}:hover {
+ background-color: var(--el-bg-color-overlay);
+}
+`
+ }
+ ],
+ [
+ /^layout-border__left$/,
+ ([], { rawSelector }) => {
+ const selector = e(rawSelector)
+ return `
+${selector}:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background-color: var(--el-border-color);
+ z-index: 3;
+}
+`
+ }
+ ],
+ [
+ /^layout-border__right$/,
+ ([], { rawSelector }) => {
+ const selector = e(rawSelector)
+ return `
+${selector}:after {
+ content: "";
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 1px;
+ height: 100%;
+ background-color: var(--el-border-color);
+ z-index: 3;
+}
+`
+ }
+ ],
+ [
+ /^layout-border__top$/,
+ ([], { rawSelector }) => {
+ const selector = e(rawSelector)
+ return `
+${selector}:before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 1px;
+ background-color: var(--el-border-color);
+ z-index: 3;
+}
+`
+ }
+ ],
+ [
+ /^layout-border__bottom$/,
+ ([], { rawSelector }) => {
+ const selector = e(rawSelector)
+ return `
+${selector}:after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 1px;
+ background-color: var(--el-border-color);
+ z-index: 3;
+}
+`
+ }
+ ]
+ ],
+ presets: [presetUno({ dark: 'class', attributify: false })],
+ // transformers: [transformerVariantGroup()],
+ shortcuts: {
+ 'wh-full': 'w-full h-full'
+ }
+})
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..44a98c9
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,88 @@
+import {resolve} from 'path'
+import type {ConfigEnv, UserConfig} from 'vite'
+import {loadEnv} from 'vite'
+import {createVitePlugins} from './build/vite'
+import {exclude, include} from "./build/vite/optimize"
+// 褰撳墠鎵цnode鍛戒护鏃舵枃浠跺す鐨勫湴鍧�(宸ヤ綔鐩綍)
+const root = process.cwd()
+
+// 璺緞鏌ユ壘
+function pathResolve(dir: string) {
+ return resolve(root, '.', dir)
+}
+
+// https://vitejs.dev/config/
+export default ({command, mode}: ConfigEnv): UserConfig => {
+ let env = {} as any
+ const isBuild = command === 'build'
+ if (!isBuild) {
+ env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root)
+ } else {
+ env = loadEnv(mode, root)
+ }
+ return {
+ base: env.VITE_BASE_PATH,
+ root: root,
+ // 鏈嶅姟绔覆鏌�
+ server: {
+ port: env.VITE_PORT, // 绔彛鍙�
+ host: "0.0.0.0",
+ open: env.VITE_OPEN === 'true',
+ // 鏈湴璺ㄥ煙浠g悊. 鐩墠娉ㄩ噴鐨勫師鍥狅細鏆傛椂娌℃湁鐢ㄩ�旓紝server 绔凡缁忔敮鎸佽法鍩�
+ proxy: {
+ ['/admin-api']: {
+ target: 'http://101.43.143.75:48180',
+ ws: false,
+ changeOrigin: true,
+ rewrite: (path) => path.replace(new RegExp(`^/admin-api`), ''),
+ },
+ },
+ },
+ // 椤圭洰浣跨敤鐨剉ite鎻掍欢銆� 鍗曠嫭鎻愬彇鍒癰uild/vite/plugin涓鐞�
+ plugins: createVitePlugins(),
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: '@use "@/styles/variables.scss" as *;',
+ javascriptEnabled: true,
+ silenceDeprecations: ["legacy-js-api"], // 鍙傝�冭嚜 https://stackoverflow.com/questions/78997907/the-legacy-js-api-is-deprecated-and-will-be-removed-in-dart-sass-2-0-0
+ }
+ }
+ },
+ resolve: {
+ extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.scss', '.css'],
+ alias: [
+ {
+ find: 'vue-i18n',
+ replacement: 'vue-i18n/dist/vue-i18n.cjs.js'
+ },
+ {
+ find: /\@\//,
+ replacement: `${pathResolve('src')}/`
+ }
+ ]
+ },
+ build: {
+ minify: 'terser',
+ outDir: env.VITE_OUT_DIR || 'dist',
+ sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
+ // brotliSize: false,
+ terserOptions: {
+ compress: {
+ drop_debugger: env.VITE_DROP_DEBUGGER === 'true',
+ drop_console: env.VITE_DROP_CONSOLE === 'true'
+ }
+ },
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ echarts: ['echarts'], // 灏� echarts 鍗曠嫭鎵撳寘锛屽弬鑰� https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 璁ㄨ
+ 'form-create': ['@form-create/element-ui'], // 鍙傝�� https://github.com/yudaocode/yudao-ui-admin-vue3/issues/148 璁ㄨ
+ 'form-designer': ['@form-create/designer'],
+ }
+ },
+ },
+ },
+ optimizeDeps: {include, exclude}
+ }
+}
diff --git a/web-types.json b/web-types.json
new file mode 100644
index 0000000..602f212
--- /dev/null
+++ b/web-types.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json.schemastore.org/web-types",
+ "framework": "vue",
+ "name": "name written in package.json",
+ "version": "version written in package.json",
+ "contributions": {
+ "html": {
+ "types-syntax": "typescript",
+ "attributes": [
+ {
+ "name": "v-hasPermi"
+ },
+ {
+ "name": "v-hasRole"
+ }
+ ]
+ }
+ }
+}
--
Gitblit v1.8.0